Automatically generated installer lang files
[moodle.git] / rating / lib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * A class representing a single rating and containing some static methods for manipulating ratings
19  *
20  * @package    core_rating
21  * @subpackage rating
22  * @copyright  2010 Andrew Davis
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
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
39  *
40  * @package   core_rating
41  * @category  rating
42  * @copyright 2010 Andrew Davis
43  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44  * @since     Moodle 2.0
45  */
46 class rating implements renderable {
48     /**
49      * @var stdClass The context in which this rating exists
50      */
51     public $context;
53     /**
54      * @var string The component using ratings. For example "mod_forum"
55      */
56     public $component;
58     /**
59      * @var string The rating area to associate this rating with
60      *             This allows a plugin to rate more than one thing by specifying different rating areas
61      */
62     public $ratingarea = null;
64     /**
65      * @var int The id of the item (forum post, glossary item etc) being rated
66      */
67     public $itemid;
69     /**
70      * @var int The id scale (1-5, 0-100) that was in use when the rating was submitted
71      */
72     public $scaleid;
74     /**
75      * @var int The id of the user who submitted the rating
76      */
77     public $userid;
79     /**
80      * @var stdclass settings for this rating. Necessary to render the rating.
81      */
82     public $settings;
84     /**
85      * @var int The Id of this rating within the rating table. This is only set if the rating already exists
86      */
87     public $id = null;
89     /**
90      * @var int The aggregate of the combined ratings for the associated item. This is only set if the rating already exists
91      */
92     public $aggregate = null;
94     /**
95      * @var int The total number of ratings for the associated item. This is only set if the rating already exists
96      */
97     public $count = 0;
99     /**
100      * @var int The rating the associated user gave the associated item. This is only set if the rating already exists
101      */
102     public $rating = null;
104     /**
105      * @var int The time the associated item was created
106      */
107     public $itemtimecreated = null;
109     /**
110      * @var int The id of the user who submitted the rating
111      */
112     public $itemuserid = null;
114     /**
115      * Constructor.
116      *
117      * @param stdClass $options {
118      *            context => context context to use for the rating [required]
119      *            component => component using ratings ie mod_forum [required]
120      *            ratingarea => ratingarea to associate this rating with [required]
121      *            itemid  => int the id of the associated item (forum post, glossary item etc) [required]
122      *            scaleid => int The scale in use when the rating was submitted [required]
123      *            userid  => int The id of the user who submitted the rating [required]
124      *            settings => Settings for the rating object [optional]
125      *            id => The id of this rating (if the rating is from the db) [optional]
126      *            aggregate => The aggregate for the rating [optional]
127      *            count => The number of ratings [optional]
128      *            rating => The rating given by the user [optional]
129      * }
130      */
131     public function __construct($options) {
132         $this->context =    $options->context;
133         $this->component =  $options->component;
134         $this->ratingarea = $options->ratingarea;
135         $this->itemid =     $options->itemid;
136         $this->scaleid =    $options->scaleid;
137         $this->userid =     $options->userid;
139         if (isset($options->settings)) {
140             $this->settings = $options->settings;
141         }
142         if (isset($options->id)) {
143             $this->id = $options->id;
144         }
145         if (isset($options->aggregate)) {
146             $this->aggregate = $options->aggregate;
147         }
148         if (isset($options->count)) {
149             $this->count = $options->count;
150         }
151         if (isset($options->rating)) {
152             $this->rating = $options->rating;
153         }
154     }
156     /**
157      * Update this rating in the database
158      *
159      * @param int $rating the integer value of this rating
160      */
161     public function update_rating($rating) {
162         global $DB;
164         $time = time();
166         $data = new stdClass;
167         $data->rating       = $rating;
168         $data->timemodified = $time;
170         $item = new stdclass();
171         $item->id = $this->itemid;
172         $items = array($item);
174         $ratingoptions = new stdClass;
175         $ratingoptions->context = $this->context;
176         $ratingoptions->component = $this->component;
177         $ratingoptions->ratingarea = $this->ratingarea;
178         $ratingoptions->items = $items;
179         $ratingoptions->aggregate = RATING_AGGREGATE_AVERAGE;//we dont actually care what aggregation method is applied
180         $ratingoptions->scaleid = $this->scaleid;
181         $ratingoptions->userid = $this->userid;
183         $rm = new rating_manager();;
184         $items = $rm->get_ratings($ratingoptions);
185         $firstitem = $items[0]->rating;
187         if (empty($firstitem->id)) {
188             // Insert a new rating
189             $data->contextid    = $this->context->id;
190             $data->component    = $this->component;
191             $data->ratingarea   = $this->ratingarea;
192             $data->rating       = $rating;
193             $data->scaleid      = $this->scaleid;
194             $data->userid       = $this->userid;
195             $data->itemid       = $this->itemid;
196             $data->timecreated  = $time;
197             $data->timemodified = $time;
198             $DB->insert_record('rating', $data);
199         } else {
200             // Update the rating
201             $data->id           = $firstitem->id;
202             $DB->update_record('rating', $data);
203         }
204     }
206     /**
207      * Retreive the integer value of this rating
208      *
209      * @return int the integer value of this rating object
210      */
211     public function get_rating() {
212         return $this->rating;
213     }
215     /**
216      * Returns this ratings aggregate value as a string.
217      *
218      * @return string ratings aggregate value
219      */
220     public function get_aggregate_string() {
222         $aggregate = $this->aggregate;
223         $method = $this->settings->aggregationmethod;
225         // only display aggregate if aggregation method isn't COUNT
226         $aggregatestr = '';
227         if ($aggregate && $method != RATING_AGGREGATE_COUNT) {
228             if ($method != RATING_AGGREGATE_SUM && !$this->settings->scale->isnumeric) {
229                 $aggregatestr .= $this->settings->scale->scaleitems[round($aggregate)]; //round aggregate as we're using it as an index
230             } else { // aggregation is SUM or the scale is numeric
231                 $aggregatestr .= round($aggregate, 1);
232             }
233         }
235         return $aggregatestr;
236     }
238     /**
239      * Returns true if the user is able to rate this rating object
240      *
241      * @param int $userid Current user assumed if left empty
242      * @return bool true if the user is able to rate this rating object
243      */
244     public function user_can_rate($userid = null) {
245         if (empty($userid)) {
246             global $USER;
247             $userid = $USER->id;
248         }
249         // You can't rate your item
250         if ($this->itemuserid == $userid) {
251             return false;
252         }
253         // You can't rate if you don't have the system cap
254         if (!$this->settings->permissions->rate) {
255             return false;
256         }
257         // You can't rate if you don't have the plugin cap
258         if (!$this->settings->pluginpermissions->rate) {
259             return false;
260         }
262         // You can't rate if the item was outside of the assessment times
263         $timestart = $this->settings->assesstimestart;
264         $timefinish = $this->settings->assesstimefinish;
265         $timecreated = $this->itemtimecreated;
266         if (!empty($timestart) && !empty($timefinish) && ($timecreated < $timestart || $timecreated > $timefinish)) {
267             return false;
268         }
269         return true;
270     }
272     /**
273      * Returns true if the user is able to view the aggregate for this rating object.
274      *
275      * @param int|null $userid If left empty the current user is assumed.
276      * @return bool true if the user is able to view the aggregate for this rating object
277      */
278     public function user_can_view_aggregate($userid = null) {
279         if (empty($userid)) {
280             global $USER;
281             $userid = $USER->id;
282         }
284         // 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
285         // Note that viewany doesnt mean you can see the aggregate or ratings of your own items
286         if ((empty($this->itemuserid) or $this->itemuserid != $userid) && $this->settings->permissions->viewany && $this->settings->pluginpermissions->viewany ) {
287             return true;
288         }
290         // if its the current user's item and they have permission to view the aggregate on their own items
291         if ($this->itemuserid == $userid && $this->settings->permissions->view && $this->settings->pluginpermissions->view) {
292             return true;
293         }
295         return false;
296     }
298     /**
299      * Returns a URL to view all of the ratings for the item this rating is for.
300      *
301      * If this is a rating of a post then this URL will take the user to a page that shows all of the ratings for the post
302      * (this one included).
303      *
304      * @param bool $popup whether of not the URL should be loaded in a popup
305      * @return moodle_url URL to view all of the ratings for the item this rating is for.
306      */
307     public function get_view_ratings_url($popup = false) {
308         $attributes = array(
309             'contextid'  => $this->context->id,
310             'component'  => $this->component,
311             'ratingarea' => $this->ratingarea,
312             'itemid'     => $this->itemid,
313             'scaleid'    => $this->settings->scale->id
314         );
315         if ($popup) {
316             $attributes['popup'] = 1;
317         }
318         return new moodle_url('/rating/index.php', $attributes);
319     }
321     /**
322      * Returns a URL that can be used to rate the associated item.
323      *
324      * @param int|null          $rating    The rating to give the item, if null then no rating param is added.
325      * @param moodle_url|string $returnurl The URL to return to.
326      * @return moodle_url can be used to rate the associated item.
327      */
328     public function get_rate_url($rating = null, $returnurl = null) {
329         if (empty($returnurl)) {
330             if (!empty($this->settings->returnurl)) {
331                 $returnurl = $this->settings->returnurl;
332             } else {
333                 global $PAGE;
334                 $returnurl = $PAGE->url;
335             }
336         }
337         $args = array(
338             'contextid'   => $this->context->id,
339             'component'   => $this->component,
340             'ratingarea'  => $this->ratingarea,
341             'itemid'      => $this->itemid,
342             'scaleid'     => $this->settings->scale->id,
343             'returnurl'   => $returnurl,
344             'rateduserid' => $this->itemuserid,
345             'aggregation' => $this->settings->aggregationmethod,
346             'sesskey'     => sesskey()
347         );
348         if (!empty($rating)) {
349             $args['rating'] = $rating;
350         }
351         $url = new moodle_url('/rating/rate.php', $args);
352         return $url;
353     }
355     /**
356     * Remove this rating from the database
357     * @return void
358     */
359     //public function delete_rating() {
360         //todo implement this if its actually needed
361     //}
362 } //end rating class definition
364 /**
365  * The rating_manager class provides the ability to retrieve sets of ratings from the database
366  *
367  * @package   core_rating
368  * @category  rating
369  * @copyright 2010 Andrew Davis
370  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
371  * @since     Moodle 2.0
372  */
373 class rating_manager {
375     /**
376      * @var array An array of calculated scale options to save us generating them for each request.
377      */
378     protected $scales = array();
380     /**
381      * Delete one or more ratings. Specify either a rating id, an item id or just the context id.
382      *
383      * @global moodle_database $DB
384      * @param stdClass $options {
385      *            contextid => int the context in which the ratings exist [required]
386      *            ratingid => int the id of an individual rating to delete [optional]
387      *            userid => int delete the ratings submitted by this user. May be used in conjuction with itemid [optional]
388      *            itemid => int delete all ratings attached to this item [optional]
389      *            component => string The component to delete ratings from [optional]
390      *            ratingarea => string The ratingarea to delete ratings from [optional]
391      * }
392      */
393     public function delete_ratings($options) {
394         global $DB;
396         if (empty($options->contextid)) {
397             throw new coding_exception('The context option is a required option when deleting ratings.');
398         }
400         $conditions = array('contextid' => $options->contextid);
401         $possibleconditions = array(
402             'ratingid'   => 'id',
403             'userid'     => 'userid',
404             'itemid'     => 'itemid',
405             'component'  => 'component',
406             'ratingarea' => 'ratingarea'
407         );
408         foreach ($possibleconditions as $option => $field) {
409             if (isset($options->{$option})) {
410                 $conditions[$field] = $options->{$option};
411             }
412         }
413         $DB->delete_records('rating', $conditions);
414     }
416     /**
417      * Returns an array of ratings for a given item (forum post, glossary entry etc). This returns all users ratings for a single item
418      *
419      * @param stdClass $options {
420      *            context => context the context in which the ratings exists [required]
421      *            component => component using ratings ie mod_forum [required]
422      *            ratingarea => ratingarea to associate this rating with [required]
423      *            itemid  =>  int the id of the associated item (forum post, glossary item etc) [required]
424      *            sort    => string SQL sort by clause [optional]
425      * }
426      * @return array an array of ratings
427      */
428     public function get_all_ratings_for_item($options) {
429         global $DB;
431         if (!isset($options->context)) {
432             throw new coding_exception('The context option is a required option when getting ratings for an item.');
433         }
434         if (!isset($options->itemid)) {
435             throw new coding_exception('The itemid option is a required option when getting ratings for an item.');
436         }
437         if (!isset($options->component)) {
438             throw new coding_exception('The component option is now a required option when getting ratings for an item.');
439         }
440         if (!isset($options->ratingarea)) {
441             throw new coding_exception('The ratingarea option is now a required option when getting ratings for an item.');
442         }
444         $sortclause = '';
445         if( !empty($options->sort) ) {
446             $sortclause = "ORDER BY $options->sort";
447         }
449         $params = array(
450             'contextid'  => $options->context->id,
451             'itemid'     => $options->itemid,
452             'component'  => $options->component,
453             'ratingarea' => $options->ratingarea,
454         );
455         $userfields = user_picture::fields('u', null, 'userid');
456         $sql = "SELECT r.id, r.rating, r.itemid, r.userid, r.timemodified, r.component, r.ratingarea, $userfields
457                   FROM {rating} r
458              LEFT JOIN {user} u ON r.userid = u.id
459                  WHERE r.contextid = :contextid AND
460                        r.itemid  = :itemid AND
461                        r.component = :component AND
462                        r.ratingarea = :ratingarea
463                        {$sortclause}";
465         return $DB->get_records_sql($sql, $params);
466     }
468     /**
469      * Adds rating objects to an array of items (forum posts, glossary entries etc). Rating objects are available at $item->rating
470      *
471      * @param stdClass $options {
472      *            context          => context the context in which the ratings exists [required]
473      *            component        => the component name ie mod_forum [required]
474      *            ratingarea       => the ratingarea we are interested in [required]
475      *            items            => array an array of items such as forum posts or glossary items. They must have an 'id' member ie $items[0]->id[required]
476      *            aggregate        => int what aggregation method should be applied. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required]
477      *            scaleid          => int the scale from which the user can select a rating [required]
478      *            userid           => int the id of the current user [optional]
479      *            returnurl        => string the url to return the user to after submitting a rating. Can be left null for ajax requests [optional]
480      *            assesstimestart  => int only allow rating of items created after this timestamp [optional]
481      *            assesstimefinish => int only allow rating of items created before this timestamp [optional]
482      * @return array the array of items with their ratings attached at $items[0]->rating
483      */
484     public function get_ratings($options) {
485         global $DB, $USER;
487         if (!isset($options->context)) {
488             throw new coding_exception('The context option is a required option when getting ratings.');
489         }
491         if (!isset($options->component)) {
492             throw new coding_exception('The component option is a required option when getting ratings.');
493         }
495         if (!isset($options->ratingarea)) {
496             throw new coding_exception('The ratingarea option is a required option when getting ratings.');
497         }
499         if (!isset($options->scaleid)) {
500             throw new coding_exception('The scaleid option is a required option when getting ratings.');
501         }
503         if (!isset($options->items)) {
504             throw new coding_exception('The items option is a required option when getting ratings.');
505         } else if (empty($options->items)) {
506             return array();
507         }
509         if (!isset($options->aggregate)) {
510             throw new coding_exception('The aggregate option is a required option when getting ratings.');
511         } else if ($options->aggregate == RATING_AGGREGATE_NONE) {
512             // Ratings arn't enabled.
513             return $options->items;
514         }
515         $aggregatestr = $this->get_aggregation_method($options->aggregate);
517         // Default the userid to the current user if it is not set
518         if (empty($options->userid)) {
519             $userid = $USER->id;
520         } else {
521             $userid = $options->userid;
522         }
524         // Get the item table name, the item id field, and the item user field for the given rating item
525         // from the related component.
526         list($type, $name) = normalize_component($options->component);
527         $default = array(null, 'id', 'userid');
528         list($itemtablename, $itemidcol, $itemuseridcol) = plugin_callback($type, $name, 'rating', 'get_item_fields', array($options), $default);
530         // Create an array of item ids
531         $itemids = array();
532         foreach ($options->items as $item) {
533             $itemids[] = $item->{$itemidcol};
534         }
536         // get the items from the database
537         list($itemidtest, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
538         $params['contextid'] = $options->context->id;
539         $params['userid']    = $userid;
540         $params['component']    = $options->component;
541         $params['ratingarea'] = $options->ratingarea;
543         $sql = "SELECT r.id, r.itemid, r.userid, r.scaleid, r.rating AS usersrating
544                   FROM {rating} r
545                  WHERE r.userid = :userid AND
546                        r.contextid = :contextid AND
547                        r.itemid {$itemidtest} AND
548                        r.component = :component AND
549                        r.ratingarea = :ratingarea
550               ORDER BY r.itemid";
551         $userratings = $DB->get_records_sql($sql, $params);
553         $sql = "SELECT r.itemid, $aggregatestr(r.rating) AS aggrrating, COUNT(r.rating) AS numratings
554                   FROM {rating} r
555                  WHERE r.contextid = :contextid AND
556                        r.itemid {$itemidtest} AND
557                        r.component = :component AND
558                        r.ratingarea = :ratingarea
559               GROUP BY r.itemid, r.component, r.ratingarea, r.contextid
560               ORDER BY r.itemid";
561         $aggregateratings = $DB->get_records_sql($sql, $params);
563         $ratingoptions = new stdClass;
564         $ratingoptions->context = $options->context;
565         $ratingoptions->component = $options->component;
566         $ratingoptions->ratingarea = $options->ratingarea;
567         $ratingoptions->settings = $this->generate_rating_settings_object($options);
568         foreach ($options->items as $item) {
569             $founduserrating = false;
570             foreach($userratings as $userrating) {
571                 //look for an existing rating from this user of this item
572                 if ($item->{$itemidcol} == $userrating->itemid) {
573                     // Note: rec->scaleid = the id of scale at the time the rating was submitted
574                     // may be different from the current scale id
575                     $ratingoptions->scaleid = $userrating->scaleid;
576                     $ratingoptions->userid = $userrating->userid;
577                     $ratingoptions->id = $userrating->id;
578                     $ratingoptions->rating = min($userrating->usersrating, $ratingoptions->settings->scale->max);
580                     $founduserrating = true;
581                     break;
582                 }
583             }
584             if (!$founduserrating) {
585                 $ratingoptions->scaleid = null;
586                 $ratingoptions->userid = null;
587                 $ratingoptions->id = null;
588                 $ratingoptions->rating =  null;
589             }
591             if (array_key_exists($item->{$itemidcol}, $aggregateratings)) {
592                 $rec = $aggregateratings[$item->{$itemidcol}];
593                 $ratingoptions->itemid = $item->{$itemidcol};
594                 $ratingoptions->aggregate = min($rec->aggrrating, $ratingoptions->settings->scale->max);
595                 $ratingoptions->count = $rec->numratings;
596             } else {
597                 $ratingoptions->itemid = $item->{$itemidcol};
598                 $ratingoptions->aggregate = null;
599                 $ratingoptions->count = 0;
600             }
602             $rating = new rating($ratingoptions);
603             $rating->itemtimecreated = $this->get_item_time_created($item);
604             if (!empty($item->{$itemuseridcol})) {
605                 $rating->itemuserid = $item->{$itemuseridcol};
606             }
607             $item->rating = $rating;
608         }
610         return $options->items;
611     }
613     /**
614      * Generates a rating settings object based upon the options it is provided.
615      *
616      * @param stdClass $options {
617      *      context           => context the context in which the ratings exists [required]
618      *      component         => string The component the items belong to [required]
619      *      ratingarea        => string The ratingarea the items belong to [required]
620      *      aggregate         => int what aggregation method should be applied. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required]
621      *      scaleid           => int the scale from which the user can select a rating [required]
622      *      returnurl         => string the url to return the user to after submitting a rating. Can be left null for ajax requests [optional]
623      *      assesstimestart   => int only allow rating of items created after this timestamp [optional]
624      *      assesstimefinish  => int only allow rating of items created before this timestamp [optional]
625      *      plugintype        => string plugin type ie 'mod' Used to find the permissions callback [optional]
626      *      pluginname        => string plugin name ie 'forum' Used to find the permissions callback [optional]
627      * }
628      * @return stdClass rating settings object
629      */
630     protected function generate_rating_settings_object($options) {
632         if (!isset($options->context)) {
633             throw new coding_exception('The context option is a required option when generating a rating settings object.');
634         }
635         if (!isset($options->component)) {
636             throw new coding_exception('The component option is now a required option when generating a rating settings object.');
637         }
638         if (!isset($options->ratingarea)) {
639             throw new coding_exception('The ratingarea option is now a required option when generating a rating settings object.');
640         }
641         if (!isset($options->aggregate)) {
642             throw new coding_exception('The aggregate option is now a required option when generating a rating settings object.');
643         }
644         if (!isset($options->scaleid)) {
645             throw new coding_exception('The scaleid option is now a required option when generating a rating settings object.');
646         }
648         // settings that are common to all ratings objects in this context
649         $settings = new stdClass;
650         $settings->scale             = $this->generate_rating_scale_object($options->scaleid); // the scale to use now
651         $settings->aggregationmethod = $options->aggregate;
652         $settings->assesstimestart   = null;
653         $settings->assesstimefinish  = null;
655         // Collect options into the settings object
656         if (!empty($options->assesstimestart)) {
657             $settings->assesstimestart = $options->assesstimestart;
658         }
659         if (!empty($options->assesstimefinish)) {
660             $settings->assesstimefinish = $options->assesstimefinish;
661         }
662         if (!empty($options->returnurl)) {
663             $settings->returnurl = $options->returnurl;
664         }
666         // check site capabilities
667         $settings->permissions = new stdClass;
668         $settings->permissions->view    = has_capability('moodle/rating:view', $options->context); // can view the aggregate of ratings of their own items
669         $settings->permissions->viewany = has_capability('moodle/rating:viewany', $options->context); // can view the aggregate of ratings of other people's items
670         $settings->permissions->viewall = has_capability('moodle/rating:viewall', $options->context); // can view individual ratings
671         $settings->permissions->rate    = has_capability('moodle/rating:rate', $options->context); // can submit ratings
673         // check module capabilities (mostly for backwards compatability with old modules that previously implemented their own ratings)
674         $pluginpermissionsarray = $this->get_plugin_permissions_array($options->context->id, $options->component, $options->ratingarea);
675         $settings->pluginpermissions = new stdClass;
676         $settings->pluginpermissions->view    = $pluginpermissionsarray['view'];
677         $settings->pluginpermissions->viewany = $pluginpermissionsarray['viewany'];
678         $settings->pluginpermissions->viewall = $pluginpermissionsarray['viewall'];
679         $settings->pluginpermissions->rate    = $pluginpermissionsarray['rate'];
681         return $settings;
682     }
684     /**
685      * Generates a scale object that can be returned
686      *
687      * @global moodle_database $DB moodle database object
688      * @param int $scaleid scale-type identifier
689      * @return stdClass scale for ratings
690      */
691     protected function generate_rating_scale_object($scaleid) {
692         global $DB;
693         if (!array_key_exists('s'.$scaleid, $this->scales)) {
694             $scale = new stdClass;
695             $scale->id = $scaleid;
696             $scale->name = null;
697             $scale->courseid = null;
698             $scale->scaleitems = array();
699             $scale->isnumeric = true;
700             $scale->max = $scaleid;
702             if ($scaleid < 0) {
703                 // It is a proper scale (not numeric)
704                 $scalerecord = $DB->get_record('scale', array('id' => abs($scaleid)));
705                 if ($scalerecord) {
706                     // We need to generate an array with string keys starting at 1
707                     $scalearray = explode(',', $scalerecord->scale);
708                     $c = count($scalearray);
709                     for ($i = 0; $i < $c; $i++) {
710                         // treat index as a string to allow sorting without changing the value
711                         $scale->scaleitems[(string)($i + 1)] = $scalearray[$i];
712                     }
713                     krsort($scale->scaleitems); // have the highest grade scale item appear first
714                     $scale->isnumeric = false;
715                     $scale->name = $scalerecord->name;
716                     $scale->courseid = $scalerecord->courseid;
717                     $scale->max = count($scale->scaleitems);
718                 }
719             } else {
720                 //generate an array of values for numeric scales
721                 for($i = 0; $i <= (int)$scaleid; $i++) {
722                     $scale->scaleitems[(string)$i] = $i;
723                 }
724             }
725             $this->scales['s'.$scaleid] = $scale;
726         }
727         return $this->scales['s'.$scaleid];
728     }
730     /**
731      * Gets the time the given item was created
732      *
733      * TODO: MDL-31511 - Find a better solution for this, its not ideal to test for fields really we should be
734      * asking the component the item belongs to what field to look for or even the value we
735      * are looking for.
736      *
737      * @param stdClass $item
738      * @return int|null return null if the created time is unavailable, otherwise return a timestamp
739      */
740     protected function get_item_time_created($item) {
741         if( !empty($item->created) ) {
742             return $item->created;//the forum_posts table has created instead of timecreated
743         }
744         else if(!empty($item->timecreated)) {
745             return $item->timecreated;
746         }
747         else {
748             return null;
749         }
750     }
752     /**
753      * Returns an array of grades calculated by aggregating item ratings.
754      *
755      * @param stdClass $options {
756      *            userid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required]
757      *            aggregationmethod => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required]
758      *            scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
759      *            itemtable => int the table containing the items [required]
760      *            itemtableusercolum => int the column of the user table containing the item owner's user id [required]
761      *            component => The component for the ratings [required]
762      *            ratingarea => The ratingarea for the ratings [required]
763      *            contextid => int the context in which the rated items exist [optional]
764      *            modulename => string the name of the module [optional]
765      *            moduleid => int the id of the module instance [optional]
766      * }
767      * @return array the array of the user's grades
768      */
769     public function get_user_grades($options) {
770         global $DB;
772         $contextid = null;
774         if (!isset($options->component)) {
775             throw new coding_exception('The component option is now a required option when getting user grades from ratings.');
776         }
777         if (!isset($options->ratingarea)) {
778             throw new coding_exception('The ratingarea option is now a required option when getting user grades from ratings.');
779         }
781         //if the calling code doesn't supply a context id we'll have to figure it out
782         if( !empty($options->contextid) ) {
783             $contextid = $options->contextid;
784         }
785         else if( !empty($options->cmid) ) {
786             //not implemented as not currently used although cmid is potentially available (the forum supplies it)
787             //Is there a convenient way to get a context id from a cm id?
788             //$cmidnumber = $options->cmidnumber;
789         }
790         else if ( !empty($options->modulename) && !empty($options->moduleid) ) {
791             $modulename = $options->modulename;
792             $moduleid   = intval($options->moduleid);
794             //going direct to the db for the context id seems wrong
795             list($ctxselect, $ctxjoin) = context_instance_preload_sql('cm.id', CONTEXT_MODULE, 'ctx');
796             $sql = "SELECT cm.* $ctxselect
797                       FROM {course_modules} cm
798                  LEFT JOIN {modules} mo ON mo.id = cm.module
799                  LEFT JOIN {{$modulename}} m ON m.id = cm.instance $ctxjoin
800                      WHERE mo.name=:modulename AND
801                            m.id=:moduleid";
802             $contextrecord = $DB->get_record_sql($sql, array('modulename'=>$modulename, 'moduleid'=>$moduleid), '*', MUST_EXIST);
803             $contextid = $contextrecord->ctxid;
804         }
806         $params = array();
807         $params['contextid']  = $contextid;
808         $params['component']  = $options->component;
809         $params['ratingarea'] = $options->ratingarea;
810         $itemtable            = $options->itemtable;
811         $itemtableusercolumn  = $options->itemtableusercolumn;
812         $scaleid              = $options->scaleid;
813         $aggregationstring    = $this->get_aggregation_method($options->aggregationmethod);
815         //if userid is not 0 we only want the grade for a single user
816         $singleuserwhere = '';
817         if ($options->userid != 0) {
818             $params['userid1'] = intval($options->userid);
819             $singleuserwhere = "AND i.{$itemtableusercolumn} = :userid1";
820         }
822         //MDL-24648 The where line used to be "WHERE (r.contextid is null or r.contextid=:contextid)"
823         //r.contextid will be null for users who haven't been rated yet
824         //no longer including users who haven't been rated to reduce memory requirements
825         $sql = "SELECT u.id as id, u.id AS userid, $aggregationstring(r.rating) AS rawgrade
826                   FROM {user} u
827              LEFT JOIN {{$itemtable}} i ON u.id=i.{$itemtableusercolumn}
828              LEFT JOIN {rating} r ON r.itemid=i.id
829                  WHERE r.contextid = :contextid AND
830                        r.component = :component AND
831                        r.ratingarea = :ratingarea
832                        $singleuserwhere
833               GROUP BY u.id";
834         $results = $DB->get_records_sql($sql, $params);
836         if ($results) {
838             $scale = null;
839             $max = 0;
840             if ($options->scaleid >= 0) {
841                 //numeric
842                 $max = $options->scaleid;
843             } else {
844                 //custom scales
845                 $scale = $DB->get_record('scale', array('id' => -$options->scaleid));
846                 if ($scale) {
847                     $scale = explode(',', $scale->scale);
848                     $max = count($scale);
849                 } else {
850                     debugging('rating_manager::get_user_grades() received a scale ID that doesnt exist');
851                 }
852             }
854             // it could throw off the grading if count and sum returned a rawgrade higher than scale
855             // 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)
856             foreach ($results as $rid=>$result) {
857                 if ($options->scaleid >= 0) {
858                     //numeric
859                     if ($result->rawgrade > $options->scaleid) {
860                         $results[$rid]->rawgrade = $options->scaleid;
861                     }
862                 } else {
863                     //scales
864                     if (!empty($scale) && $result->rawgrade > $max) {
865                         $results[$rid]->rawgrade = $max;
866                     }
867                 }
868             }
869         }
871         return $results;
872     }
874     /**
875      * Returns array of aggregate types. Used by ratings.
876      *
877      * @return array aggregate types
878      */
879     public function get_aggregate_types() {
880         return array (RATING_AGGREGATE_NONE     => get_string('aggregatenone', 'rating'),
881                       RATING_AGGREGATE_AVERAGE  => get_string('aggregateavg', 'rating'),
882                       RATING_AGGREGATE_COUNT    => get_string('aggregatecount', 'rating'),
883                       RATING_AGGREGATE_MAXIMUM  => get_string('aggregatemax', 'rating'),
884                       RATING_AGGREGATE_MINIMUM  => get_string('aggregatemin', 'rating'),
885                       RATING_AGGREGATE_SUM      => get_string('aggregatesum', 'rating'));
886     }
888     /**
889      * Converts an aggregation method constant into something that can be included in SQL
890      *
891      * @param int $aggregate An aggregation constant. For example, RATING_AGGREGATE_AVERAGE.
892      * @return string an SQL aggregation method
893      */
894     public function get_aggregation_method($aggregate) {
895         $aggregatestr = null;
896         switch($aggregate){
897             case RATING_AGGREGATE_AVERAGE:
898                 $aggregatestr = 'AVG';
899                 break;
900             case RATING_AGGREGATE_COUNT:
901                 $aggregatestr = 'COUNT';
902                 break;
903             case RATING_AGGREGATE_MAXIMUM:
904                 $aggregatestr = 'MAX';
905                 break;
906             case RATING_AGGREGATE_MINIMUM:
907                 $aggregatestr = 'MIN';
908                 break;
909             case RATING_AGGREGATE_SUM:
910                 $aggregatestr = 'SUM';
911                 break;
912             default:
913                 $aggregatestr = 'AVG'; // Default to this to avoid real breakage - MDL-22270
914                 debugging('Incorrect call to get_aggregation_method(), was called with incorrect aggregate method ' . $aggregate, DEBUG_DEVELOPER);
915         }
916         return $aggregatestr;
917     }
919     /**
920      * Looks for a callback like forum_rating_permissions() to retrieve permissions from the plugin whose items are being rated
921      *
922      * @param int $contextid The current context id
923      * @param string $component the name of the component that is using ratings ie 'mod_forum'
924      * @param string $ratingarea The area the rating is associated with
925      * @return array rating related permissions
926      */
927     public function get_plugin_permissions_array($contextid, $component, $ratingarea) {
928         $pluginpermissionsarray = null;
929         $defaultpluginpermissions = array('rate'=>false,'view'=>false,'viewany'=>false,'viewall'=>false);//deny by default
930         if (!empty($component)) {
931             list($type, $name) = normalize_component($component);
932             $pluginpermissionsarray = plugin_callback($type, $name, 'rating', 'permissions', array($contextid, $component, $ratingarea), $defaultpluginpermissions);
933         } else {
934             $pluginpermissionsarray = $defaultpluginpermissions;
935         }
936         return $pluginpermissionsarray;
937     }
939     /**
940      * Validates a submitted rating
941      *
942      * @param array $params submitted data
943      *            context => object the context in which the rated items exists [required]
944      *            component => The component the rating belongs to [required]
945      *            ratingarea => The ratingarea the rating is associated with [required]
946      *            itemid => int the ID of the object being rated [required]
947      *            scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
948      *            rating => int the submitted rating
949      *            rateduserid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required]
950      *            aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [optional]
951      * @return boolean true if the rating is valid. False if callback wasnt found and will throw rating_exception if rating is invalid
952      */
953     public function check_rating_is_valid($params) {
955         if (!isset($params['context'])) {
956             throw new coding_exception('The context option is a required option when checking rating validity.');
957         }
958         if (!isset($params['component'])) {
959             throw new coding_exception('The component option is now a required option when checking rating validity');
960         }
961         if (!isset($params['ratingarea'])) {
962             throw new coding_exception('The ratingarea option is now a required option when checking rating validity');
963         }
964         if (!isset($params['itemid'])) {
965             throw new coding_exception('The itemid option is now a required option when checking rating validity');
966         }
967         if (!isset($params['scaleid'])) {
968             throw new coding_exception('The scaleid option is now a required option when checking rating validity');
969         }
970         if (!isset($params['rateduserid'])) {
971             throw new coding_exception('The rateduserid option is now a required option when checking rating validity');
972         }
974         list($plugintype, $pluginname) = normalize_component($params['component']);
976         //this looks for a function like forum_rating_validate() in mod_forum lib.php
977         //wrapping the params array in another array as call_user_func_array() expands arrays into multiple arguments
978         $isvalid = plugin_callback($plugintype, $pluginname, 'rating', 'validate', array($params), null);
980         //if null then the callback doesn't exist
981         if ($isvalid === null) {
982             $isvalid = false;
983             debugging('rating validation callback not found for component '.  clean_param($component, PARAM_ALPHANUMEXT));
984         }
985         return $isvalid;
986     }
988     /**
989      * Initialises JavaScript to enable AJAX ratings on the provided page
990      *
991      * @param moodle_page $page
992      * @return true always returns true
993      */
994     public function initialise_rating_javascript(moodle_page $page) {
995         global $CFG;
997         //only needs to be initialized once
998         static $done = false;
999         if ($done) {
1000             return true;
1001         }
1003         if (!empty($CFG->enableajax)) {
1004             $page->requires->js_init_call('M.core_rating.init');
1005         }
1006         $done = true;
1008         return true;
1009     }
1011     /**
1012      * Returns a string that describes the aggregation method that was provided.
1013      *
1014      * @param string $aggregationmethod
1015      * @return string describes the aggregation method that was provided
1016      */
1017     public function get_aggregate_label($aggregationmethod) {
1018         $aggregatelabel = '';
1019         switch ($aggregationmethod) {
1020             case RATING_AGGREGATE_AVERAGE :
1021                 $aggregatelabel .= get_string("aggregateavg", "rating");
1022                 break;
1023             case RATING_AGGREGATE_COUNT :
1024                 $aggregatelabel .= get_string("aggregatecount", "rating");
1025                 break;
1026             case RATING_AGGREGATE_MAXIMUM :
1027                 $aggregatelabel .= get_string("aggregatemax", "rating");
1028                 break;
1029             case RATING_AGGREGATE_MINIMUM :
1030                 $aggregatelabel .= get_string("aggregatemin", "rating");
1031                 break;
1032             case RATING_AGGREGATE_SUM :
1033                 $aggregatelabel .= get_string("aggregatesum", "rating");
1034                 break;
1035         }
1036         $aggregatelabel .= get_string('labelsep', 'langconfig');
1037         return $aggregatelabel;
1038     }
1040 }//end rating_manager class definition
1042 /**
1043  * The rating_exception class provides the ability to generate exceptions that can be easily identified as coming from the ratings system
1044  *
1045  * @package   core_rating
1046  * @category  rating
1047  * @copyright 2010 Andrew Davis
1048  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1049  * @since     Moodle 2.0
1050  */
1051 class rating_exception extends moodle_exception {
1052     /**
1053      * @var string The message to accompany the thrown exception
1054      */
1055     public $message;
1056     /**
1057      * Generate exceptions that can be easily identified as coming from the ratings system
1058      *
1059      * @param string $errorcode the error code to generate
1060      */
1061     function __construct($errorcode) {
1062         $this->errorcode = $errorcode;
1063         $this->message = get_string($errorcode, 'error');
1064     }