Создание виджета для голосования с помощью jQuery.

Данная статья основана на главе 18 книги "CMS Drupal 7 Руководство по разработке системы управления веб-сайтом" (автор - Тодд Томлинсон).

В книге есть достаточно много интересных глав, но на мой взгляд она уже устарела, помимо этого в примерах часто встречаются ошибки либо неточности, которые могут быть связаны как с дальнейшим развитием Drupal, так и с опечатками (с некоторыми ошибками и их исправлениями можно ознакомиться по этой ссылке). Поэтому я решил выложить полностью работоспособный модуль Plusone. Версия Drupal - 7.19, версия jQuery - 1.4.4.

Создайте каталог в каталоге sites/all/modules/custom и назовите его plusone. В каталоге plusone создайте файл plusone.info, содержащий следующие строки:

name = Plus One
description = "A +1 voting widget for nodes."
package = Pro Drupal Development
core = 7.x

Этот файл регистрирует модуль в Drupal, чтобы его можно было активировать или отключать в административном интерфейсе.

Затем понадобится создать файл plusone.install. Функции, содержащиеся в этом PHP-файле, вызываются, когда модуль активируется, отключается, инсталлируется или деинсталлируется, обычно для создания или удаления таблиц из базы данных. В данном случае мы будем отслеживать, кто за какую ноду голосовал.

<?php
/**
 * Реализация hook_install().
 */
function plusone_install() {
    // В книге в этой строке ошибка. Мы создаём новую таблицу plusone_votes, а не plusone!
    drupal_install_schema('plusone_votes');
}

/**
 *  Реализация hook_schema().
 */
function plusone_schema() {
    $schema['plusone_votes'] = array(
        'description' => t('Stores votes from the plusone module'),
        'fields' => array(
            'uid' => array(
                'type' => 'int',
                'not null' => TRUE,
                'default' => 0,
                'description' => t('The {user}.uid of the user casting the vote.'),
            ),
            'nid' => array(
                'type' => 'int',
                'unsigned' => TRUE,
                'not null' => TRUE,
                'default' => 0,
                'description' => t('The {node}.nid of the node being voted on.'),
            ),
            'vote_count' => array(
                'type' => 'int',
                'not null' => TRUE,
                'default' => 0,
                'description' => t('The number of votes cast.'),
            ),
        ),
        'primary key' => array('uid', 'nid'),
        'indexes' => array(
            'nid' => array('nid'),
            'uid' => array('uid'),
        ),
    );
    
    return $schema;
}

Добавьте также файл sites/all/modules/custom/plusone/plusone.css. Этот файл не является абсолютно необходимым, однако он несколько улучшит внешний вид виджета для голосования:

div.plusone-widget {
    width: 100px;
    margin-bottom: 5px;
    text-align: center;
}

div.plusone-widget .score {
    padding: 10px;
    border: 1px solid #999;
    background-color: #eee;
    font-size: 175%;
}

div.plusone-widget .vote {
    padding: 1px 5px;
    margin-top: 2px;
    border: 1px solid #666;
    background-color: #ddd;
}

Теперь, когда вспомогательные файлы созданы, сосредоточим внимание на файлах модуля и JavaScript jQuery. Создайте два пустых файла: sites/all/modules/custom/plusone/plusone.js и sites/all/modules/custom/plusone/plusone.module.

Откройте пустой файл plusone.module в текстовом редакторе и поместите в него стандартный заголовок документирования Drupal:

<?php
/**
 * @file
 * Простой виджет для голосования +1.
 */

Теперь можно приступать к написанию хуков Drupal. Один из простых хуков - hook_permission() - позволяет добавить разрешение rate content в страницу управления доступом Drupal на основе ролей. Это разрешение используется, чтобы воспрепятствовать голосовать анонимным пользователям без предварительного создания учетной записи или входа на сайт.

/**
 * Реализация hook_permission().
 */
function plusone_permission() {
    $perms = array(
        'rate content' => array(
            'title' => t('Rate content'),
        ),
    );
    
    return $perms;
}

