The multi-axis patch in http://drupal.org/node/185074 does not provide for calculating the average of the axes. This is important for other users of the votingAPI, such as Views, which would otherwise be unable to sort by average rating. The VotingAPI documentation says averaging over multiple axes is the API-user's responsibility, and recommends the use of the 'vote' default tag for the average. I added the following rather rough code to the end of _fivestar_cast_vote() (after the new vote is stored)..

	// Calculate and set the user's new average vote (for multiaxis).. this could be done earlier to avoid a second call to set_votes
	if($tag!='vote'){
	  $all_user_votes = votingapi_select_votes(array('uid' => $user->uid, 'tag' => NULL) + $criteria);
	  $vote_sum = 0;
	  $vote_count = 0;
	  foreach($all_user_votes as $id => $user_vote) {
	    if($user_vote['tag']!='vote') {
		  $vote_sum += $user_vote['value'];
		  $vote_count++;
		} else {
		  $average_vote=array($user_vote);
		}
	  }
	  $criteria = array('value' => $vote_sum/$vote_count, 'tag' => 'vote') + $criteria;
      votingapi_set_votes($criteria, $average_vote);
	}

Comments

quicksketch’s picture

We'll need to give this a bit more thought. I don't think it's going to be a safe assumption that all users want to automatically average their votes and I'd like to make a more efficient method to casting these votes. Of course if the votes are handled by the Direct Voting Widget (it's not yet possible, but it should be soon), then each vote is going to have to be handled individually as the user clicks.

It's worth mentioning that you can do this manually and without modifying Fivestar by implementing hook_votingapi_insert(), then you can use the code you've posted above almost exactly within a custom module.

thatistosay’s picture

I'm sure more thought will be needed :) I just wanted to raise the issue, since some averaging mechanism will be needed. I haven't looked very thoroughly at the votingapi_hook_insert possibility.. I was aiming for just getting things running quickly when I hacked this together!

The issue of handling each vote as the user clicks is, I think, covered here. I based my node widget-display code on the example code in the multi-axis Issue, and the votes are properly handled via 'ajax' as a result of the existing multi-axis patch. Seems to work fine, the average being calculated anew each time the user clicks a star.

najibx’s picture

each criteria may have different percentage, rather than simple average i.e $vote_sum/$vote_count
But I agree, there should be a mechanism for averaging somehow.

crea’s picture

dupe

crea’s picture

This probably should go to some sort of hook. So each module or user could implement own mode of averaging

crea’s picture

crea’s picture

So for inserting and deleting average votes on 'vote' axis both hook_votingapi_insert() and hook_votingapi_delete() must be implemented.
Do we need to check for content_id in these hooks, i.e. can $votes array contain votes for different content when hook is running ?
I mean, if $votes array consists of only single vote, everything is simple, but what about complex scenarios where $votes array consists of multiple completely different votes ? Are these scenarios possible with VotingAPI and do we need to support them ?

crea’s picture

I adapted code posted by thatistosay for hook_votingapi_insert. The code works for me. I'll look into it more, meanwhile I would be happy if someone reviewed this.

/** 
 *  Implementation of hook_votingapi_insert()
 *  
 *  
*  @param $votes
 *   A vote or array of votes, each with the following structure:
 *   $vote['vote_id']
 *   $vote['content_type']  
 *   $vote['content_id']    
 *   $vote['value_type']    
 *   $vote['value']         
 *   $vote['tag']           
 *   $vote['uid']           
 *   $vote['vote_source']   
 *   $vote['timestamp']     
 */

function custom_votingapi_insert($votes) {

  //  Check if $votes is single vote or array of votes
  //  and make it array anyway 
  if (!empty($votes['content_id'])) {
    $votes = array($votes);
  } 
  
  foreach($votes as $id => $vote) {
   // skipping 'vote' axis votes that casted directly so we don't call ourself
   // and don't mess with single axis setups
    if ($vote['tag']!='vote') {    
    
       // applying mask for finding all axis votes of current vote
       $criteria = array('value' => NULL, 'vote_id' => NULL, 'tag' => NULL) + $vote;
       
       // getting all axis votes for the current vote update         
       $all_axis_votes = votingapi_select_votes($criteria);
        
       // calculating sum of all "non-vote" axis votes
       foreach($all_axis_votes as $id => $axis_vote) {
         if($axis_vote['tag']!='vote') {
           $vote_sum += $axis_vote['value'];
           $vote_count++;   
         } else {
       // old average vote that must be replaced with new value
          $oldvotes[] = $axis_vote;
         } 
       }
       $newvote = array('value' => $vote_sum/$vote_count, 'tag' => 'vote') + $criteria;
       votingapi_set_votes($newvote, $oldvotes);
    }
  } 
}
crea’s picture

Another issue is deleting average votes. Fivestar itself is bad at this: it only deletes votes for the single specified axis. Hook_votingapi_delete also seems to be bad option for this: we cannot know what is happening because it's called before actual deletes ( see votingapi_delete_votes() ). So it's getting complicated.
Would it be better idea to make "dummy" CCK-Fivestar field that would serve only as tool to trigger "average" updates ? It wouldn't allow to enter any values, just simple settings.
How about dummy field, that allows user to select one of the already attached Computed Fields as source of average value ? So it would be flexible enough and user would be able to "calculate average" any way he wants!
Unfortunatly I have very little experience of CCK, so I have questions about it. Is it possible to create such field module so it could be run after all fivestar cck fields ? Will setting "dummy field" module weight higher that Fivestar work for this ?

crea’s picture

After some thought, I think best approach would be to forget about hook_votingapi_insert and hook_votingapi_delete.
They are too general for this rather simple voting scenario. VotingAPI can have many applications and these hooks are called on every events. Because of that using them does not help, and only adds additional layer of complexity - we need to check for context inside hooks. So in simple multi-axis vote scenario, where we have "review" node and "target node" rating is only altered by review nodes - why not just use general nodeapi hooks for review nodes ? If "review" node is updated recalculate "average", when node is deleted - remove "average" vote. It already works so with fivestar cck fields, so same approach would work best for additional "average" vote.
Until we have tight and unified multi-axis configuration system in Fivestar, multi-axis voting remains "build your own system"-type feature. So casual drupalers can just use Rules module (Workflow-NG for 5.x) and don't mess with any custom modules.

quicksketch’s picture

Version: 6.x-1.x-dev » 6.x-2.x-dev
Status: Needs review » Active

Moving this to active, since there aren't any real patches to review in this issue. I've branched the 2.x version (now in HEAD), so if we're wanting this feature, it's the perfect time to figure it out since we can break old APIs. I'm still not sure if this would be included at all, and I'm at least not planning on implementing it.

crea’s picture

quicksketch, if you want this to be feature of fivestar, then best would be to provide additional "dummy" CCK fivestar field that works like computed field, only for fivestar. Field setting would include single PHP evaluate field where user must insert code for calculating average field. Also "Average" name is not needed anymore, since that field will allow to calculate additional "dummy" value using any formula. Call it "overall rating" or something like that. I think this can be done very quickly using fivestar.module as source. Just need to make sure code for that field runs after all other fivestar CCK fields.
I would code it myself, but after some research I have found that most simple way is to use Rules module :)

