Custom paging for views

Adding custom limited lists to views

Introduction

Angie/webchick here, and this is my very first article on the Lullabot site :)

One of the projects I'm working on at the moment is The World, an audio news magazine co-produced by BBC World Service, PRI and WGBH Boston. They put out a new show every week day, and the content is divided into separate sections (represented by taxonomy) such as Global Hit, Geo Quiz, and so on.

One of the requirements was to show only the content for a given day when clicking on certain sections of the site, and allow next/previous links to take you backward and forward in the list. This is in contrast to the default Drupal pager, which merely goes by X nodes per page. Further, there should not be any "dead" links; the next/previous links should always point to a date with valid content.

So this required a few things:

  1. A list of nodes, filtered by taxonomy term.
  2. A method for filtering this list by a given date.
  3. A way to show the most recent show's content if no date is specified.
  4. Logic to figure out what the next/previous dates are (we can't do a simple "current day +/- 1").
  5. Code to display the pager.
  6. Some way to tie it all together.

Here's the solution I came up with -- source code is attached at the end.

The basic stuff

The operative word in the above narrative is list. Anytime you need "lists of stuff," you should immediately think of Views. Download and enable the module, then click administer >> views to get started.

The preliminary setup was pretty straight-forward:

  • Name: taxonomy_by_date
  • Access: anonymous user and authenticated user
  • Description: View a list of taxonomy filtered by date
  • Check Provide page view (expand the Page fieldset)
  • URL: taxonomy_by_date
  • View Type: Teaser List
  • Uncheck Use pager

Fun with Arguments

The first requirement, setting up a taxonomy-filtered node list, is super easy. Expand the Arguments fieldset, select Taxonomy: Term ID from the list, and tell it to Display all values if a term isn't specified. Now taxonomy_by_date/1 will show you all of the nodes tagged with term 1, taxonomy_by_date/2 will show you all of the nodes tagged with term 2, and just taxonomy_by_date by itself will show everything.

Next, we need a way to restrict those lists further by date. So let's add a second argument, this time Node: Posted Full Date and again Display all values if none is specified. Now, you can go to a URL like taxonomy_by_date/2/20060622 to show only the content tagged with taxonomy term 2 that was created on June 22, 2006.

However, that's not quite what we want; we want to show the most recently published content if a date isn't specified. So how do we attack the problem of needing to "inject" an argument into a view where one is lacking?

The answer lies in the Argument handling code section. This is a really handy Views feature which allows you to make changes to the view on-the-fly, as it's being built! I wrote up a handbook page which explains this functionality in a bit more detail. For now though, let's check out some code:

  
// Default to term 1 if none was set
if (!$args[0]) {
  $args[0] = 1;
}

// Default to most recent date if none was set
if (!$args[1]) {
  $timezone = _views_get_timezone();
  $latest = db_result(db_query("
    SELECT DATE_FORMAT(FROM_UNIXTIME(n.created+$timezone), '%Y%m%%d') 
    FROM {node} n 
    INNER JOIN {term_node} tn ON n.nid = tn.nid WHERE tn.tid = %d 
    ORDER BY n.created DESC LIMIT 1", $args[0]));
  $args[1] = $latest;
}

return $args;
  

$args here is an array of the arguments that are passed into the current view. So in a URL like taxonomy_by_date/2/20060622, $args[0] would be 2, and $args[1] would be 20060622. This code checks to see if both $args[0] and $args[1] are set; if not, it gives them default values (1 for term and the most recent date, respectively).

We're using db_result() here because we're only interested in one value: the date if the newest content in that term formatted as YYYYMMDD.

Note this weird little bit: '%Y%m%%d' -- there are two %%'s here in order to escape %d because that has significance in db_query -- it indicates the value should be replaced with something numeric. Also, note that the code is not between <?php and ?> ... this is intentional; doing so throws an error.

So now, going to taxonomy_by_date/ will automatically show us all the most recent content in term 1. Sweet!

More Fun with the Date Pager

Now, the tricky part... how to get those next/previous date links in there?

I struggled with this for a couple days and eventually came up with placing code for the pager in the Footer text of the Page section, with PHP code as the input format. At this point, the view is built and is accessible via the global variable $GLOBALS['current_view']. Here's the code:

  

// Get current view object and its arguments
$view = $GLOBALS['current_view'];
$args = $view->args;
$term = $args[0];

// Retrieve array of unique dates
$timezone = _views_get_timezone();
$result = db_query("
  SELECT DISTINCT DATE_FORMAT(FROM_UNIXTIME(n.created+$timezone), '%Y%m%%d') 
  AS date
  FROM {node} n 
  INNER JOIN {term_node} tn ON n.nid = tn.nid 
  WHERE tn.tid = %d ORDER BY n.created DESC", $term);
while ($date = db_fetch_object($result)) {
  $date_list[] = $date->date;
}

// Find current and last positions
$current = array_search($args[1], $date_list);
$last = count($date_list) - 1;

// Find previous date
if ($current == 0) {
  $prev = NULL;
}
else {
  $prev = $date_list[$current-1];
}

// Find next date
if ($current == $last) {
  $next = NULL;
}
else {
  $next = $date_list[$current+1];
}

print theme('date_pager', $prev, $next, $term);

  

Essentially what we're doing is grabbing a list of each unique date that a node was created, and tossing that into an array. We figure out if we're on the first or the last date in the list, and pass the next/previous links into the date pager, respectively.

Finally, here's the theme_date_pager function (I just stuck this in a small custom module):

  
/**
 * Displays date pager at the bottom of the taxonomy_by_date view
 *
 * @param $prev
 *   A string containing the previous date, in the form of 
 *   YYYYMMDD, or NULL if no previous date
 * @param $next
 *   A string containing the next date, in the form of 
 *   YYYYMMDD, or NULL if no next date
 * @param $term
 *   The term that's being filtered
 * @return
 *  A string containing the HTML of the date pager
 *
 * @ingroup themeable
 */
function theme_date_pager($prev, $next, $term) {
  $output = '';
  $links = '';

  if ($prev) {
    $links .= l(t('< ') . format_date(strtotime($prev), 'custom', 'F j, Y'), 
      'taxonomy_by_date/'. $term .'/'. $prev, 
       array('class' => 'pager-previous', 'title' => t('Go to previous date')));
  }
  if ($next) {
    $links .= l(format_date(strtotime($next), 'custom', 'F j, Y') . t(' >'),
      'taxonomy_by_date/'. $term .'/'. $next,
      array('class' => 'pager-next', 'title' => t('Go to next date')));
  }

  if (!empty($links)) {
   $output .= '';
   $output .= ''. $links .'';
   $output .= '';
  }

  return $output;
}
  

This displays links like: < June 16, 2006 June 14, 2006 >

And there you have it! As promised, you can also download an export of the view.

What next?

After talking with Eaton a bit on IRC, we agreed that it seems like some type of "browser" view type could come in very handy for handling various requirements that come up. I've created a "browser" view type feature request at Drupal.org to discuss that. If anyone has any implementation ideas, feel free to jump in!

Get in touch with us

Tell us about your project or drop us a line. We'd love to hear from you!