Merge branch 'MDL-26520' of git://github.com/timhunt/moodle
[moodle.git] / rating / lib.php
blob29313e2786a05342eb2e4c0caa53269e30a76546
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 context
51 public $context;
53 /**
54 * The id of the item (forum post, glossary item etc) being rated
55 * @var int
57 public $itemid;
59 /**
60 * The id scale (1-5, 0-100) that was in use when the rating was submitted
61 * @var int
63 public $scaleid;
65 /**
66 * The id of the user who submitted the rating
67 * @var int
69 public $userid;
71 /**
72 * settings for this rating. Necessary to render the rating.
73 * @var stdclass
75 public $settings;
77 /**
78 * Constructor.
79 * @param object $options {
80 * context => context context to use for the rating [required]
81 * itemid => int the id of the associated item (forum post, glossary item etc) [required]
82 * scaleid => int The scale in use when the rating was submitted [required]
83 * userid => int The id of the user who submitted the rating [required]
84 * }
86 public function __construct($options) {
87 $this->context = $options->context;
88 $this->itemid = $options->itemid;
89 $this->scaleid = $options->scaleid;
90 $this->userid = $options->userid;
93 /**
94 * Update this rating in the database
95 * @param int $rating the integer value of this rating
96 * @return void
98 public function update_rating($rating) {
99 global $DB;
101 $data = new stdclass();
102 $table = 'rating';
104 $item = new stdclass();
105 $item->id = $this->itemid;
106 $items = array($item);
108 $ratingoptions = new stdclass();
109 $ratingoptions->context = $this->context;
110 $ratingoptions->items = $items;
111 $ratingoptions->aggregate = RATING_AGGREGATE_AVERAGE;//we dont actually care what aggregation method is applied
112 $ratingoptions->scaleid = $this->scaleid;
113 $ratingoptions->userid = $this->userid;
115 $rm = new rating_manager();
116 $items = $rm->get_ratings($ratingoptions);
117 if( empty($items) || empty($items[0]->rating) || empty($items[0]->rating->id) ) {
118 $data->contextid = $this->context->id;
119 $data->rating = $rating;
120 $data->scaleid = $this->scaleid;
121 $data->userid = $this->userid;
122 $data->itemid = $this->itemid;
124 $time = time();
125 $data->timecreated = $time;
126 $data->timemodified = $time;
128 $DB->insert_record($table, $data);
130 else {
131 $data->id = $items[0]->rating->id;
132 $data->rating = $rating;
134 $time = time();
135 $data->timemodified = $time;
137 $DB->update_record($table, $data);
142 * Retreive the integer value of this rating
143 * @return int the integer value of this rating object
145 public function get_rating() {
146 return $this->rating;
150 * Remove this rating from the database
151 * @return void
153 //public function delete_rating() {
154 //todo implement this if its actually needed
156 } //end rating class definition
159 * The rating_manager class provides the ability to retrieve sets of ratings from the database
161 * @copyright 2010 Andrew Davis
162 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
163 * @since Moodle 2.0
165 class rating_manager {
168 * Delete one or more ratings. Specify either a rating id, an item id or just the context id.
169 * @param object $options {
170 * contextid => int the context in which the ratings exist [required]
171 * ratingid => int the id of an individual rating to delete [optional]
172 * userid => int delete the ratings submitted by this user. May be used in conjuction with itemid [optional]
173 * itemid => int delete all ratings attached to this item [optional]
175 * @return void
177 public function delete_ratings($options) {
178 global $DB;
180 if( !empty($options->ratingid) ) {
181 //delete a single rating
182 $DB->delete_records('rating', array('contextid'=>$options->contextid, 'id'=>$options->ratingid) );
184 else if( !empty($options->itemid) && !empty($options->userid) ) {
185 //delete the rating for an item submitted by a particular user
186 $DB->delete_records('rating', array('contextid'=>$options->contextid, 'itemid'=>$options->itemid, 'userid'=>$options->userid) );
188 else if( !empty($options->itemid) ) {
189 //delete all ratings for an item
190 $DB->delete_records('rating', array('contextid'=>$options->contextid, 'itemid'=>$options->itemid) );
192 else if( !empty($options->userid) ) {
193 //delete all ratings submitted by a user
194 $DB->delete_records('rating', array('contextid'=>$options->contextid, 'userid'=>$options->userid) );
196 else {
197 //delete all ratings for this context
198 $DB->delete_records('rating', array('contextid'=>$options->contextid) );
203 * Returns an array of ratings for a given item (forum post, glossary entry etc)
204 * This returns all users ratings for a single item
205 * @param object $options {
206 * context => context the context in which the ratings exists [required]
207 * itemid => int the id of the associated item (forum post, glossary item etc) [required]
208 * sort => string SQL sort by clause [optional]
210 * @return array an array of ratings
212 public function get_all_ratings_for_item($options) {
213 global $DB;
215 $sortclause = '';
216 if( !empty($options->sort) ) {
217 $sortclause = "ORDER BY $options->sort";
220 $userfields = user_picture::fields('u', null, 'userid');
221 $sql = "SELECT r.id, r.rating, r.itemid, r.userid, r.timemodified, $userfields
222 FROM {rating} r
223 LEFT JOIN {user} u ON r.userid = u.id
224 WHERE r.contextid = :contextid AND
225 r.itemid = :itemid
226 {$sortclause}";
228 $params['contextid'] = $options->context->id;
229 $params['itemid'] = $options->itemid;
231 return $DB->get_records_sql($sql, $params);
235 * Adds rating objects to an array of items (forum posts, glossary entries etc)
236 * Rating objects are available at $item->rating
237 * @param object $options {
238 * context => context the context in which the ratings exists [required]
239 * items => array an array of items such as forum posts or glossary items. They must have an 'id' member ie $items[0]->id[required]
240 * aggregate => int what aggregation method should be applied. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required]
241 * scaleid => int the scale from which the user can select a rating [required]
242 * userid => int the id of the current user [optional]
243 * returnurl => string the url to return the user to after submitting a rating. Can be left null for ajax requests [optional]
244 * assesstimestart => int only allow rating of items created after this timestamp [optional]
245 * assesstimefinish => int only allow rating of items created before this timestamp [optional]
246 * plugintype => string plugin type ie 'mod' Used to find the permissions callback [optional]
247 * pluginname => string plugin name ie 'forum' Used to find the permissions callback [optional]
248 * @return array the array of items with their ratings attached at $items[0]->rating
250 public function get_ratings($options) {
251 global $DB, $USER, $PAGE, $CFG;
253 //are ratings enabled?
254 if ($options->aggregate==RATING_AGGREGATE_NONE) {
255 return $options->items;
257 $aggregatestr = $this->get_aggregation_method($options->aggregate);
259 if(empty($options->items)) {
260 return $options->items;
263 $userid = null;
264 if (empty($options->userid)) {
265 $userid = $USER->id;
266 } else {
267 $userid = $options->userid;
270 //create an array of item ids
271 $itemids = array();
272 foreach($options->items as $item) {
273 $itemids[] = $item->id;
276 //get the items from the database
277 list($itemidtest, $params) = $DB->get_in_or_equal(
278 $itemids, SQL_PARAMS_NAMED, 'itemid0000');
280 //note: all the group bys arent really necessary but PostgreSQL complains
281 //about selecting a mixture of grouped and non-grouped columns
282 $sql = "SELECT r.itemid, ur.id, ur.userid, ur.scaleid,
283 $aggregatestr(r.rating) AS aggrrating,
284 COUNT(r.rating) AS numratings,
285 ur.rating AS usersrating
286 FROM {rating} r
287 LEFT JOIN {rating} ur ON ur.contextid = r.contextid AND
288 ur.itemid = r.itemid AND
289 ur.userid = :userid
290 WHERE
291 r.contextid = :contextid AND
292 r.itemid $itemidtest
293 GROUP BY r.itemid, ur.rating, ur.id, ur.userid, ur.scaleid
294 ORDER BY r.itemid";
296 $params['userid'] = $userid;
297 $params['contextid'] = $options->context->id;
299 $ratingsrecords = $DB->get_records_sql($sql, $params);
301 //now create the rating sub objects
302 $scaleobj = new stdClass();
303 $scalemax = null;
305 //we could look for a scale id on each item to allow each item to use a different scale
306 if($options->scaleid < 0 ) { //if its a scale (not numeric)
307 $scalerecord = $DB->get_record('scale', array('id' => -$options->scaleid));
308 if ($scalerecord) {
309 $scalearray = explode(',', $scalerecord->scale);
311 //is there a more efficient way to get the indexes to start at 1 instead of 0?
312 //this will go away when scales are refactored
313 $c = count($scalearray);
314 $n = null;
315 for($i=0; $i<$c; $i++) {
316 $n = $i+1;
317 $scaleobj->scaleitems["$n"] = $scalearray[$i];//treat index as a string to allow sorting without changing the value
319 krsort($scaleobj->scaleitems);//have the highest grade scale item appear first
321 $scaleobj->id = $options->scaleid;//dont use the one from the record or we "forget" that its negative
322 $scaleobj->name = $scalerecord->name;
323 $scaleobj->courseid = $scalerecord->courseid;
325 $scalemax = count($scaleobj->scaleitems);
328 else { //its numeric
329 $scaleobj->scaleitems = $options->scaleid;
330 $scaleobj->id = $options->scaleid;
331 $scaleobj->name = null;
333 $scalemax = $options->scaleid;
336 //should $settings and $settings->permissions be declared as proper classes?
337 $settings = new stdclass(); //settings that are common to all ratings objects in this context
338 $settings->scale = $scaleobj; //the scale to use now
339 $settings->aggregationmethod = $options->aggregate;
340 if( !empty($options->returnurl) ) {
341 $settings->returnurl = $options->returnurl;
344 $settings->assesstimestart = $settings->assesstimefinish = null;
345 if( !empty($options->assesstimestart) ) {
346 $settings->assesstimestart = $options->assesstimestart;
348 if( !empty($options->assesstimefinish) ) {
349 $settings->assesstimefinish = $options->assesstimefinish;
352 //check site capabilities
353 $settings->permissions = new stdclass();
354 $settings->permissions->view = has_capability('moodle/rating:view',$options->context);//can view the aggregate of ratings of their own items
355 $settings->permissions->viewany = has_capability('moodle/rating:viewany',$options->context);//can view the aggregate of ratings of other people's items
356 $settings->permissions->viewall = has_capability('moodle/rating:viewall',$options->context);//can view individual ratings
357 $settings->permissions->rate = has_capability('moodle/rating:rate',$options->context);//can submit ratings
359 //check module capabilities (mostly for backwards compatability with old modules that previously implemented their own ratings)
360 $plugintype = !empty($options->plugintype) ? $options->plugintype : null;
361 $pluginname = !empty($options->pluginname) ? $options->pluginname : null;
362 $pluginpermissionsarray = $this->get_plugin_permissions_array($options->context->id, $plugintype, $pluginname);
364 $settings->pluginpermissions = new stdclass();
365 $settings->pluginpermissions->view = $pluginpermissionsarray['view'];
366 $settings->pluginpermissions->viewany = $pluginpermissionsarray['viewany'];
367 $settings->pluginpermissions->viewall = $pluginpermissionsarray['viewall'];
368 $settings->pluginpermissions->rate = $pluginpermissionsarray['rate'];
370 $rating = null;
371 $ratingoptions = new stdclass();
372 $ratingoptions->context = $options->context;//context is common to all ratings in the set
373 foreach($options->items as $item) {
374 $rating = null;
375 //match the item with its corresponding rating
376 foreach($ratingsrecords as $rec) {
377 if( $item->id==$rec->itemid ) {
378 //Note: rec->scaleid = the id of scale at the time the rating was submitted
379 //may be different from the current scale id
380 $ratingoptions->itemid = $item->id;
381 $ratingoptions->scaleid = $rec->scaleid;
382 $ratingoptions->userid = $rec->userid;
384 $rating = new rating($ratingoptions);
385 $rating->id = $rec->id; //unset($rec->id);
386 $rating->aggregate = $rec->aggrrating; //unset($rec->aggrrating);
387 $rating->count = $rec->numratings; //unset($rec->numratings);
388 $rating->rating = $rec->usersrating; //unset($rec->usersrating);
389 $rating->itemtimecreated = $this->get_item_time_created($item);
391 break;
394 //if there are no ratings for this item
395 if( !$rating ) {
396 $ratingoptions->itemid = $item->id;
397 $ratingoptions->scaleid = null;
398 $ratingoptions->userid = null;
400 $rating = new rating($ratingoptions);
401 $rating->id = null;
402 $rating->aggregate = null;
403 $rating->count = 0;
404 $rating->rating = null;
406 $rating->itemid = $item->id;
407 $rating->userid = null;
408 $rating->scaleid = null;
409 $rating->itemtimecreated = $this->get_item_time_created($item);
412 if( !empty($item->userid) ) {
413 $rating->itemuserid = $item->userid;
414 } else {
415 $rating->itemuserid = null;
417 $rating->settings = $settings;
418 $item->rating = $rating;
420 //Below is a nasty hack presumably here to handle scales being changed (out of 10 to out of 5 for example)
422 // it could throw off the grading if count and sum returned a grade higher than scale
423 // so to prevent it we review the results and ensure that grade does not exceed the scale, if it does we set grade = scale (i.e. full credit)
424 if ($rating->rating > $scalemax) {
425 $rating->rating = $scalemax;
427 if ($rating->aggregate > $scalemax) {
428 $rating->aggregate = $scalemax;
432 return $options->items;
435 private function get_item_time_created($item) {
436 if( !empty($item->created) ) {
437 return $item->created;//the forum_posts table has created instead of timecreated
439 else if(!empty($item->timecreated)) {
440 return $item->timecreated;
442 else {
443 return null;
448 * Returns an array of grades calculated by aggregating item ratings.
449 * @param object $options {
450 * userid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required]
451 * aggregationmethod => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required]
452 * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
453 * itemtable => int the table containing the items [required]
454 * itemtableusercolum => int the column of the user table containing the item owner's user id [required]
456 * contextid => int the context in which the rated items exist [optional]
458 * modulename => string the name of the module [optional]
459 * moduleid => int the id of the module instance [optional]
461 * @return array the array of the user's grades
463 public function get_user_grades($options) {
464 global $DB;
466 $contextid = null;
468 //if the calling code doesn't supply a context id we'll have to figure it out
469 if( !empty($options->contextid) ) {
470 $contextid = $options->contextid;
472 else if( !empty($options->cmid) ) {
473 //not implemented as not currently used although cmid is potentially available (the forum supplies it)
474 //Is there a convenient way to get a context id from a cm id?
475 //$cmidnumber = $options->cmidnumber;
477 else if ( !empty($options->modulename) && !empty($options->moduleid) ) {
478 $modulename = $options->modulename;
479 $moduleid = intval($options->moduleid);
481 //going direct to the db for the context id seems wrong
482 list($ctxselect, $ctxjoin) = context_instance_preload_sql('cm.id', CONTEXT_MODULE, 'ctx');
483 $sql = "SELECT cm.* $ctxselect
484 FROM {course_modules} cm
485 LEFT JOIN {modules} mo ON mo.id = cm.module
486 LEFT JOIN {{$modulename}} m ON m.id = cm.instance $ctxjoin
487 WHERE mo.name=:modulename AND m.id=:moduleid";
488 $contextrecord = $DB->get_record_sql($sql, array('modulename'=>$modulename, 'moduleid'=>$moduleid), '*', MUST_EXIST);
489 $contextid = $contextrecord->ctxid;
492 $params = array();
493 $params['contextid']= $contextid;
494 $itemtable = $options->itemtable;
495 $itemtableusercolumn= $options->itemtableusercolumn;
496 $scaleid = $options->scaleid;
497 $aggregationstring = $this->get_aggregation_method($options->aggregationmethod);
499 //if userid is not 0 we only want the grade for a single user
500 $singleuserwhere = '';
501 if ($options->userid!=0) {
502 $params['userid1'] = intval($options->userid);
503 $singleuserwhere = "AND i.{$itemtableusercolumn} = :userid1";
506 //MDL-24648 The where line used to be "WHERE (r.contextid is null or r.contextid=:contextid)"
507 //r.contextid will be null for users who haven't been rated yet
508 //no longer including users who haven't been rated to reduce memory requirements
509 $sql = "SELECT u.id as id, u.id AS userid, $aggregationstring(r.rating) AS rawgrade
510 FROM {user} u
511 LEFT JOIN {{$itemtable}} i ON u.id=i.{$itemtableusercolumn}
512 LEFT JOIN {rating} r ON r.itemid=i.id
513 WHERE r.contextid=:contextid
514 $singleuserwhere
515 GROUP BY u.id";
517 $results = $DB->get_records_sql($sql, $params);
519 if ($results) {
521 $scale = null;
522 $max = 0;
523 if ($options->scaleid >= 0) {
524 //numeric
525 $max = $options->scaleid;
526 } else {
527 //custom scales
528 $scale = $DB->get_record('scale', array('id' => -$options->scaleid));
529 if ($scale) {
530 $scale = explode(',', $scale->scale);
531 $max = count($scale);
532 } else {
533 debugging('rating_manager::get_user_grades() received a scale ID that doesnt exist');
537 // it could throw off the grading if count and sum returned a rawgrade higher than scale
538 // 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)
539 foreach ($results as $rid=>$result) {
540 if ($options->scaleid >= 0) {
541 //numeric
542 if ($result->rawgrade > $options->scaleid) {
543 $results[$rid]->rawgrade = $options->scaleid;
545 } else {
546 //scales
547 if (!empty($scale) && $result->rawgrade > $max) {
548 $results[$rid]->rawgrade = $max;
554 return $results;
558 * Returns array of aggregate types. Used by ratings.
560 * @return array
562 public function get_aggregate_types() {
563 return array (RATING_AGGREGATE_NONE => get_string('aggregatenone', 'rating'),
564 RATING_AGGREGATE_AVERAGE => get_string('aggregateavg', 'rating'),
565 RATING_AGGREGATE_COUNT => get_string('aggregatecount', 'rating'),
566 RATING_AGGREGATE_MAXIMUM => get_string('aggregatemax', 'rating'),
567 RATING_AGGREGATE_MINIMUM => get_string('aggregatemin', 'rating'),
568 RATING_AGGREGATE_SUM => get_string('aggregatesum', 'rating'));
572 * Converts an aggregation method constant into something that can be included in SQL
573 * @param int $aggregate An aggregation constant. For example, RATING_AGGREGATE_AVERAGE.
574 * @return string an SQL aggregation method
576 public function get_aggregation_method($aggregate) {
577 $aggregatestr = null;
578 switch($aggregate){
579 case RATING_AGGREGATE_AVERAGE:
580 $aggregatestr = 'AVG';
581 break;
582 case RATING_AGGREGATE_COUNT:
583 $aggregatestr = 'COUNT';
584 break;
585 case RATING_AGGREGATE_MAXIMUM:
586 $aggregatestr = 'MAX';
587 break;
588 case RATING_AGGREGATE_MINIMUM:
589 $aggregatestr = 'MIN';
590 break;
591 case RATING_AGGREGATE_SUM:
592 $aggregatestr = 'SUM';
593 break;
594 default:
595 $aggregatestr = 'AVG'; // Default to this to avoid real breakage - MDL-22270
596 debugging('Incorrect call to get_aggregation_method(), was called with incorrect aggregate method ' . $aggregate, DEBUG_DEVELOPER);
598 return $aggregatestr;
602 * Looks for a callback and retrieves permissions from the plugin whose items are being rated
603 * @param int $contextid The current context id
604 * @param string plugintype the type of plugin ie 'mod'
605 * @param string pluginname the name of the plugin ie 'forum'
606 * @return array rating related permissions
608 public function get_plugin_permissions_array($contextid, $plugintype=null, $pluginname=null) {
609 $pluginpermissionsarray = null;
610 $defaultpluginpermissions = array('rate'=>true,'view'=>true,'viewany'=>true,'viewall'=>true);//all true == rely on system level permissions if no plugin callback is defined
611 if ($plugintype && $pluginname) {
612 $pluginpermissionsarray = plugin_callback($plugintype, $pluginname, 'rating', 'permissions', array($contextid), $defaultpluginpermissions);
613 } else {
614 $pluginpermissionsarray = $defaultpluginpermissions;
616 return $pluginpermissionsarray;
620 * Checks if the item exists and is NOT owned by the current owner. Uses a callback to find out what table to look in.
621 * @param string plugintype the type of plugin ie 'mod'
622 * @param string pluginname the name of the plugin ie 'forum'
623 * @return boolean True if the callback doesn't exist. True if the item exists and doesn't belong to the current user. False otherwise.
625 public function check_item_and_owner($plugintype, $pluginname, $itemid) {
626 global $DB, $USER;
628 list($tablename,$itemidcol,$useridcol) = plugin_callback($plugintype, $pluginname, 'rating', 'item_check_info');
630 if (!empty($tablename)) {
631 $item = $DB->get_record($tablename, array($itemidcol=>$itemid), $useridcol);
632 if ($item) {
633 if ($item->userid!=$USER->id) {
634 return true;
638 return false;//item doesn't exist or belongs to the current user
639 } else {
640 return true;//callback doesn't exist
643 }//end rating_manager class definition