davedg629’s picture

I am very interested in this feature request. I have a multi-axis rating system set up and I use code in my node template file to display the averages of each rating axis (call this the "Overall Rating"). The problem is that I cannot sort nodes by the "Overall Rating" in a View because it is not a field of the node. I have tried many avenues to put this code in a cck field but nothing has worked. This has left me with a rating system that calculates an "Overall Rating", but cannot sort by "Overall Rating" in a table View.

Can you explain how you used the Rules module to calculate an "Overall Rating"?

crea’s picture

Use computed field, also you need to save it's values in database, that way it should be available in Views. This part is about displaying individual 'average' votes of your review nodes.
You also need this field to cast it's vote, so individual "averages" count towards overall "average" vote. You could do it in computed field code too, but I think using Rules is more flexible.
Ofcourse you can do like you did before - don't cast average votes at all and calculate it at theming stage of your product node. It's your choice.
My computed field setting:
In computed code

$average = ($node->field_axis_a[0]['rating'] + $node->field_axis_b[0]['rating'] + $node->field_axis_c[0]['rating'] + $node->field_axis_d[0]['rating']) / 4;
$node_field[0]['value'] = $average;

In display format

$display = theme('fivestar_static', $node_field_item['value'], '5');

My Rules rule that acts on updating review looks like this:

