Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]
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/>. 16 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 */ 25 26 define('RATING_UNSET_RATING', -999); 27 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); 34 35 define ('RATING_DEFAULT_SCALE', 5); 36 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 { 47 48 /** 49 * @var context The context in which this rating exists 50 */ 51 public $context; 52 53 /** 54 * @var string The component using ratings. For example "mod_forum" 55 */ 56 public $component; 57 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; 63 64 /** 65 * @var int The id of the item (forum post, glossary item etc) being rated 66 */ 67 public $itemid; 68 69 /** 70 * @var int The id scale (1-5, 0-100) that was in use when the rating was submitted 71 */ 72 public $scaleid; 73 74 /** 75 * @var int The id of the user who submitted the rating 76 */ 77 public $userid; 78 79 /** 80 * @var stdclass settings for this rating. Necessary to render the rating. 81 */ 82 public $settings; 83 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; 88 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; 93 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; 98 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; 103 104 /** 105 * @var int The time the associated item was created 106 */ 107 public $itemtimecreated = null; 108 109 /** 110 * @var int The id of the user who submitted the rating 111 */ 112 public $itemuserid = null; 113 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; 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 } 154 } 155 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; 163 164 $time = time(); 165 166 $data = new stdClass; 167 $data->rating = $rating; 168 $data->timemodified = $time; 169 170 $item = new stdclass(); 171 $item->id = $this->itemid; 172 $items = array($item); 173 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; 182 183 $rm = new rating_manager(); 184 $items = $rm->get_ratings($ratingoptions); 185 $firstitem = $items[0]->rating; 186 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 } 205 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 } 214 215 /** 216 * Returns this ratings aggregate value as a string. 217 * 218 * @return string ratings aggregate value 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 (is_numeric($aggregate) && $method != RATING_AGGREGATE_COUNT) { 228 if ($method != RATING_AGGREGATE_SUM && !$this->settings->scale->isnumeric) { 229 230 // Round aggregate as we're using it as an index. 231 $aggregatestr .= $this->settings->scale->scaleitems[round($aggregate)]; 232 } else { // Aggregation is SUM or the scale is numeric. 233 $aggregatestr .= round($aggregate, 1); 234 } 235 } 236 237 return $aggregatestr; 238 } 239 240 /** 241 * Returns true if the user is able to rate this rating object 242 * 243 * @param int $userid Current user assumed if left empty 244 * @return bool true if the user is able to rate this rating object 245 */ 246 public function user_can_rate($userid = null) { 247 if (empty($userid)) { 248 global $USER; 249 $userid = $USER->id; 250 } 251 // You can't rate your item. 252 if ($this->itemuserid == $userid) { 253 return false; 254 } 255 // You can't rate if you don't have the system cap. 256 if (!$this->settings->permissions->rate) { 257 return false; 258 } 259 // You can't rate if you don't have the plugin cap. 260 if (!$this->settings->pluginpermissions->rate) { 261 return false; 262 } 263 264 // You can't rate if the item was outside of the assessment times. 265 $timestart = $this->settings->assesstimestart; 266 $timefinish = $this->settings->assesstimefinish; 267 $timecreated = $this->itemtimecreated; 268 if (!empty($timestart) && !empty($timefinish) && ($timecreated < $timestart || $timecreated > $timefinish)) { 269 return false; 270 } 271 return true; 272 } 273 274 /** 275 * Returns true if the user is able to view the aggregate for this rating object. 276 * 277 * @param int|null $userid If left empty the current user is assumed. 278 * @return bool true if the user is able to view the aggregate for this rating object 279 */ 280 public function user_can_view_aggregate($userid = null) { 281 if (empty($userid)) { 282 global $USER; 283 $userid = $USER->id; 284 } 285 286 // 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. 287 // Note that viewany doesnt mean you can see the aggregate or ratings of your own items. 288 if ((empty($this->itemuserid) or $this->itemuserid != $userid) 289 && $this->settings->permissions->viewany 290 && $this->settings->pluginpermissions->viewany ) { 291 292 return true; 293 } 294 295 // If its the current user's item and they have permission to view the aggregate on their own items. 296 if ($this->itemuserid == $userid 297 && $this->settings->permissions->view 298 && $this->settings->pluginpermissions->view) { 299 300 return true; 301 } 302 303 return false; 304 } 305 306 /** 307 * Returns a URL to view all of the ratings for the item this rating is for. 308 * 309 * 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 310 * (this one included). 311 * 312 * @param bool $popup whether of not the URL should be loaded in a popup 313 * @return moodle_url URL to view all of the ratings for the item this rating is for. 314 */ 315 public function get_view_ratings_url($popup = false) { 316 $attributes = array( 317 'contextid' => $this->context->id, 318 'component' => $this->component, 319 'ratingarea' => $this->ratingarea, 320 'itemid' => $this->itemid, 321 'scaleid' => $this->settings->scale->id 322 ); 323 if ($popup) { 324 $attributes['popup'] = 1; 325 } 326 return new moodle_url('/rating/index.php', $attributes); 327 } 328 329 /** 330 * Returns a URL that can be used to rate the associated item. 331 * 332 * @param int|null $rating The rating to give the item, if null then no rating param is added. 333 * @param moodle_url|string $returnurl The URL to return to. 334 * @return moodle_url can be used to rate the associated item. 335 */ 336 public function get_rate_url($rating = null, $returnurl = null) { 337 if (empty($returnurl)) { 338 if (!empty($this->settings->returnurl)) { 339 $returnurl = $this->settings->returnurl; 340 } else { 341 global $PAGE; 342 $returnurl = $PAGE->url; 343 } 344 } 345 $args = array( 346 'contextid' => $this->context->id, 347 'component' => $this->component, 348 'ratingarea' => $this->ratingarea, 349 'itemid' => $this->itemid, 350 'scaleid' => $this->settings->scale->id, 351 'returnurl' => $returnurl, 352 'rateduserid' => $this->itemuserid, 353 'aggregation' => $this->settings->aggregationmethod, 354 'sesskey' => sesskey() 355 ); 356 if (!empty($rating)) { 357 $args['rating'] = $rating; 358 } 359 $url = new moodle_url('/rating/rate.php', $args); 360 return $url; 361 } 362 363 } // End rating class definition. 364 365 /** 366 * The rating_manager class provides the ability to retrieve sets of ratings from the database 367 * 368 * @package core_rating 369 * @category rating 370 * @copyright 2010 Andrew Davis 371 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 372 * @since Moodle 2.0 373 */ 374 class rating_manager { 375 376 /** 377 * @var array An array of calculated scale options to save us generating them for each request. 378 */ 379 protected $scales = array(); 380 381 /** 382 * Delete one or more ratings. Specify either a rating id, an item id or just the context id. 383 * 384 * @global moodle_database $DB 385 * @param stdClass $options { 386 * contextid => int the context in which the ratings exist [required] 387 * ratingid => int the id of an individual rating to delete [optional] 388 * userid => int delete the ratings submitted by this user. May be used in conjuction with itemid [optional] 389 * itemid => int delete all ratings attached to this item [optional] 390 * component => string The component to delete ratings from [optional] 391 * ratingarea => string The ratingarea to delete ratings from [optional] 392 * } 393 */ 394 public function delete_ratings($options) { 395 global $DB; 396 397 if (empty($options->contextid)) { 398 throw new coding_exception('The context option is a required option when deleting ratings.'); 399 } 400 401 $conditions = array('contextid' => $options->contextid); 402 $possibleconditions = array( 403 'ratingid' => 'id', 404 'userid' => 'userid', 405 'itemid' => 'itemid', 406 'component' => 'component', 407 'ratingarea' => 'ratingarea' 408 ); 409 foreach ($possibleconditions as $option => $field) { 410 if (isset($options->{$option})) { 411 $conditions[$field] = $options->{$option}; 412 } 413 } 414 $DB->delete_records('rating', $conditions); 415 } 416 417 /** 418 * Returns an array of ratings for a given item (forum post, glossary entry etc). 419 * 420 * This returns all users ratings for a single item 421 * 422 * @param stdClass $options { 423 * context => context the context in which the ratings exists [required] 424 * component => component using ratings ie mod_forum [required] 425 * ratingarea => ratingarea to associate this rating with [required] 426 * itemid => int the id of the associated item (forum post, glossary item etc) [required] 427 * sort => string SQL sort by clause [optional] 428 * } 429 * @return array an array of ratings 430 */ 431 public function get_all_ratings_for_item($options) { 432 global $DB; 433 434 if (!isset($options->context)) { 435 throw new coding_exception('The context option is a required option when getting ratings for an item.'); 436 } 437 if (!isset($options->itemid)) { 438 throw new coding_exception('The itemid option is a required option when getting ratings for an item.'); 439 } 440 if (!isset($options->component)) { 441 throw new coding_exception('The component option is now a required option when getting ratings for an item.'); 442 } 443 if (!isset($options->ratingarea)) { 444 throw new coding_exception('The ratingarea option is now a required option when getting ratings for an item.'); 445 } 446 447 $sortclause = ''; 448 if (!empty($options->sort)) { 449 $sortclause = "ORDER BY $options->sort"; 450 } 451 452 $params = array( 453 'contextid' => $options->context->id, 454 'itemid' => $options->itemid, 455 'component' => $options->component, 456 'ratingarea' => $options->ratingarea, 457 ); 458 $userfieldsapi = \core_user\fields::for_userpic(); 459 $userfields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects; 460 $sql = "SELECT r.id, r.rating, r.itemid, r.userid, r.timemodified, r.component, r.ratingarea, $userfields 461 FROM {rating} r 462 LEFT JOIN {user} u ON r.userid = u.id 463 WHERE r.contextid = :contextid AND 464 r.itemid = :itemid AND 465 r.component = :component AND 466 r.ratingarea = :ratingarea 467 {$sortclause}"; 468 469 return $DB->get_records_sql($sql, $params); 470 } 471 472 /** 473 * Adds rating objects to an array of items (forum posts, glossary entries etc). Rating objects are available at $item->rating 474 * 475 * @param stdClass $options { 476 * context => context the context in which the ratings exists [required] 477 * component => the component name ie mod_forum [required] 478 * ratingarea => the ratingarea we are interested in [required] 479 * items => array items like forum posts or glossary items. Each item needs an 'id' ie $items[0]->id [required] 480 * aggregate => int aggregation method to apply. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required] 481 * scaleid => int the scale from which the user can select a rating [required] 482 * userid => int the id of the current user [optional] 483 * returnurl => string the url to return the user to after submitting a rating. Null for ajax requests [optional] 484 * assesstimestart => int only allow rating of items created after this timestamp [optional] 485 * assesstimefinish => int only allow rating of items created before this timestamp [optional] 486 * @return array the array of items with their ratings attached at $items[0]->rating 487 */ 488 public function get_ratings($options) { 489 global $DB, $USER; 490 491 if (!isset($options->context)) { 492 throw new coding_exception('The context option is a required option when getting ratings.'); 493 } 494 495 if (!isset($options->component)) { 496 throw new coding_exception('The component option is a required option when getting ratings.'); 497 } 498 499 if (!isset($options->ratingarea)) { 500 throw new coding_exception('The ratingarea option is a required option when getting ratings.'); 501 } 502 503 if (!isset($options->scaleid)) { 504 throw new coding_exception('The scaleid option is a required option when getting ratings.'); 505 } 506 507 if (!isset($options->items)) { 508 throw new coding_exception('The items option is a required option when getting ratings.'); 509 } else if (empty($options->items)) { 510 return array(); 511 } 512 513 if (!isset($options->aggregate)) { 514 throw new coding_exception('The aggregate option is a required option when getting ratings.'); 515 } else if ($options->aggregate == RATING_AGGREGATE_NONE) { 516 // Ratings are not enabled. 517 return $options->items; 518 } 519 520 // Ensure average aggregation returns float. 521 $aggregatestr = $this->get_aggregation_method($options->aggregate); 522 $aggregatefield = 'r.rating'; 523 if ($aggregatestr === 'AVG') { 524 $aggregatefield = "1.0 * {$aggregatefield}"; 525 } 526 527 // Default the userid to the current user if it is not set. 528 if (empty($options->userid)) { 529 $userid = $USER->id; 530 } else { 531 $userid = $options->userid; 532 } 533 534 // Get the item table name, the item id field, and the item user field for the given rating item 535 // from the related component. 536 list($type, $name) = core_component::normalize_component($options->component); 537 $default = array(null, 'id', 'userid'); 538 list($itemtablename, $itemidcol, $itemuseridcol) = plugin_callback($type, 539 $name, 540 'rating', 541 'get_item_fields', 542 array($options), 543 $default); 544 545 // Create an array of item IDs. 546 $itemids = array(); 547 foreach ($options->items as $item) { 548 $itemids[] = $item->{$itemidcol}; 549 } 550 551 // Get the items from the database. 552 list($itemidtest, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED); 553 $params['contextid'] = $options->context->id; 554 $params['userid'] = $userid; 555 $params['component'] = $options->component; 556 $params['ratingarea'] = $options->ratingarea; 557 558 $sql = "SELECT r.id, r.itemid, r.userid, r.scaleid, r.rating AS usersrating 559 FROM {rating} r 560 WHERE r.userid = :userid AND 561 r.contextid = :contextid AND 562 r.itemid {$itemidtest} AND 563 r.component = :component AND 564 r.ratingarea = :ratingarea 565 ORDER BY r.itemid"; 566 $userratings = $DB->get_records_sql($sql, $params); 567 568 $sql = "SELECT r.itemid, {$aggregatestr}({$aggregatefield}) AS aggrrating, COUNT(r.rating) AS numratings 569 FROM {rating} r 570 WHERE r.contextid = :contextid AND 571 r.itemid {$itemidtest} AND 572 r.component = :component AND 573 r.ratingarea = :ratingarea 574 GROUP BY r.itemid, r.component, r.ratingarea, r.contextid 575 ORDER BY r.itemid"; 576 $aggregateratings = $DB->get_records_sql($sql, $params); 577 578 $ratingoptions = new stdClass; 579 $ratingoptions->context = $options->context; 580 $ratingoptions->component = $options->component; 581 $ratingoptions->ratingarea = $options->ratingarea; 582 $ratingoptions->settings = $this->generate_rating_settings_object($options); 583 foreach ($options->items as $item) { 584 $founduserrating = false; 585 foreach ($userratings as $userrating) { 586 // Look for an existing rating from this user of this item. 587 if ($item->{$itemidcol} == $userrating->itemid) { 588 // Note: rec->scaleid = the id of scale at the time the rating was submitted. 589 // It may be different from the current scale id. 590 $ratingoptions->scaleid = $userrating->scaleid; 591 $ratingoptions->userid = $userrating->userid; 592 $ratingoptions->id = $userrating->id; 593 $ratingoptions->rating = min($userrating->usersrating, $ratingoptions->settings->scale->max); 594 595 $founduserrating = true; 596 break; 597 } 598 } 599 if (!$founduserrating) { 600 $ratingoptions->scaleid = null; 601 $ratingoptions->userid = null; 602 $ratingoptions->id = null; 603 $ratingoptions->rating = null; 604 } 605 606 if (array_key_exists($item->{$itemidcol}, $aggregateratings)) { 607 $rec = $aggregateratings[$item->{$itemidcol}]; 608 $ratingoptions->itemid = $item->{$itemidcol}; 609 $ratingoptions->aggregate = min($rec->aggrrating, $ratingoptions->settings->scale->max); 610 $ratingoptions->count = $rec->numratings; 611 } else { 612 $ratingoptions->itemid = $item->{$itemidcol}; 613 $ratingoptions->aggregate = null; 614 $ratingoptions->count = 0; 615 } 616 617 $rating = new rating($ratingoptions); 618 $rating->itemtimecreated = $this->get_item_time_created($item); 619 if (!empty($item->{$itemuseridcol})) { 620 $rating->itemuserid = $item->{$itemuseridcol}; 621 } 622 $item->rating = $rating; 623 } 624 625 return $options->items; 626 } 627 628 /** 629 * Generates a rating settings object based upon the options it is provided. 630 * 631 * @param stdClass $options { 632 * context => context the context in which the ratings exists [required] 633 * component => string The component the items belong to [required] 634 * ratingarea => string The ratingarea the items belong to [required] 635 * aggregate => int Aggregation method to apply. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required] 636 * scaleid => int the scale from which the user can select a rating [required] 637 * returnurl => string the url to return the user to after submitting a rating. Null for ajax requests [optional] 638 * assesstimestart => int only allow rating of items created after this timestamp [optional] 639 * assesstimefinish => int only allow rating of items created before this timestamp [optional] 640 * plugintype => string plugin type ie 'mod' Used to find the permissions callback [optional] 641 * pluginname => string plugin name ie 'forum' Used to find the permissions callback [optional] 642 * } 643 * @return stdClass rating settings object 644 */ 645 protected function generate_rating_settings_object($options) { 646 647 if (!isset($options->context)) { 648 throw new coding_exception('The context option is a required option when generating a rating settings object.'); 649 } 650 if (!isset($options->component)) { 651 throw new coding_exception('The component option is now a required option when generating a rating settings object.'); 652 } 653 if (!isset($options->ratingarea)) { 654 throw new coding_exception('The ratingarea option is now a required option when generating a rating settings object.'); 655 } 656 if (!isset($options->aggregate)) { 657 throw new coding_exception('The aggregate option is now a required option when generating a rating settings object.'); 658 } 659 if (!isset($options->scaleid)) { 660 throw new coding_exception('The scaleid option is now a required option when generating a rating settings object.'); 661 } 662 663 // Settings that are common to all ratings objects in this context. 664 $settings = new stdClass; 665 $settings->scale = $this->generate_rating_scale_object($options->scaleid); // The scale to use now. 666 $settings->aggregationmethod = $options->aggregate; 667 $settings->assesstimestart = null; 668 $settings->assesstimefinish = null; 669 670 // Collect options into the settings object. 671 if (!empty($options->assesstimestart)) { 672 $settings->assesstimestart = $options->assesstimestart; 673 } 674 if (!empty($options->assesstimefinish)) { 675 $settings->assesstimefinish = $options->assesstimefinish; 676 } 677 if (!empty($options->returnurl)) { 678 $settings->returnurl = $options->returnurl; 679 } 680 681 // Check site capabilities. 682 $settings->permissions = new stdClass; 683 // Can view the aggregate of ratings of their own items. 684 $settings->permissions->view = has_capability('moodle/rating:view', $options->context); 685 // Can view the aggregate of ratings of other people's items. 686 $settings->permissions->viewany = has_capability('moodle/rating:viewany', $options->context); 687 // Can view individual ratings. 688 $settings->permissions->viewall = has_capability('moodle/rating:viewall', $options->context); 689 // Can submit ratings. 690 $settings->permissions->rate = has_capability('moodle/rating:rate', $options->context); 691 692 // Check module capabilities 693 // This is mostly for backwards compatability with old modules that previously implemented their own ratings. 694 $pluginpermissionsarray = $this->get_plugin_permissions_array($options->context->id, 695 $options->component, 696 $options->ratingarea); 697 $settings->pluginpermissions = new stdClass; 698 $settings->pluginpermissions->view = $pluginpermissionsarray['view']; 699 $settings->pluginpermissions->viewany = $pluginpermissionsarray['viewany']; 700 $settings->pluginpermissions->viewall = $pluginpermissionsarray['viewall']; 701 $settings->pluginpermissions->rate = $pluginpermissionsarray['rate']; 702 703 return $settings; 704 } 705 706 /** 707 * Generates a scale object that can be returned 708 * 709 * @global moodle_database $DB moodle database object 710 * @param int $scaleid scale-type identifier 711 * @return stdClass scale for ratings 712 */ 713 protected function generate_rating_scale_object($scaleid) { 714 global $DB; 715 if (!array_key_exists('s'.$scaleid, $this->scales)) { 716 $scale = new stdClass; 717 $scale->id = $scaleid; 718 $scale->name = null; 719 $scale->courseid = null; 720 $scale->scaleitems = array(); 721 $scale->isnumeric = true; 722 $scale->max = $scaleid; 723 724 if ($scaleid < 0) { 725 // It is a proper scale (not numeric). 726 $scalerecord = $DB->get_record('scale', array('id' => abs($scaleid))); 727 if ($scalerecord) { 728 // We need to generate an array with string keys starting at 1. 729 $scalearray = explode(',', $scalerecord->scale); 730 $c = count($scalearray); 731 for ($i = 0; $i < $c; $i++) { 732 // Treat index as a string to allow sorting without changing the value. 733 $scale->scaleitems[(string)($i + 1)] = $scalearray[$i]; 734 } 735 krsort($scale->scaleitems); // Have the highest grade scale item appear first. 736 $scale->isnumeric = false; 737 $scale->name = $scalerecord->name; 738 $scale->courseid = $scalerecord->courseid; 739 $scale->max = count($scale->scaleitems); 740 } 741 } else { 742 // Generate an array of values for numeric scales. 743 for ($i = 0; $i <= (int)$scaleid; $i++) { 744 $scale->scaleitems[(string)$i] = $i; 745 } 746 } 747 $this->scales['s'.$scaleid] = $scale; 748 } 749 return $this->scales['s'.$scaleid]; 750 } 751 752 /** 753 * Gets the time the given item was created 754 * 755 * TODO: MDL-31511 - Find a better solution for this, its not ideal to test for fields really we should be 756 * asking the component the item belongs to what field to look for or even the value we 757 * are looking for. 758 * 759 * @param stdClass $item 760 * @return int|null return null if the created time is unavailable, otherwise return a timestamp 761 */ 762 protected function get_item_time_created($item) { 763 if (!empty($item->created)) { 764 return $item->created; // The forum_posts table has created instead of timecreated. 765 } else if (!empty($item->timecreated)) { 766 return $item->timecreated; 767 } else { 768 return null; 769 } 770 } 771 772 /** 773 * Returns an array of grades calculated by aggregating item ratings. 774 * 775 * @param stdClass $options { 776 * userid => int the id of the user whose items were rated, NOT the user who submitted ratings. 0 to update all. [required] 777 * aggregationmethod => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required] 778 * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required] 779 * itemtable => int the table containing the items [required] 780 * itemtableusercolum => int the column of the user table containing the item owner's user id [required] 781 * component => The component for the ratings [required] 782 * ratingarea => The ratingarea for the ratings [required] 783 * contextid => int the context in which the rated items exist [optional] 784 * modulename => string the name of the module [optional] 785 * moduleid => int the id of the module instance [optional] 786 * } 787 * @return array the array of the user's grades 788 */ 789 public function get_user_grades($options) { 790 global $DB; 791 792 $contextid = null; 793 794 if (!isset($options->component)) { 795 throw new coding_exception('The component option is now a required option when getting user grades from ratings.'); 796 } 797 if (!isset($options->ratingarea)) { 798 throw new coding_exception('The ratingarea option is now a required option when getting user grades from ratings.'); 799 } 800 801 // If the calling code doesn't supply a context id we'll have to figure it out. 802 if (!empty($options->contextid)) { 803 $contextid = $options->contextid; 804 } else if (!empty($options->modulename) && !empty($options->moduleid)) { 805 $modulename = $options->modulename; 806 $moduleid = intval($options->moduleid); 807 808 // Going direct to the db for the context id seems wrong. 809 $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); 810 $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel)"; 811 $sql = "SELECT cm.* $ctxselect 812 FROM {course_modules} cm 813 LEFT JOIN {modules} mo ON mo.id = cm.module 814 LEFT JOIN {{$modulename}} m ON m.id = cm.instance $ctxjoin 815 WHERE mo.name=:modulename AND 816 m.id=:moduleid"; 817 $params = array('modulename' => $modulename, 'moduleid' => $moduleid, 'contextlevel' => CONTEXT_MODULE); 818 $contextrecord = $DB->get_record_sql($sql, $params, '*', MUST_EXIST); 819 $contextid = $contextrecord->ctxid; 820 } 821 822 $params = array(); 823 $params['contextid'] = $contextid; 824 $params['component'] = $options->component; 825 $params['ratingarea'] = $options->ratingarea; 826 $itemtable = $options->itemtable; 827 $itemtableusercolumn = $options->itemtableusercolumn; 828 $scaleid = $options->scaleid; 829 830 // Ensure average aggregation returns float. 831 $aggregationstring = $this->get_aggregation_method($options->aggregationmethod); 832 $aggregationfield = 'r.rating'; 833 if ($aggregationstring === 'AVG') { 834 $aggregationfield = "1.0 * {$aggregationfield}"; 835 } 836 837 // If userid is not 0 we only want the grade for a single user. 838 $singleuserwhere = ''; 839 if ($options->userid != 0) { 840 $params['userid1'] = intval($options->userid); 841 $singleuserwhere = "AND i.{$itemtableusercolumn} = :userid1"; 842 } 843 844 // MDL-24648 The where line used to be "WHERE (r.contextid is null or r.contextid=:contextid)". 845 // r.contextid will be null for users who haven't been rated yet. 846 // No longer including users who haven't been rated to reduce memory requirements. 847 $sql = "SELECT u.id as id, u.id AS userid, {$aggregationstring}({$aggregationfield}) AS rawgrade 848 FROM {user} u 849 LEFT JOIN {{$itemtable}} i ON u.id=i.{$itemtableusercolumn} 850 LEFT JOIN {rating} r ON r.itemid=i.id 851 WHERE r.contextid = :contextid AND 852 r.component = :component AND 853 r.ratingarea = :ratingarea 854 $singleuserwhere 855 GROUP BY u.id"; 856 $results = $DB->get_records_sql($sql, $params); 857 858 if ($results) { 859 860 $scale = null; 861 $max = 0; 862 if ($options->scaleid >= 0) { 863 // Numeric. 864 $max = $options->scaleid; 865 } else { 866 // Custom scales. 867 $scale = $DB->get_record('scale', array('id' => -$options->scaleid)); 868 if ($scale) { 869 $scale = explode(',', $scale->scale); 870 $max = count($scale); 871 } else { 872 debugging('rating_manager::get_user_grades() received a scale ID that doesnt exist'); 873 } 874 } 875 876 // It could throw off the grading if count and sum returned a rawgrade higher than scale 877 // so to prevent it we review the results and ensure that rawgrade does not exceed the scale. 878 // If it does we set rawgrade = scale (i.e. full credit). 879 foreach ($results as $rid => $result) { 880 if ($options->scaleid >= 0) { 881 // Numeric. 882 if ($result->rawgrade > $options->scaleid) { 883 $results[$rid]->rawgrade = $options->scaleid; 884 } 885 } else { 886 // Scales. 887 if (!empty($scale) && $result->rawgrade > $max) { 888 $results[$rid]->rawgrade = $max; 889 } 890 } 891 } 892 } 893 894 return $results; 895 } 896 897 /** 898 * Returns array of aggregate types. Used by ratings. 899 * 900 * @return array aggregate types 901 */ 902 public function get_aggregate_types() { 903 return array (RATING_AGGREGATE_NONE => get_string('aggregatenone', 'rating'), 904 RATING_AGGREGATE_AVERAGE => get_string('aggregateavg', 'rating'), 905 RATING_AGGREGATE_COUNT => get_string('aggregatecount', 'rating'), 906 RATING_AGGREGATE_MAXIMUM => get_string('aggregatemax', 'rating'), 907 RATING_AGGREGATE_MINIMUM => get_string('aggregatemin', 'rating'), 908 RATING_AGGREGATE_SUM => get_string('aggregatesum', 'rating')); 909 } 910 911 /** 912 * Converts an aggregation method constant into something that can be included in SQL 913 * 914 * @param int $aggregate An aggregation constant. For example, RATING_AGGREGATE_AVERAGE. 915 * @return string an SQL aggregation method 916 */ 917 public function get_aggregation_method($aggregate) { 918 $aggregatestr = null; 919 switch($aggregate){ 920 case RATING_AGGREGATE_AVERAGE: 921 $aggregatestr = 'AVG'; 922 break; 923 case RATING_AGGREGATE_COUNT: 924 $aggregatestr = 'COUNT'; 925 break; 926 case RATING_AGGREGATE_MAXIMUM: 927 $aggregatestr = 'MAX'; 928 break; 929 case RATING_AGGREGATE_MINIMUM: 930 $aggregatestr = 'MIN'; 931 break; 932 case RATING_AGGREGATE_SUM: 933 $aggregatestr = 'SUM'; 934 break; 935 default: 936 $aggregatestr = 'AVG'; // Default to this to avoid real breakage - MDL-22270. 937 debugging('Incorrect call to get_aggregation_method(), incorrect aggregate method ' . $aggregate, DEBUG_DEVELOPER); 938 } 939 return $aggregatestr; 940 } 941 942 /** 943 * Looks for a callback like forum_rating_permissions() to retrieve permissions from the plugin whose items are being rated 944 * 945 * @param int $contextid The current context id 946 * @param string $component the name of the component that is using ratings ie 'mod_forum' 947 * @param string $ratingarea The area the rating is associated with 948 * @return array rating related permissions 949 */ 950 public function get_plugin_permissions_array($contextid, $component, $ratingarea) { 951 $pluginpermissionsarray = null; 952 // Deny by default. 953 $defaultpluginpermissions = array('rate' => false, 'view' => false, 'viewany' => false, 'viewall' => false); 954 if (!empty($component)) { 955 list($type, $name) = core_component::normalize_component($component); 956 $pluginpermissionsarray = plugin_callback($type, 957 $name, 958 'rating', 959 'permissions', 960 array($contextid, $component, $ratingarea), 961 $defaultpluginpermissions); 962 } else { 963 $pluginpermissionsarray = $defaultpluginpermissions; 964 } 965 return $pluginpermissionsarray; 966 } 967 968 /** 969 * Validates a submitted rating 970 * 971 * @param array $params submitted data 972 * context => object the context in which the rated items exists [required] 973 * component => The component the rating belongs to [required] 974 * ratingarea => The ratingarea the rating is associated with [required] 975 * itemid => int the ID of the object being rated [required] 976 * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required] 977 * rating => int the submitted rating 978 * rateduserid => int the id of the user whose items have been rated. 0 to update all. [required] 979 * aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [optional] 980 * @return boolean true if the rating is valid, false if callback not found, throws rating_exception if rating is invalid 981 */ 982 public function check_rating_is_valid($params) { 983 984 if (!isset($params['context'])) { 985 throw new coding_exception('The context option is a required option when checking rating validity.'); 986 } 987 if (!isset($params['component'])) { 988 throw new coding_exception('The component option is now a required option when checking rating validity'); 989 } 990 if (!isset($params['ratingarea'])) { 991 throw new coding_exception('The ratingarea option is now a required option when checking rating validity'); 992 } 993 if (!isset($params['itemid'])) { 994 throw new coding_exception('The itemid option is now a required option when checking rating validity'); 995 } 996 if (!isset($params['scaleid'])) { 997 throw new coding_exception('The scaleid option is now a required option when checking rating validity'); 998 } 999 if (!isset($params['rateduserid'])) { 1000 throw new coding_exception('The rateduserid option is now a required option when checking rating validity'); 1001 } 1002 1003 list($plugintype, $pluginname) = core_component::normalize_component($params['component']); 1004 1005 // This looks for a function like forum_rating_validate() in mod_forum lib.php 1006 // wrapping the params array in another array as call_user_func_array() expands arrays into multiple arguments. 1007 $isvalid = plugin_callback($plugintype, $pluginname, 'rating', 'validate', array($params), null); 1008 1009 // If null then the callback does not exist. 1010 if ($isvalid === null) { 1011 $isvalid = false; 1012 debugging('rating validation callback not found for component '. clean_param($component, PARAM_ALPHANUMEXT)); 1013 } 1014 return $isvalid; 1015 } 1016 1017 /** 1018 * Initialises JavaScript to enable AJAX ratings on the provided page 1019 * 1020 * @param moodle_page $page 1021 * @return true always returns true 1022 */ 1023 public function initialise_rating_javascript(moodle_page $page) { 1024 global $CFG; 1025 1026 // Only needs to be initialized once. 1027 static $done = false; 1028 if ($done) { 1029 return true; 1030 } 1031 1032 $page->requires->js_init_call('M.core_rating.init'); 1033 $done = true; 1034 1035 return true; 1036 } 1037 1038 /** 1039 * Returns a string that describes the aggregation method that was provided. 1040 * 1041 * @param string $aggregationmethod 1042 * @return string describes the aggregation method that was provided 1043 */ 1044 public function get_aggregate_label($aggregationmethod) { 1045 $aggregatelabel = ''; 1046 switch ($aggregationmethod) { 1047 case RATING_AGGREGATE_AVERAGE : 1048 $aggregatelabel .= get_string("aggregateavg", "rating"); 1049 break; 1050 case RATING_AGGREGATE_COUNT : 1051 $aggregatelabel .= get_string("aggregatecount", "rating"); 1052 break; 1053 case RATING_AGGREGATE_MAXIMUM : 1054 $aggregatelabel .= get_string("aggregatemax", "rating"); 1055 break; 1056 case RATING_AGGREGATE_MINIMUM : 1057 $aggregatelabel .= get_string("aggregatemin", "rating"); 1058 break; 1059 case RATING_AGGREGATE_SUM : 1060 $aggregatelabel .= get_string("aggregatesum", "rating"); 1061 break; 1062 } 1063 $aggregatelabel .= get_string('labelsep', 'langconfig'); 1064 return $aggregatelabel; 1065 } 1066 1067 /** 1068 * Adds a new rating 1069 * 1070 * @param stdClass $cm course module object 1071 * @param stdClass $context context object 1072 * @param string $component component name 1073 * @param string $ratingarea rating area 1074 * @param int $itemid the item id 1075 * @param int $scaleid the scale id 1076 * @param int $userrating the user rating 1077 * @param int $rateduserid the rated user id 1078 * @param int $aggregationmethod the aggregation method 1079 * @since Moodle 3.2 1080 */ 1081 public function add_rating($cm, $context, $component, $ratingarea, $itemid, $scaleid, $userrating, $rateduserid, 1082 $aggregationmethod) { 1083 global $CFG, $DB, $USER; 1084 1085 $result = new stdClass; 1086 // Check the module rating permissions. 1087 // Doing this check here rather than within rating_manager::get_ratings() so we can return a error response. 1088 $pluginpermissionsarray = $this->get_plugin_permissions_array($context->id, $component, $ratingarea); 1089 1090 if (!$pluginpermissionsarray['rate']) { 1091 $result->error = 'ratepermissiondenied'; 1092 return $result; 1093 } else { 1094 $params = array( 1095 'context' => $context, 1096 'component' => $component, 1097 'ratingarea' => $ratingarea, 1098 'itemid' => $itemid, 1099 'scaleid' => $scaleid, 1100 'rating' => $userrating, 1101 'rateduserid' => $rateduserid, 1102 'aggregation' => $aggregationmethod 1103 ); 1104 if (!$this->check_rating_is_valid($params)) { 1105 $result->error = 'ratinginvalid'; 1106 return $result; 1107 } 1108 } 1109 1110 // Rating options used to update the rating then retrieve the aggregate. 1111 $ratingoptions = new stdClass; 1112 $ratingoptions->context = $context; 1113 $ratingoptions->ratingarea = $ratingarea; 1114 $ratingoptions->component = $component; 1115 $ratingoptions->itemid = $itemid; 1116 $ratingoptions->scaleid = $scaleid; 1117 $ratingoptions->userid = $USER->id; 1118 1119 if ($userrating != RATING_UNSET_RATING) { 1120 $rating = new rating($ratingoptions); 1121 $rating->update_rating($userrating); 1122 } else { // Delete the rating if the user set to "Rate..." 1123 $options = new stdClass; 1124 $options->contextid = $context->id; 1125 $options->component = $component; 1126 $options->ratingarea = $ratingarea; 1127 $options->userid = $USER->id; 1128 $options->itemid = $itemid; 1129 1130 $this->delete_ratings($options); 1131 } 1132 1133 // Future possible enhancement: add a setting to turn grade updating off for those who don't want them in gradebook. 1134 // Note that this would need to be done in both rate.php and rate_ajax.php. 1135 if ($context->contextlevel == CONTEXT_MODULE) { 1136 // Tell the module that its grades have changed. 1137 $modinstance = $DB->get_record($cm->modname, array('id' => $cm->instance)); 1138 if ($modinstance) { 1139 $modinstance->cmidnumber = $cm->id; // MDL-12961. 1140 $functionname = $cm->modname.'_update_grades'; 1141 require_once($CFG->dirroot."/mod/{$cm->modname}/lib.php"); 1142 if (function_exists($functionname)) { 1143 $functionname($modinstance, $rateduserid); 1144 } 1145 } 1146 } 1147 1148 // Object to return to client as JSON. 1149 $result->success = true; 1150 1151 // Need to retrieve the updated item to get its new aggregate value. 1152 $item = new stdClass; 1153 $item->id = $itemid; 1154 1155 // Most of $ratingoptions variables were previously set. 1156 $ratingoptions->items = array($item); 1157 $ratingoptions->aggregate = $aggregationmethod; 1158 1159 $items = $this->get_ratings($ratingoptions); 1160 $firstrating = $items[0]->rating; 1161 1162 // See if the user has permission to see the rating aggregate. 1163 if ($firstrating->user_can_view_aggregate()) { 1164 1165 // For custom scales return text not the value. 1166 // This scales weirdness will go away when scales are refactored. 1167 $scalearray = null; 1168 $aggregatetoreturn = round($firstrating->aggregate, 1); 1169 1170 // Output a dash if aggregation method == COUNT as the count is output next to the aggregate anyway. 1171 if ($firstrating->settings->aggregationmethod == RATING_AGGREGATE_COUNT or $firstrating->count == 0) { 1172 $aggregatetoreturn = ' - '; 1173 } else if ($firstrating->settings->scale->id < 0) { // If its non-numeric scale. 1174 // Dont use the scale item if the aggregation method is sum as adding items from a custom scale makes no sense. 1175 if ($firstrating->settings->aggregationmethod != RATING_AGGREGATE_SUM) { 1176 $scalerecord = $DB->get_record('scale', array('id' => -$firstrating->settings->scale->id)); 1177 if ($scalerecord) { 1178 $scalearray = explode(',', $scalerecord->scale); 1179 $aggregatetoreturn = $scalearray[$aggregatetoreturn - 1]; 1180 } 1181 } 1182 } 1183 1184 $result->aggregate = $aggregatetoreturn; 1185 $result->count = $firstrating->count; 1186 $result->itemid = $itemid; 1187 } 1188 return $result; 1189 } 1190 1191 /** 1192 * Get ratings created since a given time. 1193 * 1194 * @param stdClass $context context object 1195 * @param string $component component name 1196 * @param int $since the time to check 1197 * @return array list of ratings db records since the given timelimit 1198 * @since Moodle 3.2 1199 */ 1200 public function get_component_ratings_since($context, $component, $since) { 1201 global $DB, $USER; 1202 1203 $ratingssince = array(); 1204 $where = 'contextid = ? AND component = ? AND (timecreated > ? OR timemodified > ?)'; 1205 $ratings = $DB->get_records_select('rating', $where, array($context->id, $component, $since, $since)); 1206 // Check area by area if we have permissions. 1207 $permissions = array(); 1208 $rm = new rating_manager(); 1209 1210 foreach ($ratings as $rating) { 1211 // Check if the permission array for the area is cached. 1212 if (!isset($permissions[$rating->ratingarea])) { 1213 $permissions[$rating->ratingarea] = $rm->get_plugin_permissions_array($context->id, $component, 1214 $rating->ratingarea); 1215 } 1216 1217 if (($permissions[$rating->ratingarea]['view'] and $rating->userid == $USER->id) or 1218 ($permissions[$rating->ratingarea]['viewany'] or $permissions[$rating->ratingarea]['viewall'])) { 1219 $ratingssince[$rating->id] = $rating; 1220 } 1221 } 1222 return $ratingssince; 1223 } 1224 } // End rating_manager class definition. 1225 1226 /** 1227 * The rating_exception class for exceptions specific to the ratings system 1228 * 1229 * @package core_rating 1230 * @category rating 1231 * @copyright 2010 Andrew Davis 1232 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1233 * @since Moodle 2.0 1234 */ 1235 class rating_exception extends moodle_exception { 1236 /** 1237 * @var string The message to accompany the thrown exception 1238 */ 1239 public $message; 1240 /** 1241 * Generate exceptions that can be easily identified as coming from the ratings system 1242 * 1243 * @param string $errorcode the error code to generate 1244 */ 1245 public function __construct($errorcode) { 1246 $this->errorcode = $errorcode; 1247 $this->message = get_string($errorcode, 'error'); 1248 } 1249 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body