Теперь пора приступить к реализации ряда функциональных возможностей AJAX. Одна из замечательных особенностей jQuery заключается в возможности отправлять собственные HTTP-запросы GET и POST, благодаря чему можно отправить Drupal свои данные голосования, не обновляя при этом всю страницу. jQuery будет перехватывать щелчок на ссылке Vote (Голосовать) и отправлять Drupal запрос на сохранение голоса и возвращение обновленного итога. Затем jQuery использует новое значение для обновления результатов голосования на странице.

После перехвата щелчка на ссылке Vote jQuery посылает запрос серверу. С помощью hook_menu мы указываем callback-функцию plusone_vote(), обрабатывающую поступающие запросы. Эта функция сохраняет голос в БД и возвращает клиенту новое значение в виде данных JSON (JavaScript Object Notation).

/**
 * Реализация hook_menu().
 */
function plusone_menu() {
    $items['plusone/vote'] = array(
        'title' => 'Vote',
        'page callback' => 'plusone_vote',
        'access arguments' => array('rate content'),
        'type' => MENU_CALLBACK,
    );
    
    return $items;
}

В представленной выше функции поступающий запрос по ссылке plusone/vote обрабатывается функцией plusone_vote(), если пользователь имеет разрешение rate content.

Путь plusone/vote/3 будет соответствовать вызову функции plusone_vote(3):

/**
 * Callback-функция.
 *
 * @param $nid
 *   ID ноды.
 */
function plusone_vote($nid) {
    global $user;
    $nid = (int)$nid;
    
    $is_author = db_query('SELECT uid FROM {node} WHERE nid = :nid AND uid = :uid',
                            array(':nid' => $nid, ':uid' => $user->uid))->fetchField();
    
    // Авторы не могут голосовать за собственные ноды.
    // Проверяем таблицу node, чтобы посмотреть, не является
    // ли пользователь автором сообщения.
    if ($nid > 0 && !$is_author) {
        $vote_count = plusone_get_vote($nid, $user->uid);
        if (!$vote_count) {
            db_delete('plusone_votes')
                ->condition('uid', $user->uid)
                ->condition('nid', $nid)
                ->execute();
            db_insert('plusone_votes')
                ->fields(array(
                    'uid' => $user->uid,
                    'nid' => $nid,
                    'vote_count' => $vote_count + 1,
                ))
                ->execute();
        }
    }
    $total_votes = plusone_get_total($nid);
    // Проверка того, выполнен ли вызов с помощью jQuery. Вызов AJAX использует
    // метод POST и передает пару "ключ/значение" вида js = 1.
    if (!empty($_POST['js'])) {
        // drupal_json() используется в Drupal 6.
        // Заменяем на функцию drupal_json_output().
        drupal_json_output(array(
            'total_votes' => $total_votes,
            'voted' => t('You voted'),
            )
        );
        exit();
    }
    
    $path = drupal_get_path_alias('node/' . $nid);
    drupal_goto($path);
}

Приведенная функция plusone_vote() сохраняет текущий голос и возвращает клиенту информацию в виде ассоциативного массива, содержащего новое значение количества голосов и строку You voted (Вы проголосовали), которая заменяет строку Vote (голосовать) под виджетом для голосования. Этот массив передается в функцию drupal_json_output(), которая преобразует PHP-переменные в их JavaScript-эквиваленты, в данном случае преобразуя ассоциативный массив PHP в объект JavaScript.

В приведенном коде вызывались функции plusone_get_vote() и plusone_get_total(), поэтому создадим их:

/**
 * Возврат числа голосов для данной пары идентификатор ноды/идентификатор пользователя.
 *
 * @param $nid
 *   ID ноды.
 * @param $uid
 *   ID пользователя.
 */
function plusone_get_vote($nid, $uid) {
    return (int)db_query('SELECT vote_count FROM {plusone_votes} WHERE nid = :nid AND uid = :uid',
                        array(':nid' => $nid, ':uid' => $uid))->fetchField();
}

/**
 * Возврат общего числа голосов для данной ноды.
 *
 * @param $nid
 *   ID ноды.
 */
