ShvetsGroup

 

Создание сложного поля CCK с диаграммой

4 comments

Создание сложного поля CCK с диаграммой

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

Исходные требования

Требования к элементу на формe ноды

Колонки таблицы:

  • Year (год) — выпадающий список лет
  • Revenues — текстовое поле. Может быть только положительным и нулём, а также может быть десятичным.
  • Expenditures — текстовое поле. Может быть только положительным и нулём, а также может быть десятичным.
  • Net — текстовое поле. Значение может быть положительным, отрицательным и нулём, а также может быть десятичным.

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

Требования к отображению элемента на странице ноды

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

Отображение на странице

Реализация

После анализа требований, как обычно, были предприняты попытки найти готовые или похожие решения среди contrib-модулей, но ничего подходящего найдено не было.

В результате анализа вариантов реализации было принято решение создать новое CCK-поле. Преимущества такого выбора:

  • Использование CCK, что дает возможность использовать множественные значения (multiple values);
  • Полный контроль над каждым элементом поля (сабмит, валидация, темизация);
  • Независимость от чужой логики (модули группирующие поля);
  • Уменьшение количества кода за счет того, что модуль затачивается под конкретную задачу;
  • Уменьшение времени на разработку за счет того, что не нужно осваивать логику работы других модулей.

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

Мы не будем сейчас останавливаться на том, как сделать CCK-поле, потому что об этом написано много хороших статей как на английском, так и на русском языках - некоторые из них приведены в конце статьи.

Мы рассмотрим особенности создания составного CCK-поля.

Хранение данных

Для этого нам нужно внести измнения в hook_field_settings():


/**
 * Implementation of CCK's hook_field_settings()
 * CCK hook_field_settings (http://drupal.org/node/354365)
 */
function financial_table_field_settings($op, $field) {
  switch ($op) {
    case 'database columns':
        break;
    case 'form':
       // Global settings for field:
        $form = array();
        return $form;
    case 'validate':
        break;
    case 'save':
        break;
  }
}

Мы добавляем описания дополнительных полей для операции database columns. Формат такой же как в Schema API. Обратите внимание, что цифровые поля у нас хранятся как VARCHAR, потому что они могут быть десятичными (иметь точку и несколько знаков после запятой) — данные хранятся как строка.


    case 'database columns':
        $database_columns = array(
          'financial_table_year' => array(
            'type' => 'varchar',
            'length' => 4,
            'not null' => FALSE,
            'sortable' => TRUE,
            'views' => TRUE,
          ),
          'financial_table_revenues' => array(
            'type' => 'varchar',
            'length' => 12,
            'not null' => FALSE,
            'sortable' => TRUE,
            'views' => TRUE,
          ),
          'financial_table_expenditures' => array(
            'type' => 'varchar',
            'length' => 12,
            'not null' => FALSE,
            'sortable' => TRUE,
            'views' => TRUE,
          ),
          'financial_table_net' => array(
            'type' => 'varchar',
            'length' => 12,
            'not null' => FALSE,
            'sortable' => TRUE,
            'views' => TRUE,
          ),
        );
      return $database_columns;

Теперь создадим форму настроек поля, чтобы можно было менять диапазон лет в выпадающем списке. Формат элементов см. в Form API:


    case 'form':
       // Global settings for field:
        $form = array();
        $form['financial_table_year_range'] = array(
          '#type' => 'textfield',
          '#title' => t('Years back and forward'),
          '#default_value' => !empty($field['financial_table_year_range']) ? $field['financial_table_year_range'] : '-50:+10',
          '#size' => 10,
          '#maxsize' => 10,
          '#description' => t('Number of years to go back and forward in the year selection list, default is -50:+10.'),
        );
        return $form;

Вот как эта форма выглядит:

CCK field financial_table global settings

Далее мы создаем код для операций валидации формы настроек поля и сохранения.


    case 'validate':
        if (!preg_match('@\-[0-9]*:[\+|\-][0-9]*@', $field['financial_table_year_range'])) {
          form_set_error('financial_table_year_range', t('Years back and forward must be in the format -9:+9.'));
        }
        break;
    case 'save':
        return array('financial_table_year_range');

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

