ShvetsGroup

 

Создание cоставного поля в CCK 2.x

  • neochief's picture
10 comments

Создание cоставного поля в CCK 2.x

При создании сайта на базе системы управления контентом Drupal вы заметите, что часто необходимо задавать типы контента, к которым, помимо дефолтных Title и Body, добавлены еще и другие поля.

В Друпале, начиная с 7-ой версии, функционал полей запланирован в базовом дистрибутиве, однако в версиях 6 и ранее он реализован в пользовательском модуле CCK и других связанных модулях, которые предусматривают дополнительные типы полей для создания контента.

При создании некоторых сайтов вам придется задавать поля с несколькими значениями, например, нужно будет разместить несколько изображений (каждое из которых будет полем-изображением) в правой части страницы. Это не проблема, так как модуль CCK позволяет задать несколько значений любому полю, а во второй версии CCK можно с легкостью сортировать, удалять или добавлять элементы с помощью удобного интерфейса на AJAX.

Но что, если нужно привязать, например, подпись и термин таксономии к каждому изображению? Другими словами, что если надо добавить поля к вашему типу контента группой? Текущая версия CCK позволяет сделать это несколькими способами:

  1. Несколько отдельных полей. Создайте поле с несколькими значениями для изображения, еще одно для подписи, и еще одно для термина таксономии. Контент-менеджерам сайта порекомендуйте их синхронизировать. На самом деле, это не так уж легко – пользователям надо будет скроллить вверх–вниз по форме вставки контента для того, чтобы внести данные. Ошибки здесь неминуемы, и в итоге у вас будут надписи или термины, привязанные не к тем изображениям. В этом случае поле с несколькими значениями Content Taxonomy, будучи модулем с несколькими полями, просто предоставляет один список, поэтому выбрать конкретный термин таксономии более чем один раз, либо определять порядок (термин 1 идет с изображением 1 и т. д).
  2. Подтип контента. Создайте второй тип контента «Image Caption Term», который будет содержать изображение и привязанные к нему данные. Далее к первоначальному типу контента добавьте поле с несколькими значениями Node Reference, которое привяжет страницу к ее изображениям. На практике редактировать такой контент очень неудобно и, в конце концов, у вас соберется куча элементов Image Caption Term, которые будут засорять экран и сбивать с толку неопытных пользователей.
  3. Экспериментальный модуль. Попробуйте экспериментальный модуль Content Multigroup, который, по идее, позволит группировать поля CCK с несколькими значениями вместе. На момент написания этого обзора (декабрь 2009 г.) этот модуль работает нестабильно (и пока даже не выпущена его альфа-версия), и последний раз, когда я его испытывал (октябрь 2008 г.), он абсолютно не работал с полями изображений.
  4. Специальное поле CCK. Создайте собственный модуль поля CCK, информация в котором сгруппирована так, как вы хотите. Сделать это несложно, модуль работает хорошо и именно о нем пойдет речь в этой статье.

В этой статье я хочу рассказать, как создавать модуль, с помощью которого можно реализовать специальное «сгруппированное» или «составное» поле CCK для Drupal 6.x и CCK 2.x.

Составное поле позволяет пользователю загрузить изображение, снабдить его тегами alt и title, сделать подпись (произвольный HTML-текст), а также выбрать один или несколько терминов таксономии. Это поле можно добавить к типу контента CCK в виде поля с несколькими значениями, что позволит контент-менеджеру добавлять, редактировать, удалять и сортировать всю информацию группами (т.е. каждое изображение вместе с подписью и термином таксономии). Вот как это выглядит в действии:

Если вам нужен именно такой функционал (изображение, подпись, поле таксономии), можете использовать созданный мною модуль (можно загрузить ниже) – он неплохо работает и уже используется как минимум на одном сайте. Если вам нужен другой набор полей, надо будет повторить шаги из этой статьи и создать собственное составное поле CCK.

Если вы хотите, чтобы в созданном составном поле содержалось изображение, или другой загруженный файл, приведенные ниже примеры должны помочь вам в решении этой задачи.

Если среди компонентов вашего составного поля нет импорта файлов, все равно повторяйте общие рекомендации по этому модулю (применяйте те же хуки для тех же целей), а также обратите внимание на заметки, о том, что нужно делать иначе. Я также включил пару примеров из созданного мной другого модуля полей, который задает поле office location с несколькими текстовыми компонентами. В качестве примера рекомендую взглянуть на модуль Link.

Вот из чего состоит эта статья:

Для того чтобы понять эту статью вам нужны базовые знания PHP, системы Drupal и ее терминологии, принципов разработки модулей для Drupal (хуки, Forms API и базовая структура модулей), модулей CCK и Views (имеется в виду, как задавать типы контента и свои представления – т.е. вам не нужно знать все внутренности этих модулей).

Если в процессе чтения вы столкнетесь с незнакомыми терминами или понятиями, больше информации по ним можно найти в разделе Загрузки, справочная информация и ссылки.

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

Начало