function plusone_get_total($nid) {
    return (int)db_query('SELECT SUM(vote_count) FROM {plusone_votes} WHERE nid = :nid', array(':nid' => $nid))->fetchField();
}

Теперь отобразим виджет для голосования рядом с нодой.

/**
 * Загрузка значений, необходимых для работы виджета,
 * и вывод виджета ври вызове функции hook_node_view().
 */
function plusone_node_view($node, $view_mode) {
    global $user;
    
    $total = plusone_get_total($node->nid);
    $is_author = db_query('SELECT uid FROM {node} WHERE nid = :nid AND uid = :uid',
                            array(':nid' => $node->nid, ':uid' => $user->uid))->fetchField();
    
    if ($is_author) {
        $is_author = TRUE;
    } else {
        $is_author = FALSE;
    }
    
    $voted = plusone_get_vote($node->nid, $user->uid);
    
    if ($view_mode == 'full') {
        // Добавление файлов JavaScript и CSS.
        drupal_add_js(drupal_get_path('module', 'plusone') . '/plusone.js');
        drupal_add_css(drupal_get_path('module', 'plusone') . '/plusone.css');
        $node->content['plusone_vote'] = array(
            '#markup' => theme('plusone_widget', array('nid' => $node->nid,
                                'total' => $total, 'is_author' => $is_author, 'voted' => $voted)),
            '#weight' => 100,
        );

        return $node;
    }
}

Зарегистрируем в системе Drupal нашу функцию темизации для виджета:

/**
 * Реализация hook_theme().
 */
function plusone_theme() {
    return array(
        'plusone_widget' => array(
            'arguments' => array('nid', 'total', 'is_author', 'voted'),
            'template' => 'plusone-widget',
        ),
    );
}

Создадим файл plusone-widget.tpl.php в каталоге модуля plusone. Этот файл будет содержать следующий код:

<?php
/**
 * @file
 * Шаблон для отображения виджета для голосования.
 */
?>

<div class="plusone-widget">
    <div class="score"><?php print $total; ?></div>
    <div class="vote">
        <?php
            if ($is_author) {
                print t('Votes');
            }
            elseif ($voted > 0) {
                print t('You voted');
            }
            else {
                print l(t('Vote'), "plusone/vote/$nid", array('attributes' => array('class' => 'plusone-link')));
            }
        ?>
    </div>
</div>

Мы должны будем создать скрипт jQuery, который обработает щелчки пользователей на кнопке голосования и вызовет соответствующую функцию в модуле plusone для записи голоса пользователя. Этот скрипт добавляет к ссылке a.plusone-link обработчик событий, поэтому, когда пользователи нажимают на ссылку, он отправляет HTTP-запрос POST по указанному URL. После завершения запроса AJAX возвращаемое значение передается в качестве параметра data, и скрипт обновляет итоги голосования и изменяет текст Vote на You voted. Чтобы предотвратить перезагрузку всей страницы, возвращаемым значением функции должно быть false.

В каталоге модуля plusone понадобится создать файл plusone.js, содержащий следующий код:

(function($) {
    Drupal.behaviors.plusone = {
        attach: function (context, settings) {
            $('a.plusone-link:not(.plusone-processed)', context).click( function() {
                $.ajax({
                    type: 'POST',
                    url: this.href,
                    dataType: 'json',
                    data: 'js=1',
                    success: function(data) {
                        // Обновление количества голосов.
                        $('div.score').text(data.total_votes);
                        // Обновление строки "Vote" строкой "You voted".
                        $('div.vote').text(data.voted);                    
                    }
                });
                return false;
            })
            .addClass('plusone-processed');
        }
    }
})(jQuery);

Если сервер возвращает ответ 200 OK, но обновления виджета не происходит, то проверьте валидность JSON-объекта, возвращаемого сервером. В нашем случае он должен выглядеть примерно так: {"total_votes":1,"voted":"You voted"}.

На этом всё! Модуль готов.

Можете скачать архив и протестировать его работу: модуль Plusone.

Теги: