Программирование

Как подружить свой тип данных с Views

При разработке модулей для Drupal мы частенько создаем программно новые типы материалов (то есть нод). В процессе иногда возникает соблазн быстренько запрограммировать вывод какой-нибудь отсортированной коллекции этих материалов. Например, вывод нод-городов, отсортированных по алфавиту, или по древности, или сгруппированных по странам. Таких полезных коллекций может образоваться достаточно много и программно создавать каждую из них — не очень эффективно. Особенно если впоследствии нам захочется что-то поменять, объединить, или переделать. Гораздо аккуратнее — использовать модуль Views.

Вообще говоря, Views — один из краеугольных камней разработки на Drupal. Хотите помесячный архив нод? Views. Хотите вывести только заголовки и даты нод? Views. Хотите вывести ноды, сгруппированные по именам авторов? Views. Хотите получить список IP, с которых пользователь Василий заходил на сайт в течение Великого поста? Тоже Views. Один из самых популярных ответов на вопрос «Как в Друпале сделать…» — Views. Так что, чем быстрее разработчик подружится с этим прекрасным модулем — тем лучше.

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

О том, как это сделать — под катом.

Qt и звуки

Меня всегда радовало разнообразие возможностей по работе со звуком в Линуксе.

Сегодня, например, несколько часов подряд взрывал себе мозг на тему QSound в Qt. Дело в том, что под Виндой, чтобы воспроизвести звуковой эффект без извращений, нужно просто вызвать статический метод QSound::play("имя файла"). В Линуксе тоже так можно, но чтобы звук действительно прозвучал — надо ставить NAS. Узнав об этой необходимости, принялся ставить оный, параллельно размышляя, как лучше заставить пользователей проделать то же самое. Мозги начали нагреваться.

Когда с nasd возникли какие-то проблемы, быстренько переключился на поиск альтернатив. Уже задумался о компиляции версии под Линукс с Phonon. Остановил тот факт, что фононовский учебный проект из состава Qt SDK не собрался out of the box. Попробовал учебный проект QAudioOutput оттуда же — не работает.

Уже начал было впадать в уныние, но в конце-концов осенило:

  1. #ifdef Q_WS_X11
  2.   QProcess::startDetached("play mysound.wav");
  3. #else
  4.   QSound::play("mysound.wav");
  5. #endif

Вроде бы, ерундовая задача, а — чуть стол головой не разбил. Так жить нельзя.

Параметрический вызов формы добавления нода на разных страницах

