fromMarch 2011
Column:

How to Create New Features in Views 3

Creating a Date Pager Plugin
0

Views is one of the most extensible subsystems in the Drupal ecosystem, and it has led the way in showing Drupal developers how to create systems that are powerful and extendible. Views 3 has no less than seven different plugin types and six different handler types. In the first installment of his regular column, “How to create new features in Views 3”, Views’ author Earl Miles walks us through the steps necessary to create a pager plugin.

Nearly everyone who has spent any time at all creating a website is familiar with the concept of a pager. When a given site has an amount of content that for reasons of design or logistics won’t fit on a single page, the pager comes along to the rescue. In Drupal, the traditional pager starts with a set number of items per page and then divides the total number of items by that to determine a total number of pages. At the bottom of the content, the pager displays simple navigation links: Next, Previous, a list of page numbers local to the current page, as well as first and last page links.

Views 2 added a new, specialized pager to the Drupal developer’s toolkit: The mini pager. The mini pager, meant to be a small pager that could fit into a sidebar block, reduced this to simply Next and Previous links, as well as an indicator to tell the user what the current page was. Unlike most things in Views, I made the mistake of hard-coding this mini pager in, meaning that unlike most of the rich, extensible items in Views, the mini pager is what it is, and while you could theme it, you couldn’t extend it or come up with a totally new pager.

In Views 3, this problem was rectified by pulling the pager system out of Views entirely. Early on in Views 2, we had to get away from Drupal’s core pager system anyway, as it proved to be too inflexible to meet Views’ increasingly complicated needs. By the time we started on Views 3, the only resemblance to core’s pager system that Views’ pager system had was the global variables and the theming. Externally it looked and acted just like core’s pager, but it wasn’t.
Because of this, we decided to just make a plugin out of it. First, we identified the features that a pager needs:

  • Pagers need a way to modify the query.
  • Pagers need the ability to run additional queries, before the query is made, to determine information about the query. For example, the core pager needs to count results to determine how many pages there will be, and if the currently requested page number is valid.
  • Pagers need to be able to render their navigation bar.
  • Pagers need to be able to provide arbitrary forms to the administrator for configuration.
  • Pagers have several core features that may or may not be used by custom pagers: The ability to externally set the page number, pager identifiers so multiple pagers can be used on the same page, and offsets so early results can be skipped.
  • Pagers need to be able to interface with the exposed form system so that they can allow users to control some of their features, such as the number of results to display per page.

As an example, we’re going to create a pager plugin that is somewhat useful, and completely non-traditional for Drupal. You have, however, very likely interacted with a pager of this nature before. Instead of relying on the number of results, we’re going to rely on the post date of the content, the same way that a monthly calendar would. To make it easy for examples, we won’t make this too configurable. Instead, we will assume that the pager will:

  • Accept the year and the month from query arguments, and provide all results for that month.
  • Provide a navigation line that includes next month, previous month, next year and previous year.

To do this nicely, in a way that you might publish as a standalone module, it would be good to allow the range to be configurable (month, week, year), sort this on fields other than node created, and a host of other options. But for an example, this is perfect, and is something that is just as likely to be used on a site where no other solution currently exists.

To create a pager plugin, you will first need to implement hook_views_api() and create a views.inc file. This won’t be discussed directly in this article, but see the accompanying example code for tips on how to do this.

Views learns about plugins by implementing hook_views_plugins(). For a pager plugin, all that Views needs to know is the title, the name of the object, a little bit of administrator description, and a flag letting Views know that it contains configuration.

function views_pager_month_views_plugins() {
  // This pager is really only valid for nodes, but there is no way to
  // limit pagers based upon base tables. Maybe this should be a feature
  // request against Views.
  return array(
    'pager' => array(
      'views_pager_month' => array(
        'title' => t('EXAMPLE: Post date month'),
        'help' => t('Page by the month the node was posted.'),
        'handler' => 'views_pager_month_plugin_pager',
        'help topic' => 'pager-month',
        'uses options' => TRUE,
      ),
    ),
  );
}

Then, we need to create a plugin class, which extends views_plugin_pager to actually implement our plugin. It needs to implement the option_definition() and options_form() methods to allow the plugin to be configured, and it needs to implement the query() and render() methods to allow the plugin to work. Finally, the render() method just passes along information to a theme template to allow the user to properly style the pager, rather than hardcode the HTML. When creating any plugin, it is a very good idea to look over the base class for the plugin type, which will always give the best documentation for what methods can and should be implemented to properly define that plugin. Many behavioral switches are controlled by class methods.

