Создание гибких анкет в Drupal 7

в 6:57, , рубрики: drupal, drupal 7, php, хак, метки: , , ,

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

Введение.

Я считаю, что любой веб-разработчик рано или поздно сталкивается с задачей создания опросника или анкеты. Конечно, в наше время не так уж и трудно подыскать какое-нибудь готовое решение, и благо, что вариантов здесь у нас предостаточно:

  • Можно найти и использовать какой-нибудь мощный и самостоятельный продукт, где, по сути, в сайт интегрируется готовая система.
  • Вторым вариантом является использование какого-нибудь одного из широкого множества тематических сервисов, например SurveyMonkey или Wufoo.

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

Динамичное анкетирование.

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

  1. Необходим механизм создания и редактирования опросников через административный интерфейс;
  2. Необходима возможность выводить вопросы постепенно на нескольких страницах;
  3. Необходима возможность выводить вопросы только при выполнении некоторых условий;
  4. Условия могут быть следующих типов: «выбрано некоторое из перечисленных значений», «не выбрано некоторое из перечисленных значений»;
  5. Условий может быть сразу несколько;
  6. При комбинировании условий могут использовать операторы И/ИЛИ.

Приведём пример такого опросника:

image

Имеется три страницы с вопросами и одна – с результатами. Страница 2 может быть вовсе не показана пользователю, если на первые два вопроса он ответил «ДА». Количество вопросов на третьей странице, куда пользователь попадает всегда, варьируется в зависимости от ответов на первые два вопроса, только условия уже комбинируется через «ИЛИ». На странице результатов всегда выводится текстовое сообщение с благодарностью. Также, если пользователю была отображена страница 2, и он ответил на третий вопрос положительно, то выводится некоторый дополнительный текст.

Создание гибкой анкеты в Drupal.

Для Drupal разработан мощный модуль Webform, который как раз реализует многие из перечисленных требований, но, к сожалению, далеко не все.
Мы можем создавать и редактировать через административный интерфейс формы, мы можем присоединять к ним произвольные компоненты, мы можем даже добавлять к компонентам условия, но мы не можем задавать несколько условий и их комбинировать. Кроме того, элементам типа Markup нельзя присваивать условия, что не позволяет создавать динамичные feedback-страницы с исключительно статичным содержимым.
Исправлению этого недостатка и посвящена эта статья.
Для работы нам понадобится:

  1. Drupal 7.14
  2. Webform 7.x-3.18

Устанавливаем Drupal и модуль Webform. После этого переходим на страницу добавления материалов [Add content -> Webform|/node/add/webform), где указываем название нашего опросника и вводим его описание.
После сохранения новой ноды возвращаемся на страницу её редактирования и выбираем вкладку Webform.
На этой вкладке присоединим к форме все необходимые компоненты, отвечающие за вопросы и страницы опросника.

image

Для обозначения вопросов будем использовать кодирование по шаблону «question_X», где X – порядковый номер вопроса. Варианты ответов (Options) для простоты используем везде одинаковые, и закодируем согласно требованиям компонента типа Select (ключ|выводимое_значение).

image

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

Расширяем возможности Webform

Решим поставленную задачу модификации поведения модуля Webform несколько грубо и нетипично для Drupal, внеся ряд изменения напрямую с целью экономии времени. В будущем их вполне можно будет выделить отдельно и создать отдельный модуль.
Сначала модифицируем интерфейс. Увеличим количество возможных условий, выводимых на странице настройки компонента, с 1 до 10. За сами элементы в Drupal всегда отвечает некоторая функция, которая возвращает структуру Forms API. Чтобы найти её точное название, достаточно в клиентском HTML, например при помощи Firebug’а, определить значение элемента input[name=”form_id”], который расположен внутри формы.

image

В данном случае значение равно «webform_component_edit_form». Данная функция расположена в файле "/webform/includes/webform.components.inc".

На 529 строчке начинается код, отвечающий за условия. Теперь нам необходимо выполнить его модификацию, при этом, не поломав работоспособность функций, выполняющих обработку ввода в форму при сабмите (напр. webform_component_edit_form_validate, webform_component_edit_form_submit).
При отладке кода можно заметить вся дополнительная конфигурация компонента сохраняется в поле Extra в таблице компонентов.

