Yii: Добавляем рейтинг статей CStarRating с возможностью оценивать статью зарегистрированным пользователям

Yii: Добавляем рейтинг статей CStarRating с возможностью оценивать статью зарегистрированным пользователям Добавляем звёзды рейтинга для статей с помощью класса CStarRating, чтобы пользователи могли оценить понравившуюся статью.

Подробнее про класс CStarRating читаем здесь в документации Yii.

В базе данных MySQL делаем:

1) В таблице статей добавляем поле 'rating' (int(11) с NULL по-умолчанию). Здесь будет общий рейтинг, суммарный от всех пользователь, но "нормированный к сотне" (то есть 5 звёзд соответствуют максимальному значению 500)

2) Создадим дополнительную таблицу 'rating', здесь каждая запись, это оценка конкретного пользователя для конкретной статьи (оценка по звёздам от 1 до 5):

--
-- Структура таблицы `rating`
--

CREATE TABLE IF NOT EXISTS `rating` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `article_id` int(10) NOT NULL,
  `user_id` int(10) NOT NULL,
  `value` tinyint(3) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`),
  KEY `article_id` (`article_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

--
-- Ограничения внешнего ключа сохраненных таблиц
--

--
-- Ограничения внешнего ключа таблицы `rating`
--
ALTER TABLE `rating`
  ADD CONSTRAINT `rating_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `tbl_users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
  ADD CONSTRAINT `rating_ibfk_1` FOREIGN KEY (`article_id`) REFERENCES `tbl_article` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;

 

3) В файл вида подробной статьи добавляем оценку + возможность оценить для зарегистрированного пользователя

<div class="b-rating__with_text">
		<?php
			$data->ShowRating();
			echo '<span>&nbsp;&nbsp; '.$data->marksCount.' '
				.CLocoHelper::GetFormatWord('оценка', $data->marksCount).'</span>'
			?>
		</div>
		<div class="b-rating__with_text">
			<?php
					if (!Yii::app()->user->isGuest){
						$this->widget('CStarRating', array(
							'name' => 'user-input-rating',
							'minRating' => '1',
							'maxRating' => '5',
							'value' => $data->GetUserRating(), // mark 1...5
							'starWidth'=>'15',
							'ratingStepSize' => '1',
							//'titles'=>  CHtml::listData(RatingDesc::model()->cache(3600)->findAll(),'id','desc'),
							'allowEmpty'=>false,
							'callback' => 'function(value) {SetRating(value, ' . $data->id . ');}',
                            'cssFile'=>'/css/rating/jquery.rating.css',
						));
					echo '<span>&nbsp;&nbsp;оцените статью</span>';}
					?>
		</div>
		<?php
					$cs = Yii::app()->getClientScript();
					$cs->registerScriptFile('/js/rating.js');
		?>
Вызывается в 'callback' функция SetRating(value, article), которая находится в js/rating.js (см. ниже) и ей передаются id статьи и оценка пользователя ( от 1 до 5)
 
4) В модель Article добавляем
 
/**
     * Return rating in specific format
     * @return string rating in specific format
     */
    public function GetFormattedRating()
    {
        $rating = $this->rating / 100;
        $rating = number_format(round($rating * 4) / 4, 2);
        $rating = sprintf("%1.2f", $rating);
        $rating = trim(rtrim($rating, "0"), '.');
        return $rating;
    }
    
  /**
     * Return user mark for the good
     * @return string user mark for the good
     */
    public function GetUserRating()
    {
        $value = Rating::model()->findByAttributes(array(
                                                        'article_id' => $this->id,
                                                        'user_id' => Yii::app()->user->id,
                                                   ));
        //echo $value;
        if (isset($value))
            return sprintf("%d", $value->value);
        else
            return '0';
    }

	/**
     * Show good rating with css styles
     */
    public function ShowRating()
    {
        $good_rating = $this->GetFormattedRating();
        echo '<div class="x-rating x-rate-' . round($good_rating * 4) . '"></div>';
    }
