question import/export: MDL-22100 ' / etc. in category names confuse the import/export.
[moodle.git] / rating / lib.php
blob111a2a5a6d92203b2ded344e97c35491392c612e
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 moodlecore
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);
37 /**
38 * The rating class represents a single rating by a single user
40 * @copyright 2010 Andrew Davis
41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42 * @since Moodle 2.0
44 class rating implements renderable {
46 /**
47 * The context in which this rating exists
48 * @var context
50 public $context;
52 /**
53 * The id of the item (forum post, glossary item etc) being rated
54 * @var int
56 public $itemid;
58 /**
59 * The id scale (1-5, 0-100) that was in use when the rating was submitted
60 * @var int
62 public $scaleid;
64 /**
65 * The id of the user who submitted the rating
66 * @var int
68 public $userid;
70 /**
71 * settings for this rating. Necessary to render the rating.
72 * @var stdclass
74 public $settings;
76 /**
77 * Constructor.
78 * @param object $options {
79 * context => context context to use for the rating [required]
80 * itemid => int the id of the associated item (forum post, glossary item etc) [required]
81 * scaleid => int The scale in use when the rating was submitted [required]
82 * userid => int The id of the user who submitted the rating [required]
83 * }
85 public function __construct($options) {
86 $this->context = $options->context;
87 $this->itemid = $options->itemid;
88 $this->scaleid = $options->scaleid;
89 $this->userid = $options->userid;
92 /**
93 * Update this rating in the database
94 * @param int $rating the integer value of this rating
95 * @return void
97 public function update_rating($rating) {
98 global $DB;
100 $data = new stdclass();
101 $table = 'rating';
103 $item = new stdclass();
104 $item->id = $this->itemid;
105 $items = array($item);
107 $ratingoptions = new stdclass();
108 $ratingoptions->context = $this->context;
109 $ratingoptions->items = $items;
110 $ratingoptions->aggregate = RATING_AGGREGATE_AVERAGE;//we dont actually care what aggregation method is applied
111 $ratingoptions->scaleid = $this->scaleid;
112 $ratingoptions->userid = $this->userid;
114 $rm = new rating_manager();
115 $items = $rm->get_ratings($ratingoptions);
116 if( empty($items) || empty($items[0]->rating) || empty($items[0]->rating->id) ) {
117 $data->contextid = $this->context->id;
118 $data->rating = $rating;
119 $data->scaleid = $this->scaleid;
120 $data->userid = $this->userid;
121 $data->itemid = $this->itemid;
123 $time = time();
124 $data->timecreated = $time;
125 $data->timemodified = $time;
127 $DB->insert_record($table, $data);
129 else {
130 $data->id = $items[0]->rating->id;
131 $data->rating = $rating;
133 $time = time();
134 $data->timemodified = $time;
136 $DB->update_record($table, $data);
141 * Retreive the integer value of this rating
142 * @return int the integer value of this rating object
144 public function get_rating() {
145 return $this->rating;
149 * Remove this rating from the database
150 * @return void
152 //public function delete_rating() {
153 //todo implement this if its actually needed
155 } //end rating class definition
158 * The rating_manager class provides the ability to retrieve sets of ratings from the database
160 * @copyright 2010 Andrew Davis
161 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
162 * @since Moodle 2.0
164 class rating_manager {
167 * Delete one or more ratings. Specify either a rating id, an item id or just the context id.
168 * @param object $options {
169 * contextid => int the context in which the ratings exist [required]
170 * ratingid => int the id of an individual rating to delete [optional]
171 * itemid => int delete all ratings attached to this item [optional]
173 * @return void
175 public function delete_ratings($options) {
176 global $DB;
178 if( !empty($options->ratingid) ) {
179 //delete a single rating
180 $DB->delete_records('rating', array('contextid'=>$options->contextid, 'id'=>$options->ratingid) );
182 else if( !empty($options->itemid) ) {
183 //delete all ratings for an item
184 $DB->delete_records('rating', array('contextid'=>$options->contextid, 'itemid'=>$options->itemid) );
186 else {
187 //delete all ratings for this context
188 $DB->delete_records('rating', array('contextid'=>$options->contextid) );
193 * Returns an array of ratings for a given item (forum post, glossary entry etc)
194 * This returns all users ratings for a single item
195 * @param object $options {
196 * context => context the context in which the ratings exists [required]
197 * itemid => int the id of the associated item (forum post, glossary item etc) [required]
198 * sort => string SQL sort by clause [optional]
200 * @return array an array of ratings
202 public function get_all_ratings_for_item($options) {
203 global $DB;
205 $sortclause = '';
206 if( !empty($options->sort) ) {
207 $sortclause = "ORDER BY $options->sort";
210 $userfields = user_picture::fields('u','uid');
211 $sql = "SELECT r.id, r.rating, r.itemid, r.userid, r.timemodified,
212 $userfields
213 FROM {rating} r
214 LEFT JOIN {user} u ON r.userid = u.id
215 WHERE r.contextid = :contextid AND
216 r.itemid = :itemid
217 {$sortclause}";
219 $params['contextid'] = $options->context->id;
220 $params['itemid'] = $options->itemid;
222 return $DB->get_records_sql($sql, $params);
226 * Adds rating objects to an array of items (forum posts, glossary entries etc)
227 * Rating objects are available at $item->rating
228 * @param object $options {
229 * context => context the context in which the ratings exists [required]
230 * items => array an array of items such as forum posts or glossary items. They must have an 'id' member ie $items[0]->id[required]
231 * aggregate => int what aggregation method should be applied. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required]
232 * scaleid => int the scale from which the user can select a rating [required]
233 * userid => int the id of the current user [optional]
234 * returnurl => string the url to return the user to after submitting a rating. Can be left null for ajax requests [optional]
235 * assesstimestart => int only allow rating of items created after this timestamp [optional]
236 * assesstimefinish => int only allow rating of items created before this timestamp [optional]
237 * plugintype => string plugin type ie 'mod' Used to find the permissions callback [optional]
238 * pluginname => string plugin name ie 'forum' Used to find the permissions callback [optional]
239 * @return array the array of items with their ratings attached at $items[0]->rating
241 public function get_ratings($options) {
242 global $DB, $USER, $PAGE, $CFG;
244 //are ratings enabled?
245 if ($options->aggregate==RATING_AGGREGATE_NONE) {
246 return $options->items;
248 $aggregatestr = $this->get_aggregation_method($options->aggregate);
250 if(empty($options->items)) {
251 return $options->items;
254 if (empty($options->userid)) {
255 $userid = $USER->id;
256 } else {
257 $userid = $options->userid;
260 //create an array of item ids
261 $itemids = array();
262 foreach($options->items as $item) {
263 $itemids[] = $item->id;
266 //get the items from the database
267 list($itemidtest, $params) = $DB->get_in_or_equal(
268 $itemids, SQL_PARAMS_NAMED, 'itemid0000');
270 //note: all the group bys arent really necessary but PostgreSQL complains
271 //about selecting a mixture of grouped and non-grouped columns
272 $sql = "SELECT r.itemid, ur.id, ur.userid, ur.scaleid,
273 $aggregatestr(r.rating) AS aggrrating,
274 COUNT(r.rating) AS numratings,
275 ur.rating AS usersrating
276 FROM {rating} r
277 LEFT JOIN {rating} ur ON ur.contextid = r.contextid AND
278 ur.itemid = r.itemid AND
279 ur.userid = :userid
280 WHERE
281 r.contextid = :contextid AND
282 r.itemid $itemidtest
283 GROUP BY r.itemid, ur.rating, ur.id, ur.userid, ur.scaleid
284 ORDER BY r.itemid";
286 $params['userid'] = $userid;
287 $params['contextid'] = $options->context->id;
289 $ratingsrecords = $DB->get_records_sql($sql, $params);
291 //now create the rating sub objects
292 $scaleobj = new stdClass();
293 $scalemax = null;
295 //we could look for a scale id on each item to allow each item to use a different scale
296 if($options->scaleid < 0 ) { //if its a scale (not numeric)
297 $scalerecord = $DB->get_record('scale', array('id' => -$options->scaleid));
298 if ($scalerecord) {
299 $scalearray = explode(',', $scalerecord->scale);
301 //is there a more efficient way to get the indexes to start at 1 instead of 0?
302 //this will go away when scales are refactored
303 $c = count($scalearray);
304 $n = null;
305 for($i=0; $i<$c; $i++) {
306 $n = $i+1;
307 $scaleobj->scaleitems["$n"] = $scalearray[$i];//treat index as a string to allow sorting without changing the value
309 krsort($scaleobj->scaleitems);//have the highest grade scale item appear first
311 $scaleobj->id = $options->scaleid;//dont use the one from the record or we "forget" that its negative
312 $scaleobj->name = $scalerecord->name;
313 $scaleobj->courseid = $scalerecord->courseid;
315 $scalemax = count($scaleobj->scaleitems);
318 else { //its numeric
319 $scaleobj->scaleitems = $options->scaleid;
320 $scaleobj->id = $options->scaleid;
321 $scaleobj->name = null;
323 $scalemax = $options->scaleid;
326 //should $settings and $settings->permissions be declared as proper classes?
327 $settings = new stdclass(); //settings that are common to all ratings objects in this context
328 $settings->scale = $scaleobj; //the scale to use now
329 $settings->aggregationmethod = $options->aggregate;
330 if( !empty($options->returnurl) ) {
331 $settings->returnurl = $options->returnurl;
334 $settings->assesstimestart = $settings->assesstimefinish = null;
335 if( !empty($options->assesstimestart) ) {
336 $settings->assesstimestart = $options->assesstimestart;
338 if( !empty($options->assesstimefinish) ) {
339 $settings->assesstimefinish = $options->assesstimefinish;
342 //check site capabilities
343 $settings->permissions = new stdclass();
344 $settings->permissions->view = has_capability('moodle/rating:view',$options->context);//can view the aggregate of ratings of their own items
345 $settings->permissions->viewany = has_capability('moodle/rating:viewany',$options->context);//can view the aggregate of ratings of other people's items
346 $settings->permissions->viewall = has_capability('moodle/rating:viewall',$options->context);//can view individual ratings
347 $settings->permissions->rate = has_capability('moodle/rating:rate',$options->context);//can submit ratings
349 //check module capabilities (mostly for backwards compatability with old modules that previously implemented their own ratings)
350 $plugintype = !empty($options->plugintype) ? $options->plugintype : null;
351 $pluginname = !empty($options->pluginname) ? $options->pluginname : null;
352 $pluginpermissionsarray = $this->get_plugin_permissions_array($options->context->id, $plugintype, $pluginname);
354 $settings->pluginpermissions = new stdclass();
355 $settings->pluginpermissions->view = $pluginpermissionsarray['view'];
356 $settings->pluginpermissions->viewany = $pluginpermissionsarray['viewany'];
357 $settings->pluginpermissions->viewall = $pluginpermissionsarray['viewall'];
358 $settings->pluginpermissions->rate = $pluginpermissionsarray['rate'];
360 $rating = null;
361 $ratingoptions = new stdclass();
362 $ratingoptions->context = $options->context;//context is common to all ratings in the set
363 foreach($options->items as $item) {
364 $rating = null;
365 //match the item with its corresponding rating
366 foreach($ratingsrecords as $rec) {
367 if( $item->id==$rec->itemid ) {
368 //Note: rec->scaleid = the id of scale at the time the rating was submitted
369 //may be different from the current scale id
370 $ratingoptions->itemid = $item->id;
371 $ratingoptions->scaleid = $rec->scaleid;
372 $ratingoptions->userid = $rec->userid;
374 $rating = new rating($ratingoptions);
375 $rating->id = $rec->id; //unset($rec->id);
376 $rating->aggregate = $rec->aggrrating; //unset($rec->aggrrating);
377 $rating->count = $rec->numratings; //unset($rec->numratings);
378 $rating->rating = $rec->usersrating; //unset($rec->usersrating);
379 if( !empty($item->created) ) {
380 $rating->itemtimecreated = $item->created;//the forum_posts table has created instead of timecreated
382 else if(!empty($item->timecreated)) {
383 $rating->itemtimecreated = $item->timecreated;
385 else {
386 $rating->itemtimecreated = null;
388 break;
391 //if there are no ratings for this item
392 if( !$rating ) {
393 $ratingoptions->itemid = $item->id;
394 $ratingoptions->scaleid = null;
395 $ratingoptions->userid = null;
397 $rating = new rating($ratingoptions);
398 $rating->id = null;
399 $rating->aggregate = null;
400 $rating->count = 0;
401 $rating->rating = null;
403 $rating->itemid = $item->id;
404 $rating->userid = null;
405 $rating->scaleid = null;
406 $rating->itemtimecreated = null;
409 if( !empty($item->userid) ) {
410 $rating->itemuserid = $item->userid;
411 } else {
412 $rating->itemuserid = null;
414 $rating->settings = $settings;
415 $item->rating = $rating;
417 //Below is a nasty hack presumably here to handle scales being changed (out of 10 to out of 5 for example)
419 // it could throw off the grading if count and sum returned a grade higher than scale
420 // 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)
421 if ($rating->rating > $scalemax) {
422 $rating->rating = $scalemax;
424 if ($rating->aggregate > $scalemax) {
425 $rating->aggregate = $scalemax;
429 return $options->items;
433 * Returns an array of grades calculated by aggregating item ratings.
434 * @param object $options {
435 * userid => int the id of the user whose items have been rated. NOT the user who submitted the ratings [required]
436 * aggregationmethod => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required]
437 * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
438 * itemtable => int the table containing the items [required]
439 * itemtableusercolum => int the column of the user table containing the item owner's user id [required]
441 * contextid => int the context in which the rated items exist [optional]
443 * modulename => string the name of the module [optional]
444 * moduleid => int the id of the module instance [optional]
446 * @return array the array of the user's grades
448 public function get_user_grades($options) {
449 global $DB;
451 $contextid = null;
453 //if the calling code doesn't supply a context id we'll have to figure it out
454 if( !empty($options->contextid) ) {
455 $contextid = $options->contextid;
457 else if( !empty($options->cmid) ) {
458 //not implemented as not currently used although cmid is potentially available (the forum supplies it)
459 //Is there a convenient way to get a context id from a cm id?
460 //$cmidnumber = $options->cmidnumber;
462 else if ( !empty($options->modulename) && !empty($options->moduleid) ) {
463 $modulename = $options->modulename;
464 $moduleid = $options->moduleid;
466 //going direct to the db for the context id seems wrong
467 list($ctxselect, $ctxjoin) = context_instance_preload_sql('cm.id', CONTEXT_MODULE, 'ctx');
468 $sql = "SELECT cm.* $ctxselect
469 FROM {course_modules} cm
470 LEFT JOIN {modules} mo ON mo.id = cm.module
471 LEFT JOIN {{$modulename}} m ON m.id = cm.instance $ctxjoin
472 WHERE mo.name=:modulename AND m.id=:moduleid";
473 $contextrecord = $DB->get_record_sql($sql, array('modulename'=>$modulename, 'moduleid'=>$moduleid), '*', MUST_EXIST);
474 $contextid = $contextrecord->ctxid;
477 $params = array();
478 $params['contextid']= $contextid;
479 $itemtable = $options->itemtable;
480 $itemtableusercolumn= $options->itemtableusercolumn;
481 $scaleid = $options->scaleid;
482 $params['userid1'] = $params['userid2'] = $params['userid3'] = $options->userid;
484 $aggregationstring = $this->get_aggregation_method($options->aggregationmethod);
486 $sql = "SELECT :userid1 as id, :userid2 AS userid, $aggregationstring(r.rating) AS rawgrade
487 FROM {rating} r
488 WHERE r.contextid=:contextid
489 AND r.itemid IN (SELECT i.id AS itemid FROM {{$itemtable}} i WHERE i.{$itemtableusercolumn} = :userid3)";
491 $results = $DB->get_records_sql($sql, $params);
492 if ($results) {
493 // it could throw off the grading if count and sum returned a rawgrade higher than scale
494 // 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)
495 foreach ($results as $rid=>$result) {
496 if ($options->scaleid >= 0) {
497 //numeric
498 if ($result->rawgrade > $options->scaleid) {
499 $results[$rid]->rawgrade = $options->scaleid;
501 } else {
502 //scales
503 if ($scale = $DB->get_record('scale', array('id' => -$options->scaleid))) {
504 $scale = explode(',', $scale->scale);
505 $max = count($scale);
506 if ($result->rawgrade > $max) {
507 $results[$rid]->rawgrade = $max;
513 return $results;
517 * Returns array of aggregate types. Used by ratings.
519 * @return array
521 public function get_aggregate_types() {
522 return array (RATING_AGGREGATE_NONE => get_string('aggregatenone', 'forum'),
523 RATING_AGGREGATE_AVERAGE => get_string('aggregateavg', 'forum'),
524 RATING_AGGREGATE_COUNT => get_string('aggregatecount', 'forum'),
525 RATING_AGGREGATE_MAXIMUM => get_string('aggregatemax', 'forum'),
526 RATING_AGGREGATE_MINIMUM => get_string('aggregatemin', 'forum'),
527 RATING_AGGREGATE_SUM => get_string('aggregatesum', 'forum'));
531 * Converts an aggregation method constant into something that can be included in SQL
532 * @param int $aggregate An aggregation constant. For example, RATING_AGGREGATE_AVERAGE.
533 * @return string an SQL aggregation method
535 public function get_aggregation_method($aggregate) {
536 $aggregatestr = null;
537 switch($aggregate){
538 case RATING_AGGREGATE_AVERAGE:
539 $aggregatestr = 'AVG';
540 break;
541 case RATING_AGGREGATE_COUNT:
542 $aggregatestr = 'COUNT';
543 break;
544 case RATING_AGGREGATE_MAXIMUM:
545 $aggregatestr = 'MAX';
546 break;
547 case RATING_AGGREGATE_MINIMUM:
548 $aggregatestr = 'MIN';
549 break;
550 case RATING_AGGREGATE_SUM:
551 $aggregatestr = 'SUM';
552 break;
553 default:
554 $aggregatestr = 'AVG'; // Default to this to avoid real breakage - MDL-22270
555 debugging('Incorrect call to get_aggregation_method(), was called with incorrect aggregate method ' . $aggregate, DEBUG_DEVELOPER);
557 return $aggregatestr;
561 * Looks for a callback and retrieves permissions from the plugin whose items are being rated
562 * @param int $contextid The current context id
563 * @param string plugintype the type of plugin ie 'mod'
564 * @param string pluginname the name of the plugin ie 'forum'
565 * @return array rating related permissions
567 public function get_plugin_permissions_array($contextid, $plugintype=null, $pluginname=null) {
568 $pluginpermissionsarray = null;
569 $defaultpluginpermissions = array('rate'=>true,'view'=>true,'viewany'=>true,'viewall'=>true);//all true == rely on system level permissions if no plugin callback is defined
570 if ($plugintype && $pluginname) {
571 $pluginpermissionsarray = plugin_callback($plugintype, $pluginname, 'rating', 'permissions', array($contextid), $defaultpluginpermissions);
572 } else {
573 $pluginpermissionsarray = $defaultpluginpermissions;
575 return $pluginpermissionsarray;
579 * Checks if the item exists and is NOT owned by the current owner. Uses a callback to find out what table to look in.
580 * @param string plugintype the type of plugin ie 'mod'
581 * @param string pluginname the name of the plugin ie 'forum'
582 * @return boolean True if the callback doesn't exist. True if the item exists and doesn't belong to the current user. False otherwise.
584 public function check_item_and_owner($plugintype, $pluginname, $itemid) {
585 global $DB, $USER;
587 list($tablename,$itemidcol,$useridcol) = plugin_callback($plugintype, $pluginname, 'rating', 'item_check_info');
589 if (!empty($tablename)) {
590 $item = $DB->get_record($tablename, array($itemidcol=>$itemid), $useridcol);
591 if ($item) {
592 if ($item->userid!=$USER->id) {
593 return true;
597 return false;//item doesn't exist or belongs to the current user
598 } else {
599 return true;//callback doesn't exist
602 }//end rating_manager class definition