if ($node->status == 1) {
  $rating = ($node->field_axis_a[0]['rating'] + $node->field_axis_b[0]['rating'] + $node->field_axis_c[0]['rating'] + $node->field_axis_d[0]['rating']) / 4;
} else {
  $rating = 0;
}
$target = $node->noderef[0]['nid']; // Nodereference field of review pointing to it's target product node
_fivestar_cast_vote('node', $target, $rating, 'vote', $node->uid, FALSE, TRUE);
votingapi_recalculate_results('node', $target);

UPDATE:
Don't forget, you will also need to clear 'average' axis vote in case review is deleted. So you will need additional Rules action like this:

$rating = 0;
$target = $node->field_noderef[0]['nid'];
_fivestar_cast_vote('node', $target, $rating, 'vote', $node->uid, FALSE, TRUE);
votingapi_recalculate_results('node', $target);
borfast’s picture

That's an interesting solution but how about when we need an average for multi-axis ratings that are not provided in a review node - when the rating widgets are present on the rated node itself instead of being CCK fields on a review node?

Is there a 'vote was cast' trigger that can be used with Rules (I don't think so, I didn't see any)? If there isn't, would this be something worth implementing?

crea’s picture

That's an interesting solution but how about when we need an average for multi-axis ratings that are not provided in a review node - when the rating widgets are present on the rated node itself instead of being CCK fields on a review node?

Direct Fivestar and Fivestar-CCK are completely different realms. This solution covers only Fivestar-CCK setups. With direct rating, it's probably more simple. Most of complexity in CCK solution comes in the fact rating comes from another node type.

borfast’s picture

The only possible solution I see (I haven't tried it yet) is to use fivestar_get_votes() along with a fivestar theme function. Am I going in the right direction with this, or is there a simpler way to achieve what I want?

quicksketch’s picture

borfast, that sounds like exactly the approach I'd probably take. fivestar_get_votes() is currently limited to getting one tag at a time (I think), you might consider going even one level lower and using votingapi_select_votes().

crea’s picture

I updated #14 with instructions to clear voting in case review node is deleted.

borfast’s picture

Thanks for the tip, quicksketch.

In case this is useful to anyone, I got it working with this code in a function in template.php:

$votes = votingapi_select_votes(array('content_id' => $nid));
$rating = 0;
foreach($votes as $vote) {
  $rating += $vote['value'];
}
$count = count($votes);
$rating /= ($count ? $count : 1); // prevent division by 0
return theme_fivestar_static($rating);

I then print the result of this function wherever I need on my theme.
Far from being the most effective solution but it works...

Thanks again! :)

Jboo’s picture

Hi crea,

This seems like what I need to do, but I'm a bit confused what I need to do exactly. What I'm trying to achieve is to have a 'product' node and a 'review' node. The review node has 3 rating options (design, performance, value). I'm using the node relativity module to get a parent/child type relationship between the product and review nodes. The product node needs to display the average of 'design', 'performance' and 'value' of the all of the review nodes. I have setup each rating with an axis name the same ('design', 'performance', 'value').

I've tried to follow your instructions but I'm unsure whether this is exactly what I'd need to do for my situation, and whether I've actually done it correctly.

This is exactly what I've done so far. In the computed field for the 'product' content type I have this as the computed code:

$average = ($node->field_axis_design[0]['rating'] + $node->field_axis_performance[0]['rating'] + $node->field_axis_value[0]['rating']) / 3;
$node_field[0]['value'] = $average;

In the display format:

$display = theme('fivestar_static', $node_field_item['value'], '5');

Rules:

if ($node->status == 1) {
  $rating = ($node->field_axis_design[0]['rating'] + $node->field_axis_performance[0]['rating'] + $node->field_axis_value[0]['rating']) / 3;
} else {
  $rating = 0;
}
$target = $node->noderef[0]['nid']; // Nodereference field of review pointing to it's target product node
_fivestar_cast_vote('node', $target, $rating, 'vote', $node->uid, FALSE, TRUE);
votingapi_recalculate_results('node', $target);

and:

