Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Ratings 2.0: Difference between revisions

From MoodleDocs
m (Text replacement - "</code>" to "</syntaxhighlight>")
 
(78 intermediate revisions by 5 users not shown)
Line 1: Line 1:
==Objectives==
{{obsolete}}
 
[[Rating_API|Up-to-date info can be found in the Rating API doc]]
 
{{Moodle 2.0}}==Objectives==


Ratings are grades entered away from the gradebook. They can be entered by students and teachers and are aggregated into grades.
Ratings are grades entered away from the gradebook. They can be entered by students and teachers and are aggregated into grades.
Line 23: Line 27:
MDL-20514 Allow Aggregate Type in Glossary Activity
MDL-20514 Allow Aggregate Type in Glossary Activity


==New tables==
MDL-21657 Implement Ratings 2.0
 
==Interface==
 
[[Image:RatingUI.gif]]
 
When Javascript is enabled ajax submission means the button can be removed. In the future it would be possible to automatically interpret a 1 to 5 rating as a star rating style UI element.
 
If the user has 'post' permission as returned by modname_rating_permissions() the dropdown box and submission button will be displayed.
 
If the user has 'view' permissions the text to the left of the dropdown box will be displayed. The displayed text consists of the aggregate of the ratings, 4 out of 5 in the example. The number in brackets is the number of ratings that have been submitted. There have been two ratings submitted in the example.


===Ratings===
If the user has 'viewall' permissions they can click on the aggregate summary to view a list of all of the ratings that have been submitted. This is a listing of each user's profile pic, their name and the rating they submitted.


{| class="nicetable"
 
==Database changes==
===New tables===
 
====Ratings====
 
{| class="wikitable"
|-
|-
! Field
! Field
Line 57: Line 77:
| int(10)
| int(10)
|
|
| for example, in user profile, you can comment user's description or interests, but they share the same itemid(==userid), we need comment_area to separate them
| The user's rating
|-
|-
| userid
| userid
| int(10)
| int(10)
|
|
| who wrote this comment
| The user who submitted the rating
|-
|-
| timecreated
| timecreated
Line 75: Line 95:
|}
|}


==Altered tables==
===Altered tables===
 
