Формы, Переводы, Синхронизация: В Одном Флаконе

Главные вкладки

Аватар пользователя beliy_snow beliy_snow 20 августа 2007 в 15:40

Мат можно писать? Нет? Ну ладно...
Сегодня мы будем писать модуль без мата. Но, с прикольным функционалом. Для этого нам понадобится Друпал и руки. Ну и ещё что-нибудь, но об этом по ходу изложения. Итак, поехали.

Дано: Друпал, контент, интернационализация. Я тут немного постарался и теперь у меня есть такая штука: я создаю ноду и у меня автоматически создаются клоны этой ноды для всех языков, т.е. контент один и тот же, но языки нод разные и все это связано в перевод с помощью таблицы i18n_node и поля trid. Это все поняли? Ок, поехали дальше.

Надо: (ща будет страшно) Система синхронизации элементов интернационализации. Пипец. Что она делает? Короче. Есть админ, который создает контент (ноду с клонами), есть переводчики, которые нужные языки заполняют нужным контентом. Надо знать следующее: заходил ли какой-нибудь переводчик и правил ли он английскую ноду (или любую другую)? Если да, то все остальные переводы считаются несинхронизированными, т.е. их нужно отредактировать в соответствии с изменениями в английской ноде. Как это сделать? Легко и просто.

Реализация: С ходу понятно, что нам надо где-то хранить информацию о синхронизированности переводов. Для этого, ясный-красный, нам понадобится простенькая таблица. 3 поля: итентификатор ноды, статус ноды (синхронизирована/несинхронизирована), идентификатор перевода (тот самый trid из таблицы i18n_node). Ещё нам нужен будет новый модуль, который все это будет рулить. Обзовем его для примера "viagra_revisions" (нет, я не использую виагру, просто чтобы когда вы читали этот текст вы думали о приятном).

1. Меню
<?php function viagra_revisions_menu($may_cache)
{
global $user;
$items = array();

if ($may_cache)
{
$items[] = array(
'path' => 'admin/viagra_revisions',
'title' => t('Viagra Revisions'),
'access' => user_access('administer nodes', $user),
'callback' => 'viagra_revisions_page',
);
}

return $items;
}?>
Тут мы просто объявили пагу для интерфейса. Кстати, когда будете вносить изменения в хук меню, а именно в обработку кешированных данных не забудьте либо выключить/включить ваш модуль, либо почистить таблицу cache_menu, а то изменения не вступят в силу.