5) В контроллер ArticleController.php
/**
     * Set mark for the good by user
     * @param $article_id
     * @param $rate mark (1-5)
     */
    public function actionSetMark($article_id, $rate)
    {
        if (Yii::app()->user->isGuest) {
            echo 'you are not registered user';
            return;
        }
        $user = Yii::app()->user->id;
        $rating = Rating::model()->with(array('article', 'article.marksCount'))
                ->findByAttributes(array(
                                        'article_id' => $article_id,
                                        'user_id' => $user,
                                   ));
        if (empty($rating)) { // если никто не оценивал статью ещё
            if ($rate == 'undefined') {
                return;
            }

            $rating = new Rating;
            $rating->article_id = $article_id;
            $rating->user_id = $user;
            $rating->value = $rate;
            if ($rating->save()) {
                $good = $rating->article;
                $good->rating = round($good->rating + ($rate * 100 - $good->rating) / ($good->marksCount + 1));
                $good->save();
                echo 'success';
            }
            else
                echo 'fail saving';
        } else {
            if ($rating->value != $rate) {
                if ($rate == 'undefined') {
                    echo 'rate deleted';
                    $rating->delete();
                    return;
                }
                $old_mark = $rating->value;
                $rating->value = $rate;
                if ($rating->save()) {
                    $good = $rating->article;
                    $good->rating = round($good->rating + ($rate * 100 - $old_mark * 100) / $good->marksCount);
                    $good->save();
                    echo 'success';
                }
                else
                    echo 'fail saving';
            } else
                echo 'no need to change';
        }
    }

6) В /js/rating.js

/*
 * Send new user choice, then recieve object where define what need to change
 * and change selects
 */

function SetRating(value, good) {
        $.ajax({
                url:"/review/setmark",
                type:'GET',
                data: "good_id="+good+"&rate=" + value,
                success: RatingSet
        });
}

function RatingSet(data){
	
}

7) В css/main.css

.x-rating {
  float:left;
  height:16px;
  margin-bottom:0;
  margin-left:20px;
  margin-right:0;
  margin-top:1px;
  width:75px;
}

.x-rate-0 {background:url(../css/star0.gif) -75px 0 no-repeat;}
.x-rate-1 {background:url(../css/star1.gif) -75px 0 no-repeat;}
.x-rate-2 {background:url(../css/star2.gif) -75px 0 no-repeat;}
.x-rate-3 {background:url(../css/star3.gif) -75px 0 no-repeat;}
.x-rate-4 {background:url(../css/star0.gif) -60px 0 no-repeat;}
.x-rate-5 {background:url(../css/star1.gif) -60px 0 no-repeat;}
.x-rate-6 {background:url(../css/star2.gif) -60px 0 no-repeat;}
.x-rate-7 {background:url(../css/star3.gif) -60px 0 no-repeat;}
.x-rate-8 {background:url(../css/star0.gif) -45px 0 no-repeat;}
.x-rate-9 {background:url(../css/star1.gif) -45px 0 no-repeat;}
.x-rate-10 {background:url(../css/star2.gif) -45px 0 no-repeat;}
.x-rate-11 {background:url(../css/star3.gif) -45px 0 no-repeat;}
.x-rate-12 {background:url(../css/star0.gif) -30px 0 no-repeat;}
.x-rate-13 {background:url(../css/star1.gif) -30px 0 no-repeat;}
.x-rate-14 {background:url(../css/star2.gif) -30px 0 no-repeat;}
.x-rate-15 {background:url(../css/star3.gif) -30px 0 no-repeat;}
.x-rate-16 {background:url(../css/star0.gif) -15px 0 no-repeat;}
.x-rate-17 {background:url(../css/star1.gif) -15px 0 no-repeat;}
.x-rate-18 {background:url(../css/star2.gif) -15px 0 no-repeat;}
.x-rate-19 {background:url(../css/star3.gif) -15px 0 no-repeat;}
.x-rate-20 {background:url(../css/star0.gif) 0 0 no-repeat;}

