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 component using ratings. For example "mod_forum"
60 * The rating area to associate this rating with.
61 * This allows a plugin to rate more than one thing by specifying different rating areas.
64 public $ratingarea = null;
67 * The id of the item (forum post, glossary item etc) being rated
73 * The id scale (1-5, 0-100) that was in use when the rating was submitted
79 * The id of the user who submitted the rating
85 * settings for this rating. Necessary to render the rating.
91 * The Id of this rating within the rating table.
92 * This is only set if the rating already exists
98 * The aggregate of the combined ratings for the associated item.
99 * This is only set if the rating already exists
103 public $aggregate = null;
106 * The total number of ratings for the associated item.
107 * This is only set if the rating already exists
114 * The rating the associated user gave the associated item
115 * This is only set if the rating already exists
119 public $rating = null;
122 * The time the associated item was created
126 public $itemtimecreated = null;
129 * The id of the user who submitted the rating
133 public $itemuserid = null;
137 * @param object $options {
138 * context => context context to use for the rating [required]
139 * component => component using ratings ie mod_forum [required]
140 * ratingarea => ratingarea to associate this rating with [required]
141 * itemid => int the id of the associated item (forum post, glossary item etc) [required]
142 * scaleid => int The scale in use when the rating was submitted [required]
143 * userid => int The id of the user who submitted the rating [required]
144 * settings => Settings for the rating object [optional]
145 * id => The id of this rating (if the rating is from the db) [optional]
146 * aggregate => The aggregate for the rating [optional]
147 * count => The number of ratings [optional]
148 * rating => The rating given by the user [optional]
151 public function __construct($options) {
152 $this->context
= $options->context
;
153 $this->component
= $options->component
;
154 $this->ratingarea
= $options->ratingarea
;
155 $this->itemid
= $options->itemid
;
156 $this->scaleid
= $options->scaleid
;
157 $this->userid
= $options->userid
;
159 if (isset($options->settings
)) {
160 $this->settings
= $options->settings
;
162 if (isset($options->id
)) {
163 $this->id
= $options->id
;
165 if (isset($options->aggregate
)) {
166 $this->aggregate
= $options->aggregate
;
168 if (isset($options->count
)) {
169 $this->count
= $options->count
;
171 if (isset($options->rating
)) {
172 $this->rating
= $options->rating
;
177 * Update this rating in the database
178 * @param int $rating the integer value of this rating
181 public function update_rating($rating) {
186 $data = new stdClass
;
187 $data->rating
= $rating;
188 $data->timemodified
= $time;
190 $item = new stdclass();
191 $item->id
= $this->itemid
;
192 $items = array($item);
194 $ratingoptions = new stdClass
;
195 $ratingoptions->context
= $this->context
;
196 $ratingoptions->component
= $this->component
;
197 $ratingoptions->ratingarea
= $this->ratingarea
;
198 $ratingoptions->items
= $items;
199 $ratingoptions->aggregate
= RATING_AGGREGATE_AVERAGE
;//we dont actually care what aggregation method is applied
200 $ratingoptions->scaleid
= $this->scaleid
;
201 $ratingoptions->userid
= $this->userid
;
203 $rm = new rating_manager();;
204 $items = $rm->get_ratings($ratingoptions);
205 $firstitem = $items[0]->rating
;
207 if (empty($firstitem->id
)) {
208 // Insert a new rating
209 $data->contextid
= $this->context
->id
;
210 $data->component
= $this->component
;
211 $data->ratingarea
= $this->ratingarea
;
212 $data->rating
= $rating;
213 $data->scaleid
= $this->scaleid
;
214 $data->userid
= $this->userid
;
215 $data->itemid
= $this->itemid
;
216 $data->timecreated
= $time;
217 $data->timemodified
= $time;
218 $DB->insert_record('rating', $data);
221 $data->id
= $firstitem->id
;
222 $DB->update_record('rating', $data);
227 * Retreive the integer value of this rating
228 * @return int the integer value of this rating object
230 public function get_rating() {
231 return $this->rating
;
235 * Returns this ratings aggregate value as a string.
239 public function get_aggregate_string() {
241 $aggregate = $this->aggregate
;
242 $method = $this->settings
->aggregationmethod
;
244 // only display aggregate if aggregation method isn't COUNT
246 if ($aggregate && $method != RATING_AGGREGATE_COUNT
) {
247 if ($method != RATING_AGGREGATE_SUM
&& !$this->settings
->scale
->isnumeric
) {
248 $aggregatestr .= $this->settings
->scale
->scaleitems
[round($aggregate)]; //round aggregate as we're using it as an index
249 } else { // aggregation is SUM or the scale is numeric
250 $aggregatestr .= round($aggregate, 1);
254 return $aggregatestr;
258 * Returns true if the user is able to rate this rating object
260 * @param int $userid Current user assumed if left empty
263 public function user_can_rate($userid = null) {
264 if (empty($userid)) {
268 // You can't rate your item
269 if ($this->itemuserid
== $userid) {
272 // You can't rate if you don't have the system cap
273 if (!$this->settings
->permissions
->rate
) {
276 // You can't rate if you don't have the plugin cap
277 if (!$this->settings
->pluginpermissions
->rate
) {
281 // You can't rate if the item was outside of the assessment times
282 $timestart = $this->settings
->assesstimestart
;
283 $timefinish = $this->settings
->assesstimefinish
;
284 $timecreated = $this->itemtimecreated
;
285 if (!empty($timestart) && !empty($timefinish) && ($timecreated < $timestart ||
$timecreated > $timefinish)) {
292 * Returns true if the user is able to view the aggregate for this rating object.
294 * @param int|null $userid If left empty the current user is assumed.
297 public function user_can_view_aggregate($userid = null) {
298 if (empty($userid)) {
303 // if the item doesnt belong to anyone or its another user's items and they can see the aggregate on items they don't own
304 // Note that viewany doesnt mean you can see the aggregate or ratings of your own items
305 if ((empty($this->itemuserid
) or $this->itemuserid
!= $userid) && $this->settings
->permissions
->viewany
&& $this->settings
->pluginpermissions
->viewany
) {
309 // if its the current user's item and they have permission to view the aggregate on their own items
310 if ($this->itemuserid
== $userid && $this->settings
->permissions
->view
&& $this->settings
->pluginpermissions
->view
) {
318 * Returns a URL to view all of the ratings for the item this rating is for.
320 * If this is a rating of a post then this URL will take the user to a page that shows all
321 * of the ratings for the post (this one included).
326 public function get_view_ratings_url($popup = false) {
328 'contextid' => $this->context
->id
,
329 'component' => $this->component
,
330 'ratingarea' => $this->ratingarea
,
331 'itemid' => $this->itemid
,
332 'scaleid' => $this->settings
->scale
->id
335 $attributes['popup'] = 1;
337 return new moodle_url('/rating/index.php', $attributes);
341 * Returns a URL that can be used to rate the associated item.
343 * @param int|null $rating The rating to give the item, if null then no rating
345 * @param moodle_url|string $returnurl The URL to return to.
348 public function get_rate_url($rating = null, $returnurl = null) {
349 if (empty($returnurl)) {
350 if (!empty($this->settings
->returnurl
)) {
351 $returnurl = $this->settings
->returnurl
;
354 $returnurl = $PAGE->url
;
358 'contextid' => $this->context
->id
,
359 'component' => $this->component
,
360 'ratingarea' => $this->ratingarea
,
361 'itemid' => $this->itemid
,
362 'scaleid' => $this->settings
->scale
->id
,
363 'returnurl' => $returnurl,
364 'rateduserid' => $this->itemuserid
,
365 'aggregation' => $this->settings
->aggregationmethod
,
366 'sesskey' => sesskey()
368 if (!empty($rating)) {
369 $args['rating'] = $rating;
371 $url = new moodle_url('/rating/rate.php', $args);
376 * Remove this rating from the database
379 //public function delete_rating() {
380 //todo implement this if its actually needed
382 } //end rating class definition
385 * The rating_manager class provides the ability to retrieve sets of ratings from the database
387 * @copyright 2010 Andrew Davis
388 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
391 class rating_manager
{
394 * An array of calculated scale options to save us generating them for each request.
397 protected $scales = array();
400 * Delete one or more ratings. Specify either a rating id, an item id or just the context id.
402 * @global moodle_database $DB
403 * @param stdClass $options {
404 * contextid => int the context in which the ratings exist [required]
405 * ratingid => int the id of an individual rating to delete [optional]
406 * userid => int delete the ratings submitted by this user. May be used in conjuction with itemid [optional]
407 * itemid => int delete all ratings attached to this item [optional]
408 * component => string The component to delete ratings from [optional]
409 * ratingarea => string The ratingarea to delete ratings from [optional]
413 public function delete_ratings($options) {
416 if (empty($options->contextid
)) {
417 throw new coding_exception('The context option is a required option when deleting ratings.');
420 $conditions = array('contextid' => $options->contextid
);
421 $possibleconditions = array(
423 'userid' => 'userid',
424 'itemid' => 'itemid',
425 'component' => 'component',
426 'ratingarea' => 'ratingarea'
428 foreach ($possibleconditions as $option => $field) {
429 if (isset($options->{$option})) {
430 $conditions[$field] = $options->{$option};
433 $DB->delete_records('rating', $conditions);
437 * Returns an array of ratings for a given item (forum post, glossary entry etc)
438 * This returns all users ratings for a single item
439 * @param stdClass $options {
440 * context => context the context in which the ratings exists [required]
441 * component => component using ratings ie mod_forum [required]
442 * ratingarea => ratingarea to associate this rating with [required]
443 * itemid => int the id of the associated item (forum post, glossary item etc) [required]
444 * sort => string SQL sort by clause [optional]
446 * @return array an array of ratings
448 public function get_all_ratings_for_item($options) {
451 if (!isset($options->context
)) {
452 throw new coding_exception('The context option is a required option when getting ratings for an item.');
454 if (!isset($options->itemid
)) {
455 throw new coding_exception('The itemid option is a required option when getting ratings for an item.');
457 if (!isset($options->component
)) {
458 throw new coding_exception('The component option is now a required option when getting ratings for an item.');
460 if (!isset($options->ratingarea
)) {
461 throw new coding_exception('The ratingarea option is now a required option when getting ratings for an item.');
465 if( !empty($options->sort
) ) {
466 $sortclause = "ORDER BY $options->sort";
470 'contextid' => $options->context
->id
,
471 'itemid' => $options->itemid
,
472 'component' => $options->component
,
473 'ratingarea' => $options->ratingarea
,
475 $userfields = user_picture
::fields('u', null, 'userid');
476 $sql = "SELECT r.id, r.rating, r.itemid, r.userid, r.timemodified, r.component, r.ratingarea, $userfields
478 LEFT JOIN {user} u ON r.userid = u.id
479 WHERE r.contextid = :contextid AND
480 r.itemid = :itemid AND
481 r.component = :component AND
482 r.ratingarea = :ratingarea
485 return $DB->get_records_sql($sql, $params);
489 * Adds rating objects to an array of items (forum posts, glossary entries etc)
490 * Rating objects are available at $item->rating
491 * @param stdClass $options {
492 * context => context the context in which the ratings exists [required]
493 * component => the component name ie mod_forum [required]
494 * ratingarea => the ratingarea we are interested in [required]
495 * items => array an array of items such as forum posts or glossary items. They must have an 'id' member ie $items[0]->id[required]
496 * aggregate => int what aggregation method should be applied. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required]
497 * scaleid => int the scale from which the user can select a rating [required]
498 * userid => int the id of the current user [optional]
499 * returnurl => string the url to return the user to after submitting a rating. Can be left null for ajax requests [optional]
500 * assesstimestart => int only allow rating of items created after this timestamp [optional]
501 * assesstimefinish => int only allow rating of items created before this timestamp [optional]
502 * @return array the array of items with their ratings attached at $items[0]->rating
504 public function get_ratings($options) {
507 if (!isset($options->context
)) {
508 throw new coding_exception('The context option is a required option when getting ratings.');
511 if (!isset($options->component
)) {
512 throw new coding_exception('The component option is a required option when getting ratings.');
515 if (!isset($options->ratingarea
)) {
516 throw new coding_exception('The ratingarea option is a required option when getting ratings.');
519 if (!isset($options->scaleid
)) {
520 throw new coding_exception('The scaleid option is a required option when getting ratings.');
523 if (!isset($options->items
)) {
524 throw new coding_exception('The items option is a required option when getting ratings.');
525 } else if (empty($options->items
)) {
529 if (!isset($options->aggregate
)) {
530 throw new coding_exception('The aggregate option is a required option when getting ratings.');
531 } else if ($options->aggregate
== RATING_AGGREGATE_NONE
) {
532 // Ratings arn't enabled.
533 return $options->items
;
535 $aggregatestr = $this->get_aggregation_method($options->aggregate
);
537 // Default the userid to the current user if it is not set
538 if (empty($options->userid
)) {
541 $userid = $options->userid
;
544 // Get the item table name, the item id field, and the item user field for the given rating item
545 // from the related component.
546 list($type, $name) = normalize_component($options->component
);
547 $default = array(null, 'id', 'userid');
548 list($itemtablename, $itemidcol, $itemuseridcol) = plugin_callback($type, $name, 'rating', 'get_item_fields', array($options), $default);
550 // Create an array of item ids
552 foreach ($options->items
as $item) {
553 $itemids[] = $item->{$itemidcol};
556 // get the items from the database
557 list($itemidtest, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED
);
558 $params['contextid'] = $options->context
->id
;
559 $params['userid'] = $userid;
560 $params['component'] = $options->component
;
561 $params['ratingarea'] = $options->ratingarea
;
563 $sql = "SELECT r.id, r.itemid, r.userid, r.scaleid, r.rating AS usersrating
565 WHERE r.userid = :userid AND
566 r.contextid = :contextid AND
567 r.itemid {$itemidtest} AND
568 r.component = :component AND
569 r.ratingarea = :ratingarea
571 $userratings = $DB->get_records_sql($sql, $params);
573 $sql = "SELECT r.itemid, $aggregatestr(r.rating) AS aggrrating, COUNT(r.rating) AS numratings
575 WHERE r.contextid = :contextid AND
576 r.itemid {$itemidtest} AND
577 r.component = :component AND
578 r.ratingarea = :ratingarea
579 GROUP BY r.itemid, r.component, r.ratingarea, r.contextid
581 $aggregateratings = $DB->get_records_sql($sql, $params);
583 $ratingoptions = new stdClass
;
584 $ratingoptions->context
= $options->context
;
585 $ratingoptions->component
= $options->component
;
586 $ratingoptions->ratingarea
= $options->ratingarea
;
587 $ratingoptions->settings
= $this->generate_rating_settings_object($options);
588 foreach ($options->items
as $item) {
589 $founduserrating = false;
590 foreach($userratings as $userrating) {
591 //look for an existing rating from this user of this item
592 if ($item->{$itemidcol} == $userrating->itemid
) {
593 // Note: rec->scaleid = the id of scale at the time the rating was submitted
594 // may be different from the current scale id
595 $ratingoptions->scaleid
= $userrating->scaleid
;
596 $ratingoptions->userid
= $userrating->userid
;
597 $ratingoptions->id
= $userrating->id
;
598 $ratingoptions->rating
= min($userrating->usersrating
, $ratingoptions->settings
->scale
->max
);
600 $founduserrating = true;
604 if (!$founduserrating) {
605 $ratingoptions->scaleid
= null;
606 $ratingoptions->userid
= null;
607 $ratingoptions->id
= null;
608 $ratingoptions->rating
= null;
611 if (array_key_exists($item->{$itemidcol}, $aggregateratings)) {
612 $rec = $aggregateratings[$item->{$itemidcol}];
613 $ratingoptions->itemid
= $item->{$itemidcol};
614 $ratingoptions->aggregate
= min($rec->aggrrating
, $ratingoptions->settings
->scale
->max
);
615 $ratingoptions->count
= $rec->numratings
;
617 $ratingoptions->itemid
= $item->{$itemidcol};
618 $ratingoptions->aggregate
= null;
619 $ratingoptions->count
= 0;
622 $rating = new rating($ratingoptions);
623 $rating->itemtimecreated
= $this->get_item_time_created($item);
624 if (!empty($item->{$itemuseridcol})) {
625 $rating->itemuserid
= $item->{$itemuseridcol};
627 $item->rating
= $rating;
630 return $options->items
;
634 * Generates a rating settings object based upon the options it is provided.
636 * @param stdClass $options {
637 * context => context the context in which the ratings exists [required]
638 * component => string The component the items belong to [required]
639 * ratingarea => string The ratingarea the items belong to [required]
640 * aggregate => int what aggregation method should be applied. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required]
641 * scaleid => int the scale from which the user can select a rating [required]
642 * returnurl => string the url to return the user to after submitting a rating. Can be left null for ajax requests [optional]
643 * assesstimestart => int only allow rating of items created after this timestamp [optional]
644 * assesstimefinish => int only allow rating of items created before this timestamp [optional]
645 * plugintype => string plugin type ie 'mod' Used to find the permissions callback [optional]
646 * pluginname => string plugin name ie 'forum' Used to find the permissions callback [optional]
650 protected function generate_rating_settings_object($options) {
652 if (!isset($options->context
)) {
653 throw new coding_exception('The context option is a required option when generating a rating settings object.');
655 if (!isset($options->component
)) {
656 throw new coding_exception('The component option is now a required option when generating a rating settings object.');
658 if (!isset($options->ratingarea
)) {
659 throw new coding_exception('The ratingarea option is now a required option when generating a rating settings object.');
661 if (!isset($options->aggregate
)) {
662 throw new coding_exception('The aggregate option is now a required option when generating a rating settings object.');
664 if (!isset($options->scaleid
)) {
665 throw new coding_exception('The scaleid option is now a required option when generating a rating settings object.');
668 // settings that are common to all ratings objects in this context
669 $settings = new stdClass
;
670 $settings->scale
= $this->generate_rating_scale_object($options->scaleid
); // the scale to use now
671 $settings->aggregationmethod
= $options->aggregate
;
672 $settings->assesstimestart
= null;
673 $settings->assesstimefinish
= null;
675 // Collect options into the settings object
676 if (!empty($options->assesstimestart
)) {
677 $settings->assesstimestart
= $options->assesstimestart
;
679 if (!empty($options->assesstimefinish
)) {
680 $settings->assesstimefinish
= $options->assesstimefinish
;
682 if (!empty($options->returnurl
)) {
683 $settings->returnurl
= $options->returnurl
;
686 // check site capabilities
687 $settings->permissions
= new stdClass
;
688 $settings->permissions
->view
= has_capability('moodle/rating:view', $options->context
); // can view the aggregate of ratings of their own items
689 $settings->permissions
->viewany
= has_capability('moodle/rating:viewany', $options->context
); // can view the aggregate of ratings of other people's items
690 $settings->permissions
->viewall
= has_capability('moodle/rating:viewall', $options->context
); // can view individual ratings
691 $settings->permissions
->rate
= has_capability('moodle/rating:rate', $options->context
); // can submit ratings
693 // check module capabilities (mostly for backwards compatability with old modules that previously implemented their own ratings)
694 $pluginpermissionsarray = $this->get_plugin_permissions_array($options->context
->id
, $options->component
, $options->ratingarea
);
695 $settings->pluginpermissions
= new stdClass
;
696 $settings->pluginpermissions
->view
= $pluginpermissionsarray['view'];
697 $settings->pluginpermissions
->viewany
= $pluginpermissionsarray['viewany'];
698 $settings->pluginpermissions
->viewall
= $pluginpermissionsarray['viewall'];
699 $settings->pluginpermissions
->rate
= $pluginpermissionsarray['rate'];
705 * Generates a scale object that can be returned
707 * @global moodle_database $DB
708 * @param type $scaleid
711 protected function generate_rating_scale_object($scaleid) {
713 if (!array_key_exists('s'.$scaleid, $this->scales
)) {
714 $scale = new stdClass
;
715 $scale->id
= $scaleid;
717 $scale->courseid
= null;
718 $scale->scaleitems
= array();
719 $scale->isnumeric
= true;
720 $scale->max
= $scaleid;
723 // It is a proper scale (not numeric)
724 $scalerecord = $DB->get_record('scale', array('id' => abs($scaleid)));
726 // We need to generate an array with string keys starting at 1
727 $scalearray = explode(',', $scalerecord->scale
);
728 $c = count($scalearray);
729 for ($i = 0; $i < $c; $i++
) {
730 // treat index as a string to allow sorting without changing the value
731 $scale->scaleitems
[(string)($i +
1)] = $scalearray[$i];
733 krsort($scale->scaleitems
); // have the highest grade scale item appear first
734 $scale->isnumeric
= false;
735 $scale->name
= $scalerecord->name
;
736 $scale->courseid
= $scalerecord->courseid
;
737 $scale->max
= count($scale->scaleitems
);
740 //generate an array of values for numeric scales
741 for($i = 0; $i <= (int)$scaleid; $i++
) {
742 $scale->scaleitems
[(string)$i] = $i;
745 $this->scales
['s'.$scaleid] = $scale;
747 return $this->scales
['s'.$scaleid];
751 * Gets the time the given item was created
753 * TODO: Find a better solution for this, its not ideal to test for fields really we should be
754 * asking the component the item belongs to what field to look for or even the value we
757 * @param stdClass $item
760 protected function get_item_time_created($item) {
761 if( !empty($item->created
) ) {
762 return $item->created
;//the forum_posts table has created instead of timecreated
764 else if(!empty($item->timecreated
)) {
765 return $item->timecreated
;
773 * Returns an array of grades calculated by aggregating item ratings.
774 * @param object $options {
775 * userid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required]
776 * aggregationmethod => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required]
777 * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
778 * itemtable => int the table containing the items [required]
779 * itemtableusercolum => int the column of the user table containing the item owner's user id [required]
780 * component => The component for the ratings [required]
781 * ratingarea => The ratingarea for the ratings [required]
783 * contextid => int the context in which the rated items exist [optional]
785 * modulename => string the name of the module [optional]
786 * moduleid => int the id of the module instance [optional]
788 * @return array the array of the user's grades
790 public function get_user_grades($options) {
795 if (!isset($options->component
)) {
796 throw new coding_exception('The component option is now a required option when getting user grades from ratings.');
798 if (!isset($options->ratingarea
)) {
799 throw new coding_exception('The ratingarea option is now a required option when getting user grades from ratings.');
802 //if the calling code doesn't supply a context id we'll have to figure it out
803 if( !empty($options->contextid
) ) {
804 $contextid = $options->contextid
;
806 else if( !empty($options->cmid
) ) {
807 //not implemented as not currently used although cmid is potentially available (the forum supplies it)
808 //Is there a convenient way to get a context id from a cm id?
809 //$cmidnumber = $options->cmidnumber;
811 else if ( !empty($options->modulename
) && !empty($options->moduleid
) ) {
812 $modulename = $options->modulename
;
813 $moduleid = intval($options->moduleid
);
815 //going direct to the db for the context id seems wrong
816 list($ctxselect, $ctxjoin) = context_instance_preload_sql('cm.id', CONTEXT_MODULE
, 'ctx');
817 $sql = "SELECT cm.* $ctxselect
818 FROM {course_modules} cm
819 LEFT JOIN {modules} mo ON mo.id = cm.module
820 LEFT JOIN {{$modulename}} m ON m.id = cm.instance $ctxjoin
821 WHERE mo.name=:modulename AND
823 $contextrecord = $DB->get_record_sql($sql, array('modulename'=>$modulename, 'moduleid'=>$moduleid), '*', MUST_EXIST
);
824 $contextid = $contextrecord->ctxid
;
828 $params['contextid'] = $contextid;
829 $params['component'] = $options->component
;
830 $params['ratingarea'] = $options->ratingarea
;
831 $itemtable = $options->itemtable
;
832 $itemtableusercolumn = $options->itemtableusercolumn
;
833 $scaleid = $options->scaleid
;
834 $aggregationstring = $this->get_aggregation_method($options->aggregationmethod
);
836 //if userid is not 0 we only want the grade for a single user
837 $singleuserwhere = '';
838 if ($options->userid
!= 0) {
839 $params['userid1'] = intval($options->userid
);
840 $singleuserwhere = "AND i.{$itemtableusercolumn} = :userid1";
843 //MDL-24648 The where line used to be "WHERE (r.contextid is null or r.contextid=:contextid)"
844 //r.contextid will be null for users who haven't been rated yet
845 //no longer including users who haven't been rated to reduce memory requirements
846 $sql = "SELECT u.id as id, u.id AS userid, $aggregationstring(r.rating) AS rawgrade
848 LEFT JOIN {{$itemtable}} i ON u.id=i.{$itemtableusercolumn}
849 LEFT JOIN {rating} r ON r.itemid=i.id
850 WHERE r.contextid = :contextid AND
851 r.component = :component AND
852 r.ratingarea = :ratingarea
855 $results = $DB->get_records_sql($sql, $params);
861 if ($options->scaleid
>= 0) {
863 $max = $options->scaleid
;
866 $scale = $DB->get_record('scale', array('id' => -$options->scaleid
));
868 $scale = explode(',', $scale->scale
);
869 $max = count($scale);
871 debugging('rating_manager::get_user_grades() received a scale ID that doesnt exist');
875 // it could throw off the grading if count and sum returned a rawgrade higher than scale
876 // 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)
877 foreach ($results as $rid=>$result) {
878 if ($options->scaleid
>= 0) {
880 if ($result->rawgrade
> $options->scaleid
) {
881 $results[$rid]->rawgrade
= $options->scaleid
;
885 if (!empty($scale) && $result->rawgrade
> $max) {
886 $results[$rid]->rawgrade
= $max;
896 * Returns array of aggregate types. Used by ratings.
900 public function get_aggregate_types() {
901 return array (RATING_AGGREGATE_NONE
=> get_string('aggregatenone', 'rating'),
902 RATING_AGGREGATE_AVERAGE
=> get_string('aggregateavg', 'rating'),
903 RATING_AGGREGATE_COUNT
=> get_string('aggregatecount', 'rating'),
904 RATING_AGGREGATE_MAXIMUM
=> get_string('aggregatemax', 'rating'),
905 RATING_AGGREGATE_MINIMUM
=> get_string('aggregatemin', 'rating'),
906 RATING_AGGREGATE_SUM
=> get_string('aggregatesum', 'rating'));
910 * Converts an aggregation method constant into something that can be included in SQL
911 * @param int $aggregate An aggregation constant. For example, RATING_AGGREGATE_AVERAGE.
912 * @return string an SQL aggregation method
914 public function get_aggregation_method($aggregate) {
915 $aggregatestr = null;
917 case RATING_AGGREGATE_AVERAGE
:
918 $aggregatestr = 'AVG';
920 case RATING_AGGREGATE_COUNT
:
921 $aggregatestr = 'COUNT';
923 case RATING_AGGREGATE_MAXIMUM
:
924 $aggregatestr = 'MAX';
926 case RATING_AGGREGATE_MINIMUM
:
927 $aggregatestr = 'MIN';
929 case RATING_AGGREGATE_SUM
:
930 $aggregatestr = 'SUM';
933 $aggregatestr = 'AVG'; // Default to this to avoid real breakage - MDL-22270
934 debugging('Incorrect call to get_aggregation_method(), was called with incorrect aggregate method ' . $aggregate, DEBUG_DEVELOPER
);
936 return $aggregatestr;
940 * Looks for a callback like forum_rating_permissions() to retrieve permissions from the plugin whose items are being rated
941 * @param int $contextid The current context id
942 * @param string component the name of the component that is using ratings ie 'mod_forum'
943 * @param string ratingarea The area the rating is associated with
944 * @return array rating related permissions
946 public function get_plugin_permissions_array($contextid, $component, $ratingarea) {
947 $pluginpermissionsarray = null;
948 $defaultpluginpermissions = array('rate'=>false,'view'=>false,'viewany'=>false,'viewall'=>false);//deny by default
949 if (!empty($component)) {
950 list($type, $name) = normalize_component($component);
951 $pluginpermissionsarray = plugin_callback($type, $name, 'rating', 'permissions', array($contextid, $component, $ratingarea), $defaultpluginpermissions);
953 $pluginpermissionsarray = $defaultpluginpermissions;
955 return $pluginpermissionsarray;
959 * Validates a submitted rating
960 * @param array $params submitted data
961 * context => object the context in which the rated items exists [required]
962 * component => The component the rating belongs to [required]
963 * ratingarea => The ratingarea the rating is associated with [required]
964 * itemid => int the ID of the object being rated [required]
965 * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
966 * rating => int the submitted rating
967 * rateduserid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required]
968 * aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [optional]
969 * @return boolean true if the rating is valid. False if callback wasnt found and will throw rating_exception if rating is invalid
971 public function check_rating_is_valid($params) {
973 if (!isset($params['context'])) {
974 throw new coding_exception('The context option is a required option when checking rating validity.');
976 if (!isset($params['component'])) {
977 throw new coding_exception('The component option is now a required option when checking rating validity');
979 if (!isset($params['ratingarea'])) {
980 throw new coding_exception('The ratingarea option is now a required option when checking rating validity');
982 if (!isset($params['itemid'])) {
983 throw new coding_exception('The itemid option is now a required option when checking rating validity');
985 if (!isset($params['scaleid'])) {
986 throw new coding_exception('The scaleid option is now a required option when checking rating validity');
988 if (!isset($params['rateduserid'])) {
989 throw new coding_exception('The rateduserid option is now a required option when checking rating validity');
992 list($plugintype, $pluginname) = normalize_component($params['component']);
994 //this looks for a function like forum_rating_validate() in mod_forum lib.php
995 //wrapping the params array in another array as call_user_func_array() expands arrays into multiple arguments
996 $isvalid = plugin_callback($plugintype, $pluginname, 'rating', 'validate', array($params), null);
998 //if null then the callback doesn't exist
999 if ($isvalid === null) {
1001 debugging('rating validation callback not found for component '. clean_param($component, PARAM_ALPHANUMEXT
));
1007 * Initialises JavaScript to enable AJAX ratings on the provided page
1009 * @param moodle_page $page
1012 public function initialise_rating_javascript(moodle_page
$page) {
1015 //only needs to be initialized once
1016 static $done = false;
1021 if (!empty($CFG->enableajax
)) {
1022 $page->requires
->js_init_call('M.core_rating.init');
1030 * Returns a string that describes the aggregation method that was provided.
1032 * @param string $aggregationmethod
1035 public function get_aggregate_label($aggregationmethod) {
1036 $aggregatelabel = '';
1037 switch ($aggregationmethod) {
1038 case RATING_AGGREGATE_AVERAGE
:
1039 $aggregatelabel .= get_string("aggregateavg", "rating");
1041 case RATING_AGGREGATE_COUNT
:
1042 $aggregatelabel .= get_string("aggregatecount", "rating");
1044 case RATING_AGGREGATE_MAXIMUM
:
1045 $aggregatelabel .= get_string("aggregatemax", "rating");
1047 case RATING_AGGREGATE_MINIMUM
:
1048 $aggregatelabel .= get_string("aggregatemin", "rating");
1050 case RATING_AGGREGATE_SUM
:
1051 $aggregatelabel .= get_string("aggregatesum", "rating");
1054 $aggregatelabel .= get_string('labelsep', 'langconfig');
1055 return $aggregatelabel;
1058 }//end rating_manager class definition
1060 class rating_exception
extends moodle_exception
{
1062 function __construct($errorcode) {
1063 $this->errorcode
= $errorcode;
1064 $this->message
= get_string($errorcode, 'error');