Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
/rating/ -> lib.php (source)

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   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 stdClass 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          $aggregatestr = $this->get_aggregation_method($options->aggregate);
 520  
 521          // Default the userid to the current user if it is not set.
 522          if (empty($options->userid)) {
 523              $userid = $USER->id;
 524          } else {
 525              $userid = $options->userid;
 526          }
 527  
 528          // Get the item table name, the item id field, and the item user field for the given rating item
 529          // from the related component.
 530          list($type, $name) = core_component::normalize_component($options->component);
 531          $default = array(null, 'id', 'userid');
 532          list($itemtablename, $itemidcol, $itemuseridcol) = plugin_callback($type,
 533                                                                             $name,
 534                                                                             'rating',
 535                                                                             'get_item_fields',
 536                                                                             array($options),
 537                                                                             $default);
 538  
 539          // Create an array of item IDs.
 540          $itemids = array();
 541          foreach ($options->items as $item) {
 542              $itemids[] = $item->{$itemidcol};
 543          }
 544  
 545          // Get the items from the database.
 546          list($itemidtest, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
 547          $params['contextid'] = $options->context->id;
 548          $params['userid']    = $userid;
 549          $params['component']    = $options->component;
 550          $params['ratingarea'] = $options->ratingarea;
 551  
 552          $sql = "SELECT r.id, r.itemid, r.userid, r.scaleid, r.rating AS usersrating
 553                    FROM {rating} r
 554                   WHERE r.userid = :userid AND
 555                         r.contextid = :contextid AND
 556                         r.itemid {$itemidtest} AND
 557                         r.component = :component AND
 558                         r.ratingarea = :ratingarea
 559                ORDER BY r.itemid";
 560          $userratings = $DB->get_records_sql($sql, $params);
 561  
 562          $sql = "SELECT r.itemid, $aggregatestr(r.rating) AS aggrrating, COUNT(r.rating) AS numratings
 563                    FROM {rating} r
 564                   WHERE r.contextid = :contextid AND
 565                         r.itemid {$itemidtest} AND
 566                         r.component = :component AND
 567                         r.ratingarea = :ratingarea
 568                GROUP BY r.itemid, r.component, r.ratingarea, r.contextid
 569                ORDER BY r.itemid";
 570          $aggregateratings = $DB->get_records_sql($sql, $params);
 571  
 572          $ratingoptions = new stdClass;
 573          $ratingoptions->context = $options->context;
 574          $ratingoptions->component = $options->component;
 575          $ratingoptions->ratingarea = $options->ratingarea;
 576          $ratingoptions->settings = $this->generate_rating_settings_object($options);
 577          foreach ($options->items as $item) {
 578              $founduserrating = false;
 579              foreach ($userratings as $userrating) {
 580                  // Look for an existing rating from this user of this item.
 581                  if ($item->{$itemidcol} == $userrating->itemid) {
 582                      // Note: rec->scaleid = the id of scale at the time the rating was submitted.
 583                      // It may be different from the current scale id.
 584                      $ratingoptions->scaleid = $userrating->scaleid;
 585                      $ratingoptions->userid = $userrating->userid;
 586                      $ratingoptions->id = $userrating->id;
 587                      $ratingoptions->rating = min($userrating->usersrating, $ratingoptions->settings->scale->max);
 588  
 589                      $founduserrating = true;
 590                      break;
 591                  }
 592              }
 593              if (!$founduserrating) {
 594                  $ratingoptions->scaleid = null;
 595                  $ratingoptions->userid = null;
 596                  $ratingoptions->id = null;
 597                  $ratingoptions->rating = null;
 598              }
 599  
 600              if (array_key_exists($item->{$itemidcol}, $aggregateratings)) {
 601                  $rec = $aggregateratings[$item->{$itemidcol}];
 602                  $ratingoptions->itemid = $item->{$itemidcol};
 603                  $ratingoptions->aggregate = min($rec->aggrrating, $ratingoptions->settings->scale->max);
 604                  $ratingoptions->count = $rec->numratings;
 605              } else {
 606                  $ratingoptions->itemid = $item->{$itemidcol};
 607                  $ratingoptions->aggregate = null;
 608                  $ratingoptions->count = 0;
 609              }
 610  
 611              $rating = new rating($ratingoptions);
 612              $rating->itemtimecreated = $this->get_item_time_created($item);
 613              if (!empty($item->{$itemuseridcol})) {
 614                  $rating->itemuserid = $item->{$itemuseridcol};
 615              }
 616              $item->rating = $rating;
 617          }
 618  
 619          return $options->items;
 620      }
 621  
 622      /**
 623       * Generates a rating settings object based upon the options it is provided.
 624       *
 625       * @param stdClass $options {
 626       *      context           => context the context in which the ratings exists [required]
 627       *      component         => string The component the items belong to [required]
 628       *      ratingarea        => string The ratingarea the items belong to [required]
 629       *      aggregate         => int Aggregation method to apply. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required]
 630       *      scaleid           => int the scale from which the user can select a rating [required]
 631       *      returnurl         => string the url to return the user to after submitting a rating. Null for ajax requests [optional]
 632       *      assesstimestart   => int only allow rating of items created after this timestamp [optional]
 633       *      assesstimefinish  => int only allow rating of items created before this timestamp [optional]
 634       *      plugintype        => string plugin type ie 'mod' Used to find the permissions callback [optional]
 635       *      pluginname        => string plugin name ie 'forum' Used to find the permissions callback [optional]
 636       * }
 637       * @return stdClass rating settings object
 638       */
 639      protected function generate_rating_settings_object($options) {
 640  
 641          if (!isset($options->context)) {
 642              throw new coding_exception('The context option is a required option when generating a rating settings object.');
 643          }
 644          if (!isset($options->component)) {
 645              throw new coding_exception('The component option is now a required option when generating a rating settings object.');
 646          }
 647          if (!isset($options->ratingarea)) {
 648              throw new coding_exception('The ratingarea option is now a required option when generating a rating settings object.');
 649          }
 650          if (!isset($options->aggregate)) {
 651              throw new coding_exception('The aggregate option is now a required option when generating a rating settings object.');
 652          }
 653          if (!isset($options->scaleid)) {
 654              throw new coding_exception('The scaleid option is now a required option when generating a rating settings object.');
 655          }
 656  
 657          // Settings that are common to all ratings objects in this context.
 658          $settings = new stdClass;
 659          $settings->scale             = $this->generate_rating_scale_object($options->scaleid); // The scale to use now.
 660          $settings->aggregationmethod = $options->aggregate;
 661          $settings->assesstimestart   = null;
 662          $settings->assesstimefinish  = null;
 663  
 664          // Collect options into the settings object.
 665          if (!empty($options->assesstimestart)) {
 666              $settings->assesstimestart = $options->assesstimestart;
 667          }
 668          if (!empty($options->assesstimefinish)) {
 669              $settings->assesstimefinish = $options->assesstimefinish;
 670          }
 671          if (!empty($options->returnurl)) {
 672              $settings->returnurl = $options->returnurl;
 673          }
 674  
 675          // Check site capabilities.
 676          $settings->permissions = new stdClass;
 677          // Can view the aggregate of ratings of their own items.
 678          $settings->permissions->view    = has_capability('moodle/rating:view', $options->context);
 679          // Can view the aggregate of ratings of other people's items.
 680          $settings->permissions->viewany = has_capability('moodle/rating:viewany', $options->context);
 681          // Can view individual ratings.
 682          $settings->permissions->viewall = has_capability('moodle/rating:viewall', $options->context);
 683          // Can submit ratings.
 684          $settings->permissions->rate    = has_capability('moodle/rating:rate', $options->context);
 685  
 686          // Check module capabilities
 687          // This is mostly for backwards compatability with old modules that previously implemented their own ratings.
 688          $pluginpermissionsarray = $this->get_plugin_permissions_array($options->context->id,
 689                                                                        $options->component,
 690                                                                        $options->ratingarea);
 691          $settings->pluginpermissions = new stdClass;
 692          $settings->pluginpermissions->view    = $pluginpermissionsarray['view'];
 693          $settings->pluginpermissions->viewany = $pluginpermissionsarray['viewany'];
 694          $settings->pluginpermissions->viewall = $pluginpermissionsarray['viewall'];
 695          $settings->pluginpermissions->rate    = $pluginpermissionsarray['rate'];
 696  
 697          return $settings;
 698      }
 699  
 700      /**
 701       * Generates a scale object that can be returned
 702       *
 703       * @global moodle_database $DB moodle database object
 704       * @param int $scaleid scale-type identifier
 705       * @return stdClass scale for ratings
 706       */
 707      protected function generate_rating_scale_object($scaleid) {
 708          global $DB;
 709          if (!array_key_exists('s'.$scaleid, $this->scales)) {
 710              $scale = new stdClass;
 711              $scale->id = $scaleid;
 712              $scale->name = null;
 713              $scale->courseid = null;
 714              $scale->scaleitems = array();
 715              $scale->isnumeric = true;
 716              $scale->max = $scaleid;
 717  
 718              if ($scaleid < 0) {
 719                  // It is a proper scale (not numeric).
 720                  $scalerecord = $DB->get_record('scale', array('id' => abs($scaleid)));
 721                  if ($scalerecord) {
 722                      // We need to generate an array with string keys starting at 1.
 723                      $scalearray = explode(',', $scalerecord->scale);
 724                      $c = count($scalearray);
 725                      for ($i = 0; $i < $c; $i++) {
 726                          // Treat index as a string to allow sorting without changing the value.
 727                          $scale->scaleitems[(string)($i + 1)] = $scalearray[$i];
 728                      }
 729                      krsort($scale->scaleitems); // Have the highest grade scale item appear first.
 730                      $scale->isnumeric = false;
 731                      $scale->name = $scalerecord->name;
 732                      $scale->courseid = $scalerecord->courseid;
 733                      $scale->max = count($scale->scaleitems);
 734                  }
 735              } else {
 736                  // Generate an array of values for numeric scales.
 737                  for ($i = 0; $i <= (int)$scaleid; $i++) {
 738                      $scale->scaleitems[(string)$i] = $i;
 739                  }
 740              }
 741              $this->scales['s'.$scaleid] = $scale;
 742          }
 743          return $this->scales['s'.$scaleid];
 744      }
 745  
 746      /**
 747       * Gets the time the given item was created
 748       *
 749       * TODO: MDL-31511 - Find a better solution for this, its not ideal to test for fields really we should be
 750       * asking the component the item belongs to what field to look for or even the value we
 751       * are looking for.
 752       *
 753       * @param stdClass $item
 754       * @return int|null return null if the created time is unavailable, otherwise return a timestamp
 755       */
 756      protected function get_item_time_created($item) {
 757          if (!empty($item->created)) {
 758              return $item->created; // The forum_posts table has created instead of timecreated.
 759          } else if (!empty($item->timecreated)) {
 760              return $item->timecreated;
 761          } else {
 762              return null;
 763          }
 764      }
 765  
 766      /**
 767       * Returns an array of grades calculated by aggregating item ratings.
 768       *
 769       * @param stdClass $options {
 770       *      userid => int the id of the user whose items were rated, NOT the user who submitted ratings. 0 to update all. [required]
 771       *      aggregationmethod => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required]
 772       *      scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
 773       *      itemtable => int the table containing the items [required]
 774       *      itemtableusercolum => int the column of the user table containing the item owner's user id [required]
 775       *      component => The component for the ratings [required]
 776       *      ratingarea => The ratingarea for the ratings [required]
 777       *      contextid => int the context in which the rated items exist [optional]
 778       *      modulename => string the name of the module [optional]
 779       *      moduleid => int the id of the module instance [optional]
 780       * }
 781       * @return array the array of the user's grades
 782       */
 783      public function get_user_grades($options) {
 784          global $DB;
 785  
 786          $contextid = null;
 787  
 788          if (!isset($options->component)) {
 789              throw new coding_exception('The component option is now a required option when getting user grades from ratings.');
 790          }
 791          if (!isset($options->ratingarea)) {
 792              throw new coding_exception('The ratingarea option is now a required option when getting user grades from ratings.');
 793          }
 794  
 795          // If the calling code doesn't supply a context id we'll have to figure it out.
 796          if (!empty($options->contextid)) {
 797              $contextid = $options->contextid;
 798          } else if (!empty($options->modulename) && !empty($options->moduleid)) {
 799              $modulename = $options->modulename;
 800              $moduleid   = intval($options->moduleid);
 801  
 802              // Going direct to the db for the context id seems wrong.
 803              $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
 804              $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel)";
 805              $sql = "SELECT cm.* $ctxselect
 806                        FROM {course_modules} cm
 807                   LEFT JOIN {modules} mo ON mo.id = cm.module
 808                   LEFT JOIN {{$modulename}} m ON m.id = cm.instance $ctxjoin
 809                       WHERE mo.name=:modulename AND
 810                             m.id=:moduleid";
 811              $params = array('modulename' => $modulename, 'moduleid' => $moduleid, 'contextlevel' => CONTEXT_MODULE);
 812              $contextrecord = $DB->get_record_sql($sql, $params, '*', MUST_EXIST);
 813              $contextid = $contextrecord->ctxid;
 814          }
 815  
 816          $params = array();
 817          $params['contextid']  = $contextid;
 818          $params['component']  = $options->component;
 819          $params['ratingarea'] = $options->ratingarea;
 820          $itemtable            = $options->itemtable;
 821          $itemtableusercolumn  = $options->itemtableusercolumn;
 822          $scaleid              = $options->scaleid;
 823          $aggregationstring    = $this->get_aggregation_method($options->aggregationmethod);
 824  
 825          // If userid is not 0 we only want the grade for a single user.
 826          $singleuserwhere = '';
 827          if ($options->userid != 0) {
 828              $params['userid1'] = intval($options->userid);
 829              $singleuserwhere = "AND i.{$itemtableusercolumn} = :userid1";
 830          }
 831  
 832          // MDL-24648 The where line used to be "WHERE (r.contextid is null or r.contextid=:contextid)".
 833          // r.contextid will be null for users who haven't been rated yet.
 834          // No longer including users who haven't been rated to reduce memory requirements.
 835          $sql = "SELECT u.id as id, u.id AS userid, $aggregationstring(r.rating) AS rawgrade
 836                    FROM {user} u
 837               LEFT JOIN {{$itemtable}} i ON u.id=i.{$itemtableusercolumn}
 838               LEFT JOIN {rating} r ON r.itemid=i.id
 839                   WHERE r.contextid = :contextid AND
 840                         r.component = :component AND
 841                         r.ratingarea = :ratingarea
 842                         $singleuserwhere
 843                GROUP BY u.id";
 844          $results = $DB->get_records_sql($sql, $params);
 845  
 846          if ($results) {
 847  
 848              $scale = null;
 849              $max = 0;
 850              if ($options->scaleid >= 0) {
 851                  // Numeric.
 852                  $max = $options->scaleid;
 853              } else {
 854                  // Custom scales.
 855                  $scale = $DB->get_record('scale', array('id' => -$options->scaleid));
 856                  if ($scale) {
 857                      $scale = explode(',', $scale->scale);
 858                      $max = count($scale);
 859                  } else {
 860                      debugging('rating_manager::get_user_grades() received a scale ID that doesnt exist');
 861                  }
 862              }
 863  
 864              // It could throw off the grading if count and sum returned a rawgrade higher than scale
 865              // so to prevent it we review the results and ensure that rawgrade does not exceed the scale.
 866              // If it does we set rawgrade = scale (i.e. full credit).
 867              foreach ($results as $rid => $result) {
 868                  if ($options->scaleid >= 0) {
 869                      // Numeric.
 870                      if ($result->rawgrade > $options->scaleid) {
 871                          $results[$rid]->rawgrade = $options->scaleid;
 872                      }
 873                  } else {
 874                      // Scales.
 875                      if (!empty($scale) && $result->rawgrade > $max) {
 876                          $results[$rid]->rawgrade = $max;
 877                      }
 878                  }
 879              }
 880          }
 881  
 882          return $results;
 883      }
 884  
 885      /**
 886       * Returns array of aggregate types. Used by ratings.
 887       *
 888       * @return array aggregate types
 889       */
 890      public function get_aggregate_types() {
 891          return array (RATING_AGGREGATE_NONE     => get_string('aggregatenone', 'rating'),
 892                        RATING_AGGREGATE_AVERAGE  => get_string('aggregateavg', 'rating'),
 893                        RATING_AGGREGATE_COUNT    => get_string('aggregatecount', 'rating'),
 894                        RATING_AGGREGATE_MAXIMUM  => get_string('aggregatemax', 'rating'),
 895                        RATING_AGGREGATE_MINIMUM  => get_string('aggregatemin', 'rating'),
 896                        RATING_AGGREGATE_SUM      => get_string('aggregatesum', 'rating'));
 897      }
 898  
 899      /**
 900       * Converts an aggregation method constant into something that can be included in SQL
 901       *
 902       * @param int $aggregate An aggregation constant. For example, RATING_AGGREGATE_AVERAGE.
 903       * @return string an SQL aggregation method
 904       */
 905      public function get_aggregation_method($aggregate) {
 906          $aggregatestr = null;
 907          switch($aggregate){
 908              case RATING_AGGREGATE_AVERAGE:
 909                  $aggregatestr = 'AVG';
 910                  break;
 911              case RATING_AGGREGATE_COUNT:
 912                  $aggregatestr = 'COUNT';
 913                  break;
 914              case RATING_AGGREGATE_MAXIMUM:
 915                  $aggregatestr = 'MAX';
 916                  break;
 917              case RATING_AGGREGATE_MINIMUM:
 918                  $aggregatestr = 'MIN';
 919                  break;
 920              case RATING_AGGREGATE_SUM:
 921                  $aggregatestr = 'SUM';
 922                  break;
 923              default:
 924                  $aggregatestr = 'AVG'; // Default to this to avoid real breakage - MDL-22270.
 925                  debugging('Incorrect call to get_aggregation_method(), incorrect aggregate method ' . $aggregate, DEBUG_DEVELOPER);
 926          }
 927          return $aggregatestr;
 928      }
 929  
 930      /**
 931       * Looks for a callback like forum_rating_permissions() to retrieve permissions from the plugin whose items are being rated
 932       *
 933       * @param int $contextid The current context id
 934       * @param string $component the name of the component that is using ratings ie 'mod_forum'
 935       * @param string $ratingarea The area the rating is associated with
 936       * @return array rating related permissions
 937       */
 938      public function get_plugin_permissions_array($contextid, $component, $ratingarea) {
 939          $pluginpermissionsarray = null;
 940          // Deny by default.
 941          $defaultpluginpermissions = array('rate' => false, 'view' => false, 'viewany' => false, 'viewall' => false);
 942          if (!empty($component)) {
 943              list($type, $name) = core_component::normalize_component($component);
 944              $pluginpermissionsarray = plugin_callback($type,
 945                                                        $name,
 946                                                        'rating',
 947                                                        'permissions',
 948                                                        array($contextid, $component, $ratingarea),
 949                                                        $defaultpluginpermissions);
 950          } else {
 951              $pluginpermissionsarray = $defaultpluginpermissions;
 952          }
 953          return $pluginpermissionsarray;
 954      }
 955  
 956      /**
 957       * Validates a submitted rating
 958       *
 959       * @param array $params submitted data
 960       *      context => object the context in which the rated items exists [required]
 961       *      component => The component the rating belongs to [required]
 962       *      ratingarea => The ratingarea the rating is associated with [required]
 963       *      itemid => int the ID of the object being rated [required]
 964       *      scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
 965       *      rating => int the submitted rating
 966       *      rateduserid => int the id of the user whose items have been rated. 0 to update all. [required]
 967       *      aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [optional]
 968       * @return boolean true if the rating is valid, false if callback not found, throws rating_exception if rating is invalid
 969       */
 970      public function check_rating_is_valid($params) {
 971  
 972          if (!isset($params['context'])) {
 973              throw new coding_exception('The context option is a required option when checking rating validity.');
 974          }
 975          if (!isset($params['component'])) {
 976              throw new coding_exception('The component option is now a required option when checking rating validity');
 977          }
 978          if (!isset($params['ratingarea'])) {
 979              throw new coding_exception('The ratingarea option is now a required option when checking rating validity');
 980          }
 981          if (!isset($params['itemid'])) {
 982              throw new coding_exception('The itemid option is now a required option when checking rating validity');
 983          }
 984          if (!isset($params['scaleid'])) {
 985              throw new coding_exception('The scaleid option is now a required option when checking rating validity');
 986          }
 987          if (!isset($params['rateduserid'])) {
 988              throw new coding_exception('The rateduserid option is now a required option when checking rating validity');
 989          }
 990  
 991          list($plugintype, $pluginname) = core_component::normalize_component($params['component']);
 992  
 993          // This looks for a function like forum_rating_validate() in mod_forum lib.php
 994          // wrapping the params array in another array as call_user_func_array() expands arrays into multiple arguments.
 995          $isvalid = plugin_callback($plugintype, $pluginname, 'rating', 'validate', array($params), null);
 996  
 997          // If null then the callback does not exist.
 998          if ($isvalid === null) {
 999              $isvalid = false;
1000              debugging('rating validation callback not found for component '.  clean_param($component, PARAM_ALPHANUMEXT));
1001          }
1002          return $isvalid;
1003      }
1004  
1005      /**
1006       * Initialises JavaScript to enable AJAX ratings on the provided page
1007       *
1008       * @param moodle_page $page
1009       * @return true always returns true
1010       */
1011      public function initialise_rating_javascript(moodle_page $page) {
1012          global $CFG;
1013  
1014          // Only needs to be initialized once.
1015          static $done = false;
1016          if ($done) {
1017              return true;
1018          }
1019  
1020          $page->requires->js_init_call('M.core_rating.init');
1021          $done = true;
1022  
1023          return true;
1024      }
1025  
1026      /**
1027       * Returns a string that describes the aggregation method that was provided.
1028       *
1029       * @param string $aggregationmethod
1030       * @return string describes the aggregation method that was provided
1031       */
1032      public function get_aggregate_label($aggregationmethod) {
1033          $aggregatelabel = '';
1034          switch ($aggregationmethod) {
1035              case RATING_AGGREGATE_AVERAGE :
1036                  $aggregatelabel .= get_string("aggregateavg", "rating");
1037                  break;
1038              case RATING_AGGREGATE_COUNT :
1039                  $aggregatelabel .= get_string("aggregatecount", "rating");
1040                  break;
1041              case RATING_AGGREGATE_MAXIMUM :
1042                  $aggregatelabel .= get_string("aggregatemax", "rating");
1043                  break;
1044              case RATING_AGGREGATE_MINIMUM :
1045                  $aggregatelabel .= get_string("aggregatemin", "rating");
1046                  break;
1047              case RATING_AGGREGATE_SUM :
1048                  $aggregatelabel .= get_string("aggregatesum", "rating");
1049                  break;
1050          }
1051          $aggregatelabel .= get_string('labelsep', 'langconfig');
1052          return $aggregatelabel;
1053      }
1054  
1055      /**
1056       * Adds a new rating
1057       *
1058       * @param stdClass $cm course module object
1059       * @param stdClass $context context object
1060       * @param string $component component name
1061       * @param string $ratingarea rating area
1062       * @param int $itemid the item id
1063       * @param int $scaleid the scale id
1064       * @param int $userrating the user rating
1065       * @param int $rateduserid the rated user id
1066       * @param int $aggregationmethod the aggregation method
1067       * @since Moodle 3.2
1068       */
1069      public function add_rating($cm, $context, $component, $ratingarea, $itemid, $scaleid, $userrating, $rateduserid,
1070                                  $aggregationmethod) {
1071          global $CFG, $DB, $USER;
1072  
1073          $result = new stdClass;
1074          // Check the module rating permissions.
1075          // Doing this check here rather than within rating_manager::get_ratings() so we can return a error response.
1076          $pluginpermissionsarray = $this->get_plugin_permissions_array($context->id, $component, $ratingarea);
1077  
1078          if (!$pluginpermissionsarray['rate']) {
1079              $result->error = 'ratepermissiondenied';
1080              return $result;
1081          } else {
1082              $params = array(
1083                  'context'     => $context,
1084                  'component'   => $component,
1085                  'ratingarea'  => $ratingarea,
1086                  'itemid'      => $itemid,
1087                  'scaleid'     => $scaleid,
1088                  'rating'      => $userrating,
1089                  'rateduserid' => $rateduserid,
1090                  'aggregation' => $aggregationmethod
1091              );
1092              if (!$this->check_rating_is_valid($params)) {
1093                  $result->error = 'ratinginvalid';
1094                  return $result;
1095              }
1096          }
1097  
1098          // Rating options used to update the rating then retrieve the aggregate.
1099          $ratingoptions = new stdClass;
1100          $ratingoptions->context = $context;
1101          $ratingoptions->ratingarea = $ratingarea;
1102          $ratingoptions->component = $component;
1103          $ratingoptions->itemid  = $itemid;
1104          $ratingoptions->scaleid = $scaleid;
1105          $ratingoptions->userid  = $USER->id;
1106  
1107          if ($userrating != RATING_UNSET_RATING) {
1108              $rating = new rating($ratingoptions);
1109              $rating->update_rating($userrating);
1110          } else { // Delete the rating if the user set to "Rate..."
1111              $options = new stdClass;
1112              $options->contextid = $context->id;
1113              $options->component = $component;
1114              $options->ratingarea = $ratingarea;
1115              $options->userid = $USER->id;
1116              $options->itemid = $itemid;
1117  
1118              $this->delete_ratings($options);
1119          }
1120  
1121          // Future possible enhancement: add a setting to turn grade updating off for those who don't want them in gradebook.
1122          // Note that this would need to be done in both rate.php and rate_ajax.php.
1123          if ($context->contextlevel == CONTEXT_MODULE) {
1124              // Tell the module that its grades have changed.
1125              $modinstance = $DB->get_record($cm->modname, array('id' => $cm->instance));
1126              if ($modinstance) {
1127                  $modinstance->cmidnumber = $cm->id; // MDL-12961.
1128                  $functionname = $cm->modname.'_update_grades';
1129                  require_once($CFG->dirroot."/mod/{$cm->modname}/lib.php");
1130                  if (function_exists($functionname)) {
1131                      $functionname($modinstance, $rateduserid);
1132                  }
1133              }
1134          }
1135  
1136          // Object to return to client as JSON.
1137          $result->success = true;
1138  
1139          // Need to retrieve the updated item to get its new aggregate value.
1140          $item = new stdClass;
1141          $item->id = $itemid;
1142  
1143          // Most of $ratingoptions variables were previously set.
1144          $ratingoptions->items = array($item);
1145          $ratingoptions->aggregate = $aggregationmethod;
1146  
1147          $items = $this->get_ratings($ratingoptions);
1148          $firstrating = $items[0]->rating;
1149  
1150          // See if the user has permission to see the rating aggregate.
1151          if ($firstrating->user_can_view_aggregate()) {
1152  
1153              // For custom scales return text not the value.
1154              // This scales weirdness will go away when scales are refactored.
1155              $scalearray = null;
1156              $aggregatetoreturn = round($firstrating->aggregate, 1);
1157  
1158              // Output a dash if aggregation method == COUNT as the count is output next to the aggregate anyway.
1159              if ($firstrating->settings->aggregationmethod == RATING_AGGREGATE_COUNT or $firstrating->count == 0) {
1160                  $aggregatetoreturn = ' - ';
1161              } else if ($firstrating->settings->scale->id < 0) { // If its non-numeric scale.
1162                  // Dont use the scale item if the aggregation method is sum as adding items from a custom scale makes no sense.
1163                  if ($firstrating->settings->aggregationmethod != RATING_AGGREGATE_SUM) {
1164                      $scalerecord = $DB->get_record('scale', array('id' => -$firstrating->settings->scale->id));
1165                      if ($scalerecord) {
1166                          $scalearray = explode(',', $scalerecord->scale);
1167                          $aggregatetoreturn = $scalearray[$aggregatetoreturn - 1];
1168                      }
1169                  }
1170              }
1171  
1172              $result->aggregate = $aggregatetoreturn;
1173              $result->count = $firstrating->count;
1174              $result->itemid = $itemid;
1175          }
1176          return $result;
1177      }
1178  
1179      /**
1180       * Get ratings created since a given time.
1181       *
1182       * @param  stdClass $context   context object
1183       * @param  string $component  component name
1184       * @param  int $since         the time to check
1185       * @return array list of ratings db records since the given timelimit
1186       * @since Moodle 3.2
1187       */
1188      public function get_component_ratings_since($context, $component, $since) {
1189          global $DB, $USER;
1190  
1191          $ratingssince = array();
1192          $where = 'contextid = ? AND component = ? AND (timecreated > ? OR timemodified > ?)';
1193          $ratings = $DB->get_records_select('rating', $where, array($context->id, $component, $since, $since));
1194          // Check area by area if we have permissions.
1195          $permissions = array();
1196          $rm = new rating_manager();
1197  
1198          foreach ($ratings as $rating) {
1199              // Check if the permission array for the area is cached.
1200              if (!isset($permissions[$rating->ratingarea])) {
1201                  $permissions[$rating->ratingarea] = $rm->get_plugin_permissions_array($context->id, $component,
1202                                                                                          $rating->ratingarea);
1203              }
1204  
1205              if (($permissions[$rating->ratingarea]['view'] and $rating->userid == $USER->id) or
1206                      ($permissions[$rating->ratingarea]['viewany'] or $permissions[$rating->ratingarea]['viewall'])) {
1207                  $ratingssince[$rating->id] = $rating;
1208              }
1209          }
1210          return $ratingssince;
1211      }
1212  } // End rating_manager class definition.
1213  
1214  /**
1215   * The rating_exception class for exceptions specific to the ratings system
1216   *
1217   * @package   core_rating
1218   * @category  rating
1219   * @copyright 2010 Andrew Davis
1220   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1221   * @since     Moodle 2.0
1222   */
1223  class rating_exception extends moodle_exception {
1224      /**
1225       * @var string The message to accompany the thrown exception
1226       */
1227      public $message;
1228      /**
1229       * Generate exceptions that can be easily identified as coming from the ratings system
1230       *
1231       * @param string $errorcode the error code to generate
1232       */
1233      public function __construct($errorcode) {
1234          $this->errorcode = $errorcode;
1235          $this->message = get_string($errorcode, 'error');
1236      }
1237  }