Предположим, мы создали модуль и в нем определили новый тип нода. Пусть это будет нод «Город» с внутренним именем 'city'. В форме добавления нашего нода с адреса 'node/add/city', кроме всего прочего, мы сделаем селектор стран, чтобы можно было выбирать, в какой стране находится город:

  1. function city_form(&$node, $form_state) {
  2.   //...
  3.   $form['countries'] = array(
  4.     '#type' => 'select',
  5.     '#title' => t('Country'),
  6.     '#options' => _get_countries_list(), //возвращает список стран в формате code => t('Country')
  7.     '#default_value' => variable_get('cotranslate_default_country', 'ru'),
  8.   );
  9.   //...
  10.   return $form;

Теперь предположим, что мы создали страницы с описаниями разных стран. И хотим чтобы пользователь мог добавить город не через меню «Создать материал» — «Город», а со страницы с описанием страны. Что логично: вот пользователь смотрит на страницу «Россия» и сразу хочет добавить свой родной город.

Первое, что мы можем по этому поводу сделать — это разместить ссылку на 'node/add/city'. Но в таком случае пользователю придется в форме добавления города выбирать страну из списка, а ведь он уже сделал этот выбор, нажав «Добавить город» именно на странице с описанием нужной страны. Очевидно, следует учесть выбор пользователя и при создании формы установить заданную страну как дефолтную в списке 'countries'.

О том, какой фокус для этого надо проделать — под катом.

Вышел Qt 4.6.0

Сегодня в блоге Qt Labs появилась благая весть: вышел релиз Qt 4.6.0 и его можно скачать.

Новость очень приятная. Как говорится, надо брать.

Schema API и запись данных в Друпале

Так получилось, что еще года четыре назад я немного баловался Друпалом 4.x. Там новые записи добавлялись в таблицы базы данных через db_query(), которая никуда не делась и в шестой версии. Пример добавления записи выглядел так:

  1. $result = db_query("INSERT INTO {my_table} (field1, field2, field3, ...)  
  2.                     VALUES ('%s', '%s', %d, ...)",
  3.                     $field1,$field2,$field3,...);
Когда стал работать с Друпалом 6.x, по инерции продолжил пользоваться этим же способом, даже несмотря на то, что был в курсе наличия Schema API и использовал drupal_install_schema().

И только недавно выяснил, что использовать drupal_write_record() — куда изящнее и удобнее.

  1. $my_table_entry = new stdClass();
  2. $my_table_entry->field1 = 'field1';
  3. $my_table_entry->field2 = 'field2';
  4. $my_table_entry->field3 = 3;
  5. //...
  6. drupal_write_record('my_table',$my_table_entry);
Казалось бы, предыдущий вариант несколько компактнее. Однако что если мы сделали новый тип нода и реализуем hook_insert()? Тогда запись
  1. function mymodule_insert($node) {
  2.   $result = db_query("INSERT INTO {my_table}  (field1, field2, field3, ...)  
  3.                       VALUES ('%s', '%s', %d, ...)",
  4.                       $node->field1,$node->field2,$node->field3,...);
  5. }
превратится в
  1. function mymodule_insert($node) {
  2.   drupal_write_record('my_table',$node);
  3. }
Аналогичная метаморфоза ожидает и hook_update(), ведь для обновления достаточно только добавить третьим аргументом названия полей primary keys, по которым обновляемая запись будет обнаружена.

Ну и, наконец, по окончании работы функции, в сохраняемом объекте появятся значения всех serial-полей вновь созданной записи, то есть вызывать n-е количество раз db_last_insert_id() будет уже не нужно.

В общем, если в нашем модуле таблицы БД созданы через Schema API (а они должны быть так созданы) — использовать drupal_write_record() очень удобно.

Табы в Друпале

При просмотре содержимого того или иного нода в CMF Drupal можно заметить наличие вкладок «Просмотр», «Изменить» и прочих:

Выбор одной из них позволяет пользователю осуществить то или иное действие с просматриваемым нодом. Вполне естественно, что при создании новых типов нодов нам может захотеться добавить к ним новые вкладки.

Делается это через систему меню, а точнее — в hook_menu. Предположим, нам нужно добавить к нодам локал таск tab1. Ноды доступны по внутреннему пути 'node/$nid', значит таб будет доступен по адресу 'node/$nid/tab1':

  1. function mymodule_menu() {
  2.   $items = array();
  3.  
  4.   $items['node/%/tab1'] = array(
  5.     'title' => 'Tab1',
  6.     'page callback' => '_view_tab1_page',
  7.     'page arguments' => array(1),
  8.     'access callback' => '_my_localtask_access',
  9.     'access arguments' => array(1),
  10.     'type' => MENU_LOCAL_TASK,
  11.   );
  12.  
  13.   return $items;
  14. } //function mymodule_menu

где _view_tab1_page генерирует содержимое таба, а _my_localtask_access — управляет доступом к этому содержимому.

Знак % — это так называемый wildcard (он же шаблон, метасимвол, символ подстановки), использующийся для передачи элемента пути в аргумент функции. В данном случае нам нужно передать элемент пути, соответствующий arg(1), в функции генерации страницы и контроля доступа. Предполагается, что элемент пути — это ID просматриваемого нода. Например для пути 'node/64/tab1' arg(1) вернет 64. Однако поскольку путь может быть намеренно изменен пользователем в адресной строке браузера на что угодно, его следовало бы валидировать — проверить, что это число, убедиться, что нод с таким ID существует и т.д.

Для удобства разработчики Друпала предусмотрели метасимвол с загрузкой. Запись '%node' в примере ниже позволяет передать в функции вместо arg(1) сразу результат работы node_load(arg(1)). То есть и валидировать элемент пути, и сразу загрузить нужный нод. Это, кстати, избавляет нас от необходимости дважды вызывать node_load() — сначала в функции доступа, а затем в функции генерации страницы.

  1. function mymodule_menu() {
  2.   $items = array();
  3.  
  4.   $items['node/%node/tab1'] = array(
  5.     'title' => 'Tab1',
  6.     'page callback' => '_view_tab1_page',
  7.     'page arguments' => array(1),
  8.     'access callback' => '_my_localtask_access',
  9.     'access arguments' => array(1),
  10.     'type' => MENU_LOCAL_TASK,
  11.   );
  12.  
  13.   return $items;
  14. } //function mymodule_menu

Возникает вопрос: а зачем вообще нужна функция доступа? Ведь мы можем просто передать 'access content' или что-то еще в стандартный коллбэк user_access(). Функция доступа _my_localtask_access используется для того, чтобы ограничить появление вновь созданных табов за пределами нашего нового типа нода (как известно, если функция доступа возвращает FALSE, элемент меню не генерируется).
  1. function _my_localtask_access($node) {
  2.   if ($node->type != 'my_node_type') return FALSE;
  3.        
  4.   return user_access('access content');
  5. } //function _my_localtask_access

С точки зрения логики это не совсем правильно: ведь мы не хотим запрещать доступ к локал таскам для других нодов, мы хотим, чтобы для них нашего таба просто не существовало. Но других вариантов нет.

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

Cheat Sheets для веб-разработчика

Пришел по рассылке адресок, содержащий 25 чит-шитов для веб-разработчика. Например:

Счастье мое не знает предела. Сейчас разденусь догола, обклеюсь разноцветными чит-шитами и буду танцевать при луне.

Советы по jQuery

Бороздя интернеты, случайно обнаружил статью «Совершенствуйтесь в jQuery — 25 отличных советов».

Советы действительно отличные, без дураков. Особенно для ребят, которые еще неопытны в предмете (вроде меня). Некоторые из пунктов побудили срочно бежать и переделывать всякое. Хорошая статья, стоит прочесть.

Сохранение формы в Drupal с помощью AJAX

Сегодня речь пойдет о том, как запрограммировать сохранение формы в Друпале через AJAX. В принципе, для этих целей уже существует модуль Ajax. Но вдруг у нас крайне хитрые потребности, или мы просто не хотим ради сабмита пары форм тащить за собой целый модуль?

В общем, задача такова: создать средствами Друпала простую форму, вывести ее на страницу, а затем — сохранять через AJAX-запросы, выводя на ту же страницу результаты сохранения и ошибки валидации формы, если таковые были. Без обновления страницы, естественно. Для решения задачи будем использовать Form API Друпала и jQuery с плагином Form. Оформим все в виде отдельного модуля.

Рассмотрим решение по порядку. Кому не хочется читать статью целиком, тот может сразу скачать архив с исходниками тестового модуля и поиграться с ним.

Остальных приглашаю под кат.

Создание нескольких одинаковых форм на одной странице

Недавно возникла нужда написать функцию, создающую несколько одинаковых форм на одной странице. Формы небольшие, состоят из одного текстового поля и кнопки, а также (что важно) двух переменных для внутреннего использования:

  1. function _my_form(&$form_state,$id1,$id2) {
  2.   $form = array();
  3.   $form['mytext'] = array(
  4.     '#type'  => 'textarea',
  5.     '#title' => t("Enter text"),
  6.     '#required'  => 'true',
  7.   );
  8.   $form['#id1'] = $id1;
  9.   $form['#id2'] = $id2;
  10.   $form['submit'] = array(
  11.     '#type'      => 'submit',
  12.     '#value' => t('Save'),
  13.   );
  14.   return $form;
  15. }

По нажатию кнопки содержимое текстового поля записывается в БД вместе с $id1 и $id2, которые меняются от одной формы к другой.
Сделать хотелось примерно вот что:
  1.   //...
  2.   $id2 = x;
  3.   foreach ($ids as $id1) {
  4.     $output .= drupal_get_form('_my_form',$id1,$id2);
  5.   }
  6.   //...
  7.   return $output;

Однако очевидно, что данный код работать не будет. Загвоздка в том, что все формы генерируются с одинаковым form_ID и при сохранении любой формы Друпал будет считать, что сохраняется самая первая. Поэтому $id1 и $id2 всегда будут равны указанным при создании первой формы на странице. Значит, нужно сделать, чтобы, с одной стороны, у всех форм при создании был разный ID, а с другой — чтобы создавались и обрабатывались они одной функцией.

Порывшись изрядно по Drupal.org было найдено решение в виде хука hook_forms. Собственно, будь на странице в Drupal API правильное описание хука, искать изрядно и не пришлось бы. А так — несколько раз натыкался, и каждый раз думал, что это не выход.

Дело все в том, что в описании hook_forms функция для Drupal 6 указана без аргументов, тогда как на деле она принимает и $form_id, и $args. Все сразу встало на свои места. Вот так для всех форм, начинающихся с _my_form, задается единый конструктор:

  1. /**
  2.  * Реализует hook_forms
  3.  *
  4.  * @param string $form_id - ID формы
  5.  * @param array $args - дополнительные параметры, полученные от drupal_get_form
  6.  * @return array
  7.  */
  8. function mymodule_forms($form_id,$args) {
  9.   $forms = array();
  10.   if (strpos($form_id,'_my_form') === 0) {
  11.     $forms[$form_id] = array(
  12.       'callback' => '_my_form',
  13.       'callback arguments' => $args,
  14.     );
  15.   }
  16.   return $forms;
  17. } //function mymodule_forms

Но поскольку формы у нас стали с разными ID, Друпал при сохранении формы будет искать разные функции-обработчики. Так что нужно указать единый валидатор и обработчик при создании формы:
  1. function _my_form(&$form_state,$id1,$id2) {
  2.   //...
  3.   //валидатор и обработчик
  4.   $form['#submit'] = array('_my_form_submit');
  5.   $form['#validate'] = array('_my_form_validate');
  6.   return $form;
  7. }

Дело сделано. Теперь остается только немного изменить генерацию данных на странице, чтобы формы создавались с разными ID:
  1.   //...
  2.   $id2 = x;
  3.   foreach ($ids as $id1) {
  4.     //$id1, естественно, должен быть уникальным
  5.     $output .= drupal_get_form('_my_form'.$id1,$id1,$id2);
  6.   }
  7.   //...
  8.   return $output;