Ratings 2.0

Revision as of 03:39, 16 February 2010 by Andrew Davis (talk | contribs) (static make_rating_subobjs($posts,$ratings))

Jump to: navigation, search

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

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) 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
timecreated int(10)
timemodified int(10)

Altered tables

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.

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

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

data_ratings

forum_ratings

glossary_ratings

Migrating Scales

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.

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.

Please see this discussion thread and document for an alternative proposal on improving scales:

http://moodle.org/mod/forum/discuss.php?d=140907#p627944

https://docs.moodle.org/en/Development:Scales

Ratings API

lib/ratinglib.php will contain...

class rating

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

Initialize a class instance.

update_rating($rating)

Add rating to database

get_rating()

get the rating

delete_rating()

delete the rating

render_rating($page = null)

Load the rating renderer and use it to output this rating's UI. See Rating Submission for required fields.

static ratings_load_ratings($contextid, $aggregate='AVG', $items, $userid = null)

Returns an result set of ratings including aggregated ratings. $items is an array of objects with an id member variable ie $items[0]->id.

function ratings_load_ratings($contextid, $aggregate='AVG', $items, $userid = null) {
 global $DB, $USER;
 
    if (isnull($userid)) {
        $userid = $USER->id;
    }
 
    list($itemidtest, $params) = $DB->get_in_or_equal(
            $itemids, SQL_PARAMS_NAMED, 'itemid0000');
 
    $sql = <<<ENDSQL
SELECT r.itemid,
    $aggregate(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 AND
GROUP BY r.itemid, ur.rating
ENDSQL;
 
    $params['userid'] = $userid;
    $params['contextid'] = $context->id;
 
    return $DB->get_records_sql($sql, $params);
}

static make_rating_subobjs($posts,$ratings)

Similar to make_context_subobj(). Iterate over forum $posts (forum posts, glossary items etc) and create $post->rating, $post->rating->aggregate and $post->rating->count.

Ratings Renderer

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.

core_rating_renderer methods

render_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.
 
//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
$ratings = Rating->ratings_load_ratings($contextid, $aggregate, $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*/, $userid);
//Or, just return all ratings for the given context.
$ratings = Rating->ratings_load_ratings($contextid, $aggregate);
 
//Iterate over the posts and converts the result set into objects
//$post->rating, $post->rating->aggregate and $post->rating->count
//Similar to make_context_subobj().
$posts = Rating->make_rating_subobjs($posts,$ratings);
 
foreach ($posts as $postid => $post) {
    $forumoutput->post($post);//access the rating info at $post->rating, $post->rating->aggregate and $post->rating->count
}
 
$permissions = forum_rating_permission();//implemented by the module. returns array('view'=>true/false,'post'=>true/false)
$scaleid = //load scale id from forum or glossary
 
// 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
echo $post->rating->render_rating($this->page,$permissions,$scaleid);
// ... output the rest.
 
 
// and finally in rating/lib.php in the Rating class:
 
public function Rating::render_rating($page = null, $permissions=null,$scaleid) {
    if (is_null($page) {
        global $PAGE;
        $page = $PAGE;
    }
    $ratingoutput = $page->get_renderer('core', 'rating');
    return $ratingoutput->render_rating($this,$permissions,$scaleid);
}


The key points there are:

  1. 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.
  2. 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.

Ratings Aggregation

Forums currently support multiple forms of rating aggregation such as average, maximum, sum etc. How will these aggregates be retrieved/calculated?

Method 1: not sure.

Method 2: Its easy enough to iterate over the 2d array and calculated aggregates

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.

The supplied fields should consist of

Field Type Default Info
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 scales being changed.
rating 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
userid int(10) who wrote this comment
returnurl string 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::set_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.

Moodle modules callback

modname_rating_permissions

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)

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.

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

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.