2. nodeapi()
Мы работаем с нодами, поэтому нам неплохо бы отлавливать моменты, когда нода создается, редактируется, удаляется. Для этого друпал предоставил нам самое широкое место для проникновения - nodeapi(). Пользоваться им, надеюсь, умеют все. Поэтому на каждую операцию, описанную выше, мы подставим свои функции, а именно: _viagra_revisions_create($node), _viagra_revisions_update($node), _viagra_revisions_delete($node).
Проще наверное запостить эти функции, чем каждую из них описывать.
<?php function _viagra_revisions_create($node)
{
$node = (object)$node;
// english viagra must be syncronised anyway
if ('en' == $node->language)
{
db_query("INSERT INTO {viagra_revisions} (nid, status, trid) VALUES (%d, '1', %d)", $node->nid, $node->trid);
}
else
{
db_query("INSERT INTO {viagra_revisions} (nid, status, trid) VALUES (%d, '0', %d)", $node->nid, $node->trid);
}
}
function _viagra_revisions_update($node)
{
$node = (object)$node;
if ('en' == $node->language)
{
// english viagra must be syncronised anyway
_viagra_revisions_active($node->nid);
// all other translations must be set to unsync status
foreach(translation_node_get_translations(array('nid' => $node->nid), FALSE) as $lang => $object)
{
_viagra_revisions_passive($object->nid);
}
}
else
{
// unsync edited node
_viagra_revisions_passive($node->nid);
}
?>
Функцию удаления нет смысла описывать, и так понятно что она делает. Вы наверное увидели 2 незнакомые функции (_viagra_revisions_passive($node->nid) и _viagra_revisions_active($node->nid)). Одна ставит статус ноды в 0, другая соответственно в 1 в таблице viagra_revisions. C реализацией разобрались? Теперь самое интересное - интерфейс! Тут есть одна мощная заковыка, с которой я прое%;№ся полдня, пока не нашел решение.

3. Интерфейс
В хуке меню мы объявили калбэкную функцию, которая будет вызываться каждый раз при загрузке страницы. Интерфейс у нас будет следующий: будет строчка с чекбоксами, который будет показывать - синхронизирована нода или нет, после чекбоксов будет сабмитовая кнопка. Т.к. переводов у нах дох и больше - таких чекбоксов будет во солько раз больше, сколько у нас языков в системе. Поэтому на каждый перевод у нас будет своя форма. Ага, вот тут и жопа-кеды. Давайте сначала код - потом описание.
<?php function viagra_revisions_page()
{
$output = '';
$first = TRUE;
foreach(viagra_get_all('en') as $nid => $title)
{
$output .= drupal_get_form('viagra_revisions_state_form' . $nid, $nid);
$first = FALSE;
}

return $output;
}?>
Видали? В аутпут мы кидаем форму с динамическим именем, при чем это все в цикле. Ахренеть, да? Вот ещё ссылка на сайт, откуда я взял это решение. Кто в английском не силен - читают дальше, кто силен - тот уже наверное понял смысл всей статьи. В общем смысл такой, что нам нужно обрабатывать одну и ту же форму, которая повторяется несколько раз на одной странице. Для этого в 5 Друпале и в его Forms API 2.0 реализован хук forms(), который мы щас и заюзаем.
<?php function viagra_revisions_forms()
{
$args = func_get_args();
$form_id = $args[0][0];
$forms = array();
if (strpos($form_id, "viagra_revisions_state_form") === 0)
{
$forms[$form_id] = array('callback' => 'viagra_revisions_state_form');
}

return $forms;
}
?>
Зачем оно надо? А затем, что если мы будем пихать в контент одну и ту же функцию формы - мы будем иметь следующее: домустим у нас 10 переводов, соответственно 10 форм, при нажатии на второй, третий, пятый, десятый сабмит мы получим данные из первой формы, потому что форм_ид, форм_билд_ид и прочая хрень - одинаковая. Поэтому нам надо эти формы по-хитрому разрулить с помощью хука forms(). Он разрулил, а мы идем дальше. Вот сама форма:
<?php function viagra_revisions_state_form($nid)
{
$form['#theme'] = 'viagra_revisions';
$form['nid'] = array('#type' => 'hidden','#value' => $nid);

foreach(viagra_revisions_load(viagra_node_get_trid($nid)) as $node_id => $status)
{
$form[i18n_node_get_lang($node_id)] = array(
'#type' => 'checkbox',
'#title' => '',
'#default_value' => $status,
'#disabled' => ('en' == i18n_node_get_lang($node_id)) ? TRUE : FALSE,
);
}
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Synchronize'),
);
$form['#submit'] = array('viagra_revisions_state_form_submit' => 1);

return $form;
}
?>
Все понятно? Создаем форму, пихаем данные. Из особенностей: параметр '#theme' - темовая функция для отрисовки нашей формы (будет ниже), параметр '#submit' - если не указывать, то сабмитовая функция у нас будет динамическая, как собственно и имя формы а рулить это лишний раз нам не обязательно, поэтому каждый раз при вызове формы просто указываем сабмитовую функцию для обработки формы.
Итак, функция темы:
<?php function theme_viagra_revisions(&$form)
{
$header = array(t('Viagra'));
foreach(i18n_supported_languages() as $lang => $name)
{
$names = explode(',', $name);
$header[] = t('name', array('name' => format_plural(count($names), $name, $names[0])));
}
$header[] = t('Synchronize');

$rows = array();
$row = array();
$row[] = l(viagra_node_get_title($form['nid']['#value']), 'node/' . $form['nid']['#value']);
foreach(i18n_supported_languages() as $lang => $name)
{
$row[] = drupal_render($form[$lang]);
}
$row[] = drupal_render($form['submit']);
$rows[] = $row;

$output = theme('table', $header, $rows);
$output .= drupal_render($form);

return $output;
}?>
Особенно радует меня вот эти строчки:
<?php
$names = explode(',', $name);
$header[] = t('name', array('name' => format_plural(count($names), $name, $names[0])));
?>Вообще тащусь... Зачем я так сделал? Все просто, есть такой язык - Португальский, он закосится в систему под именем "Portugese, Portugal" и при выводе формы это имя просто блять раздражает! (простите, не удержался) Поэтому режу пополам и вывожу первую часть. Вот. Теперь сабмит:
<?php function viagra_revisions_state_form_submit($form_id, $form_values)
{
$nid = $form_values['nid'];
foreach(i18n_supported_languages() as $lang => $name)
{
if ('en' == $lang)
{
_viagra_revisions_set($nid, '1');
}
else
{
_viagra_revisions_set(object_value(array_value(translation_node_get_translations(array('nid' => $nid)), $lang), 'nid'), $form_values[$lang]);
}
}
drupal_set_message(t('name revision statuses changed', array('name' => viagra_node_get_title($nid))));
return 'admin/viagra_revisions';
}
?> Тут 2 незнакомые функции и одна большая строчка, попробуйте догадаться что делают эти функции и как работает эта строчка.