Мы используем для этого операцию validate в hook_field(). В массиве $items мы получаем все строки данных, которые ввел пользователь на форме. Логика простая — если хотя бы один из элементов строки не заполнен, то нужно выдать сообщение об ошибке.


/**
 * Implementation of CCK's hook_field().
 * http://drupal.org/node/342996
 */
function financial_table_field($op, $node, $field, &$items, $teaser, $page) {
  switch ($op) {
    case 'validate':
      $fields_names = array('year', 'revenues', 'expenditures', 'net');
      foreach ($items as $delta => $item) {
        $row_has_been_filled =  FALSE;
        foreach ($fields_names as $key => $name) {
          $empty_field = '';
          $element_value = $item['financial_table_' . $name];
          if ($key == 0) {
            if (empty($element_value)) {
              $empty_field = $name;
            }
            else {
              $row_has_been_filled =  TRUE;
            }
          }
          elseif (empty($element_value) && $row_has_been_filled) {
            if ($name != 'year') {
                if (strlen($element_value)  t($field['widget']['label']))));
            break;
          }
        }
      }
      return $items;
  }
}

Мы определили CCK-поле. Его добавление на странице управления полями выглядит так:

Добавление сложного CCK-поля

Нам ещё нужно создать виджет (widget — элемент формы) для нашего поля.

Создание виджета составного CCK-поля

Особенность нашего виджета в том, что нас поле имеет несколько элементов, а не один. Несколько элементов также имеют поля Money и Location. Используем hook_widget():


/**
 * Implementation of CCK's hook_widget().
 * http://drupal.org/node/344142
 */
function financial_table_widget(&$form, &$form_state, $field, $items, $delta = 0) {
  //$field['columns'] содержит имена всех элементов
  $field_names = array_keys($field['columns']);
  $widget = $field['widget'];
  //Получаем размеры всех элементов
  $size = _financial_table_get_widget_sizes($widget);
  //Преобразуем массив в переменные
  extract($size);

  //Создаем элемент формы для года:
  $element['financial_table_year'] = array(
    '#type'             => 'select',
    // Получаем массив лет с учётом диапазона указанного в настройках поля:
    '#options'          => _financial_table_select_options($field['financial_table_year_range']),
    '#default_value'    => isset($items[$delta][$field_names[0]]) ? $items[$delta][$field_names[0]] : '',
    //Эти аттрибуты помогают расположить элементы не вертикально, а горизонтально:
    '#attributes'       => array('class' => 'financial_table_year', 'style' => 'width:'. $year_size .'px;float:left;margin:0 10px 0 0;'),
  );

  $element['financial_table_revenues'] = array(
    '#type'             => 'textfield',
    '#default_value'    =>   isset($items[$delta][$field_names[1]]) ? $items[$delta][$field_names[1]] : '',
    '#size'             => 10,
    '#maxlength'        => 12,
    '#attributes'       => array('class' => 'financial_table_revenues', 'style' => 'width:'. $revenues_size .'px;float:left;margin:0 10px 0 10px;'),
    '#element_validate' => array('_financial_table_validate_demical_positive'),
  );

  $element['financial_table_expenditures'] = array(
    '#type'             => 'textfield',
    '#default_value'    =>   isset($items[$delta][$field_names[2]]) ? $items[$delta][$field_names[2]] : '',
    '#size'             => 10,
    '#maxlength'        => 12,
    '#attributes'       => array('class' => 'financial_table_expenditures', 'style' => 'width:'. $expenditures_size .'px;float:left;margin:0 10px 0 10px;'),
    //Добавляем функцию валидатор (приведена ниже):
    '#element_validate' => array('_financial_table_validate_demical_positive'),
  );
  $element['financial_table_net'] = array(
    '#type'             => 'textfield',
    '#default_value'    =>   isset($items[$delta][$field_names[3]]) ? $items[$delta][$field_names[3]] : '',
    '#size'             => 10,
    '#maxlength'        => 12,
    '#attributes'       => array('class' => 'financial_table_net', 'style' => 'width:'. $net_size .'px;margin:0 10px 0 10px;'),
    //Добавляем функцию валидатор (приведена ниже):
    '#element_validate' => array('_financial_table_validate_demical'),
  );

  if (empty($form['#parents'])) {
    $form['#parents'] = array();
  }

  //Заполняем специальный элемент массива $element['_error_element'] 
  //значениями для того, чтобы можно было подстветить элемент при ошибке
  $fields_names = array('year', 'revenues', 'expenditures', 'net');
  foreach ($fields_names as $key => $name) {
    $element['_error_element']['financial_table_' . $name] = array(
      '#type' => 'value',
      '#value' => implode('][', array_merge(array($field[$name], $delta, 'financial_table_' . $name))),
    );
  }

  //Задаем функцию валидации для виджета
  if (empty($element['#element_validate'])) {
    $element['#element_validate'] = array();
  }
  $element['#element_validate'][] = 'financial_table_widget_validate';
  return $element;
}

