3 // This file is part of Moodle - http://moodle.org/
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.
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/>.
19 * A class representing a single rating and containing some static methods for manipulating ratings
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);
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
45 class rating
implements renderable
{
48 * The context in which this rating exists
54 * The id of the item (forum post, glossary item etc) being rated
60 * The id scale (1-5, 0-100) that was in use when the rating was submitted
66 * The id of the user who submitted the rating
72 * settings for this rating. Necessary to render the rating.
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]
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
;
94 * Update this rating in the database
95 * @param int $rating the integer value of this rating
98 public function update_rating($rating) {
101 $data = new stdclass();
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
;
125 $data->timecreated
= $time;
126 $data->timemodified
= $time;
128 $DB->insert_record($table, $data);
131 $data->id
= $items[0]->rating
->id
;
132 $data->rating
= $rating;
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
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
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]
177 public function delete_ratings($options) {
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
) );
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) {
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
223 LEFT JOIN {user} u ON r.userid = u.id
224 WHERE r.contextid = :contextid AND
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
;
264 if (empty($options->userid
)) {
267 $userid = $options->userid
;
270 //create an array of item ids
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
287 LEFT JOIN {rating} ur ON ur.contextid = r.contextid AND
288 ur.itemid = r.itemid AND
291 r.contextid = :contextid AND
293 GROUP BY r.itemid, ur.rating, ur.id, ur.userid, ur.scaleid
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();
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
));
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);
315 for($i=0; $i<$c; $i++
) {
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
);
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'];
371 $ratingoptions = new stdclass();
372 $ratingoptions->context
= $options->context
;//context is common to all ratings in the set
373 foreach($options->items
as $item) {
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);
394 //if there are no ratings for this item
396 $ratingoptions->itemid
= $item->id
;
397 $ratingoptions->scaleid
= null;
398 $ratingoptions->userid
= null;
400 $rating = new rating($ratingoptions);
402 $rating->aggregate
= null;
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
;
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
;
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) {
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
;
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
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
517 $results = $DB->get_records_sql($sql, $params);
523 if ($options->scaleid
>= 0) {
525 $max = $options->scaleid
;
528 $scale = $DB->get_record('scale', array('id' => -$options->scaleid
));
530 $scale = explode(',', $scale->scale
);
531 $max = count($scale);
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) {
542 if ($result->rawgrade
> $options->scaleid
) {
543 $results[$rid]->rawgrade
= $options->scaleid
;
547 if (!empty($scale) && $result->rawgrade
> $max) {
548 $results[$rid]->rawgrade
= $max;
558 * Returns array of aggregate types. Used by ratings.
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;
579 case RATING_AGGREGATE_AVERAGE
:
580 $aggregatestr = 'AVG';
582 case RATING_AGGREGATE_COUNT
:
583 $aggregatestr = 'COUNT';
585 case RATING_AGGREGATE_MAXIMUM
:
586 $aggregatestr = 'MAX';
588 case RATING_AGGREGATE_MINIMUM
:
589 $aggregatestr = 'MIN';
591 case RATING_AGGREGATE_SUM
:
592 $aggregatestr = 'SUM';
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);
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) {
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);
633 if ($item->userid
!=$USER->id
) {
638 return false;//item doesn't exist or belongs to the current user
640 return true;//callback doesn't exist
643 }//end rating_manager class definition