7a) Забыл. Положите в папку css папку rating (для оценивания зарегистрированными пользователями, там стили и картинки): rating.zip

Ред. 09.08.2012: В виде вызов CStarRating (просмотр статьи полностью):

...
<div class="b-rating__with_text">
<?php
if (!Yii::app()->user->isGuest){
$this->widget('CStarRating', array(
'name' => 'user-input-rating',
'minRating' => '1',
'maxRating' => '5',
'value' => $data->GetUserRating(), // mark 1...5
'starWidth'=>'15',
'ratingStepSize' => '1',
//'titles'=>  CHtml::listData(RatingDesc::model()->cache(3600)->findAll(),'id','desc'),
'allowEmpty'=>false,
'callback' => 'function(value) {SetRating(value, ' . $data->id . ');}',
                            'cssFile'=>'/css/rating/jquery.rating.css',
));
echo '<span>&nbsp;&nbsp;оцените статью</span>';}
?>
</div>
<?php
$cs = Yii::app()->getClientScript();
$cs->registerScriptFile('/js/rating.js');
?>
Ред. 21.08.2012: добавляю содержимое модели models/Rating.php:
<?php

/**
 * This is the model class for table "rating".
 *
 * The followings are the available columns in table 'rating':
 * @property integer $id
 * @property integer $article_id
 * @property integer $user_id
 * @property integer $value
 *
 * The followings are the available model relations:
 * @property Users $user
 * @property Article $article
 */

class Rating extends CActiveRecord
{
/**
* Returns the static model of the specified AR class.
* @param string $className active record class name.
* @return Rating the static model class
*/
public static function model($className=__CLASS__)
{
return parent::model($className);
}

/**
* @return string the associated database table name
*/
public function tableName()
{
return 'rating';
}

/**
* @return array validation rules for model attributes.
*/
public function rules()
{
// NOTE: you should only define rules for those attributes that
// will receive user inputs.
return array(
array('article_id, user_id, value', 'required'),
array('article_id, user_id, value', 'numerical', 'integerOnly'=>true),
// The following rule is used by search().
// Please remove those attributes that should not be searched.
array('id, article_id, user_id, value', 'safe', 'on'=>'search'),
);
}

/**
* @return array relational rules.
*/
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(
'user' => array(self::BELONGS_TO, 'Users', 'user_id'),
'article' => array(self::BELONGS_TO, 'Article', 'article_id'),
);
}

/**
* @return array customized attribute labels (name=>label)
*/
public function attributeLabels()
{
return array(
'id' => 'ID',
'article_id' => 'Article',
'user_id' => 'User',
'value' => 'Value',
);
}

/**
* Retrieves a list of models based on the current search/filter conditions.
* @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions.
*/
public function search()
{
// Warning: Please modify the following code to remove attributes that
// should not be searched.
$criteria=new CDbCriteria;
$criteria->compare('id',$this->id);
$criteria->compare('article_id',$this->article_id);
$criteria->compare('user_id',$this->user_id);
$criteria->compare('value',$this->value);
return new CActiveDataProvider($this, array(
'criteria'=>$criteria,
));
}
}

Ред. 04.10.2012: Ещё я использую helper, который обозвал CLocoHelper.php для склонения слова "оценка" и поместил в protected/helpers (создал папку helpers)

<?php
class CLocoHelper
{
    ...

    public static function GetFormatWord($word, $number)
    {
        $num = $number % 10;
        if ($word == 'оценка') {
            if ($num == 1)
                return 'оценка';
            elseif ($num > 1 && $num < 5)
                return 'оценки';
            else
                return 'оценок';
        }
    }

    ...

}    

 

Источник: loco.ru

almix
Разработчик Loco, автор статей по веб-разработке на Yii, CodeIgniter, MODx и прочих инструментах. Создатель Team Sense.

