Создание виджета для голосования с помощью 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 скриптов. Создайте два пустых файла: 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;
}

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

После перехвата щелчка на ссылке Vote скрипт посылает запрос серверу. С помощью 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 обработчик событий, поэтому, когда пользователи нажимают на ссылку, он отправляет 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"}.

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

Предыдущая запись Следующая запись