Из моих наворотов: ну, инсталлер, который создает таблицу и хук init(), который каждый раз проверяет - все ли ноды с типом Виагра в нашей таблице и если не все, то тупо их туда пихает.
Если что ещё вспомню - напишу.

Все, вроде бы все. Вопросы?

Комментарии

Аватар пользователя beliy_snow beliy_snow 20 августа 2007 в 16:54

Какого Макса? Простите, не в курсе Smile пишу сам, ни на кого не смотрю... просто пытался в один пост уместить много всего

Аватар пользователя edhel edhel 20 августа 2007 в 16:43

Много букавок, ниасилил. Мне вот скоро тоже нужно будет сделать облегченные версии сайта на 5+ языках... Думаю сделаю страницу с 5+ полями и буду выводить в зависимости от переменной сессии $_SESSION нужное поле... и язык в друпале тоже переключать буду в зависимости от ключика выбранного языка в $_SESSION.

Аватар пользователя beliy_snow beliy_snow 20 августа 2007 в 16:56

Весь функционал по переключению языков уже представлен в модуле i18n, лучше я думаю с ним разобраться, чем изобретать велосипед... есть наработки по переключению между алиасами переводов... если надо - могу описать в следующем посту

Аватар пользователя Konstantin Boyandin@drupal.org Konstantin Boya... 20 августа 2007 в 18:57

Интересно, только в техническом тексте художества ни к чему, однозначно. Smile Да и варваризмов многовато. Может, стоило сразу по-ангельски писать?

Причесать стиль, добавить все исходники - и будет очень хороший урок по написанию модулей...

"И я с таблицы памяти своей все суетные записи сотру..."

Аватар пользователя marazmus marazmus 21 августа 2007 в 8:37

Про макса и прочую фигню (закосы, поносы) - все сказано зря.

Текст отличный, побольше бы таких на друпал.ру. Ибо практиков много, но крайне мало тех, кто может немного отвлечься от кодирования и описать то, что он умеет делать, простым человеческим языком.

Аватар пользователя jason32 jason32 21 августа 2007 в 11:20

всё хорошо, только надо правильно упаковать, создать viagra_revisions.info, viagra_revisions.module, viagra_revisions.install и тд, запаковать в архив и выложить на тестирование - у меня вот нет желания это самому делать, чтобы проверить работоспособность и ошибки в коде. А так да, тема хорошая, сравнивать аффтара с Максом глупо - никакой идеологии, всё по делу, правда как-то заскоки по мату и приколам несколько тревожат Smile