$rating = 0;
$target = $node->field_noderef[0]['nid'];
_fivestar_cast_vote('node', $target, $rating, 'vote', $node->uid, FALSE, TRUE);
votingapi_recalculate_results('node', $target);

I'd really appreciate it if you could point out whether this is the right approach for what I'm trying to achieve, and what I've done wrong.

Many thanks.

crea’s picture

Jboo,

I'm using the node relativity module to get a parent/child type relationship between the product and review nodes.

You have to use nodereference field for relationship, to use this tutorial! If you don't understand the code, don't use it, find another guy who will do it for you. For noderelativity module you'll have to figure out how to fetch nid of product node from the review node.

davedg629’s picture

#14 is a great little tutorial. Wouldn't you need to add a rule that triggers when a "Review" node is created? Otherwise, the average of all the "Overall Ratings" is not recalculated when a new "Review" node is created.

davedg629’s picture

I've been getting some requests to explain how to accomplish this feature. Here is a rough draft of a tutorial:

Tutorial Objective: Create an "Overall Rating" using a multi-axis rating system described here and integrate with Views.

Step 1: Read this tutorial and use it to create a multi-axis rating system with the Fivestar, CCK, Voting API, Computed Fields, Rules, and Views Module.

Step 2: Calculate Overall Rating by averaging each voting axis

Two voting axes were created using the previously mentioned tutorial - reliability and value. The ratings of these two axes will be averaged to create an "Overall Rating". The "Overall Rating" will be calculated in a Computed Field. Add a computed field (make sure you install the Computed Field module first!) to the "Review" content type. Then place the following php code in the "Computed Code" section of the computed field:

$Overall_Score = ($node->field_reliabilty[0]['rating'] + $node->field_value[0]['rating']) / 2;

$node_field[0]['value'] = $Overall_Score;

This will calculate the average of the two voting axes and store them in a variable that the Computed Field module can display.

Put the following code into the "Display Format" section of the computed field:

$display = theme('fivestar_static', $node_field_item['value'], '5');

This will display the Overall Rating in the fivestar format.

