Complete guide for multiple CStarRatings on same page

Yii comes with a CStarRating widget, it is something visitors probably is familiar with since youtube and many other sites use something similair.

But how to use it?

A demo of the finished product is here: http://demo.wedson.se/howto

*Please note, this webhost is very slow imho so it can be some delay when voting,but atleast you can see how it all works before trying to follow this tutorial.

What we need and want is:

*The CStarWidget (duh)

*A model which can have a rating

*A callback that runs everytime a user click on the stars.

*A backend controller action that can recieve data from the callback and update the database.

*The backend controller action also needs to return something to the widget.

*When the user has voted, the CStarWidget should become read-only ( CStarWidget has a built in option for this)

*If we put the CStarWidget in our views/modelName/_view.php it needs to get an unique name so we can know what row in the database we should update, where to show the confirmation message and which widget we should turn into read-only.

*We also want the widget to show the current average rating (and fill the stars accordingly, say a model has an average rating of 5 out of a maximum 10, we want 5 stars to be filled when the page loads)

*We want to show the current rating in decimal form, ie 7,46 aswell as the number of votes.

So we start by creating a table to store all ratings.


DROP TABLE IF EXISTS `tbl_rating`;

CREATE TABLE IF NOT EXISTS `tbl_rating` (

  `id` int(11) NOT NULL AUTO_INCREMENT,

  `vote_count` int(11) NOT NULL DEFAULT '0',

  `vote_average` varchar(50) CHARACTER SET armscii8 COLLATE armscii8_bin NOT NULL DEFAULT '5',

  `vote_sum` int(11) NOT NULL DEFAULT '0',

  PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

Note: this is not optimized for performance.

Lets say we want the user to give a rating to our blog posts.

We add a column in our posts table, and name it rating_id.

We then go to protected/models/post.php and define a relation:

‘rating’=>array( self::BELONGS_TO, ‘Rating’, ‘rating_id’),

In posts/controller we create a new action.

This action can recieve an id from an ajaxRequest, updates the total vote count and calculate the new average and after saving the new data,it sends some information back to the view.




public function actionRating()

	{			

             

		if ( Yii::app()->request->isAjaxRequest )

		{

		$rating = Rating::model()->findByPk($_GET['id']);

			$rating->vote_count = $rating->vote_count + 1;

			$rating->vote_sum = $rating->vote_sum + $_GET['val'];

			$rating->vote_average = round($rating->vote_sum / $rating->vote_count,2);

			

			if ( $rating->save() ) 

			{

			echo CJSON::encode( array (

			'status'=>'success', 

			'div'=>'Thank you for voting!',	

			'info'=>"Rating: " . $rating->vote_average ." " . $rating->vote_count . " votes",

			) );

			}

		}

	}



We then go to protected/views/post/_view.php

This is where we put the CStarWidget, since the file is called once for each post we have to assign an unique name for the widget. We also need unique div ids so we can present the confirmation at the right place.


<div  id="rating_info_<?=$data->rating_id?>">

<?php 

	if ( $rating = Rating::model()->findByPk($data->rating_id) ) //if the record has an rating_id we echo it

		{	

			echo "Rating: <strong>" . $rating->vote_average ."</strong>";

			echo " " . $rating->vote_count . " votes";

		}

?>

	</div>


	

<?php // rating

	$this->widget('CStarRating',array(

    'name'=>'rating'.$data->rating_id, // an unique name

	'starCount'=>10,

	'readOnly'=>false,

	'resetText'=>'',

	'value'=>round($rating->vote_average,0), // this makes the widget shows the current rating rounded to //closest number(1,2,3,4,5,6,7,8,9 or 10)

    'callback'=>' // updates the div with the new rating info, displays a message for 5 seconds and makes the //widget readonly

        function(){

        	url = "/post/rating";

			jQuery.getJSON(url, {id: '.$data->rating_id.', val: $(this).val()}, function(data) {

				if (data.status == "success"){

					$("#rating_success_'.$data->rating_id.'").html(data.div);

					$("#rating_success_'.$data->rating_id.'").fadeIn("slow");		

					var pause = setTimeout("$(\"#rating_success_'.$data->rating_id.'\").fadeOut(\"slow\")",5000);

					$("#rating_info_'.$data->rating_id.'").html(data.info);

					$("input[id*='.$data->rating_id.'_]").rating("readOnly",true);

					}

				});}'

			));

?> 	

<div id="rating_success_<?=$data->id;?>" style="display:none"></div> <!-- the div in which the confirmation message is shown-->



And this is it! This way you can use one rating table for as many models as you want.

NOTE! I wrote the comments for the code here on the forum, so to be sure please remove them when u understand the code so you do not get any weird errors!

Hope it helps someone!

//Sampa

Thanks for your tutorial…Can you give some hints about how you make the login/logout button in your website?

Ofc, I just bootstrap extention by Chris83 , bootmodal.

Basiclly it works like this:

  1. I create a modal window, the content I get by sending an ajax request to my controller action that returns the view for modal login (the same action shows a normal form if the user sees the login page in any other way then by opening the modal window (example trying to access a page he does not access to).

When the user then submits the form inside the modal dialog, I make another ajax request to the same controller action.

if the controller action sees that $_POST[‘submit’] is set, it validates the form input and sends back another json response to the user and logs him in.

The logout action I use is the default…

I paste some code now, but I dont have time to support it. But maybe helps you some.

login action (in SiteController for me)


	public function actionLogin()

	{

		$model=new LoginForm;


		// if it is ajax validation request

		if(isset($_POST['ajax']) && $_POST['ajax']==='login-form')

		{

			echo CActiveForm::validate($model);

			Yii::app()->end();

		}

	

		// collect user input data

		if(isset($_POST['LoginForm']))

		{

			$model->attributes=$_POST['LoginForm'];

			// validate user input and redirect to the previous page if valid

			if($model->validate() && $model->login()){

				  if (Yii::app()->request->isAjaxRequest)

					{			                		

                    echo CJSON::encode(array(

                        'status'=>'success', 

                        'div'=>'Login',

                        'title'=>'',

                        ));

                    exit;             

					} else{				

						$this->redirect(Yii::app()->user->returnUrl);

						}

				}

		}

		if (Yii::app()->request->isAjaxRequest)

        {

            echo CJSON::encode(array(

                'status'=>'render', 

                'div'=>$this->renderPartial('modalLogin', array('model'=>$model), true)));

            exit;               

        } else{

		// display the login form

		$this->render('login',array('model'=>$model));

		}

register action (for me in UserController,but doesnt matter…)




public function actionRegister()

	{

		$model = new User;

		// Uncomment the following line if AJAX validation is needed

		 $this->performAjaxValidation( $model , 'user-form' );

	

		if ( isset ( $_POST['User'] ) )

		{

			$model->attributes = $_POST['User'];


		//User::beforeSave() to see what is done with the values from the form

			if ( $model->validate() )

				{ if ( $model->save() )

					{

						$dir = User::USER_DIR . $model->id; 

						

						mkdir($dir,0777,true); 

						if ( Yii::app()->request->isAjaxRequest )

							{

								echo CJSON::encode( array(

									'status'=>'success', 

									'div'=>'Sign up successfull, you can login now if you want',

									'title'=>'',

									));

								exit;             

							}

						else{

							  $this->redirect( array( 'view','id' => $model->id ) );

							}

					}

				}

		}


	if ( Yii::app()->request->isAjaxRequest )

		{

			echo CJSON::encode( array (

				'status'=>'render', 

				'div'=>$this->renderPartial('modalReg',array('model'=>$model),true,true),

				'title'=>'',

				) );

			exit;             

		} else 

		{

			$this->render('Reg',array(

			'model'=>$model,

			));

		}

	}



And last some code for the views:




//the buttons

		<?php

		if ( $this->isGuest ):

		?>	<button class="btn btn-primary" id="loginButton"><!-- loginbutton-->

				Login

			</button> <!--login button-->

			

			<button class="btn btn-primary" id="regButton"><!-- sign up button-->

				Sign up

			</button> <!--sign up button-->

			<!-- files with modalwindow, ajax calls etc for easier reading -->

		<?php $this->renderPartial('//site/_login'); 

		      $this->renderPartial('//site/_reg');

		endif;

                 ?>


_login.php

<?php $this->beginWidget('bootstrap.widgets.BootModal', array(

    'id'=>'dialogLogin',

    'htmlOptions'=>array('class'=>'hide'),

    'events'=>array(

        'show'=>"js:function() { console.log('dialogLogin show.'); }",

        'shown'=>"js:function() { console.log('dialogLogin shown.'); }",

        'hide'=>"js:function() { console.log('dialogLogin hide.'); }",

        'hidden'=>"js:function() { console.log('dialogLogin hidden.'); }",

    ),

)); ?>

<div id="modal_login" class="divForForm well" style="padding:25px;"></div>

<?php $this->endWidget(); ?> 

<script>

function getLogin(){

  <?php echo CHtml::ajax(array(

            'url'=>array('/site/login'),

            'data'=> "js:$(this).serialize()",

            'type'=>'post',

            'dataType'=>'json',

            'success'=>"function(data)

            {

                if (data.status == 'render')

                {

                    $('#modal_login').html(data.div );

                          // Here is the trick: on submit-> once again this function!

                    $('#dialogLogin div.divForForm form').submit(getLogin);

                }

                else

                {  /* $('#dialogLogin').modal('close') */

                    $('#dialogLogin div.succesDiv').html(data.div);

                }

 

            }",

            )); ?>

}


$("#loginButton").click(function(){

getLogin();

$('#dialogLogin').modal('show');

return false; 

});

</script>

<!-- end login -->


//_register.php

<?php

	$this->beginWidget('bootstrap.widgets.BootModal', 

	array(

		'id'=>'dialogReg',

		'htmlOptions'=>array('class'=>'hide'),

		'events'=>array(

			'show'=>"js:function() { console.log('dialogReg show.'); }",

			'shown'=>"js:function() { console.log('dialogReg shown.'); }",

			'hide'=>"js:function() { console.log('dialogReg hide.'); }",

			'hidden'=>"js:function() { console.log('dialogReg hidden.'); }",

		),

	)); 

?>

<div id="modal_registration" class="divForForm well" style="padding:15px;"></div>

<script>

function getReg(){

  <?php echo CHtml::ajax(array(

            'url'=>array('/user/register'),

            'data'=> "js:$(this).serialize()",

            'type'=>'post',

            'dataType'=>'json',

            'success'=>"function(data)

            {

                if (data.status == 'render')

                {

                    $('#modal_registration').html(data.div );

                          // Here is the trick: on submit-> once again this function!

                    $('#dialogReg div.divForForm form').submit(getReg);

                }

                else

                {  /* $('#dialogStep').bootModal('close') */

                    $('#dialogReg div.succesDiv').html(data.div);

                }

 

            }",

            )); ?>

}

</script>


<?php $this->endWidget(); ?> 

<script>


$("#regButton").click(function(){

getReg();

$('#dialogReg').modal('show');

return false; 

});

</script>

<!-- end reg -->


//and modalLogin.php as u can see I use in the login controller action

//basiclly just a bootstrap designed login form view

	<div class="form">

<?php 

	$form = $this->beginWidget('BootActiveForm',array(

		'id'=>'login-form',

		'enableAjaxValidation'=>true,

	)); 

?>

	<div class="modal-header">

		<a class="close" data-dismiss="modal">&times;</a>

		<h3>Please fill out the following form with your login credentials</h3>

	</div>


	<div class="modal-body">

		<p class="note">Fields with <span class="required">*</span> are required.</p>


		<div class="row">

			<?php echo $form->labelEx($model,'username'); ?>

			<?php echo $form->textField($model,'username'); ?>

			<?php echo $form->error($model,'username'); ?>

		</div>


		<div class="row">

			<?php echo $form->labelEx($model,'password'); ?>

			<?php echo $form->passwordField($model,'password'); ?>

			<?php echo $form->error($model,'password'); ?>

			<p class="hint">

				Hint: You may login with  <tt>admin/admin</tt>.

			</p>

		</div>


		<div class="row rememberMe">

			<?php echo $form->checkBox($model,'rememberMe'); ?>

			<?php echo $form->label($model,'rememberMe'); ?>

			<?php echo $form->error($model,'rememberMe'); ?>

		</div>


	</div>

	<div class="modal-footer">

		<?php echo CHtml::submitButton('Login',array('class'=>'btn btn-primary')); ?>

		<?php echo CHtml::link('Close', '#', array('class'=>'btn', 'data-dismiss'=>'modal')); ?>

	</div>


<?php $this->endWidget(); ?>

</div><!-- form -->




Hey, Thanks so much for your help!

I should really buy you a dinner! haha :D

I have a question about your rating, though.

Is there any change i can put them in the view.php instead of _view.php?

So where should I get $data…?

Also…looks like this code is kind of old because jquery will conflict now since Yii has already import jquery…

The last question… ‘value’=>round($rating->vote_average,0), this line seems very buggy… it gives me errors all the time.

Thanks anyway for your help…!!

Have a good day!

In your view.php $data is called $model :) ($data is passed to _view.php from view)

Its actually not old code at all :P I use jQuery that is imported with Yii, I dont add anything :P

round() is a php function (check at php.net) that take a value and rounds it up to fewer decimals.

if we have $foo = 9.573823153313

and do round($foo,2) we get 9.57.

round($foo,0) should give 10.

“‘value’=>round($rating->vote_average,0),”

means I set the value of the cStarRating to 0 decimals, say the vote is 6,3 I round it to 6. without any decimals, this is because I want stars to be filled with the current rating :P

try the diffrence between ‘value’=>‘6,3’, and ‘value’=>‘6’, and u will see what I mean.

What errors do you get from that line?

it gives me try to get non object from …

And also if I use the JavaScript part in your code. The star will disappear and will be changed to some radio button…that means, i guess, there is some conflict between jquery and your code…(I think it’s because jquery is loaded twice, since if I delete your JavaScript, the star will appear…)

Anyway, thanks so much for your help :slight_smile:

Thanks in advance!!!!!!

Here is my code…

In my "[size="4"]NotesController[/size]"


public function actionRating()

        {                       

             

                if ( Yii::app()->request->isAjaxRequest )

                {

                $rating = Rating::model()->findByPk($_GET['id']);    //is this line correct? I have created a model called rating based on your database;

                        $rating->vote_count = $rating->vote_count + 1;

                        $rating->vote_sum = $rating->vote_sum + $_GET['val'];

                        $rating->vote_average = round($rating->vote_sum / $rating->vote_count,2);

                        

                        if ( $rating->save() ) 

                        {

                        echo CJSON::encode( array (

                        'status'=>'success', 

                        'div'=>'Thank you for voting!', 

                        'info'=>"Rating: " . $rating->vote_average ." " . $rating->vote_count . " votes",

                        ) );

                        }

                }

        }



in my Notes.php(model)


public function relations()

	{

		// NOTE: you may need to adjust the relation name and the related

		// class name for the relations automatically generated below.

		return array(

			'owner' => array(self::BELONGS_TO, 'Users', 'owner_id'),

			'courses' => array(self::BELONGS_TO, 'Courses', 'courses_id'),

                        'rating'=>array( self::BELONGS_TO, 'Rating', 'rating_id'),

            		);

	}



Finally, in my view.php


<div  id="rating_info_<?=$model->rating_id?>">

<?php 

        if ( $rating = Rating::model()->findByPk($model->rating_id) ) //if the record has an rating_id we echo it

                {       

                        echo "Rating: <strong>" . $rating->vote_average ."</strong>";

                        echo " " . $rating->vote_count . " votes";

                }

?>

        </div>


        

<?php // rating

        $this->widget('CStarRating',array(

    'name'=>'rating'.$model->rating_id, // an unique name

        'starCount'=>10,

        'readOnly'=>false,

        'resetText'=>'',

        'value'=>round($rating->vote_average,0), ////////ERROR:Trying to get property of non-object...........

    'callback'=>' /////////////////// if i add this part. the star will disappear and be changed to some radio buttons......and nothing will be saved in database...

        function(){

                url = "/post/rating";

                        jQuery.getJSON(url, {id: '.$model->rating_id.', val: $(this).val()}, function(model) {

                                if (model.status == "success"){

                                        $("#rating_success_'.$model->rating_id.'").html(model.div);

                                        $("#rating_success_'.$model->rating_id.'").fadeIn("slow");               

                                        var pause = setTimeout("$(\"#rating_success_'.$model->rating_id.'\").fadeOut(\"slow\")",5000);

                                        $("#rating_info_'.$model->rating_id.'").html(model.info);

                                        $("input[id*='.$model->rating_id.'_]").rating("readOnly",true);

                                        }

                                });}'

                        ));

?>      

<div id="rating_success_<?=$model->id;?>" style="display:none"></div> 

Thanks!!!!!!


   

if ( $rating = Rating::model()->findByPk($model->rating_id) ) 

//the $model doesnt have a rating yet, so this returns false $rating is empty

//this is why you get error for this line:

'value'=>round($rating->vote_average,0), ////////ERROR:Trying to get property of non-object...........



So instead for this:


?php // rating

        $this->widget('CStarRating',array(

    'name'=>'rating'.$model->rating_id, // an unique name

        'starCount'=>10,

        'readOnly'=>false,

        'resetText'=>'',

        'value'=>round($rating->vote_average,0), ////////ERROR:Trying to get property of non-object.....

try this:




<?php // rating

        if ( is_numeric($rating->vote_average) ){

         $value = round($rating->vote_average,0);

         }else{

         $value = 0;

         }

        $this->widget('CStarRating',array(

         'name'=>'rating'.$model->rating_id, // an unique name

        'starCount'=>10,

        'readOnly'=>false,

        'resetText'=>'',

        'value'=>$value, ////////ERROR:Trying to get property of non-object.....




Still give me the same error Trying to get property of non-object…

in line:

[php] if ( is_numeric($rating->vote_average) ){

     &#036;value = round(&#036;rating-&gt;vote_average,0);


     }else{


     &#036;value = 0;


     }

[php]

Also…I can’t do the javascript part in your code. if so, the star will be replaced by radio button and also all jquery effect in my pages will disappear.

Thanks so much for your help.

Sorry for the trouble I caused.

did you change to this to?

‘value’=>$value, ////////ERROR:Trying to get property of non-object…

and please copy the error message.

Yes I changed this.

The error message is :

Trying to get property of non-object

/var/www/html/./protected/views/notes/view.php(63)

The error line is "$value = round($rating->vote_average,0);" (even I changed to your code)


if ( is_numeric($rating->vote_average) ){

         $value = round($rating->vote_average,0);

         }else{

         $value = 0;

         }



try this instead:




if ( $rating )){

         $value = round($rating->vote_average,0);

         }else{

         $value = 0;

         }




or




if ( isset($rating->vote_average) ){

         $value = round($rating->vote_average,0);

         }else{

         $value = 0;

         }




OR




if ( $rating->vote_count > 0 ){

         $value = round($rating->vote_average,0);

         }else{

         $value = 0;

         }






One of these should work

Yes, no errors…

but still. if i HAVE the callback javascript in my code,

it gives me 10 radio buttons instead of stars. And nothing happens when I click on it(also nothing in database).

If I delete the callback. then nothing stored in my database.


 'callback'=>' 

        function(){

                url = "/post/rating";

                        jQuery.getJSON(url, {id: '.$model->rating_id.', val: $(this).val()}, function(model) {

                                if (model.status == "success"){

                                        $("#rating_success_'.$model->rating_id.'").html(model.div);

                                        $("#rating_success_'.$model->rating_id.'").fadeIn("slow");               

                                        var pause = setTimeout("$(\"#rating_success_'.$model->rating_id.'\").fadeOut(\"slow\")",5000);

                                        $("#rating_info_'.$model->rating_id.'").html(model.info);

                                        $("input[id*='.$model->rating_id.'_]").rating("readOnly",true);

                                        }

                                });}'

                        ));



You can ofc not delete the callback, it does the request to the controller action that updates the database. Your problem is something that is caused by jQuery stuff colliding I think.

Dont know how to solve it but it obviously doesnt like something thats on your site at the same time.

I find out the problem… it’s because each time rating_id will not be generated automatically…Why…?

You have an column in your Note database table that is named rating_id ?

in NoteController actionCreate()

make sure you have


	$rating = new Rating();

			$rating->save();

			$model->rating_id = $rating->id;


			if ( $model->save() )

yes… I have figured it out. Thanks so much for your help.

hey, i have the same problem: the stars turns into radio buttons when the callback is on, and it won’t save it to the database.

did you find a solution?

how can i use it with ListView with ajax? :s

i have this and works


'afterAjaxUpdate'=>'function(id,data) {

        	$("span[id^=\'rating\'] > input").rating({\'required\':true});

but i need to update to, for example, this:


'afterAjaxUpdate'=>'function(id,data) {

$("span[id^=\'rating\'] > input").rating({'required':true,'callback':       							

function(){                 							

url = "/music/rating";                     							

jQuery.getJSON(url, {id: 1, mid: 4, val: $(this).val()}, function(data) {                         							

if (data.status == "success"){                             							

$("#rating_success_4").html(data.div);                             							

$("#rating_success_4").fadeIn("slow");                                                      	

var pause =  setTimeout("$(\"#rating_success_4\").fadeOut(\"slow\")",5000);                             							

$("input[id*=4_]").rating("readOnly",true);                         							

}                     							

});               	                       

 }}); 



some values are created in the _view :s and i have this code in index of view…

I want to call ajax function on clicking empty (or cancel) button. Any suggestions?