Весь этот код нужен для того, чтобы построить псевдо-таблицу. «Псевдо» потому, что у нас все элементы будут в одной ячейке таблицы, чтобы не нарушать логику модуля CCK для работы с сортировкой полей с неограниченным количеством значений.

Вспомогательные функции

Для валидации элементов используются 2 функции, назначение которых очевидно:


/**
 * Helper form element validator : integer > 0.
 */
function _financial_table_validate_demical_positive($element, &$form_state) {
  $element_key = _financial_table_get_element_key($element);
  $element_delta = _financial_table_get_element_delta($element);
  $value = $element['#value'];
  $start = $value;
  $value = preg_replace('@[^0-9\.]@', '', $value);
  if ($start != $value) {
    form_error($element, t('%name in %row row could be positive or zero. Only numbers and decimals are allowed.',
            array('%name' => $element_key, '%row' => $element_delta)));
  }
}

/**
 * Helper form element validator : integer.
 */
function _financial_table_validate_demical($element, &$form_state) {
  $element_key = _financial_table_get_element_key($element);
  $element_delta = _financial_table_get_element_delta($element);
  $value = $element['#value'];
  $start = $value;
  $value = preg_replace('@[^-0-9\.]@', '', $value);
  if ($start != $value) {
    form_error($element, t('Only numbers and decimals are allowed in %name in %row row.',
            array('%name' => $element_key, '%row' => $element_delta)));
  }
}

Функция, которая получает размеры всех элементов из настроек виджета:


function _financial_table_get_widget_sizes($widget) {
  $size['year_size'] = ((isset($widget['year_size']) && is_numeric($widget['year_size'])) ? $widget['year_size'] : '100');
  $size['revenues_size'] = ((isset($widget['revenues_size']) && is_numeric($widget['revenues_size'])) ? $widget['revenues_size'] : '100');
  $size['expenditures_size'] = ((isset($widget['expenditures_size']) && is_numeric($widget['expenditures_size'])) ? $widget['expenditures_size'] : '100');
  $size['net_size'] = ((isset($widget['net_size']) && is_numeric($widget['net_size'])) ? $widget['net_size'] : '100');
  return $size;
}

Для того, чтобы получить эти размеры для каждого из элементов, — нам их нужно задать.

Для задания размеров элементов виджета мы используем hook_widget_settings(), в котором на операции form мы создаем форму, а на операции save указываем какие переменные нужно сохранить:


/**
 * Implementation of CCK hook_widget_settings().
 * http://drupal.org/node/354369
 */
function financial_table_widget_settings($op, $widget) {
  //Получаем размеры всех элементов виджета:
  $size = _financial_table_get_widget_sizes($widget);
  extract($size);
  switch ($op) {
    case 'form':
      $form = array();
      $form['size'] = array(
        '#type' => 'fieldset',
        '#title' => t('Size of fields'),
      );

      $form['size']['year_size'] = array(
        '#type' => 'textfield',
        '#title' => t('Size of Year field (px)'),
        '#default_value' => $year_size,
        //Добавляем валидацию для этого элемента формы:
        '#element_validate' => array('_element_validate_integer_positive'),
        '#required' => TRUE,
      );
      $form['size']['revenues_size'] = array(
        '#type' => 'textfield',
        '#title' => t('Size of Revenues field (px)'),
        '#default_value' => $revenues_size,
        '#element_validate' => array('_element_validate_integer_positive'),
        '#required' => TRUE,
      );
      $form['size']['expenditures_size'] = array(
        '#type' => 'textfield',
        '#title' => t('Size of Expenditures field (px)'),
        '#default_value' => $expenditures_size,
        '#element_validate' => array('_element_validate_integer_positive'),
        '#required' => TRUE,
      );
      $form['size']['net_size'] = array(
        '#type' => 'textfield',
        '#title' => t('Size of Net field (px)'),
        '#default_value' => $net_size,
        '#element_validate' => array('_element_validate_integer_positive'),
        '#required' => TRUE,
      );
      return $form;

    case 'save':
      return array('year_size', 'revenues_size', 'expenditures_size', 'net_size');
  }
}