Make sure the "Store using the database settings below" box is checked. Select "float" for the data type and enter "64" for the Data Length (I don't exactly know how big this value needs to be). Make sure the "Sortable" box is checked. Save the field.

Now create a new Review and when the Review is displayed it should automatically calculate the Overall Rating and display it.

Step 3: Use the Rules module to calculate the average of all "Overall Ratings"

Create a new "Triggered Rule" and label it "create_overall_rating". Select "After saving new content" for the Event. Add the "Content has type" condition and select the Product node type under "Content types".

Then add an Action and select "Execute custom PHP code" under Select an action to add. Add the following code under PHP Code:

if ($node->status == 1) {
  $rating = ($node->field_reliabilty[0]['rating'] + $node->field_value[0]['rating']) / 2;
} else {
  $rating = 0;
}

$target = $node->noderef[0]['nid']; //name of the node reference field set up when the review node type was created

_fivestar_cast_vote('node', $target, $rating, 'vote', $node->uid, FALSE, TRUE);
votingapi_recalculate_results('node', $target);

This will update the average of the "Overall Ratings" every time a new review is submitted. If you want to update the average of the "Overall Ratings" every time a review is updated or deleted, follow comment #14 in this issue

Step 4: Make the "Overall Rating" available to a view displaying the "Product" nodes.

Create a view of the "node" type and add a "Page" display. Make it a "Table" style view and give it the title "Products and their Overall Ratings". Add the node title field to the view and add a node type filter that selects nodes of the content type "Product". Then add a relationship of the type "Node: Voting results". Label it "Overall Rating", select "Percent" for the Value type, select "Vote" for the Vote tag, and select "Average" for the Aggregation function.

Then add a field of the type "Voting API results: Value". Select "Fivestar Stars (display only) for the Appearance and "Overall Rating" for the Relationship. Label the field "Overall Rating" and then save the field.

Then go to the "Table" settings and make the Overall Rating field sortable. You should now have a view that lists all of your Product nodes and their respective Overall Ratings.

Please review and correct if I made any mistakes

ctalley5’s picture

Thanks for putting this together Dave!

Anybody know if this will still apply with the next Fivestar release? Or is it more of a current fix...

thatistosay’s picture

Or another way, if you're only concerned about average results for all votes (not just individuals), is to just do this:

/**
 * Implementation of hook_votingapi_results_alter()
 *
 * Finds content with vote tags other than 'vote'
 * and replaces the vote average with an average
 * of the other tags. For multi-axis voting.
 */
function MYMODULE_votingapi_results_alter(&$cache, $content_type, $content_id) {
  $vote_avg_sum = 0;
  $vote_avg_count = 0;
  $vote_tags = 0;
  
  foreach($cache as $tag => $data) {
    if($tag != 'vote') {
      $vote_avg_sum += $data['percent']['average'];
      $vote_avg_count += $data['percent']['count'];
      $vote_tags++;
    }
  }
  if($vote_tags > 0) {
    $cache['vote']['percent']['average'] = $vote_avg_sum/$vote_tags;
    $cache['vote']['percent']['count'] = $vote_avg_count/$vote_tags;
  }
}
arbel’s picture

How would this part change:

if ($node->status == 1) {
$rating = ($node->field_reliabilty[0]['rating'] + $node->field_value[0]['rating']) / 2;
} else {
$rating = 0;
}

$target = $node->noderef[0]['nid']; //name of the node reference field set up when the review node type was created

_fivestar_cast_vote('node', $target, $rating, 'vote', $node->uid, FALSE, TRUE);
votingapi_recalculate_results('node', $target);

if i'm using node comments module instead of a field reference

friolator’s picture

@davedg629:

Make sure the "Store using the database settings below" box is checked. Select "float" for the data type and enter "64" for the Data Length (I don't exactly know how big this value needs to be). Make sure the "Sortable" box is checked. Save the field

We're on PHP 5.2.10 and MySQL 5.0.45, and when we use a Float of length 64, we get SQL errors in watchdog:

Incorrect column specifier for column 'field_overall_computed_value' query: ALTER TABLE content_type_review ADD `field_overall_computed_value` FLOAT(64) DEFAULT NULL in /path/to/database.mysqli.inc on line 128.

...when saving the field settings. I don't get this error if the length is 32 -- which I'd think would be more than enough precision for something that's only based on 5 stars!

I don't know if this is a limitation of MySQL or what, but I think in most cases 32 would be enough.

Thanks for the tutorial, by the way - you saved me a ton of time figuring this out on my own!

friolator’s picture

@arbel:

we're doing this the same way you are, with Node Comment. Change the code that goes into the Rule so that the $target variable gets the current nodecomment's parent node id. This is in the node object as comment_target_nid. so:

<?php
$target = $node->noderef[0]['nid']; //name of the node reference field set up when the review node type was created
?>

should become:

<?php
$target = $node->comment_target_nid; //node id of the parent node for this nodecomment
?>

That should do it.

gulliverrr’s picture

@#24 - At Step 3's first line: "Add the "Content has type" condition and select the *Product* node type under "Content types"." should be for *Rating* node type. At least thats what makes sense to me and what makes my example work :)
Thanks a lot Dave for all the info put together!!!

abaddon’s picture

i have 3 axis which users vote on, the default "vote" one is hidden and its used to calculate totals for the other 3 axis, and im using it in views to sort etc, im sure this is a common setup, im using 2.x-dev and not CCK+extra review node, just basic widget+comments widget to require reviews, comments one seems to modify the node one ok, so all good, and i dont care much if they use the node one to make partial votes (just 1 axis out of 3), just that commenting requires all 3 of them (its actually a photo review site)

regarding #26, im using it with a small modification, if a user just votes on 1 axis, ill get totals like "10.333"... so ive fixed that:

        $vote_max_count = 0 ;
...
                        $vote_max_count = $data['percent']['count'] > $vote_max_count ? $data['percent']['count'] : $vote_max_count ;
...
                $cache['vote']['percent']['count'] = $vote_max_count ;

also, see my other 2 posts on fivestar
http://drupal.org/node/786224#comment-2934690 , you need this to be able to hide the "vote" tag widget and let the others axis display, otherwise they all get their settings from "vote" regardless of their own settings, so cant hide "vote" or make it static while the others are clickable, use this patch to make it work
http://drupal.org/node/791414 , you actually need this to make comment voting work

kompressaur’s picture

Ive got my multi axis voting system almost working thanks to dave's guide and other posts and threads on drupal. Ive got it set up with notecomments and after a straight 60hrs working on it almost it's starting to take shape. I have a few issues but the one thats most puzzling me at the moment is on setting up the noderef selection field. I have it set up via a View with football team names on it. You can view it here
http://onlinebanter.com/dundee-fc

Is there anyway to set it up so as the selection box choses the node contextually? I forgot to mention that i am using panels also. As you can see from that dundee page it will just select the top most team (airdrie) Ive made the mistake myself of adding reviews to the wrong team. Actually a way around it could be just not to place the nodecomment box on a team page but rather a division. anyhow would it be possible to set up the noderef view you think so as it choses the right team?

Also similarly i would love to be able to just put the code that i have put in my node-product.tpl to display the reults into a custom pane but i dont think it was displaying the correct votes. Is this a known issue?

thanks.

kompressaur’s picture

Also would it be possible for me to have 8 or 9 voting axis for each node? I havent seen it mentioned before. someone seemed real proud having 3 axis. am i living beyond my dreams thinking i could have 9 in there? Knowing this might save me a lot of work. thanks.

kompressaur’s picture

I just realised 2 things there. 1) the comment form has to be on the page that its commenting on to show up in the view and 2) this thread is all about fivestar 6.2