Вы можете почитать все статьи от almix'а.



Другие статьи по этой теме:

Комментарии (13)     Подпишитесь на RSS комментариев к этой статье.

13 комментариев

#414
Виктор говорит:
April 4, 2012 at 12:54 pm
Супер!! Работает!
#577
Виталий говорит:
July 16, 2012 at 04:48 pm

А у меня, вот, ничего не работает. Выдаёт ошибку:

include(Rating.php) [<a href='function.include'>function.include</a>]: failed to open stream: No such file or directory

Ищет модель Rating, а её то нет.

Я так понял, эта статья является продолжением другой статьи. Если не тяжело, напишите пожалуйста какой.

Спасибо!

#600
almix говорит:
August 2, 2012 at 01:35 pm

Нет, Виталий, эта статья сама по себе. Ошибка

include(Rating.php) [<a href='function.include'>function.include</a>]: failed to open stream: No such file or directory

У меня возникает постоянно, когда кодировка всего файла не UTF-8, редактор ставит свою для mac кодировку. Решается проблема перекодировкой и сохранением файла.
#603
Porcelanosa говорит:
August 6, 2012 at 12:08 am
В /js/rating.js  все до конца прописано?
#605
almix говорит:
August 9, 2012 at 11:19 am

Porselanosa, проверил, всё до конца (17 строк) в rating.js проверьте подключили ли вы его в <head>? Смотрите дописал в статью выше.

#613
AB говорит:
August 19, 2012 at 10:55 pm
Пожалуйста, добавьте в статью содержимое модели Rating, а то с relations не могу разобраться...
#620
almix говорит:
August 21, 2012 at 10:38 am
AB, добавил.
#621
AB говорит:
August 23, 2012 at 12:22 am
Спасибо. Месяц назад пытался сделать рейтинг, но слишком сложно без этой модели тогда было, и вот набрался опыта... все таки сам сделал уже, переделав для себя. Ваш сайт очень помог мне в изучении Yii! Делаю свой большой сайт, многие Ваши примеры очень помогли!
#652
Сергей говорит:
September 26, 2012 at 08:23 pm
А article.marksCount это что такое?
#662
almix говорит:
October 5, 2012 at 01:49 am

Сергей, правильный вопрос. marksCount - это введённая в модели Article связь - количество оценок для статьи с указанным id. То есть подсчитывает сколько оценок у этой статьи, через self::STAT из таблицы rating:

public function relations()
{
...
'commentCount' => array(self::STAT, 'Comment', 'article_id', 'condition'=>'status='.Comment::STATUS_APPROVED),
'marksCount' => array(self::STAT, 'Rating', 'article_id'),
);
}        
#721
Alex говорит:
December 4, 2012 at 09:35 pm
6 пункт
url:"/review/setmark",
почему там review а не article, если все же так и должно быть, то что находиться в ревиеве
#825
alex говорит:
April 3, 2013 at 10:36 pm
Все работает отлично, кроме 'starWidth'
Меняю значение , но размер не меняется. в чем может быть проблема ?
#849
Borg говорит:
April 9, 2013 at 07:45 pm

Большое спасибо за рейтинг!

Очень пригодилось, не знаю сколько бы сам писал этот функционал и не думаю, что получилось бы так хорошо.

Хочу уточнить небольшую деталь, заметил, что не совсем корректно считает рейтинг, не сходились цифры. Например оцениваем в первый раз на 5 звезд, в таблицу статей вносится рейтинг 250, в 2 раза меньше, чем положено 500.

Как мне кажется в ArticleController нужно изменить строку

$good->rating = round($good->rating + ($rate * 100 - $good->rating) / ($good->marksCount + 1));
убрать выделенное красным.

Плюсовать 1 не надо т.к. количество голосов marksCount берется после сохранения модели уже с учетом проголосовавшего.

Может быть это только у меня так, т.к. код кое где правил под себя, но после исправления арифметика стала считаться правильно.