Jonathan Hedstrom's blog

  • Roll your own context conditions

    Oct 07, 2009

    The context module provides great flexibility in terms of the available ways to set a context, but it's also remarkably straightforward to define custom conditions.

    In order to set arbitrary contexts, hook_context_conditions() is implemented to define values and such. This example will use the value of a CCK field, called field_foo:

    /**
     * Implementation of hook_context_conditions().
     */
    function mymodule_context_conditions() {
      $items = array();
      // The key used here will be used below when setting the context.
      $items['mymodule_foo'] = array(
        '#title' => t('Field Foo Value'),
        '#description' => t('Set this when field_foo has a certain value.'),
        '#options' => array(
          // Note, these are hardcoded here for simplicity, but could
          // easily use something like CCK's content_allowed_values() api
          // function for more dynamic population.
          'value1' => t('Label 1'),
          'value2' => t('Label 2'),
        ),
        '#type' => 'checkboxes',
      );
      return $items;
    }

    The only other step is to actually fire the code that sets the context somewhere. In the example of setting a context based on the value of a CCK field, this can be done in hook_nodeapi(). But something like hook_init() can also be used.

    function mymodule_nodeapi(&$node, $op, $teaser, $page) {
      if (isset($node->field_foo[0]['value']) &&  $op == 'view' && $page && menu_get_object() === $node) {
        // Use the same key here as used to define the context condition
        // above.  Note, this logic would need some re-working if
        // field_foo allowed multiple values.
        context_set_by_condition('mymodule_foo', check_plain($node->field_foo[0]['value']));
      }
    }

    That's it. Contexts can now be added through the UI or through code that react to the value of field_foo.

  • Simple usability enhancements for a site workflow

    Jul 27, 2009

    The workflow module allows a piece of content to be transitioned through arbitrary states. It is most commonly used (as I've seen it) as a replacement to the core node workflow of published/unpublished. Recently I was working on a pair of sites that required a very simple workflow of draft/published (if you're wondering why core couldn't be used in this case, it has to do with the very high level of permissions required to toggle the published/unpublished bit). The sites also made extensive use of node reference, views and context for positioning various parts of the node. Not having visual indicators within those views for the end user to determine which nodes were in a draft state, and which ones were published quickly became an obvious usability issue.

    The first step in the improvements applied was to add the workflow state field to every view in question.

    Views edit screen showing workflow current state field

    Workflow w/o enhancements

    However, as packaged with workflow, it doesn't provide a very nice output, and only nodes in the Draft state needed a visual indicator.

    Using the following preprocess function, the fields for undesired workflow states are removed, and a key CSS class is added (in this case, Published content didn't need to be highlighted in such a manner).

    /**
     * Preprocess function for template_preprocess_views_view_fields().
     *
     * - Adds workflow class for rows in the 'Draft' state, while
     *   removing results in 'Published state'.
     */
    function os_custom_preprocess_views_view_fields(&$vars) {
      foreach ($vars['fields'] as $name => &$field) {
        if ($name == 'sid' && $field->handler->table == 'workflow_node') {
          // Get complete workflow state.
          $state = workflow_get_state($field->raw);
          if ($state['state'] != t('Draft')) {
            // Hide workflow if not in draft mode.
            unset($vars['fields'][$name]);
          }
          else {
            // Add workflow-{state} class.
            $field->class .= ' workflow-' . strtolower($state['state']);
            // Replace default 'Worfklow name: workflow state' formatting with
            // only the workflow state.
            $field->content = $state['state'];
          }
        }
      }
    }

    Then, by borrowing some CSS that has been hanging around in the Zen theme for the core workflow, the content in draft state is easily brought to the attention of those working on the site:

    Workflow with CSS enhancements

    The same is then done for nodes by a different preprocess function,

    /**
     * Preprocess function for template_preprocess_node().
     */
    function os_custom_preprocess_node(&$vars) {
      $state = workflow_get_state($vars['node']->_workflow);
      if ($state['state'] == t('Draft')) {
        $workflow_class =  'node-workflow-' . strtolower($state['state']);
        // Prepend content with workflow state information.
        $vars['content'] = '<div class="' . $workflow_class . '">' . $state['state'] . '</div>' . "\n\n" . $vars['content'];
      }
    }

    and and using the aforementioned, and slightly-modified, Zen CSS:

    .node-workflow-draft,
    .workflow-draft,
    .node-unpublished div.unpublished, /* The word "Unpublished" displayed beneath the content. */
    .comment-unpublished div.unpublished {
        height: 0;
        overflow: visible;
        color: #f77;
        font-size: 75px;
        line-height: 1;
        font-family: Impact, "Arial Narrow", Helvetica, sans-serif;
        font-weight: bold;
        text-transform: uppercase;
        text-align: center;
        word-wrap: break-word; /* A very nice CSS3 property */
    }
     
    .workflow-draft {
      font-size: 42px; /* Smaller font for workflow display within views. */
    }

    the result is again obvious to people staging content:

    Individual node page with workflow enhancements

  • Test-driven Drupal development, take 2

    May 18, 2009

    A while ago I wrote about a method we'd been using internally here at OpenSourcery for testing existing Drupal configurations. There are currently a few issues in the works that would get this functionality into core-SimpleTest:

    In the meantime, however, since this functionality has immediate benefits to making more robust Drupal sites, I've decided to release the module we're using internally until this is part of core-SimpleTest, and gets back-ported to Drupal 6.x. I've placed the module on GitHub rather than on Drupal.org so as not to clog things up with placeholder modules. I've also attached a tar-ball to this post for those without Git.

    Update: This module has also been released on Drupal.org in order to allow people to track updates.

    To use the module, it must be enabled, and then included at the top of any test file that extend the class:

    // Include SimpleTest Clone
    module_load_include('test', 'simpletest_clone');

    tests then extend the SimpleTestCloneTestCase class instead of DrupalWebTestCase:
    class CustomTestCase extends SimpleTestCloneTestCase {
    ...
    }

    Occasionally, on a given site or set of tests, it may not be desirable to clone every table because this can make tests run for a very long time. Because of this, there is a method in place for excluding tables from being cloned:

      /**
       * When testing data-intensive sites, if the tests will still run correctly,
       * tables can be excluded here. The structure will still be cloned, but the
       * data will not be copied over.
       */
      function __construct($testId = NULL) {
        parent::__construct($testId);
        $this->excludeTables = array(
          // List of tables to exclude goes here.
          'example_table_foo',
          'example_table_bar',
        );
      }