Automatically generated installer lang files
[moodle.git] / rating / lib.php
CommitLineData
a09aeee4 1<?php
a09aeee4
AD
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/>.
16
17/**
18 * A class representing a single rating and containing some static methods for manipulating ratings
19 *
8c335cff 20 * @package core_rating
5d354ded
PS
21 * @subpackage rating
22 * @copyright 2010 Andrew Davis
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
a09aeee4
AD
24 */
25
a09aeee4
AD
26define('RATING_UNSET_RATING', -999);
27
63e87951 28define ('RATING_AGGREGATE_NONE', 0); //no ratings
a09aeee4
AD
29define ('RATING_AGGREGATE_AVERAGE', 1);
30define ('RATING_AGGREGATE_COUNT', 2);
31define ('RATING_AGGREGATE_MAXIMUM', 3);
32define ('RATING_AGGREGATE_MINIMUM', 4);
33define ('RATING_AGGREGATE_SUM', 5);
34
5bdf0010 35define ('RATING_DEFAULT_SCALE', 5);
786f3cc7 36
a09aeee4 37/**
a8e85df6 38 * The rating class represents a single rating by a single user
a09aeee4 39 *
8c335cff
JF
40 * @package core_rating
41 * @category rating
a09aeee4
AD
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 */
46class rating implements renderable {
7ac928a7
AD
47
48 /**
8c335cff 49 * @var stdClass The context in which this rating exists
7ac928a7
AD
50 */
51 public $context;
52
2c2ff8d5 53 /**
8c335cff 54 * @var string The component using ratings. For example "mod_forum"
2c2ff8d5
AD
55 */
56 public $component;
57
2b04c41c 58 /**
8c335cff
JF
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
2b04c41c
SH
61 */
62 public $ratingarea = null;
63
7ac928a7 64 /**
8c335cff 65 * @var int The id of the item (forum post, glossary item etc) being rated
7ac928a7
AD
66 */
67 public $itemid;
68
69 /**
8c335cff 70 * @var int The id scale (1-5, 0-100) that was in use when the rating was submitted
7ac928a7
AD
71 */
72 public $scaleid;
73
74 /**
8c335cff 75 * @var int The id of the user who submitted the rating
7ac928a7
AD
76 */
77 public $userid;
78
8b69c341 79 /**
8c335cff 80 * @var stdclass settings for this rating. Necessary to render the rating.
8b69c341
AD
81 */
82 public $settings;
83
e1e613d5 84 /**
8c335cff 85 * @var int The Id of this rating within the rating table. This is only set if the rating already exists
2b04c41c
SH
86 */
87 public $id = null;
88
89 /**
8c335cff 90 * @var int The aggregate of the combined ratings for the associated item. This is only set if the rating already exists
2b04c41c
SH
91 */
92 public $aggregate = null;
93
94 /**
8c335cff 95 * @var int The total number of ratings for the associated item. This is only set if the rating already exists
2b04c41c
SH
96 */
97 public $count = 0;
98
99 /**
8c335cff 100 * @var int The rating the associated user gave the associated item. This is only set if the rating already exists
2b04c41c
SH
101 */
102 public $rating = null;
103
104 /**
8c335cff 105 * @var int The time the associated item was created
2b04c41c
SH
106 */
107 public $itemtimecreated = null;
108
109 /**
8c335cff 110 * @var int The id of the user who submitted the rating
2b04c41c
SH
111 */
112 public $itemuserid = null;
113
114 /**
115 * Constructor.
8c335cff
JF
116 *
117 * @param stdClass $options {
2b04c41c
SH
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 */
b1721f67 131 public function __construct($options) {
2b04c41c
SH
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;
138
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 }
a09aeee4 154 }
a09aeee4 155
e1e613d5 156 /**
2b04c41c 157 * Update this rating in the database
8c335cff 158 *
2b04c41c 159 * @param int $rating the integer value of this rating
2b04c41c 160 */
e1e613d5
AD
161 public function update_rating($rating) {
162 global $DB;
163
2b04c41c
SH
164 $time = time();
165
166 $data = new stdClass;
167 $data->rating = $rating;
168 $data->timemodified = $time;
e1e613d5
AD
169
170 $item = new stdclass();
171 $item->id = $this->itemid;
172 $items = array($item);
173
2b04c41c 174 $ratingoptions = new stdClass;
b1721f67 175 $ratingoptions->context = $this->context;
2c2ff8d5 176 $ratingoptions->component = $this->component;
2b04c41c 177 $ratingoptions->ratingarea = $this->ratingarea;
b1721f67 178 $ratingoptions->items = $items;
63e87951 179 $ratingoptions->aggregate = RATING_AGGREGATE_AVERAGE;//we dont actually care what aggregation method is applied
b1721f67
AD
180 $ratingoptions->scaleid = $this->scaleid;
181 $ratingoptions->userid = $this->userid;
a8e85df6 182
2b04c41c 183 $rm = new rating_manager();;
63e87951 184 $items = $rm->get_ratings($ratingoptions);
2b04c41c
SH
185 $firstitem = $items[0]->rating;
186
187 if (empty($firstitem->id)) {
188 // Insert a new rating
e1e613d5 189 $data->contextid = $this->context->id;
2b04c41c
SH
190 $data->component = $this->component;
191 $data->ratingarea = $this->ratingarea;
e1e613d5
AD
192 $data->rating = $rating;
193 $data->scaleid = $this->scaleid;
194 $data->userid = $this->userid;
195 $data->itemid = $this->itemid;
2b04c41c 196 $data->timecreated = $time;
e1e613d5 197 $data->timemodified = $time;
2b04c41c
SH
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 }
e1e613d5 205
2b04c41c
SH
206 /**
207 * Retreive the integer value of this rating
8c335cff 208 *
2b04c41c
SH
209 * @return int the integer value of this rating object
210 */
211 public function get_rating() {
212 return $this->rating;
213 }
214
215 /**
216 * Returns this ratings aggregate value as a string.
217 *
8c335cff 218 * @return string ratings aggregate value
2b04c41c
SH
219 */
220 public function get_aggregate_string() {
221
222 $aggregate = $this->aggregate;
223 $method = $this->settings->aggregationmethod;
224
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 }
e1e613d5 233 }
e1e613d5 234
2b04c41c
SH
235 return $aggregatestr;
236 }
e1e613d5 237
2b04c41c
SH
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
8c335cff 242 * @return bool true if the user is able to rate this rating object
2b04c41c
SH
243 */
244 public function user_can_rate($userid = null) {
245 if (empty($userid)) {
246 global $USER;
247 $userid = $USER->id;
e1e613d5 248 }
2b04c41c
SH
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 }
261
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;
a09aeee4
AD
270 }
271
e1e613d5 272 /**
2b04c41c
SH
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.
8c335cff 276 * @return bool true if the user is able to view the aggregate for this rating object
2b04c41c
SH
277 */
278 public function user_can_view_aggregate($userid = null) {
279 if (empty($userid)) {
280 global $USER;
281 $userid = $USER->id;
282 }
283
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 }
289
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 }
294
295 return false;
296 }
297
298 /**
299 * Returns a URL to view all of the ratings for the item this rating is for.
300 *
8c335cff
JF
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).
2b04c41c 303 *
8c335cff
JF
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.
2b04c41c
SH
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 }
320
321 /**
322 * Returns a URL that can be used to rate the associated item.
323 *
8c335cff 324 * @param int|null $rating The rating to give the item, if null then no rating param is added.
2b04c41c 325 * @param moodle_url|string $returnurl The URL to return to.
8c335cff 326 * @return moodle_url can be used to rate the associated item.
2b04c41c
SH
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;
a09aeee4
AD
353 }
354
e1e613d5
AD
355 /**
356 * Remove this rating from the database
4e5c0484 357 * @return void
e1e613d5 358 */
63e87951 359 //public function delete_rating() {
e1e613d5 360 //todo implement this if its actually needed
63e87951 361 //}
a8e85df6 362} //end rating class definition
a09aeee4 363
a8e85df6
AD
364/**
365 * The rating_manager class provides the ability to retrieve sets of ratings from the database
366 *
8c335cff
JF
367 * @package core_rating
368 * @category rating
a8e85df6
AD
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 */
373class rating_manager {
63e87951
AD
374
375 /**
8c335cff 376 * @var array An array of calculated scale options to save us generating them for each request.
2b04c41c
SH
377 */
378 protected $scales = array();
379
2b04c41c
SH
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 * }
2b04c41c 392 */
63e87951
AD
393 public function delete_ratings($options) {
394 global $DB;
3a11c09f 395
2b04c41c
SH
396 if (empty($options->contextid)) {
397 throw new coding_exception('The context option is a required option when deleting ratings.');
279fcfcf 398 }
2b04c41c
SH
399
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 }
63e87951 412 }
2b04c41c 413 $DB->delete_records('rating', $conditions);
63e87951
AD
414 }
415
e1e613d5 416 /**
8c335cff
JF
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 *
2b04c41c
SH
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 */
63e87951 428 public function get_all_ratings_for_item($options) {
e1e613d5
AD
429 global $DB;
430
2b04c41c
SH
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 }
443
63e87951
AD
444 $sortclause = '';
445 if( !empty($options->sort) ) {
446 $sortclause = "ORDER BY $options->sort";
447 }
448
2b04c41c
SH
449 $params = array(
450 'contextid' => $options->context->id,
451 'itemid' => $options->itemid,
452 'component' => $options->component,
453 'ratingarea' => $options->ratingarea,
454 );
7bbe9715 455 $userfields = user_picture::fields('u', null, 'userid');
2b04c41c
SH
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}";
e1e613d5
AD
464
465 return $DB->get_records_sql($sql, $params);
a09aeee4
AD
466 }
467
e1e613d5 468 /**
8c335cff
JF
469 * Adds rating objects to an array of items (forum posts, glossary entries etc). Rating objects are available at $item->rating
470 *
2b04c41c
SH
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 */
63e87951 484 public function get_ratings($options) {
2b04c41c 485 global $DB, $USER;
e1e613d5 486
2b04c41c
SH
487 if (!isset($options->context)) {
488 throw new coding_exception('The context option is a required option when getting ratings.');
489 }
490
491 if (!isset($options->component)) {
492 throw new coding_exception('The component option is a required option when getting ratings.');
493 }
494
495 if (!isset($options->ratingarea)) {
496 throw new coding_exception('The ratingarea option is a required option when getting ratings.');
54bc6de2 497 }
54bc6de2 498
2b04c41c
SH
499 if (!isset($options->scaleid)) {
500 throw new coding_exception('The scaleid option is a required option when getting ratings.');
501 }
502
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 }
508
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.
b1721f67 513 return $options->items;
e1e613d5 514 }
2b04c41c 515 $aggregatestr = $this->get_aggregation_method($options->aggregate);
a09aeee4 516
2b04c41c 517 // Default the userid to the current user if it is not set
63e87951 518 if (empty($options->userid)) {
e1e613d5 519 $userid = $USER->id;
63e87951
AD
520 } else {
521 $userid = $options->userid;
a09aeee4 522 }
e1e613d5 523
2b04c41c
SH
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);
529
530 // Create an array of item ids
e1e613d5 531 $itemids = array();
2b04c41c
SH
532 foreach ($options->items as $item) {
533 $itemids[] = $item->{$itemidcol};
534 }
e1e613d5 535
2b04c41c
SH
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;
542
02f40510 543 $sql = "SELECT r.id, r.itemid, r.userid, r.scaleid, r.rating AS usersrating
675c2f53
AD
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);
552
553 $sql = "SELECT r.itemid, $aggregatestr(r.rating) AS aggrrating, COUNT(r.rating) AS numratings
2b04c41c 554 FROM {rating} r
2b04c41c
SH
555 WHERE r.contextid = :contextid AND
556 r.itemid {$itemidtest} AND
557 r.component = :component AND
558 r.ratingarea = :ratingarea
675c2f53 559 GROUP BY r.itemid, r.component, r.ratingarea, r.contextid
2b04c41c 560 ORDER BY r.itemid";
675c2f53 561 $aggregateratings = $DB->get_records_sql($sql, $params);
e1e613d5 562
2b04c41c
SH
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) {
02f40510
AD
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);
579
580 $founduserrating = true;
581 break;
582 }
583 }
584 if (!$founduserrating) {
2b04c41c
SH
585 $ratingoptions->scaleid = null;
586 $ratingoptions->userid = null;
587 $ratingoptions->id = null;
675c2f53
AD
588 $ratingoptions->rating = null;
589 }
02f40510 590
675c2f53
AD
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};
2b04c41c
SH
598 $ratingoptions->aggregate = null;
599 $ratingoptions->count = 0;
2b04c41c 600 }
e1e613d5 601
2b04c41c
SH
602 $rating = new rating($ratingoptions);
603 $rating->itemtimecreated = $this->get_item_time_created($item);
604 if (!empty($item->{$itemuseridcol})) {
605 $rating->itemuserid = $item->{$itemuseridcol};
e1e613d5 606 }
2b04c41c 607 $item->rating = $rating;
e1e613d5 608 }
a09aeee4 609
2b04c41c
SH
610 return $options->items;
611 }
612
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 * }
8c335cff 628 * @return stdClass rating settings object
2b04c41c
SH
629 */
630 protected function generate_rating_settings_object($options) {
631
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.');
a09aeee4 646 }
a09aeee4 647
2b04c41c
SH
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
b1721f67 651 $settings->aggregationmethod = $options->aggregate;
2b04c41c
SH
652 $settings->assesstimestart = null;
653 $settings->assesstimefinish = null;
a09aeee4 654
2b04c41c
SH
655 // Collect options into the settings object
656 if (!empty($options->assesstimestart)) {
63e87951
AD
657 $settings->assesstimestart = $options->assesstimestart;
658 }
2b04c41c 659 if (!empty($options->assesstimefinish)) {
63e87951
AD
660 $settings->assesstimefinish = $options->assesstimefinish;
661 }
2b04c41c
SH
662 if (!empty($options->returnurl)) {
663 $settings->returnurl = $options->returnurl;
664 }
63e87951 665
2b04c41c
SH
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
672
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'];
d251b259
AD
677 $settings->pluginpermissions->viewany = $pluginpermissionsarray['viewany'];
678 $settings->pluginpermissions->viewall = $pluginpermissionsarray['viewall'];
2b04c41c 679 $settings->pluginpermissions->rate = $pluginpermissionsarray['rate'];
8b69c341 680
2b04c41c
SH
681 return $settings;
682 }
a09aeee4 683
2b04c41c
SH
684 /**
685 * Generates a scale object that can be returned
686 *
8c335cff
JF
687 * @global moodle_database $DB moodle database object
688 * @param int $scaleid scale-type identifier
689 * @return stdClass scale for ratings
2b04c41c
SH
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;
701
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 }
63e87951 719 } else {
2b04c41c
SH
720 //generate an array of values for numeric scales
721 for($i = 0; $i <= (int)$scaleid; $i++) {
722 $scale->scaleitems[(string)$i] = $i;
723 }
63e87951 724 }
2b04c41c 725 $this->scales['s'.$scaleid] = $scale;
e1e613d5 726 }
2b04c41c 727 return $this->scales['s'.$scaleid];
e1e613d5 728 }
a8e85df6 729
2b04c41c
SH
730 /**
731 * Gets the time the given item was created
732 *
8c335cff 733 * TODO: MDL-31511 - Find a better solution for this, its not ideal to test for fields really we should be
2b04c41c
SH
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
8c335cff 738 * @return int|null return null if the created time is unavailable, otherwise return a timestamp
2b04c41c
SH
739 */
740 protected function get_item_time_created($item) {
55d95d90
AD
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 }
751
63e87951 752 /**
2b04c41c 753 * Returns an array of grades calculated by aggregating item ratings.
8c335cff
JF
754 *
755 * @param stdClass $options {
2b04c41c
SH
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]
2b04c41c 763 * contextid => int the context in which the rated items exist [optional]
2b04c41c
SH
764 * modulename => string the name of the module [optional]
765 * moduleid => int the id of the module instance [optional]
8c335cff 766 * }
2b04c41c
SH
767 * @return array the array of the user's grades
768 */
63e87951
AD
769 public function get_user_grades($options) {
770 global $DB;
771
772 $contextid = null;
773
2b04c41c
SH
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 }
780
63e87951
AD
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;
06807529 792 $moduleid = intval($options->moduleid);
3a11c09f 793
63e87951
AD
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
2b04c41c
SH
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";
63e87951
AD
802 $contextrecord = $DB->get_record_sql($sql, array('modulename'=>$modulename, 'moduleid'=>$moduleid), '*', MUST_EXIST);
803 $contextid = $contextrecord->ctxid;
804 }
805
806 $params = array();
2b04c41c
SH
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);
63e87951 814
06807529
AD
815 //if userid is not 0 we only want the grade for a single user
816 $singleuserwhere = '';
2b04c41c 817 if ($options->userid != 0) {
06807529
AD
818 $params['userid1'] = intval($options->userid);
819 $singleuserwhere = "AND i.{$itemtableusercolumn} = :userid1";
63b4eb05 820 }
63e87951 821
34ceda85
AD
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
06807529 825 $sql = "SELECT u.id as id, u.id AS userid, $aggregationstring(r.rating) AS rawgrade
2b04c41c
SH
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";
63e87951 834 $results = $DB->get_records_sql($sql, $params);
63b4eb05 835
63e87951 836 if ($results) {
63b4eb05
AD
837
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 }
853
63e87951
AD
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
63b4eb05
AD
864 if (!empty($scale) && $result->rawgrade > $max) {
865 $results[$rid]->rawgrade = $max;
63e87951
AD
866 }
867 }
868 }
869 }
63b4eb05 870
63e87951
AD
871 return $results;
872 }
873
874 /**
875 * Returns array of aggregate types. Used by ratings.
876 *
8c335cff 877 * @return array aggregate types
63e87951
AD
878 */
879 public function get_aggregate_types() {
2b04c41c
SH
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'));
63e87951
AD
886 }
887
a8e85df6 888 /**
2b04c41c 889 * Converts an aggregation method constant into something that can be included in SQL
8c335cff 890 *
2b04c41c
SH
891 * @param int $aggregate An aggregation constant. For example, RATING_AGGREGATE_AVERAGE.
892 * @return string an SQL aggregation method
893 */
63e87951 894 public function get_aggregation_method($aggregate) {
a8e85df6
AD
895 $aggregatestr = null;
896 switch($aggregate){
897 case RATING_AGGREGATE_AVERAGE:
898 $aggregatestr = 'AVG';
899 break;
900 case RATING_AGGREGATE_COUNT:
a6fb9d0d 901 $aggregatestr = 'COUNT';
a8e85df6
AD
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;
5cc4e210
EL
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);
a8e85df6
AD
915 }
916 return $aggregatestr;
917 }
d251b259 918
aeafd436 919 /**
2b04c41c 920 * Looks for a callback like forum_rating_permissions() to retrieve permissions from the plugin whose items are being rated
8c335cff 921 *
2b04c41c 922 * @param int $contextid The current context id
8c335cff
JF
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
2b04c41c
SH
925 * @return array rating related permissions
926 */
927 public function get_plugin_permissions_array($contextid, $component, $ratingarea) {
d251b259 928 $pluginpermissionsarray = null;
778361c3 929 $defaultpluginpermissions = array('rate'=>false,'view'=>false,'viewany'=>false,'viewall'=>false);//deny by default
2c2ff8d5
AD
930 if (!empty($component)) {
931 list($type, $name) = normalize_component($component);
2b04c41c 932 $pluginpermissionsarray = plugin_callback($type, $name, 'rating', 'permissions', array($contextid, $component, $ratingarea), $defaultpluginpermissions);
d251b259
AD
933 } else {
934 $pluginpermissionsarray = $defaultpluginpermissions;
935 }
936 return $pluginpermissionsarray;
937 }
aeafd436
AD
938
939 /**
2c2ff8d5 940 * Validates a submitted rating
8c335cff 941 *
2c2ff8d5
AD
942 * @param array $params submitted data
943 * context => object the context in which the rated items exists [required]
2b04c41c
SH
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]
2c2ff8d5
AD
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]
778361c3 951 * @return boolean true if the rating is valid. False if callback wasnt found and will throw rating_exception if rating is invalid
2c2ff8d5 952 */
2b04c41c
SH
953 public function check_rating_is_valid($params) {
954
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 }
aeafd436 973
2b04c41c
SH
974 list($plugintype, $pluginname) = normalize_component($params['component']);
975
976 //this looks for a function like forum_rating_validate() in mod_forum lib.php
2c2ff8d5 977 //wrapping the params array in another array as call_user_func_array() expands arrays into multiple arguments
778361c3 978 $isvalid = plugin_callback($plugintype, $pluginname, 'rating', 'validate', array($params), null);
3a11c09f 979
2c2ff8d5
AD
980 //if null then the callback doesn't exist
981 if ($isvalid === null) {
982 $isvalid = false;
2b04c41c 983 debugging('rating validation callback not found for component '. clean_param($component, PARAM_ALPHANUMEXT));
aeafd436 984 }
2c2ff8d5 985 return $isvalid;
aeafd436 986 }
2b04c41c
SH
987
988 /**
989 * Initialises JavaScript to enable AJAX ratings on the provided page
990 *
991 * @param moodle_page $page
8c335cff 992 * @return true always returns true
2b04c41c
SH
993 */
994 public function initialise_rating_javascript(moodle_page $page) {
995 global $CFG;
996
5e36e104
AD
997 //only needs to be initialized once
998 static $done = false;
999 if ($done) {
2b04c41c
SH
1000 return true;
1001 }
1002
1003 if (!empty($CFG->enableajax)) {
1004 $page->requires->js_init_call('M.core_rating.init');
1005 }
5e36e104 1006 $done = true;
2b04c41c 1007
2b04c41c
SH
1008 return true;
1009 }
1010
1011 /**
1012 * Returns a string that describes the aggregation method that was provided.
1013 *
1014 * @param string $aggregationmethod
8c335cff 1015 * @return string describes the aggregation method that was provided
2b04c41c
SH
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 }
1039
5cc4e210 1040}//end rating_manager class definition
778361c3 1041
8c335cff
JF
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 */
778361c3 1051class rating_exception extends moodle_exception {
8c335cff
JF
1052 /**
1053 * @var string The message to accompany the thrown exception
1054 */
778361c3 1055 public $message;
8c335cff
JF
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 */
778361c3
AD
1061 function __construct($errorcode) {
1062 $this->errorcode = $errorcode;
1063 $this->message = get_string($errorcode, 'error');
1064 }
1065}