soz

I'll just have to fix it all in time.

stuartgoff’s picture

@14 Shouldn't you delete the DB entry if the 'review' is deleted? If you set to '0' you skew the results.

stuartgoff’s picture

@24 & @14 - Went through the process you detailed and worked with no problems. Thanks! I was just wondering if you could drop a step and setup a 5* field on the review for the avg and use rules to set the value.

danadeek’s picture

Thank you davedg629 but how can we display the result in the product node ??? Btw i am using node comment

stuartgoff’s picture

Yes, I am having an issue when a node (the set of votes) is deleted the Overall or averaged vote is still in the system. Any help?

Found the item to put in the 'Rule'
votingapi_delete_vote($vobj)
but how do I find the object($vobj)?

TheodorosPloumis’s picture

I think that this line at the module from http://drupal.org/node/335493#comment-1775512
$vote_avg_count += $data['percent']['count'];

should be:
$vote_points_count += $data['points']['count']; //only points are usefull. Otherwise there will be a zero calculation

and also this line
$cache['vote']['percent']['count'] = $vote_avg_count/$vote_tags;

should be:
$cache['vote']['points']['count'] = $vote_points_count/$vote_tags; //points are used for count

Here is a complete example of the module which calculates for axis vote (overall axis) sum of all other axis points, percent of axis and count of reviews:

<?php
/**
* Implementation of hook_votingapi_results_alter()
*
* Finds content with vote tags other than 'vote'
* and replaces the vote average with an average
* of the other tags. For multi-axis voting.
*/
function overall_rating_votingapi_results_alter(&$cache, $content_type, $content_id) {
  $vote_avg_sum = 0;
  $vote_points_count = 0;
  $vote_points_sum = 0;
  $vote_tags = 0;
 
  foreach($cache as $tag => $data) {
    if($tag != 'vote') {
      $vote_avg_sum += $data['percent']['average'];
      $vote_points_count += $data['points']['count'];
      $vote_points_sum += $data['points']['sum'];
      $vote_tags++;
    }
  }
  if($vote_tags > 0) {
    $cache['vote']['percent']['average'] = $vote_avg_sum/$vote_tags;
    $cache['vote']['points']['count'] = $vote_points_count/$vote_tags;
    $cache['vote']['points']['sum'] = $vote_points_sum;
  }
}
wizonesolutions’s picture

The snippet in http://drupal.org/node/335493#comment-1775512 worked well for me. In my case, I changed 'vote' to use a custom tag and related my view to that telling it to filter on "Other and typing in my custom tag name. Worked like a charm and doesn't break the default vote axis.

whiteph’s picture

Version: 6.x-2.x-dev » 7.x-2.x-dev
Issue summary: View changes
chris_h’s picture

This works well as a snippet for a multi-axis average on a referenced node: https://drupal.org/comment/8200647#comment-8200647

whiteph’s picture

@chris_h - thanks, will look at that shortly.