The forum, glossary and data tables need to be altered to contain the required fields specificed in [https://docs.moodle.org/en/Development:Ratings_2.0#Ratings_Settings]
===Scale===
Add two new columns to the table Scale. A non-numeric scale has a list of comma separated values in the field "scale". A numeric scale has a null "scale" and instead has a "scale_min" and "scale_max". Numeric scales should not appear in lists of scales presented to the user.
{| class="nicetable"
|-
! Field
! Type
! Default
! Info
|-
| scale_max
| int(10)
|
| For numerical scales. The maximum value.
|-
| scale_min
| int(10)
|
| For numerical scales. The minimum value.
|}
 
When the administrator selects a numerical scale, check for a suitable one in the Scales table. Create one if one isn't found.


When a rating is made the scale id will be included in the new row in the rating table. This will allow for intelligent handling of previously entered ratings should the scale be changed after some ratings have been entered.
===Removed database tables===
 
==Removed database tables==


The following tables will have their data migrated to the above ratings table and then be removed:
The following tables will have their data migrated to the above ratings table and then be removed:
Line 111: Line 108:
glossary_ratings
glossary_ratings


==Migrating Scales==
===Scales===


Course modules will continue to store the scale associated with their ratings. For example the glossary table has a scale column.
Course modules will continue to store the scale associated with their ratings. For example the glossary table has a scale column.


Previously a Scale value, like glossary.scale, that was greater than zero indicated a numerical rating with minimum value of zero and the Scale value being the maximum value. Numbers below zero were the primary key of a row in the scale table (-2 = a primary key of 2). The behaviour of a scale with the value 0 is unknown. This behaviour made joins in SQL difficult or impossible and meant it wasn't posssible to have a non-zero minimum value for numerical scales.
Scales are going to be refactored as part of a separate issue. See MDL-17258.
 
Existing scale columns will need to be converted. Numerical scales used will need to be added to the Scale table and the Scale column, ie glossary.scale, updated to reference this new row in Scale. Once that is done any scales with negative values can have their values flipped, -4 to 4, so that they also reference a row in the Scale table.
 
This should cause all Scale values like Glossary.Scale and every row in the Rating table being able to be joined with a row in the Scale table.


==Ratings API==
==Ratings code changes==


lib/ratinglib.php will contain...
rating/lib.php will contain...


===class rating===
===class rating===


====__construct($contextid, $itemid, $scaleid, $userid)====
====__construct($options)====


Initialize a class instance.
Initialize a class instance. Requires context, itemid, scaleid and userid.


====update_rating($rating)====
====update_rating($rating)====


Add rating to database
Add or update the numerical value of the rating in the database


====get_rating()====
====get_rating()====


get the rating
get the numerical value of the rating


====delete_rating()====
====delete_rating()====


delete the rating
delete the rating. Not implemented yet as it hasn't been required.


====render_rating($page = null)====
===class rating_manager===


Load the rating renderer and use it to output this rating's UI. See [https://docs.moodle.org/en/Development:Ratings_2.0#Rating_Submission Rating Submission] for required fields.
====public get_ratings($options)====
Returns the supplied set of items with a rating instance attached to each item.


====ratings_load_ratings($context, $itemids, $aggregate = 'AVG', $userid = null)====
$items is an array of objects with an id member variable ie $items[0]->id.
Returns an result set of ratings including aggregated ratings
$options requires context, items, aggregate and scaleid.


==Ratings Renderer==
items is an array of items such as forum posts or glossary items. They must have an 'id' member ie $items[0]->id.
aggregate is the the aggregation method to apply. RATING_AGGREGATE_AVERAGE etc.


Located in mod/ratings/renderer.php the new class core_rating_renderer should extend plugin_renderer_base defined in lib/outputrenderers.php Almost all renderers appear to inherit from plugin_renderer_base rather than core_renderer.
Optionally options may include userid, returnurl, assesstimestart and assesstimefinish.


===core_rating_renderer methods===
If userid is omitted the current user's id will be used.
returnurl is the url to return the user to after submitting a rating. Can be left null for ajax requests.
assesstimestart. Only allow rating of items created after this timestamp.
assesstimefinish. Only allow rating of items created before this timestamp.


=====render_rating($rating)=====
<syntaxhighlight lang="php">
function get_ratings($options) {
global $DB, $USER;
    if (isnull($userid)) {
        $userid = $USER->id;
    }


returns rating UI html snippet. Used to include ratings in pages.
    $itemids = array();
    foreach($items as $item) {
        $itemids[] = $item->id;
    }
    list($itemidtest, $params) = $DB->get_in_or_equal(
            $itemids, SQL_PARAMS_NAMED, 'itemid0000');
    $sql = "SELECT r.itemid, ur.id, ur.userid, ur.scaleid,
    $aggregatestr(r.rating) AS aggrrating,
    COUNT(r.rating) AS numratings,
    ur.rating AS usersrating
FROM {ratings} r
LEFT JOIN {ratings} ur ON ur.contextid = r.contextid AND
        ur.itemid = r.itemid AND
        ur.userid = :userid
WHERE
    r.contextid = :contextid AND
    r.itemid $itemidtest
GROUP BY r.itemid, ur.rating
ORDER BY r.itemid";
    $params['userid'] = $userid;
    $params['contextid'] = $context->id;
    //add ratings to the items at $item->rating. Similar to make_context_subobj().
    //Iterate over forum $items (forum posts, glossary items etc) and create the  $item->rating objects. Properties of the individual ratings, such as $item->rating->aggregate and $item->rating->rating, are stored on the rating object directly.
    //Settings common to the ratings are stored at $item->rating->settings->aggregationmethod (for example).
}
</syntaxhighlight>


===Using the rating renderer===
====get_aggregation_method($aggregate)====
Converts the aggregation method constants to a string that can be included in SQL


The process to render ratings is as follows:
====get_all_ratings_for_item($options)====
Load all ratings for a given item. Used to display a listing of submitted ratings to users with 'viewall' permission.


There are two possible methods that could be implemented to retrieve ratings from the database.
Requires context, itemid and optionally accepts an SQL order by clause.


====Method 1====
====get_user_grades($options)====
Access an item's rating like this: $post->rating
Returns a grade for a user based on other user's rating of their items.


How do we use this method to return all of the ratings for a given forum post?
===Rendering ratings===


If retrieving a single rating (the user can't view the aggregate) this will result in only one database query.
As rendering a rating will consist of only a single function call a new method called render_rating() will be added to the core renderer.


<code php>
If necessary a ratings renderer could be added. Located in mod/ratings/renderer.php this new class core_rating_renderer should extend plugin_renderer_base defined in lib/outputrenderers.php Almost all renderers appear to inherit from plugin_renderer_base rather than core_renderer.
// in Some forum/lib.php function call that loads forum posts (for example)
$sql = "select forumfields {rating_system::get_ratings_sql_fields()} from forumtables {rating_system::get_ratings_sql_join()}";
$rs = $DB->get_recordset_sql($sql);


foreach ($rs as $post) {
=====core_renderer::render_rating(rating $rating)=====
$post = make_rating_subobj($post);
$posts[$post->id] = $post;
}


foreach ($posts as $postid => $post) {
returns rating UI html snippet. Used to include ratings in pages.
    $forumoutput->post($post, $post->rating);//or rating member variable could just be accessed with the function instead of being passed in
}
</code>


This would require that lib/ratinglib.php define three functions:
===Using the rating renderer===
*get_ratings_sql_fields(): The field names to go into the select part of an SQL query
*get_ratings_sql_join(): The table join to go into an SQL query
*make_rating_subobj(): Converts fields from a result set into an object. This will work in the same was as make_context_subobj() in lib/accesslib.php


====Method 2====
The process to render ratings is as follows:
Access an item's rating like this: $ratings[$post->id]
 
If retrieving a single rating (the user can't view the aggregate) this will result in two database queries. One to get the forum posts and one to get the associated ratings.


Retrieving a collection of ratings is no different than retrieving one.
<syntaxhighlight lang="php">
<code php>
// in mod/forum/discuss.php (for example)
// in mod/forum/discuss.php (for example)
$posts = // Some forum/lib.php function call.
$posts = // Some forum/lib.php function call.
//these are supplied by the calling module (forum etc)
$aggregate =
$scaleid =
$userid =
$returnurl =


//The current scaleid comes from the forum or glossary object and may be changed at any time so supply it each time
//The current scaleid comes from the forum or glossary object and may be changed at any time so supply it each time
//Also, a user should only see their own ratings
//Also, a user should only see their own ratings
$ratings = rating_system::load_ratings($contextid, $scaleid, $userid, $items = null/* Optional array of items (forum posts or glossary items) with an 'id' property. If null returns all ratings for the context by the user*/);
$posts = rating::load_ratings($context,  
    $posts/* Optional array of items (forum posts or glossary items) with an 'id' property. If null returns all ratings for the context by the user*/,
    $aggregate,
    $scaleid,
    $userid,
    $returnurl);


//Or, just return all ratings for the given context. Used when you want to aggregate the ratings in some way.
//ratings are now attached to the post objects. $posts[0]->rating
$ratings = rating_system::load_ratings($contextid);
/*
This will probably return a multidimensional array accessed by item id (the forum post or glossary item id) then user id.
 
//access the user's rating like this
$myrating = $ratings[$itemid][$userid];
 
foreach($ratings[$itemid] as $itemrating) {
//do something with the ratings for this item like aggregating it
}
*/
 
// $posts and $ratings are both indexed by $postid == $itemid.


foreach ($posts as $postid => $post) {
foreach ($posts as $postid => $post) {
     $forumoutput->post($post, $ratings[$postid]);
     $forumoutput->post($post);//access the rating info at $post->rating, $post->rating->aggregate and $post->rating->count
}
}
</code>


====Common code====
// in mod/forum/renderer.php, in the post($post) method:
The following is common to both methods
<code php>
$permissions = forum_rating_permission();//implemented by the module. returns array('view'=>true/false,'post'=>true/false)
 
// in mod/forum/renderer.php, in the post(post, $rating) method:


// ... output most of the post ... starts around line 5813 of mod/forum/lib.php
// ... output most of the post ... starts around line 5813 of mod/forum/lib.php
echo $post->rating->render_rating($this->page,$permissions);
echo $OUTPUT->render($item->rating);
// ... output the rest.
// ... output the rest.




// and finally in rating/lib.php in the Rating class:
// and finally in rating/lib.php in the rating class:


public function Rating::render_rating($page = null, $permissions=null) {
public function core_renderer::render_rating(rating $rating) {
     if (is_null($page) {
     //return html representation of the rating
        global $PAGE;
        $page = $PAGE;
    }
    $ratingoutput = $page->get_renderer('core', 'rating');
    return $ratingoutput->render_rating($this,$permissions);
}
}
</code>
</syntaxhighlight>
 
===Ratings Aggregation===
Forums currently support multiple forms of rating aggregation such as average, maximum, sum etc. These options should be available everywhere that ratings are available.
 
They are calculated within rating::ratings_load_ratings()
 
===Ratings Settings===
Settings for ratings are stored by the module. Each module table, for example forum, must contain the following columns.
 
{| class="wikitable"
|-
! Field
! Type
! Default
! Info
|-
| assessed
| int(10)
|
| The aggregation method to apply. A value of 0 means ratings should be disabled. Currently the glossary stores an "allcanrate" flag in the assessed column. "allcanrate" will disappear in favour of proper permissions.
|-
| assesstimestart
| int(10)
|
| From when can users submit ratings
|-
| assesstimefinish
| int(10)
|
| When must users submit ratings by
|-
| scale
| int(10)
|
| What scale to use
|}


The "Restrict ratings to posts with dates in this range" flag is calculated in the course/moodleforum_mod.php method moodleform::data_preprocessing() and is not stored in the database.


The key points there are:
====Settings interface====
# the details of how the rating is rendered (that is, that you need to get a particular sort of renderer) should be encapsulated, rather than having to be duplicated everywhere that a rating is output. This is the approach I have adopted in my new question engine, where there is even more complication to hide. Try following though the code for outputting a question, starting from http://github.com/timhunt/Moodle-Question-Engine-2/blob/new_qe/mod/quiz/attempt.php#L150.
=====moodleform_mod::standard_coursemodule_elements()=====
# the rating data for all the ratings needed by a particular page need to be loaded from the database in a single query, that should be hidden behind some convenient API. It has to be a single query for performance reasons. Actually, it would be even better if the ratings could be loaded as part of the same query that loads the forum posts, by left-joining on the ratings table. That requires closer coupling between the forum and rating code, but is worth it for the performance gain.
Adds elements to an instance of moodle form. The ratings elements should appear in a separate block from Common Module Settings.


==Ratings Aggregation==
It will determine whether to include ratings settings by calling plugin_supports() found in lib/moodlelib.php like this...
Forums currently support multiple forms of rating aggregation such as average, maximum, sum etc. How will these aggregates be retrieved/calculated?
<syntaxhighlight lang="php">
if (plugin_supports('mod', $this->_modname, FEATURE_RATINGS, false)) {
    //include ratings elements
}
</syntaxhighlight>


Method 1: not sure.
mod/%modulename%/lib.php defines a function called %modulename%_supports() that lists the elements that the module supports.


Method 2: Its easy enough to iterate over the 2d array and calculated aggregates
FEATURE_MOD_RATINGS will have to be added to lib/moodlelib.php


==Rating Submission==
===Rating Submission===


lib/rating.php will be the target for posted ratings. Currently each module implements their own ratings submission. For example mod/glossary/rate.php within the glossary module.
rating/rate.php will be the target for posted ratings. Previously each module implemented their own ratings submission. For example mod/glossary/rate.php within the glossary module.


The supplied fields should consist of
The supplied fields should consist of
{| class="nicetable"
{| class="wikitable"
|-
|-
! Field
! Field
Line 276: Line 323:
|-
|-
| contextid
| contextid
| int(10)
| PARAM_INT
|
|
| The context id defined in context table - identifies the instance of plugin owning the comment.
| The context id defined in context table - identifies the instance of plugin owning the comment.
|-
|-
| itemid
| itemid
| int(10)
| PARAM_INT
|
|
| Some plugin specific item id (eg. forum post blog entry)
| Some plugin specific item id (eg. forum post blog entry)
|-
|-
| scaleid
| scaleid
| int(10)
| PARAM_INT
|
|
| ID of the scale (1-5, 0-100, custom) from which the user selected their rating. Including this allows smarter handling of scales being changed.
| ID of the scale (1-5, 0-100, custom) from which the user selected their rating. Including this allows smarter handling of scales being changed.
|-
|-
| rating
| rating
| int(10)
| PARAM_INT
|
|
| for example, in user profile, you can comment user's description or interests, but they share the same itemid(==userid), we need comment_area to separate them
| for example, in user profile, you can comment user's description or interests, but they share the same itemid(==userid), we need comment_area to separate them
|-
| userid
| int(10)
|
| who wrote this comment
|-
|-
| returnurl
| returnurl
| string
| PARAM_LOCALURL
|
|
| Null for ajax requests. If not null the url to which the user should be redirected after recording the rating
| Null for ajax requests. If not null the url to which the user should be redirected after recording the rating
Line 308: Line 350:
The process to record a rating is as follows:
The process to record a rating is as follows:


<code php>
<syntaxhighlight lang="php">
$permissions = forum_rating_permission();
$permissions = forum_rating_permission();
if($permissions['post']) {
if($permissions['post']) {
$rating = N; //the actual rating from the user
$rating = N; //the actual rating from the user
$ratingObj = new Rating($contextid, $scaleid, $userid, array($itemid));
$ratingObj = new rating($contextid, $scaleid, $userid, array($itemid));
$ratingObj->set_rating($rating);
$ratingObj->set_rating($rating);
//redirect to return url if supplied
//redirect to return url if supplied
}
}


//within the class Rating
//within the class rating
function Rating::set_rating($rating) {
function Rating::update_rating($rating) {
$ratings = rating_system::load_ratings($scaleid, $userid, $contextid, array($itemid));
$ratings = rating_system::load_ratings($scaleid, $userid, $contextid, array($itemid));
if( !$ratings || sizeof($ratings)==0) {
if( !$ratings || sizeof($ratings)==0) {
Line 334: Line 376:


}
}
</code>
</syntaxhighlight>


====Ajax submission====
Ajax submission of ratings must be possible for sites with ajax enabled. ForumNG (http://moodle.org/mod/data/view.php?d=13&rid=2927) written by Sam Marshall contains an ajax implementation of the rating UI elements that may be useful to reference.


Check if ajax is enabled like this...
<syntaxhighlight lang="php">
if (empty($CFG->enableajax)) {
    //no ajax
}
else {
    //add ajax stuff
}
</syntaxhighlight>
=== Permissions changes ===
Ratings is dependent on two things: core capabilities and the result of a module callback (which may itself use module capabilities).
====New ratings permissions====
New system-wide ratings permissions will be added.  These will be checked IN ADDITION to local permissions in existing modules.
It is anticipated most new modules will just use these.
The new capabilities are:
moodle/rating:view - allows the user to view aggregated ratings made on their own items
moodle/rating:viewany - allows the user to view aggregated ratings made on other people's items


== Moodle modules callback ==
moodle/rating:viewall - allows the user to see individual ratings


===modname_rating_permissions===
moodle/rating:rate - allows the user to make ratings on other people's items
Modules can implement a function named '''modname_rating_permissions''' to control post and view permission. This is called prior to rendering a set of ratings. It is also called by lib/rating.php when it receives and ajax rating submission.


This function will return an array: array('view'=>true,'post'=>true)
====Handling of old permissions====


==Interface==
Pre-existing module-specific permissions will be extended to have matching names/behaviour with the new rating permissions.
 
*mod/data:rate  - unchanged
*mod/data:viewrating  - unchanged
*mod/data:viewanyrating - cloned from old mod/data:viewrating
*mod/data:viewallratings - cloned from old mod/data:viewrating
*mod/forum:rate - unchanged
*mod/forum:viewrating - unchanged
*mod/forum:viewanyrating - unchanged
*mod/forum:viewallratings - cloned from old mod/forum:viewanyrating
*mod/glossary:rate  - unchanged
*mod/glossary:viewrating  - unchanged
*mod/glossary:viewanyrating - cloned from old mod/glossary:viewrating
*mod/glossary:viewallratings - cloned from old mod/glossary:viewrating
 
===Module callbacks===
 
These allow modules to control how ratings behave.
 
====modname_rating_validate====
 
As of Moodle 2.0.3 modules must implement a function named '''modname_rating_validate''' to verify the validity of submitted ratings.
 
This function must return true if the rating is valid or throw an instance of rating_exception if the rating is invalid. Note: false is used to indicate that the module hasn't implemented this callback.
 
This example shows how this would work for the forum module
<syntaxhighlight lang="php">
function forum_rating_validate($params) {
    if (!array_key_exists('itemid', $params) || !array_key_exists('context', $params) || !array_key_exists('rateduserid', $params)) {
        throw new rating_exception('missingparameter');
    }
    return true;
}
</syntaxhighlight>
The rating_exception argument is the name of a string in the error language file.
 
The $params argument contains:
* context - object the context in which the rated items exists [required]
* itemid - int the ID of the object being rated
* scaleid - int the scale from which the user can select a rating. Used for bounds checking. [required]
* rating - int the submitted rating
* rateduserid - int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required]
* aggregation - int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required]
 
====modname_rating_permissions====
Modules must implement a function named '''modname_rating_permissions''' to control post and view permission. This is called prior to rendering a set of ratings. It is also called by rating/rate.php and rate/rate_ajax.php when they receive rating submissions.
 
Modules do not need to implement this.  It's mostly provided for backward compatibility with modules that had complicated ratings related logic or for modules that use settings other than capabilities to control ratings behavior.
 
This function will return an array: array('view'=>true, 'viewany'=>true, 'viewall'=>true, 'rate'=>true)


[[Image:RatingUI.gif]]
This example shows how this would work for the forum module
<syntaxhighlight lang="php">
function forum_rating_permissions($context) {
    return array('view'=>has_capability('mod/forum:viewrating',$context),
                'viewany'=>has_capability('mod/forum:viewanyrating',$context),
                'viewall'=>has_capability('mod/forum:viewallratings',$context),
                'rate'=>has_capability('mod/forum:rate',$context));
}
</syntaxhighlight>


When Javascript is enabled ajax submission means the button can be removed. In the future it would be possible to automatically interpret a 1 to 5 rating as a star rating style UI element.
==See also==


The dropdown box and submission button will be displayed if the user has 'post' permission as returned by modname_rating_permissions().
* MDL-21657 Implement Ratings 2.0


The text to the left of the dropdown box will be displayed if the user has 'view' permissions. The displayed text consists of the aggregate of the ratings, 4 out of 5 in the example. The number in brackets is the number of ratings that have been submitted. There have been two ratings submitted in the example.
[[Category:Grades]]

Latest revision as of 20:21, 14 July 2021

Warning: This page is no longer in use. The information contained on the page should NOT be seen as relevant or reliable.


Up-to-date info can be found in the Rating API doc

Moodle 2.0

Objectives

Ratings are grades entered away from the gradebook. They can be entered by students and teachers and are aggregated into grades.

The goals of ratings 2.0:

  • Manage ratings centrally
  • Use a consistent approach for all ratings throughout Moodle
  • Easily integrate ratings 2.0 with existing modules
  • Remove duplicate implementations of ratings functionality

Overview

The ratings 2.0 provides APIs to:

  1. Add ratings
  2. Update ratings
  3. Access collected ratings for grade calculation purposes
  4. Delete ratings

Current tracker issues

MDL-21389 Write spec for separate Ratings 2.0

MDL-20514 Allow Aggregate Type in Glossary Activity

MDL-21657 Implement Ratings 2.0

Interface

RatingUI.gif

When Javascript is enabled ajax submission means the button can be removed. In the future it would be possible to automatically interpret a 1 to 5 rating as a star rating style UI element.

If the user has 'post' permission as returned by modname_rating_permissions() the dropdown box and submission button will be displayed.

If the user has 'view' permissions the text to the left of the dropdown box will be displayed. The displayed text consists of the aggregate of the ratings, 4 out of 5 in the example. The number in brackets is the number of ratings that have been submitted. There have been two ratings submitted in the example.

If the user has 'viewall' permissions they can click on the aggregate summary to view a list of all of the ratings that have been submitted. This is a listing of each user's profile pic, their name and the rating they submitted.


Database changes

New tables

Ratings

Field Type Default Info
id int(10) auto-incrementing The unique ID for this comment.
contextid int(10) The context id defined in context table - identifies the instance of plugin owning the comment.
itemid int(10) Some plugin specific item id (eg. forum post blog entry)
scaleid int(10) ID of the scale (1-5, 0-100, custom) from which the user selected their rating. Including this allows smarter handling of previously entered ratings should the scales be changed.
rating int(10) The user's rating
userid int(10) The user who submitted the rating
timecreated int(10)
timemodified int(10)

Altered tables

The forum, glossary and data tables need to be altered to contain the required fields specificed in [1]

Removed database tables

The following tables will have their data migrated to the above ratings table and then be removed:

data_ratings

forum_ratings

glossary_ratings

Scales

Course modules will continue to store the scale associated with their ratings. For example the glossary table has a scale column.

Scales are going to be refactored as part of a separate issue. See MDL-17258.

Ratings code changes

rating/lib.php will contain...

class rating

__construct($options)

Initialize a class instance. Requires context, itemid, scaleid and userid.

update_rating($rating)

Add or update the numerical value of the rating in the database

get_rating()

get the numerical value of the rating

delete_rating()

delete the rating. Not implemented yet as it hasn't been required.

class rating_manager

public get_ratings($options)

Returns the supplied set of items with a rating instance attached to each item.

$items is an array of objects with an id member variable ie $items[0]->id. $options requires context, items, aggregate and scaleid.

items is an array of items such as forum posts or glossary items. They must have an 'id' member ie $items[0]->id. aggregate is the the aggregation method to apply. RATING_AGGREGATE_AVERAGE etc.

Optionally options may include userid, returnurl, assesstimestart and assesstimefinish.

If userid is omitted the current user's id will be used. returnurl is the url to return the user to after submitting a rating. Can be left null for ajax requests. assesstimestart. Only allow rating of items created after this timestamp. assesstimefinish. Only allow rating of items created before this timestamp.

function get_ratings($options) {
 global $DB, $USER;
 
    if (isnull($userid)) {
        $userid = $USER->id;
    }

    $itemids = array();
    foreach($items as $item) {
        $itemids[] = $item->id;
    }
 
    list($itemidtest, $params) = $DB->get_in_or_equal(
            $itemids, SQL_PARAMS_NAMED, 'itemid0000');
 
    $sql = "SELECT r.itemid, ur.id, ur.userid, ur.scaleid,
    $aggregatestr(r.rating) AS aggrrating,
    COUNT(r.rating) AS numratings,
    ur.rating AS usersrating
FROM {ratings} r
LEFT JOIN {ratings} ur ON ur.contextid = r.contextid AND
        ur.itemid = r.itemid AND
        ur.userid = :userid
WHERE
    r.contextid = :contextid AND
    r.itemid $itemidtest
GROUP BY r.itemid, ur.rating
ORDER BY r.itemid";
 
    $params['userid'] = $userid;
    $params['contextid'] = $context->id;
 
    //add ratings to the items at $item->rating. Similar to make_context_subobj().
    //Iterate over forum $items (forum posts, glossary items etc) and create the  $item->rating objects. Properties of the individual ratings, such as $item->rating->aggregate and $item->rating->rating, are stored on the rating object directly.
    //Settings common to the ratings are stored at $item->rating->settings->aggregationmethod (for example).
}

get_aggregation_method($aggregate)

Converts the aggregation method constants to a string that can be included in SQL

get_all_ratings_for_item($options)

Load all ratings for a given item. Used to display a listing of submitted ratings to users with 'viewall' permission.

Requires context, itemid and optionally accepts an SQL order by clause.

get_user_grades($options)

Returns a grade for a user based on other user's rating of their items.

Rendering ratings

As rendering a rating will consist of only a single function call a new method called render_rating() will be added to the core renderer.

If necessary a ratings renderer could be added. Located in mod/ratings/renderer.php this new class core_rating_renderer should extend plugin_renderer_base defined in lib/outputrenderers.php Almost all renderers appear to inherit from plugin_renderer_base rather than core_renderer.

core_renderer::render_rating(rating $rating)

returns rating UI html snippet. Used to include ratings in pages.

Using the rating renderer

The process to render ratings is as follows:

// in mod/forum/discuss.php (for example)
$posts = // Some forum/lib.php function call.

//these are supplied by the calling module (forum etc)
$aggregate = 
$scaleid = 
$userid =
$returnurl = 

//The current scaleid comes from the forum or glossary object and may be changed at any time so supply it each time
//Also, a user should only see their own ratings
$posts = rating::load_ratings($context, 
    $posts/* Optional array of items (forum posts or glossary items) with an 'id' property. If null returns all ratings for the context by the user*/, 
    $aggregate, 
    $scaleid, 
    $userid, 
    $returnurl);

//ratings are now attached to the post objects. $posts[0]->rating

foreach ($posts as $postid => $post) {
    $forumoutput->post($post);//access the rating info at $post->rating, $post->rating->aggregate and $post->rating->count
}

// in mod/forum/renderer.php, in the post($post) method:

// ... output most of the post ... starts around line 5813 of mod/forum/lib.php
echo $OUTPUT->render($item->rating);
// ... output the rest.


// and finally in rating/lib.php in the rating class:

public function core_renderer::render_rating(rating $rating) {
    //return html representation of the rating
}

Ratings Aggregation

Forums currently support multiple forms of rating aggregation such as average, maximum, sum etc. These options should be available everywhere that ratings are available.

They are calculated within rating::ratings_load_ratings()

Ratings Settings

Settings for ratings are stored by the module. Each module table, for example forum, must contain the following columns.

Field Type Default Info
assessed int(10) The aggregation method to apply. A value of 0 means ratings should be disabled. Currently the glossary stores an "allcanrate" flag in the assessed column. "allcanrate" will disappear in favour of proper permissions.
assesstimestart int(10) From when can users submit ratings
assesstimefinish int(10) When must users submit ratings by
scale int(10) What scale to use

The "Restrict ratings to posts with dates in this range" flag is calculated in the course/moodleforum_mod.php method moodleform::data_preprocessing() and is not stored in the database.

Settings interface

moodleform_mod::standard_coursemodule_elements()

Adds elements to an instance of moodle form. The ratings elements should appear in a separate block from Common Module Settings.

It will determine whether to include ratings settings by calling plugin_supports() found in lib/moodlelib.php like this...

if (plugin_supports('mod', $this->_modname, FEATURE_RATINGS, false)) {
    //include ratings elements
}

mod/%modulename%/lib.php defines a function called %modulename%_supports() that lists the elements that the module supports.

FEATURE_MOD_RATINGS will have to be added to lib/moodlelib.php

Rating Submission

rating/rate.php will be the target for posted ratings. Previously each module implemented their own ratings submission. For example mod/glossary/rate.php within the glossary module.

The supplied fields should consist of

Field Type Default Info
contextid PARAM_INT The context id defined in context table - identifies the instance of plugin owning the comment.
itemid PARAM_INT Some plugin specific item id (eg. forum post blog entry)
scaleid PARAM_INT ID of the scale (1-5, 0-100, custom) from which the user selected their rating. Including this allows smarter handling of scales being changed.
rating PARAM_INT for example, in user profile, you can comment user's description or interests, but they share the same itemid(==userid), we need comment_area to separate them
returnurl PARAM_LOCALURL Null for ajax requests. If not null the url to which the user should be redirected after recording the rating

The process to record a rating is as follows:

$permissions = forum_rating_permission();
if($permissions['post']) {
$rating = N; //the actual rating from the user
$ratingObj = new rating($contextid, $scaleid, $userid, array($itemid));
$ratingObj->set_rating($rating);
//redirect to return url if supplied
}

//within the class rating
function Rating::update_rating($rating) {
$ratings = rating_system::load_ratings($scaleid, $userid, $contextid, array($itemid));
if( !$ratings || sizeof($ratings)==0) {
$data->contextid    = $this->contextid;
$data->scaleid      = $this->scaleid;
$data->userid       = $this->userid;
$data->rating       = $rating;
$DB->insert_record($this->table, $data);
}
else {
$data->id       = $this->id;
$data->rating   = $rating;
$DB->update_record($this->table, $data);
}

}

Ajax submission

Ajax submission of ratings must be possible for sites with ajax enabled. ForumNG (http://moodle.org/mod/data/view.php?d=13&rid=2927) written by Sam Marshall contains an ajax implementation of the rating UI elements that may be useful to reference.

Check if ajax is enabled like this...

if (empty($CFG->enableajax)) {
    //no ajax
}
else {
    //add ajax stuff
}

Permissions changes

Ratings is dependent on two things: core capabilities and the result of a module callback (which may itself use module capabilities).

New ratings permissions

New system-wide ratings permissions will be added. These will be checked IN ADDITION to local permissions in existing modules.

It is anticipated most new modules will just use these.

The new capabilities are:

moodle/rating:view - allows the user to view aggregated ratings made on their own items

moodle/rating:viewany - allows the user to view aggregated ratings made on other people's items

moodle/rating:viewall - allows the user to see individual ratings

moodle/rating:rate - allows the user to make ratings on other people's items

Handling of old permissions

Pre-existing module-specific permissions will be extended to have matching names/behaviour with the new rating permissions.

  • mod/data:rate - unchanged
  • mod/data:viewrating - unchanged
  • mod/data:viewanyrating - cloned from old mod/data:viewrating
  • mod/data:viewallratings - cloned from old mod/data:viewrating
  • mod/forum:rate - unchanged
  • mod/forum:viewrating - unchanged
  • mod/forum:viewanyrating - unchanged
  • mod/forum:viewallratings - cloned from old mod/forum:viewanyrating
  • mod/glossary:rate - unchanged
  • mod/glossary:viewrating - unchanged
  • mod/glossary:viewanyrating - cloned from old mod/glossary:viewrating
  • mod/glossary:viewallratings - cloned from old mod/glossary:viewrating

Module callbacks

These allow modules to control how ratings behave.

modname_rating_validate

As of Moodle 2.0.3 modules must implement a function named modname_rating_validate to verify the validity of submitted ratings.

This function must return true if the rating is valid or throw an instance of rating_exception if the rating is invalid. Note: false is used to indicate that the module hasn't implemented this callback.

This example shows how this would work for the forum module

function forum_rating_validate($params) {
    if (!array_key_exists('itemid', $params) || !array_key_exists('context', $params) || !array_key_exists('rateduserid', $params)) {
        throw new rating_exception('missingparameter');
    }
    return true;
}

The rating_exception argument is the name of a string in the error language file.

The $params argument contains:

  • context - object the context in which the rated items exists [required]
  • itemid - int the ID of the object being rated
  • scaleid - int the scale from which the user can select a rating. Used for bounds checking. [required]
  • rating - int the submitted rating
  • rateduserid - int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required]
  • aggregation - int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required]

modname_rating_permissions

Modules must implement a function named modname_rating_permissions to control post and view permission. This is called prior to rendering a set of ratings. It is also called by rating/rate.php and rate/rate_ajax.php when they receive rating submissions.

Modules do not need to implement this. It's mostly provided for backward compatibility with modules that had complicated ratings related logic or for modules that use settings other than capabilities to control ratings behavior.

This function will return an array: array('view'=>true, 'viewany'=>true, 'viewall'=>true, 'rate'=>true)

This example shows how this would work for the forum module

function forum_rating_permissions($context) {
    return array('view'=>has_capability('mod/forum:viewrating',$context), 
                 'viewany'=>has_capability('mod/forum:viewanyrating',$context), 
                 'viewall'=>has_capability('mod/forum:viewallratings',$context), 
                 'rate'=>has_capability('mod/forum:rate',$context));
}

See also