Первый шаг в создании нашего модуля - выбор названий. Модуль, который мы создаем – составное поле CCK для изображения, подписи и термина таксономии. Я решил назвать поле CCK «Image Caption Taxonomy» с машиночитаемой версией img_cap_tax. Далее по тексту (field) относится к машиночитаемой версии названия поля. Чтобы провести различие между названием поля CCK и модуля CCK, я назвал модуль «Image Caption Taxonomy Field» с машиночитаемым названием img_cap_tax_fld; далее в статье я буду писать просто (module).

Теперь, для реализации модуля Drupal нам надо создать файлы (module).info и (module).module.

Несколько замечаний:

  • Так как этот модуль реализует поле CCK, в вопросе упорядочивания таблиц базы данных мы можем рассчитывать на модуль CCK. Поэтому, для этого модуля файл .install нам не нужен.
  • В файле .info нам надо будет задать некоторую зависимость, так как это описано ниже. Список содержится в готовом файле .info в разделе загрузок.
  • В одном модуле возможно задать несколько полей CCK. Но, я однажды это уже делал и не рекомендую вам повторять мою ошибку – это только усложнит файл модуля и сделает его менее «модульным» (например, кому-то понадобится лишь одно из ваших полей и для того, чтобы реализовать у себя на сайте весь функционал, ему придется загружать весь модуль с несколькими полями).

Назад к Содержанию

Задаем поле

После создания файлов можем переходить к следующему шагу – реализации базовых и специальных CCK хуков, которые расскажут CCK о нашем поле. Реализация этих хуков происходит в файле .module.

CCK hook_field_info()

Для начала, дадим CCK машиночитаемое и «человеческое» имена поля, а также описание, с помощью хука CCK hook_field_info():

function img_cap_tax_fld_field_info() {
  return array(
    'img_cap_tax' => array(
      'label' => t('Image Caption Taxonomy'),
      'description' => t('Stores an image file, text for alt and title tags, a caption, and a taxonomy term'),
    )
  );
}

Как видите, возвращаемое значение hook_field_info() – ассоциативный массив, ключами которого являются машиночитаемые имена полей CCK, которое мы задаем. Каждый элемент возвращенного массива является ассоциативным массивом с компонентами 'label', выдающим «человеческое» имя нашего поля и 'description', с более подробным описанием. Так как все «человеческие» тексты в модулях Drupal должны быть готовыми к интернационализации, заключаем их в функцию t().

Core hook_install(), hook_uninstall(), hook_enable(), hook_disable()

Далее реализуем базовые хуки hook_install(), hook_uninstall(), hook_enable(), and hook_disable() для того, чтобы наше поле правильно добавлялось и удалялось из системы и CCK.

Они все работают одинаково — мы даем Drupal команду позволить CCK справиться с процессом с помощью его функции content_notify(), которая возьмет на себя все детали. Например, вот функция базового хука hook_install() Друпала:

function img_cap_tax_fld_install() {
  content_notify('install', 'img_cap_tax');
}

Три других хука абсолютно идентичны – просто подставляем "uninstall", "enable" или "disable" вместо "install". Эти функции можно внедрить в файл модуля (module).install вместо (module).module, но поскольку в нашем случае они такие маленькие (а в файле (module).install делать нам больше нечего), я решил оставить их в файле (module).module.

Еще одна деталь, эти функции непосредственно вызывают функцию content_notify() CCK модуля. Поэтому нам надо убедиться в том, что функция загружена тогда, когда мы ее вызываем:

  • Сделайте наш модуль зависимым от модуля CCK (машиночитаемое имя которого "content") – эта зависимость будет создана в файле .info.
  • Проверьте, в каком файле задана функция content_notify() – если это включаемый файл, а не главный .module, поместите директиву включения в hook_init() нашего модуля.

Это же проделаем и для других функций модуля, которые мы будем вызывать позже в этой статье, поэтому советую взглянуть на файл .info готового модуля (справочные материалы в конце статьи), чтобы увидеть зависимости модуля, а также на функцию img_cap_tax_fld_init() (реализация hook_init()), чтобы четко увидеть, какие включаемые файлы нам нужно загрузить.

CCK hook_field_settings()

Теперь мы должны «рассказать» CCK о настройках для этого поля, используя hook_field_settings(). Это что-то вроде универсального хука для CCK, с несколькими операциями, каждая из которых позволяет нашему модулю поля предоставлять CCK разную информацию.

Наше поле CCK основано на изображении и должно вести себя подобно тому, как ведет себя поле изображений существующего модуля ImageField. Это поле, в свою очередь, (в текущей версии ImageField) — обычное поле файла FileField, со специальным «виджетом» и «форматтером» (см. разделы ниже), что позволяет ему выглядеть как изображение, а не как файл.

Что касается нашего поля, мы много чего позаимствуем у модуля FileField по части функции настройки поля. Для операций, которые выполняют не такие задачи, как FileField, мы вызовем другие функции, которые описаны в следующем разделе. Вот эта функция:

function img_cap_tax_fld_field_settings( $op, $field ) {
  switch( $op ) {
    case 'form':
      return img_cap_tax_fld_field_settings_form( $field );

    case 'save':
      return img_cap_tax_fld_field_settings_save( $field );

    default:
      return filefield_field_settings( $op, $field );
  }
}