image

Поэтому не будет ничего страшного, если мы её расширим, попросту выполнив клонирование элементов 10 раз. Магическое число 10 сделаем слабой константой, то есть определим через функцию Drupal variable_get для того, чтобы была гипотетическая возможность сделать административный интерфейс для модификации этого значения.
Заменим весь код, представляющий условные компоненты, следующим:

if ($conditional_components) {
  $extra_cond = array(
    'conditional_component' => array(
      '#type' => 'select', 
      '#title' => t('Component'), 
      '#options' => webform_component_list($node, $conditional_components, FALSE, TRUE), 
      '#description' => t('Select another component to decide whether to show or hide this component. You can only select components occurring before the most recent pagebreak.') 
    ), 
    'conditional_operator' => array(
      '#type' => 'select', 
      '#title' => t('Operator'), 
      '#options' => array(
        '=' => t('Is one of'), 
        '!=' => t('Is not one of') 
      ), 
      '#description' => t('Determines whether the list below is inclusive or exclusive.') 
    ), 
    'conditional_values' => array(
      '#type' => 'textarea', 
      '#title' => t('Values'), 
      '#description' => t('List values, one per line, that will trigger this action. If you leave this blank, this component will always display.') 
    ) 
  );
  
  $extra = $component['extra'];
  for ($i = 0; $i < variable_get('webform:max_conditional_rules', 10); $i++) {
    $condition_id = 'condition_'. $i;
    if (isset($extra[$condition_id]['values'])) {
      $condition_component_struct = $extra[$condition_id]['values'];
      $extra_cond['conditional_component']['#default_value'] = $condition_component_struct['conditional_component']; 
      $extra_cond['conditional_operator']['#default_value'] = $condition_component_struct['conditional_operator']; 
      $extra_cond['conditional_values']['#default_value'] = $condition_component_struct['conditional_values'];       
    } 
    $form['extra'][$condition_id] = array(
      '#type' => 'fieldset',
      '#title' => t('Conditional rule !num', array('!num' => $i + 1, )),
      '#collapsed' => TRUE,
      '#collapsible' => TRUE,
      '#tree' => TRUE,    
      'values' => $extra_cond, 
    );
  }        
}

Выполним проверку. Перейдём на страницу настройки произвольного компонента, расположенного на любой странице, кроме первой. Можно заметить, что число условных компонентов увеличилось.
Добавим в эту форму ещё одну дополнительную настройку – выбор алгоритма, по которому вычисляется конечный результат выполнения условий, если их сразу несколько. Пусть у нас будет два алгоритма: AND и OR. Для этого после предыдущего кода сразу добавим следующий:

$form['extra']['algorithm'] = array(
  '#type' => 'select', 
  '#options' => array(
    'and' => t('AND'), 
    'or' => t('OR'), 
  ), 
  '#title' => t('Algorithm to evaluate conditions'), 
  '#default_value' => isset($extra['algorithm']) ? $extra['algorithm'] : 'and', 
);

Теперь у нас есть сразу несколько условий и настройка алгоритма их комбинирования, что уже хорошо, но только со стороны административного интерфейса. При обработке конечной клиентской вебфомы, когда пользователь будет заполнять поля, остается старый код, который умеет работать с максимум одним условием и ничего не знает про алгоритмы комбинирования.
Осталось найти этот код. Перейдём на страницу формы со стороны клиента (просто нажав View) и отыщем соответствующий input[name=«form_id»].

image

Его значение равно webform_client_form_1 и при поиске ровно этой функции результат получается плачевным. Но, как известно, в Drupal есть способ задания правил соответствия form_id конечной функции через hook_forms(), и если найти соответствующий хук, то в нём есть указание на callback webform_client_form, а эта функция имеет место быть:

/**
 * Implements hook_forms().
 *
 * All webform_client_form forms share the same form handler
 */
function webform_forms($form_id) {
  $forms = array();
  if (strpos($form_id, 'webform_client_form_') === 0) {
    $forms[$form_id]['callback'] = 'webform_client_form';
  }
  return $forms;
}

В этой функции (и в стандартном callback’е, выполняемом при сабмите – webform_client_form_submit) нет ничего, обрабатывающего условия, но если проанализировать её код, то можно заметить переопределение submit callback’ов через:

$form['#submit'] = array('webform_client_form_pages', 'webform_client_form_submit');

Перейдём к функции webform_client_form_pages и найдём в ней вызов _webform_client_form_rule_check, возвращающий TRUE или FALSE, и управляющий отображением или пропуском компонента. А это как раз то, что нам нужно.
В _webform_client_form_rule_check функцию передается аргумент $component, который представляет собой конфигурации какого-либо компонента веб-формы. Получим из неё сведения об алгоритме и все причитающиеся условия:

$conditional_values = isset($component['extra']['conditional_values']) ? $component['extra']['conditional_values'] : NULL;
$conditional_component = isset($component['extra']['conditional_component']) && isset($node->webform['components'][$component['extra']['conditional_component']]) ? $node->webform['components'][$component['extra']['conditional_component']] : NULL;
$conditional_cid = $conditional_component['cid'];

Также установим в самом начале флаг, который, в конечном счете, определит (в зависимости от выбранного алгоритма) показывать компонент или нет:

$one_rule_passed = FALSE;

Следующей задачей является расширение непосредственного кода проверки, который начинается сразу за:

// Check the individual component rules.
$show_component = $show_parent; 

Поступим следующим образом: выполним поочерёдно каждое условие, результат проверки зафиксируем в массиве, который потом проанализируем согласно алгоритму. Если используется алгоритм «OR», то наличие хотя бы одного положительного результата делает всё выражение положительным, если же алгоритм «AND», то наоборот, один отрицательный результат делает всё выражение отрицательным.
Для этого заменим код проверки на следующий:

// Check the individual component rules.
$show_component = $show_parent;
if ($show_component && ($page_num == 0 || $component['page_num'] == $page_num) && $conditional_component && strlen(trim($conditional_values))) {
  $input_values = array();
  if (isset($form_state)) {
    $input_value = isset($form_state['values']['submitted'][$conditional_cid]) ? $form_state['values']['submitted'][$conditional_cid] : NULL;
    $input_values = is_array($input_value) ? $input_value : array($input_value);
  }
  elseif (isset($submission)) {
    $input_values = isset($submission->data[$conditional_cid]['value']) ? $submission->data[$conditional_cid]['value'] : array();
  }

  $test_values = array_map('trim', explode("n", $conditional_values));
  if (empty($input_values) && !empty($test_values)) {
    $show_component = FALSE;
  }
  else {
    foreach ($input_values as $input_value) {
      if ($show_component = in_array($input_value, $test_values)) {
        break;
      }
    }
  }

  if ($component['extra']['conditional_operator'] == '!=') {
    $show_component = !$show_component;
  }
}

И определим сразу же вычисление результата алгоритма:

// Make the final decision
if ($component['extra']['algorithm'] == 'or') {
  $result = $one_rule_passed;
} elseif ($component['extra']['algorithm'] == 'and') {
  $result = array_search(FALSE, $show_component) !== FALSE;
} else {
  $result = TRUE;
}

// Allow other modules to alter conditional check result
$context = array('node' => $node, 'component' => $component, 'show_component' => $show_component, 'one_rule_passed' => $one_rule_passed, );
drupal_alter('webform_conditional_rules_result', $result, $context);

// Private component?
if ($component['extra']['private']) {
  $result = webform_results_access($node);
}
           
return $result; 

Вызов «drupal_alter» позволит другим модулям переопределять результат по мере необходимости, о чём необходимо всегда помнить, разрабатывая модули для Drupal.

Теперь у нас есть всё, что необходимо для реализации опросника, который представлен выше в статье.
Для этого, вернувшись на страницу со списком компонентов веб-формы в административном интерфейсе, у компонента page_2 зададим правила:

  • Question 1 is one of [yes]
  • Question 2 is one of [yes]
  • Укажем алгоритм «AND».

image

На странице 3 правила отображение (согласно схеме) есть только у вопроса 4. Настроим их:

  1. Question 1 is one of [yes]
  2. Question 2 is not one of [no]
  3. Укажем алгоритм «OR».

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

  1. Question 3 is one of [yes].

После всех описанных в статье действий можно смело перейти на страницу формы с клиентской стороны и убедиться, что всё работает так, как требуется.

Ссылки:

Автор: k0teg


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js