The option_definition() method is a critical component to any Views’ plugin or handler. This method defines all options so that Views knows how to store and export them, what the default values should be when new plugins are instantiated, and whether or not the values are translatable so that multi-lingual systems can work nicely with Views. In our example, our options are minimal, but we are allowing the user to specify the exact query arguments. We also default them to simply ‘month’ and ‘year’.

  function option_definition() {
    $options = parent::option_definition();

    // Provide options for what $_GET keys to fetch the month and year from.
    $options['month'] = array('default' => 'month');
    $options['year'] = array('default' => 'year');

    return $options;
  }

The options_form() method is a normal FAPI form which contains just the form widgets we need. In our case, there is no validation or special handling, so we don’t need to do anything on submit, because the form identifiers match what’s found in the option definition.

  /*
   * Provide the form for setting options.
   */
  function options_form(&$form, &$form_state) {
    $form['month'] = array(
      '#title' => t('Month attribute'),
      '#type' => 'textfield',
      '#description' => t('The query attribute to fetch month data from in the URL.'),
      '#default_value' => $this->options['month'],
      '#required' => TRUE,
    );

    $form['year'] = array(
      '#title' => t('Year attribute'),
      '#type' => 'textfield',
      '#description' => t('The query attribute to fetch year data from in the URL.'),
      '#default_value' => $this->options['year'],
      '#required' => TRUE,
    );
  }

The real meat of this plugin comes in the query() method, which gives the pager a chance to modify the query. The query needs to fetch the actual year and month from the URL, convert this into SQL and add a SQL WHERE clause to reduce the result set to include just records that match those dates.

  function query() {
    // First, make sure that there is actually a node available. If not
    // we will bail immediately.
    $this->table_alias = $this->view->query->ensure_table('node');
    if (empty($this->table_alias)) {
      return;
    }

    // By fetching our data from the exposed input, it is possible to
    // feed pager data through some method other than $_GET.
    $input = $this->view->get_exposed_input();

    // We store the month and year on $this to make it easy for the
    // renderer to see what ended up selected.

    // Currently we are using only year in the form of YYYY.
    if (empty($input[$this->options['year']])) {
      $this->year = format_date(time(), 'custom', 'Y', 0);
    }
    else {
      $this->year = $input[$this->options['year']];
    }

    // This pager supports the month as a digit from 1 to 12 only.
    if (empty($input[$this->options['month']])) {
      $this->month = format_date(time(), 'custom', 'n', 0);
    }
    else {
      $this->month = $input[$this->options['month']];
    }

    // Now, turn the month and year into two dates for the filter.
    $start = mktime(0, 0, 0, $this->month, 1, $this->year);
    // Figure out next month:
    $next_month = $this->month + 1;
    $next_year = $this->year;

    // Roll it over if we passed 12.
    if ($next_month == 13) {
      $next_month = 1;
      $next_year++;
    }

    // The last day of the month is the same as the 0th day of the month
    // after it.
    $end = mktime(23, 59, 59, $next_month, 0, $next_year);

    // @todo We are not adjusting this to the user timezone. That is left as
    // an exercise.

    // Finally, add the filter.
    $this->view->query->add_where(0, "$this->table_alias.created >= %d AND $this->table_alias.created <= %d", $start, $end);
  }

And now, we’re almost done. We just need to provide a render() method for the pager. To do this properly, we have to register a theme function and use it.

  function render($input) {
    // This adds all of our template suggestions based upon the view name and display id.
    $pager_theme = views_theme_functions('views_pager_month', $this->view, $this->display);
    // Then, using that, just pass through to the theme.
    return theme($pager_theme, $this, $input);
  }

The actual theme template and preprocess used is relatively straightforward, except that this is using dates, and dates are always a little bit difficult to use in PHP. Handling that isn’t the focus of this article, so we will leave the exploration of that, as well as a couple of supporting methods to switch off paging features we are not using, to those who want to explore the full piece of code.

The full example code for this article may be found at in the Views Plugin Examples module at http://drupal.org/project/views_plugin_examples.