Для каждого элемента мы указываем callback-функцию-валидатор: _element_validate_integer_positive() — это стандартный валидатор.

На странице настроек поля эта форма выглядит так:

CCK-поле настройки виджета

С настройкой виджета мы тоже закончили.

Темизация виджета формы

Виджет должен выглядеть вот так:

виджет составного CCK-поля в виде таблицы

Но он выглядит так:

CCK-поле без кода в template.php

Для того, чтобы темизировать вывод поля нам нужно создать в файле template.php текущей темы функцию THEME_NAME_content_multiple_values($element).
В нашем случае она будет выглядеть так:


/**
 * Overriding of content module default rendering for multiple values
 * Used for node creation form.
 * Combine multiple values into a table with drag-n-drop reordering.
 */
function THEME_NAME_content_multiple_values($element) {
  $field_name = $element['#field_name'];
  $field = content_fields($field_name);
  $output = '';
  // here is the definition of fields don't need to be reordered manually
  $fields_no_manual_reordering = array('field_employment', 'field_education');
  $noReordering = in_array($field_name, $fields_no_manual_reordering);

  if ($field['multiple'] >= 1) {
    $table_id = $element['#field_name'] .'_values';
    $order_class = $element['#field_name'] .'-delta-order';
    $required = !empty($element['#required']) ? '*' : '';

    if ($field['type'] == 'financial_table') {
      if(function_exists('financial_table_get_widget_header')) {
        //Заменяем стандартную шапку таблицы своей, где указаны названия наших колонок
        //Количество колонок при этом в таблице не меняется
        $header = financial_table_get_widget_header($field, $required);
      }
    }
    else {
      $header = array(
        array(
          'data' => t('!title: !required', array('!title' => $element['#title'], '!required' => $required)),
          'colspan' => 2
        ),
        $noReordering ? null : t('Order'),
      );
    }

    $rows = array();

    // Sort items according to '_weight' (needed when the form comes back after
    // preview or failed validation)
    $items = array();
    foreach (element_children($element) as $key) {
      if ($key !== $element['#field_name'] .'_add_more') {
        $items[] = &$element[$key];
      }
    }
    usort($items, '_content_sort_items_value_helper');

    // Add the items as table rows.
    foreach ($items as $key => $item) {
      $item['_weight']['#attributes']['class'] = $order_class;
      $delta_element = drupal_render($item['_weight']);
      $cells = array(
        array('data' => '', 'class' => 'content-multiple-drag'),
        drupal_render($item),
        $noReordering ? null : array('data' => $delta_element, 'class' => 'delta-order'),
      );
      $rows[] = array(
        'data' => $cells,
        'class' => 'draggable',
      );
    }
    $output .= theme('table', $header, $rows,
      array('id'=> $table_id, 'class' => 'content-multiple-table')
    );
    $output .= $element['#description'] ? '
'. $element['#description'] .'
' : ''; $output .= drupal_render($element[$element['#field_name'] .'_add_more']); if (!$noReordering) { drupal_add_tabledrag($table_id, 'order', 'sibling', $order_class); } } else { foreach (element_children($element) as $key) { $output .= drupal_render($element[$key]); } } if ($output) { if (($field['type'] == 'financial_table') && $field['display_settings ']['label']['format'] == 'above') { //Выводим лэйбл только, если его вывод задан в настроках поля $output = '' . $output; } } return $output; }

После добавления этих изменений внешний вид нашего поля будет такой как нам нужно — с шапкой и в виде таблицы.

Создание форматтера для составного поля

Форматтер отвечает за отображение поля на странице ноды. У нас был определён стандартный форматтер для поля:


/*
 * Theme function for 'plain' example field formatter.
 * Default formatter is replaced in content-field.tpl.php
 */
function theme_financial_table_formatter_default($element) {
  $output .= strip_tags($element['#item']['financial_table_year']) . ' | ';
  $output .= strip_tags($element['#item']['financial_table_revenues']) . ' | ';
  $output .= strip_tags($element['#item']['financial_table_expenditures']) . ' | ';
  $output .= strip_tags($element['#item']['financial_table_net']) . ' | ';
  return $output;
}

Он будет может выводить данные и будет это выглядеть вот так:

Составное CCK-поле форматтер по умолчанию

Этот форматтер можно использовать для тестирования. После того, как мы приведём внешний вид в порядок он больше нам не понадобится.

Любой форматтер поля, какой бы мы ни создали, оперирует только с элементами одного ряда,— а нам нужно построить таблицу, которая содержит все ряды. Для этого в теме переопределим вывод всего этого поля. Таким образом форматтер по умолчанию не будет использоваться, а будет выводится красивая таблица с данными.

В файле template.php текущей темы нужно добавить функцию THEME_NAME_preprocess_content_field (&$vars). И добавить в неё код:


function green_evolution_preprocess_content_field (&$vars) {
  if ($vars['field']['type'] == 'financial_table') {
    if (!$vars['field_empty']) {
      $header = array(t('Year'), t('Revenues'), t('Expenditures'), t('Net'));
      $rows = array();
      foreach ($vars['items'] as $delta => $item) {
        if (!$item['empty']) {
          if(isset($item['financial_table_year']) &&
            isset($item['financial_table_revenues']) &&
            isset($item['financial_table_expenditures']) &&
            isset($item['financial_table_net']))
          {
            $rows[] = array(
              $item['financial_table_year'],
              $item['financial_table_revenues'],
              $item['financial_table_expenditures'],
              $item['financial_table_net'],
            );
          }
        }
      }
      if(count($rows) > 0) {
        $vars['field']['rendered_table'] = theme('table', $header, $rows, array('id' => 'field-' . $field_name_css, 'class' => $field['type']));
      }
    }
  }
}

Этот код обрабатывает данные поля из всех строк, форматирует таблицу, которую мы выведем в шаблоне. Для этого нам понадобится дополнительный файл шаблона в теме. Создаём файл content-field.tpl.php в папке текущей темы с таким контентом (обычно его там нет):


$item) : if (!$item['empty']) : ?>

Если поле имеет тип financial_table, то будет выводится наша таблица, которую мы отрендерили раньше в файле template.php. Во всех остальных случаях используется стандартный вывод. После сохранения кода мы можем видеть такой результат:

отображение таблицы составного CCK-поля на странице ноды

Напомню, что раньше мы сделали виджет формы, который выглядит так:

виджет формы составного CCK-поля

Самое время сделать закладку для этой статьи, потому что это ещё далеко не всё. Во второй части статьи мы добавим динамическую графику в наше составное CCK-поле.

Библиография

Материалы по CCK-полям

Интересные материалы, которые могут пригодиться

Comments

Михаил
November 21st, 2010

Где ссылка на оригинал статьи? Или откуда скачать можно исходники примера, нифига понять немогу по этим обрывкам
Как к своему мульти-полю прикрепить разные виджеты на каждое поле?
т.е. Есть составное-поле test_g в нем поля test_1 test_2 e.t.c
вот как полю test_1 прикрутить виджет от таксономии , и
к полю test_2 прикрутить тоже виджет от таксономии но независимы от test_1 ?

явно как-то через вот эту фичу:
'#options' => _financial_table_select_options($field['financial_table_year_range']),

делаю по аналогии - тупо белый экран :((((

Alexander Shvets
January 6th, 2011

Это оригинал. Полный код мы не можем предоставить.

Anton Sidashin
February 21st, 2011

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

Alexander
March 13th, 2011

Вообще то уже есть модуль создающий поле таблицы в CCK(http://drupal.org/project/tablefield). может проще было его под нужды подогнать нежели изобретать вел...

Got anything to add?