Creative Solutions for Innovation 150 Challenges

An image of an echidna at a blackboard, with "Ask Echidna" written on it.

[Editor's note: As we approach Canada Day and our celebration of 150 years of Confederation, we thought it'd be a perfect time to talk about one of our recent projects -- the Innovation 150 website launch.

And in the spirit of innovation, what better time than now to roll out a new feature on our blog post that allows us to incorporate snippets of code into the content. With this new feature, we're hoping to expand our blog coverage to include far more technical discussions, with practical examples that you can use with your own development efforts. -- Jay]

We faced a number of technical challenges during the building of the bilingual Innovation 150 website that required some creativity and clever coding to solve. I'd like to discuss a couple in this piece and provide you with some of the code and in-line documentation that we used to solve those challenges.

First off, we needed to address the challenge that was posed by the desire to save a node as a draft, even if it's in an invalid state. This was particularly important in this project as there are often times when the content providers will want to alternate between translations before they've finished a version of the form. As a result, we had to build in functionality that would allow for the saving of drafts in an invalid state, with missing required fields.

The following code snippet outlines our solution.

ISSUE: Saving Invalid Nodes as a Draft
<?php

/**
 * Processes relevant node forms after they are built.
 *
 * Hides text format section. Unrequires applicable fields depending on submission type.
 */
function innovation_public_forms_node_form_after_build($form, &$form_state) {
  $form['body'][$form['body']['#language']][0]['format']['#access'] = FALSE;
  if (isset($form['field_relation_to_innovation150'])) {
    $form['field_relation_to_innovation150'][$form['field_relation_to_innovation150']['#language']][0]['format']['#access'] = FALSE;
  }

  // Skip required fields if saving draft or switching language.
  if (isset($form_state['triggering_element'])) {
    // Clear the preview so it doesn't appear for rebuilt forms.
    unset($form['#prefix']);

    $unrequired_submits = array();
    if (isset($form['actions']['submit_draft']['#value'])) {
      $unrequired_submits[] = $form['actions']['submit_draft']['#value'];
    }
    if (isset($form['actions']['submit_translate'])) {
      $unrequired_submits[] = $form['actions']['submit_translate']['#value'];
    }
    if (isset($form['actions']['submit_translate_existing'])) {
      $unrequired_submits[] = $form['actions']['submit_translate_existing']['#value'];
    }
    if ($form_state['process_input']
      && in_array($form_state['triggering_element']['#value'], $unrequired_submits)) {

      _innovation_public_forms_unrequire($form);
    }
  }
  return $form;
}

/**
 * Make a field not required before submission.
 */
function _innovation_public_forms_unrequire(&$elements) {
  foreach (element_children($elements) as $key) {
    if (isset($elements[$key]) && $elements[$key]) {
      _innovation_public_forms_unrequire($elements[$key]);
    }
  }
  if (!empty($elements['#required'])) {
    $elements['#required'] = FALSE;
  }
}

Related to that issue of validation, we also created functionality that validates versions of a form in its other languages when it's being submitted. The reason behind this functionality is that we wanted to avoid the scenario when a user could prematurely submit a form, without completing all versions of it. The potential existed that a user could create an invalid draft of a form in one language, then fully fill out the same form in another language

As you can see in the inline documentation of the snippet, figuring out how to solve this challenge was a little more involved.

ISSUE: Validating Multiple Versions of a Form
<?php

/**
 * Load the node form for the given language.
 *
 * @param object $node
 *   The node to load the form for.
 * @param string $langcode
 *   The language code for the language the form should be generated for.
 *
 * @return array
 *   Keyed array containing the node form and form state, formatted like:
 *    array(
 *      'form' => $form,
 *      'form_state' => $form_state,
 *    )
 */
function innovation_public_forms_load_lang_node_form($node, $langcode) {
  // Temporarily switch language.
  $form_translation_handler = entity_translation_current_form_get_handler();
  $original_langcode = $form_translation_handler->getFormLanguage();
  $form_translation_handler->setFormLanguage($langcode);

  // Prepare the form state and form.
  $form_state = form_state_defaults();
  $form_state['values'] = array();
  $form_state['build_info']['args'] = array($node);
  $form_state['programmed'] = TRUE;
  $form = drupal_retrieve_form($node->type . '_node_form', $form_state);
  $form_state['must_validate'] = TRUE;
  $form_state['input'] = array();
  
  drupal_prepare_form($node->type . '_node_form', $form, $form_state);
  form_builder($node->type . '_node_form', $form, $form_state);

  // Add 'submit' as the triggering element if it's available.
  // Not sure if this is strictly required but prevents a warning in
  // file.module if file fields are being validated.
  if (isset($form['actions']['submit'])) {
    $form_state['triggering_element'] = $form['actions']['submit'];
  }

  // Limit validation errors to only fields actually used by the node. We don't
  // care if, e.g., the agree field on the other language field is checked since
  // it isn't actually saved.
  $form_state['triggering_element']['#limit_validation_errors'] = array();
  foreach (array_keys(field_info_instances('node', $node->type)) as $key) {
    $form_state['triggering_element']['#limit_validation_errors'][] = array($key);
  }
  
  // Switch the language back.
  $form_translation_handler->setFormLanguage($original_langcode);

  // Return the results.
  return array(
    'form' => $form,
    'form_state' => $form_state,
  );
}

/**
 * Load the node form for the given language.
 *
 * @param object $node
 *   The node to load the form for.
 * @param string $langcode
 *   The language code for the language the form should be generated for.
 *
 * @return array
 *   An array of field validation errors formatted like
 *    array(
 *      'field_key' => 'text of error',
 *    )
 */
function innovation_public_forms_validate_lang_node_form($node, $langcode) {
  extract(innovation_public_forms_load_lang_node_form($node, $langcode));

  // Preserve any current errors.
  $original_errors = form_get_errors();
  $original_errors = $original_errors ?: array();
  form_clear_error();

  // Validate the other node form and get the errors. This seems to switch
  // the form language so we need to switch it back afterwards.
  $orig_form_language = entity_translation_current_form_get_handler()->getFormLanguage();
  drupal_validate_form($node->type . '_node_form', $form, $form_state);
  entity_translation_current_form_get_handler()->setFormLanguage($orig_form_language);
  $errors = form_get_errors();
  form_clear_error();
  
  // Return errors to the state they were in when this function was called.
  foreach ($original_errors as $key => $error) {
    form_set_error($key, $error);
  }

  return $errors;
}

This was a great project to work on and I encourage you to visit the site to learn more about 150 years of innovation.

Images of the Innovation 150 work screens

 

Questions Answered

How do you validate multiple versions of a form?

How do you save a node as a draft when it's in an invalid state?

SUBSCRIBE TO OUR E-NEWSLETTER

CONNECT WITH US

Twitter Facebook Linkedin RSS