(Естественно нам надо убедиться в том, что мы проставили зависимость от FileField, как описано выше).

Если ваше поле CCK не содержит файловых загрузок, вам нужно будет поступить немного иначе. Все операции те же самые (см. раздел ниже), но вам самим придется их выполнить. Если вам не нужно другой валидации, кроме той, которую предоставляет и выполняет CCK, и если не нужна специальная форма настроек, в которой чаще всего заключается проблема, вы можете получить функцию, как в примере поля, которое задает office location (возможно, с какими-то дополнительными операциями, похожими на пример выше).

function office_field_field_settings($op, $field) {
  switch ($op) {
    case 'database columns':
      $columns['loc_name'] = array('type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'sortable' => TRUE, 'default' => '');
      $columns['phone'] = array('type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'sortable' => FALSE, 'default' => '');
      $columns['fax'] = array('type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'sortable' => FALSE, 'default' => '');
      $columns['address'] = array('type' => 'text', 'not null' => FALSE, 'sortable' => FALSE, 'default' => '');
      return $columns;
  }
}

Функции операций — filefield_field_settings()

Следующий шаг – задать индивидуальные функции операций, которые вызываются из функции настройки поля в предыдущем разделе. В этом модуле функции находятся во включаемом файле (module)_field.inc. Начнем с операций "form" и "save", задающих форму параметров поля, которую можно использовать для настройки поля (кроме той части, с которой справился модуль FileField), а также определяющих, какие поля в форме параметров должны быть сохранены в базу данных. Кроме того, есть еще и операция "validate", позволяющая запустить проверку внесенных данных, но, так как нам не нужно ничего из того, с чем не справится модуль FileField, эту операцию описывать здесь мы не будем.

В нашей форме параметров поля мы начнем с того, что делает модуль FileField со своим полем, также добавим большую часть того, что делает модуль Content Taxonomy со своим полем, и позволим пользователю выбирать между простым текстом и форматированным (с помощью фильтрованного формата ввода) в поле подписи.

Мы также внесем некоторые изменения в работу Content Taxonomy. Во-первых, у Content Taxonomy есть настройки, позволяющие взять термины таксономии модуля и применить их к ноде, как к целому. Так как в составном поле такая опция бессмысленна, мы ее предусматривать не будем. Во-вторых, мы также хотим позволить пользователю выбирать, когда термин таксономии будет опциональным, а когда с несколькими значениями. Кроме того, чтобы сгруппировать поля мы добавим несколько наборов полей (fieldsets) в форму.

Вот наши функции операций "form" и "save":

function img_cap_tax_fld_field_settings_form( $field ) {
  $form1 = filefield_field_settings( 'form', $field );

  $form2 = content_taxonomy_field_settings( 'form', $field );
  $form2['save_term_node']['#type'] = 'hidden';
  $form2['taxonomy_group'] = array(
    '#type' => 'fieldset',
    '#title' => 'Taxonomy',
    '#collapsible' => 0,
  );
  $form2['taxonomy_group']['vid'] = $form2['vid'];
  unset( $form2['vid'] );
  $form2['taxonomy_group']['allow_multiple'] = array( 
    '#type' => 'checkbox', 
    '#title' => t('Allow multiple taxonomy terms'),
    '#default_value' => is_numeric($field['allow_multiple']) ? $field['allow_multiple'] : 0,
    '#description' => t('If this option is checked, the user can select multiple taxonomy terms for each image; otherwise, at most one.'),
  );
  $form2['taxonomy_group']['required_term'] = array( 
    '#type' => 'checkbox', 
    '#title' => t('Taxonomy required'),
    '#default_value' => is_numeric($field['required_term']) ? $field['required_term'] : 0,
    '#description' => t('If this option is checked, the user must select at least one taxonomy term for each image; otherwise, it is optional.'),
  );
  $form2['hierarchical_vocabulary']['#weight'] = 100;

  $form3 = array( 'text_processing' => array( 
    '#type' => 'radios',
    '#title' => t('Text processing for Caption'),
    '#default_value' => is_numeric($field['text_processing']) ? $field['text_processing'] : 0,
    '#options' => array( 0 => t('Plain text'), 1 => t('Filtered text (user selects input format)')),
  ));

  return $form1 + $form3 + $form2;
}

function img_cap_tax_fld_field_settings_save( $field ) {
  $flds1 = filefield_field_settings( 'save', $field );
  $flds2 = content_taxonomy_field_settings( 'save', $field );
  $flds2[] = 'allow_multiple';
  $flds2[] = 'required_term';
  $flds3 = array( 'text_processing' );

  return array_merge( $flds1, $flds2, $flds3 );
}

Также есть операция hook_field_settings() для "database columns", с помощью которой можно задать колонки базы данных, которые CCK будет использовать для хранения данных вашего поля. Однако в этом случае модуль FileField создает сериализованное поле данных в базе (т.е. масив сохраненный строкой, а не каждое значение в своей колонке). Мы внесем всю информацию, такую как теги alt и title, подпись и термин таксономии в массив "data", чтобы не добавлять дополнительные колонки базы данных. (Пример поля office location выше по тексту показывает, как при необходимости создавать колонки базы данных в вашем модуле).

И, наконец, операция hook_field_settings() для "views data", позволяющая полю CCK выдавать информацию о том, как она может быть использована при сортировке и фильтровании модуля Views. (Любое поле CCK может быть автоматически включено как Field in Views, за это нам волноваться не нужно). Если учитывать, что наше поле полезно лишь когда оно добавлено к типу контента в качестве поля с несколькими значениями (иначе составное поле и не понадобится), сортировка нод в представлении по какому-либо признаку этого поля по большому счету бессмысленна. Однако, я могу представить несколько способов фильтрации представления с помощью этого поля:

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

CCK hook_field()

На следующем этапе нам нужно задать то, что включено в CCK hook_field(), который, по сути, говорит CCK, нужно ли ему делать что-то особенное, когда поле загружается из, или сохраняется в базу данных. С изображением в нашем поле нужно сделать следующее: переместить файл изображения из временного в постоянное место нахождения, и сделать пометку об этом месте в поле базы данных (у модуля FileField есть функция, которая реализует эту команду). Что касается подписи, нам нужно очистить ("sanitize") ее согласно формату ввода (например, убедиться, что в ее теле присутствуют только разрешенные HTML-теги). С термином таксономии ничего делать не нужно. Вот так выглядит наша операция hook_field():

function img_cap_tax_fld_field($op, $node, $field, &$items, $teaser, $page) {
  if( $op == 'sanitize' ) {
    img_cap_tax_fld_field_sanitize( $node, $field, $items, $teaser, $page );
  }
  return filefield_field( $op, $node, $field, $items, $teaser, $page );
}

В функции операции "sanitize" реализуем многое из того, что делает модуль Text field в CCK для улучшения текстовых полей: разберемся, какой формат ввода был выбран, отфильтруем согласно этому формату ввода, и сохраним результат с префиксом "safe", чтобы потом его можно было использовать в темизации. Вот сама функция (я включил ее в файл (module)_field.inc file):

function img_cap_tax_fld_field_sanitize($node, $field, &$items, $teaser, $page) {
  $isplain = empty( $field['text_processing'] );
  $check_access = is_null( $node ) || 
    ( isset($node->build_mode) && $node->build_mode == NODE_BUILD_PREVIEW );

  foreach( $items as $delta => $item ) {
    $dat = $item['data'];
    if( !is_array( $dat )) {
      $dat = unserialize( $dat );
    }
    $text = isset( $dat['caption'] ) ? $dat['caption'] : '';
    if( $isplain ) {
      $text = check_plain( $text );
    } else {
      $text = check_markup( $text, $item['format'], $check_access );
    }
    $items[$delta]['safe_caption'] = $text;
  }
}

Следует заметить, что массив "data" из модуля FileField иногда выступает в этой функции в сериализованном формате. Так что эта функция проверяет, нужна ли ей десериализация перед началом работы.

Если вы задаете нефайловое поле CCK, типичными операциями в hook_field() для вас будут сериализовать и десериализовать данные, и очищать данные перед просмотром. Большинству модулей нужна будет только очистка, вот пример из поля office location, которое считает, что все его поля должны быть в виде обычного текста:

function office_field_field($op, &$node, $field, &$items, $teaser, $page) {
  switch ($op) {
    case 'sanitize':
      foreach ($items as $delta => $item) {
        foreach ( $item as $col => $dat ) {
          $items[$delta]['safe_' . $col ] = check_plain($item[ $col ]);
        }
      }
      break;
  }
}

CCK hook_content_is_empty(), hook_default_value()

CCK нужна еще кое-какая информация о нашем поле – как определить, не является ли оно «пустым» в плане информации (CCK hook_content_is_empty()), и каким должно быть значение по умолчанию (CCK hook_default_value()). Так как главным компонентом нашего поля является изображение, мы определим, что поле считается пустым, если в нем нет файла изображения, а модуль FileField это выполнит:

function img_cap_tax_fld_content_is_empty( $item, $field ) {
  return filefield_content_is_empty( $item, $field );
}

Вот еще один пример из поля office location – считаем, что поле пустое, если нет ни названия места расположения, ни адреса:

function office_field_content_is_empty($item, $field) {
  if (empty($item['loc_name']) && empty( $item['address'])) {
    return TRUE;
  }
  return FALSE;
}

В нашем модуле изображений/подписей нет значения по умолчанию, (это бывает редко), но на всякий случай, если модуль FileField в будущем реализует эту опцию, позволим ему задать значение по-умолчанию:

function img_cap_tax_fld_default_value(&$form, &$form_state, $field, $delta) {
  return filefield_default_value($form, $form_state, $field, $delta);
}

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

Назад к Содержанию

Задаем виджет

Теперь, когда у нас есть заданное поле CCK, наша следующая задача – задать виджет — форму для редактирования поля. Необходимо, чтобы наш виджет давал возможность загружать файл с изображением, просматривать/изменять загруженный файл, вводить теги alt и title, текст подписи, выбирать формат ввода для подписи и термин таксономии (для этого мы используем выпадающий список). Как и прежде, позволим модулям FileField и Content Taxonomy выполнять свои функции, и все что нам останется – добавить текстовые поля и формат изображений. Мы создадим виджет под названием «Image, Caption, Taxonomy Select», с машиночитаемым именем img_cap_tax_sel_widget, далее по тексту - (widget).

Замечу, что модуль CCK может задавать несколько виджетов для одного поля. Например, у модуля Content Taxonomy есть несколько опций: выпадающий список, текстовое поле с автозаполнением, и т.д. Внизу есть примечания по поводу того, что надо изменить, чтобы добавить дополнительные виджеты.

CCK hook_widget_info()

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

function img_cap_tax_fld_widget_info() {
  return array(
    'img_cap_tax_sel_widget' => array(
      'label' => t('Image, Caption, Taxonomy Select'),
      'field types' => array('img_cap_tax'),
      'multiple values' => CONTENT_HANDLE_CORE,
      'callbacks' => array('default value' => CONTENT_CALLBACK_CUSTOM),
      'description' => t('An edit widget for Image Caption Taxonomy fields
 that allows upload/preview of the image, and chooses taxonomy terms from a
 drop-down select list.' ),
    ),
  );
}

Как это часто бывает в Друпале, возвращаемое значение этого хука – ассоциативный массив ассоциативных массивов, и если мы хотим реализовать в нашем модуле несколько виджетов, нам нужно будет добавить дополнительные элементы к «внешнему» массиву.

FAPI hook_elements()

Далее, нам необходимо задать форму ввода данных виджета и способ ее обработки. Это можно реализовать с помощью базового хука Forms API hook_elements().

Этот хук выдает ассоциативный массив с одним элементом (или больше, чем одним, если у вас несколько виджетов), ключем которого является машиночитаемое имя виджета, и значение которого является ассоциативным массивом, который в свою очередь либо полностью задает форму (как в модуле Content Taxonomy), либо возвращает process-колбэк и некоторую другую информацию (как это делается в ImageField; этот метод, в основном, также рекомендуется для Forms API). Мы воспользуемся методом возвращения process-колбэка и позволим модулям ImageField и FileField задать большую часть массива:

function img_cap_tax_fld_elements() {
  $imgel = imagefield_elements();
  $elements = array( 'img_cap_tax_sel_widget' => $imgel[ 'imagefield_widget' ]);
  $elements['img_cap_tax_sel_widget']['#process'][] = 'img_cap_tax_fld_widget_process';
  $elements['img_cap_tax_sel_widget']['#element_validate']= array('img_cap_tax_fld_widget_validate');

  return $elements;
}

Нам нужно будет задать функцию process-колбэка, которая и задает элементы формы (см. раздел ниже). Также у нас есть специальный валидатор, потому что функция, используемая модулем FileField (и ImageField) не работает в нашем модуле (см. раздел ниже). Еще одна деталь – нам также надо зарегистрировать темизируемый элемент для формы этого виджета – ответ на вопрос, как это сделать, а также другую информацию по теме вы найдете в разделе о Форматере и темизации.

Если ваше поле CCK не содержит файл, вам необходимо задать весь массив элементов самостоятельно. Для большинства составных полей это довольно простая задача, так как для этого не нужна никакая специальная обработка. Вот пример из поля office location:

function office_field_elements() {
  $elements = array( 'loc_entry' => 
    array(
      '#input' => TRUE,
      '#process' => array( 'office_field_loc_entry_process' ),
    ),
  );

  return $elements;
}

process-колбэк и валидатор формы виджета

Именно process-колбэк из массива формы из предыдущего раздела задает и возвращает элементы формы (массив Forms API). Обработку мы предоставили модулю ImageField и добавили функцию обработки в конце; саму функцию поместим в файл (module)_widget.inc.

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

Есть модули, которые основываются на этом условном обозначении, например, модуль WYSIWYG, чтобы выбрать поля для прикрепления редактора, ищет текстовые поля, за которыми идут поля формата "format".

function img_cap_tax_sel_widget_process($element, $edit, &$form_state, $form) {
  $defaults = $element['#value']['data'];
  if( !is_array( $defaults )) {
    $defaults = unserialize( $defaults );
  }

  $field = content_fields($element['#field_name'], $element['#type_name']);

  $element['data']['caption'] = array( 
    '#title' => t( 'Caption' ),
    '#type' => 'textarea',
    '#rows' => $field['widget']['rows'],
    '#cols' => $field['widget']['cols'],
    '#default_value' => $defaults['caption'],
    '#weight' => 4,
  );

  if (!empty($field['text_processing'])) {
    $filt = isset( $defaults['format'] ) ? $defaults['format'] : FILTER_FORMAT_DEFAULT;
    $par = $element['#parents'];
    $par[] = 'data';
    $par[] = 'format';
    $element['data']['format'] = filter_form( $filt, 1, $par );
    $element['data']['format']['#weight'] = 5;
  }

  $mult = $field['allow_multiple'];
  $req = $field['required_term'];
  $opts = content_taxonomy_allowed_values( $field );
  if( !$req && !$mult ) {
    $none = theme( 'content_taxonomy_options_widgets_none', $field );
    $opts = array( '' => $none ) + $opts;
  }
  $element['data']['value'] = array( 
    '#title' => t( 'Taxonomy Terms' ),
    '#type' => 'select',
    '#default_value' => $defaults['value'],
    '#options' => $opts,
    '#weight' => 6,
  );
  if( $mult ) {
    $element['data']['value']['#multiple'] = TRUE;
  }

  return $element;
}

Обратите внимание, что настройки строк и столбцов в подписи, также как и настройки того, будет ли термин таксономии один, или их будет несколько и нужен ли он, исходят из формы настройки виджета (см. CCK hook_widget_settings() ниже).

Если вы задаете составное поле CСK, которое не нуждается в загрузке файлов, вот, в качестве примера, функция обработки для поля office location:

function office_field_loc_entry_process($element, $edit, &$form_state, $form) {

  $defaults = $element['#value'];
  $field = content_fields($element['#field_name'], $element['#type_name']);

  $element['loc_name'] = array( 
    '#title' => t( 'Location name' ),
    '#type' => 'textfield',
    '#default_value' => $defaults['loc_name'],
    '#weight' => 2,
  );

  $element['phone'] = array( 
    '#title' => t( 'Phone' ),
    '#type' => 'textfield',
    '#default_value' => $defaults['phone'],
    '#weight' => 3,
  );

  $element['fax'] = array( 
    '#title' => t( 'Fax' ),
    '#type' => 'textfield',
    '#default_value' => $defaults['fax'],
    '#weight' => 4,
  );

  $element['address'] = array( 
    '#title' => t( 'Address' ),
    '#type' => 'textarea',
    '#default_value' => $defaults['address'],
    '#weight' => 5,
  );

  return $element;
}

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

К сожалению, функция проверки модуля FileField (filefield_widget_validate() в filefield_widget.inc) не будет работать сама по себе в нашем модуле, поскольку она автоматически считает, что имя типа поля 'filefield', а в нашем поле оно другое. Поэтому, нам нужно создать собственную функцию для замены. Если человек, использующий это поле, загружает свои изображения и не делает никаких глупостей, все, что нам нужно сделать – это убедиться в существовании файла. Функция выглядит таким образом:

function img_cap_tax_fld_widget_validate(&$element, &$form_state) {
  if (empty($element['fid']['#value'])) {
    return;
  }

  $field = content_fields($element['#field_name'], $element['#type_name']);
  $ftitle = $field['widget']['label'];

  if ( !( $file = field_file_load($element['fid']['#value']))) {
    form_error($element, t('The file referenced by the %field field does not exist.', array('%field' => $ftitle )));
  }
}

Функция темизации виджета

Для того, чтобы отобразить виджет редактирования, нам надо задать функцию темы, theme_(widget), которая является оберткой для каждого элемента в списке с несколькими значениями с данными об изображениях, подписях и терминах таксономии для ноды. Мы просто даем ему команду отобразить форму, об остальном (например, о выводе мини-картинки) позаботятся модули ImageField и FileField. Вот наша функция:

function theme_img_cap_tax_sel_widget(&$element) {
  return theme( 'form_element', $element, $element['#children'] );
}

Обратите внимание на то, что файл FileField CSS делает виджет-редактор слишком узким. Возможно, вы захотите добавить эти строки в файл CSS вашей темы:

.filefield-element .widget-edit, .filefield-element .widget-preview {
  float: none;
}

CCK hook_widget()

Следующий хук CCK, который надо реализовать для того, чтобы задать виджет-редактор — hook_widget().

Этот хук будет вызываться каждый раз, когда одно из наших полей добавляется в форму, причем параметр $delta показывает индекс элемента, если у вас форма с множественными значениями (0 для первого элемента, 1 – для второго и т.д.).

Возвращаемое значение – массив Forms API, который должен устанавливать значения по умолчанию для формы и задавать функцию обратного вызова. Как всегда, большинством деталей займутся FileField и ImageField (модуль Content Taxonomy не должен выполнять особых действий), для этого нужно вызвать функцию filefield_widget(). Эта функция предполагает, что:

  • Наш модуль содержит файл под названием (module)_widget.inc (функция filefield_widget() его загрузит).
  • Массив $items['delta'] был установлен в качестве массива со значениями по умолчанию для текстовых полей в нашем составном поле перед вызовом файла filefield_widget().

Таким образом, реализация hook_widget() выглядит так:

function img_cap_tax_fld_widget(&$form, &$form_state, $field, $items, $delta = 0) {
  if (empty($items[$delta])) {
    $items[$delta] = array('alt' => '', 'title' => '', 'caption' => '', 'value' => 0);
  }

  $element = filefield_widget($form, $form_state, $field, $items, $delta);
  $element['#upload_validators'] += imagefield_widget_upload_validators($field);

  return $element;
}

Если ваш модуль содержит более одного виджета, в реализации hook_widget() вы, возможно, захотите сделать что-то вроде этого:

switch( $field['widget']['type'] ) {
  case 'first_widget_machine_name':
    (code for this widget)
    break;

  case 'second_widget_machine_name':
    (code for this widget)
    break;
}

А вот что делает поле office location:

function office_field_widget(&$form, &$form_state, $field, $items, $delta = 0) {
  $element = array(
    '#type' => $field['widget']['type'],
    '#default_value' => isset($items[$delta]) ? $items[$delta] : '',
  );
  return $element;
}

CCK hook_widget_settings()

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

Проверкой займется ImageField, а мы создадим собственные функции для работы формы и операции сохранения:

function img_cap_tax_fld_widget_settings( $op, $widget ) {
  switch ($op) {
    case 'form':
      return img_cap_tax_fld_widget_settings_form($widget);
    case 'validate':
      return imagefield_widget_settings_validate($widget);
    case 'save':
      return img_cap_tax_fld_widget_settings_save($widget);
  }
}

Наша форма office location, как и многие другие формы CCK, не содержит никаких настроек для выбора параметров виджета.

Обратите внимание, что если ваш модуль содержит несколько виджетов, вы можете включить $widget['type'] для настройки различных виджетов в реализации hook_widget_settings().

filefield_widget_settings() callbacks

В реализации hook_widget_settings() задано две функции обратного вызова операций. Операция формы выдает форму настройки виджета, операция сохранения выдает список того, какие данные формы должны быть сохранены в базу данных.

Для начала, давайте поработаем над формой настроек. Существующие модули FileField и ImageField содержат форму настройки виджета, которая позволяет пользователю устанавливать путь файла, расширения, максимальный размер файла и другие параметры. Мы воспользуемся этой формой, но изменим ее таким образом, что теги alt и title всегда могут настраиваться, а не оставаться опциями, как в ImageField (мы хотим, чтобы настройки были там, чтобы другие вызываемые функции модуля ImageField имели правильно настроенные значения).

Виджет списка модуля Content Taxonomy содержит настройки структурирования и группирования терминов, что нам тоже нужно. И наконец, большинство текстовых полей с несколькими строками позволяют пользователю выбирать, сколько рядов/колонок отображать – эта настройка нам будет нужна для поля подписи. Все вместе:

function img_cap_tax_fld_widget_settings_form( $widget ) {
  $form = imagefield_widget_settings_form( $widget );

  $form['custom_alt'] = $form['alt_settings']['custom_alt'];
  $form['custom_alt']['#type'] = 'hidden';
  $form['custom_alt']['#value'] = 1;
  $form['alt'] = $form['alt_settings']['alt'];
  $form['alt']['#type'] = 'hidden';
  $form['alt']['#value'] = '';
  unset( $form['alt']['#suffix'] );
  unset( $form['alt_settings'] );

  $form['custom_title'] = $form['title_settings']['custom_title'];
  $form['custom_title']['#type'] = 'hidden';
  $form['custom_title']['#value'] = 1;
  $form['title'] = $form['title_settings']['title'];
  $form['title']['#type'] = 'hidden';
  $form['title']['#value'] = '';
  unset( $form['title']['#suffix'] );
  unset( $form['title_settings'] );

  $rows = (isset($widget['rows']) && is_numeric($widget['rows'])) ? $widget['rows'] : 5;

  $form['rows'] = array(
    '#type' => 'textfield',
    '#title' => t('Number of rows in caption field'),
    '#default_value' => $rows,
    '#element_validate' => array('_text_widget_settings_row_validate'),
    '#required' => TRUE,
    '#weight' => 8,
  );

  $cols = (isset($widget['cols']) && is_numeric($widget['cols'])) ? $widget['cols'] : 40;

  $form['cols'] = array(
    '#type' => 'textfield',
    '#title' => t('Number of columns in caption field'),
    '#default_value' => $cols,
    '#element_validate' => array('_text_widget_settings_row_validate'),
    '#required' => TRUE,
    '#weight' => 9,
  );

  $form2 = content_taxonomy_options_widget_settings( 'form', $widget );
  $form2['settings']['#title'] = t( 'Settings for Taxonomy' );

  $form = $form + $form2;
  return $form;
}

Что касается валидации, здесь не нужно делать ничего особенного, поскольку ни ImageField, ни Content Taxonomy не требуют действий, на которые неспособен FileField. Поэтому эту функцию мы задавать не будем. Операция сохранения выдает список полей для сохранения из формы настроек:

function img_cap_tax_fld_widget_settings_save( $widget ) {
  $arr = imagefield_widget_settings_save( $widget );
  $arr[] = 'rows';
  $arr[] = 'cols';
  $arr2 = content_taxonomy_options_widget_settings( 'save', $widget );
  $arr2[] = 'allow_multiple';
  $arr2[] = 'required_term';
  return array_merge( $arr, $arr2 );
}

Назад к Содержанию

Задаем форматер и темизацию

Задав наше поле и виджет-редактор единственное, что остается сделать – задать отображение для посетителей сайта (конечно, ваша тема может переопределить это). Мы решаем эту проблему задав формат поля; также как и виджетами, в одном поле программ, задающих форматирование может быть несколько. Однако в данном примере мы создадим только одну.

Дадим нашей программе-форматеру машиночитаемое имя 'default', далее по тексту - (formatter), а также «человеческое» имя «Image with Caption and Taxonomy Terms». Обратите внимание на то, что машиночитаемые имена для форматтеров не обязательно должны быть уникальными, исключение – в рамках одного модуля.

CCK hook_field_formatter_info()

Для того, чтобы снабдить CCK информацией о нашем форматтере реализуем hook_field_formatter_info():

function img_cap_tax_fld_field_formatter_info() {
  return array( 
    'default' => array( 
       'label' => t( 'Image with Caption and Taxonomy Terms' ),
       'field types' => array( 'img_cap_tax' ),
    ),
  );
}

Core hook_theme()

Как только мы задали форматтер, CCK будет считать, что существует соответствующий темизированный элемент под названием (module)_formatter_(formatter), который нужно зарегистрировать в хуке hook_theme(). Как замечено выше, нам также нужно зарегистрировать темизированный элемент для формы виджета, поэтому реализовываем хук hook_theme() следующим образом:

function img_cap_tax_fld_theme() {
  return array(
    'img_cap_tax_sel_widget' => array( 
       'arguments' => array('element' => NULL), 
    ),

    'img_cap_tax_fld_formatter_default' => array( 
       'arguments' => array('element' => NULL), 
    ),
  );
}

Функции темизации

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

function theme_img_cap_tax_fld_formatter_default( $element = NULL ) {

  if( empty( $element['#item'] )) {
    return '';
  }

  $img = theme( 'imagefield_formatter_image_plain', $element );

  $cap = $element['#item']['safe_caption'];

  $tax = '';
  $sep = '';
  $val = $element['#item']['data']['value'];
  if( !is_array( $val )) {
    $val = array( $val );
  }
  foreach( $val as $tid ) {
    $term = taxonomy_get_term( $tid );
    $tax .= $sep . check_plain( $term->name );
    $sep = ', ';
  }

  return '
' . '
' . $img . '
' . '
' . $cap . '
' . '
' . $tax . '
' . '
'; }

Назад к Содержанию

Загрузки, история и ссылки

Скачать готовый модуль, описанный в статье. Файл весит 13.6 KB, текущая версия модуля — 6.x-1.3.

Этот модуль был разработан с помощью системы Drupal 6.9 и следующих модулей:

  • Content Construction Kit (CCK) версии 6.x-2.2 и включенных в него модулей Option Widgets и Text.
  • ImageField версии 6.x-3.0. Примечание: Релиз этого модуля и его альфа версия сильно отличаются! Модуль полей по ссылке выше не будет работать с альфа-версиями модулей ImageField и FileField.
  • FileField версии 6.x-3.0 – см. Примечание верху об альфа-версии.
  • ImageAPI версии 6.x-1.6
  • Content Taxonomy версии 6.x-1.0-beta6, а также включенный модуль Content Taxonomy Options

Справочная информация и ссылки:

Creating a Compound Field Module for CCK in Drupal 6.x. Перевод Александр Швец, Ника Гук.

Comments

Murz
December 29th, 2009

А как же модуль Flexifield http://drupal.org/project/flexifield ?
Он как раз позволяет сделать то что надо, правда реализует это через отдельную ноду, но интерфейс вполне приличный....

PVasili
December 29th, 2009

Возможно, автор не в курсе (среди >4500 модулей сложно бывает найти), отсюда и велик...

maks
December 29th, 2009

Спасибо большое за велик, а если сделать два таких модуля и в одном из них будет вызываться второе, такое будет работать?

Alexander Shvets
January 3rd, 2010

Да.

Alexander Shvets
December 31st, 2009

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

Anton Sidashin
February 1st, 2010

Спасибо за пост. Некоторое время назад, при спешном создании http://drupal.org/project/simplest_gmap, кажется, у меня была мысль написать похожий текст. Но, после некоторых размышлений, я на этот тяжелый труд забил. Мне кажется, у подобных статей целевая аудитория достаточно (очень) узкая - большая часть программистов, которые осознают плюсы CCK, и дошли до создания составных полей, просто залезут в код /sites/all/modules/content/... или /imagefield и чтение кода будет более эффективно, чем чтение пошагового туториала.
У тех же, кто в состоянии такое поле сделать только базируясь на статье, при малейшем шаге влево-вправо возникнут совершенно непреодолимые трудности, и они будут грустить в своих топиках на d.ru и в issue threads на d.org, в которых им никто не ответит...

btw, multigroup использую сейчас в одном проекте, вроде багов особых не вылазит (пикчи засовывать не пробовал).

Yuriy Gerasimov
March 18th, 2010

Большое спасибо за пост. Как после создания составного ССК поля можно "потом" менять структуру? Например было создано составное поле с 3 текстовыми полями, а потом появилась необходимость добавить еще одно текстовое поле либо что-то еще. При чем данные уже были внесены и их нужно сохранить.

Vlad Savitsky
April 22nd, 2010

Правильная ссылка на архив с модулем: http://www.poplarware.com/sites/poplarware.com/files/downloads/img_cap_t...

Alexander
June 18th, 2010

Жаль, готовый модуль больше не открывается.

Vlad Savitsky
December 26th, 2010

Опять спам!

Got anything to add?