Nodequeues, subqueues, and conditional display

My company reviews and rates media such as mobile apps, video games, movies, etc in detail for parents and teachers to find appropriate content for their kids and students. We’re currently in the process of upgrading our site from Drupal 6 to 7, which includes a redesign and some new functionality. I was asked to upgrade the functionality for a small block of content that appears on each review node’s page titled Top Advice and Articles; this block contains links to other pieces of editorial content on our site.

The requirements were:

  • Allow up to 3 items to display in the block
  • Items that can be displayed must be of a certain node content type
  • Each item will display its image as a thumbnail and the item title, both linked to the item’s node page
  • Ideally, this would be done using nodequeues, which the editors are already familiar with and like the functionality thereof
  • Ideally, there would be a separate set of items for each of the types of content we review, i.e. they can tailor these based on whether it’s displaying in a movie review versus a website review, and so on

For the rest of this tutorial, I’m assuming you’re familiar with the Nodequeue module… Ok, so they wanted to use nodequeues, and based on their requirements it would need to be a separate nodequeue for each type of item we review. Nodequeues, as you may already know, are actually a two level hierarchy: the queue itself, which stores config information and one or more subqueues that use the parent queue’s config to store the actual nodes in the queue. Out of the box, Nodequeue only gives you the UI to create simple queues, i.e. queues that have one single subqueue. This gets the job done for most uses but in this case I didn’t want to clutter the UI with one simple queue per content type, so I decided to see if subqueues would do what I needed. Turns out, yes!

Creating a Subqueue

As I mentioned above, the UI doesn’t give you a way to do this visually, so it’s all done in your custom module, in this case a module named csm_review.

First, I defined the new type of queue I was creating.

/**
 * Implements hook_nodequeue_info().
 */
function csm_review_nodequeue_info() {
  return array('csm_review_block_top_advice' => array(
      'title' => t('Review - Top Advice and Articles'),
      'description' => t('Each entertainment product content type has its own unique subqueue. You can place nodes into any of these subqueues and they will show up on review pages for that content type.'),
    ));
}

Then, to make the code repeatable in our nightly upgrade builds, I wrote a custom function that I could run via drush to create the nodequeue and its children subqueues. The first query adds the parent queue and its config, and returns the new queue’s ID (autoincrement). The column definition is:

name
Machine name of the queue
title
Queue’s display name
size
Max items each subqueue can contain
owner
The name of the custom module implementing this queue. This becomes relevant later.
subqueue title
Not sure what this is used for but it’s a required column and all simple queues have an empty string stored

To meet the requirement of only allowing nodes of certain content types to be queued, I added some rows to the queue type filtering table. This is the equivalent of checking off all those content type boxes in the UI.

qid
The queue ID that I’m configuring, from the previous query
type
Machine name of the content type that is allowed to queued

Then I inserted one row into the subqueues table for each type of content. We have one central content type called csm_review that has the review metadata, which references one of seven different media content types that contains the actual products, so seven subqueues were created.

qid
The parent queue’s ID, from the previous query
reference
An arbitrary string that your new queue will use to differentiate between subqueues. In simple queues, the single subqueue’s reference is the queue ID. Because each subqueue was to be used for each type of product reviewed, I set the reference to the machine name of the content type of the reviewed product.

Here’s the final function, that I called via drush: drush php-eval "csm_review_build_top_advice_nodequeues()"

/**
 * Creates the top advice block queue and subqueues.
 */
function csm_review_build_top_advice_nodequeues() {

  // Create the primary nodequeue
  $qid = db_insert('nodequeue_queue')
    ->fields(array(
        'name'  => 'block_reviews_top_advice',
        'title' => 'Reviews Top Advice Block',
  	'size'  => 3,
        'owner' => 'csm_review',
        'subqueue_title' => '',
      ))
    ->execute();

  // Limit the types that can be selected
  $types = array('csm_blog', 'csm_ed_blog', 'csm_interactive_guide', 'csm_top_picks', 'csm_video',);
  foreach ($types as $type) {
    db_insert('nodequeue_types')
      ->fields(array(
          'qid'  => $qid,
          'type' => $type,
        ))
      ->execute();
  }

  // Create the ent prod subqueues
  $entertainment_products = array(
    'csm_app'     => 'App',
    'csm_book'    => 'Book',
    'csm_game'    => 'Game',
    'csm_movie'   => 'Movie',
    'csm_music'   => 'Music',
    'csm_tv'      => 'TV',
    'csm_website' => 'Website',
  );
  foreach ($entertainment_products as $type => $name) {
    db_insert('nodequeue_subqueue')
      ->fields(array(
          'qid'       => $qid,
          'reference' => $type,
          'title'     => "Top Advice and Articles Block: $name Reviews",
	))
      ->execute();
  }
}

After running my function, this is what my new queue and subqueues looked like:
Queue Parent
Subqueues

That’s great, but there’s one last step to get the subqueues visible in the Nodequeue tab that shows for each node if you have permissions to add nodes to queues. Because Nodequeue only knows how to manage simple queues, we have to tell it how to retrieve the subqueues that belong to our new custom queue. It took a while to find out how to do this, because this is technically not a hook, although it acts like one. Nodequeue looks for a function named $queue->owner . '_nodequeue_subqueues', so for consistency with Drupal’s hook system, that’s why I set the owner column of my custom queue to the module I coded this in. This function expects a returned string or array of the references of all subqueues belonging to this custom queue. Remember that reference column we set earlier? This is where it starts coming into play, and even more later.

/**
 * Implements hook_nodequeue_subqueues().
 */
function csm_review_nodequeue_subqueues(&$queue, $node) {
  // We're only at this point if the node is of a type that can belong to the top advice queue
  // so just return the references for all the subqueues in the top advice queue
  return db_query("SELECT reference FROM {nodequeue_subqueue} WHERE qid = :qid", array(':qid' => $queue->qid))->fetchCol();
}

And here’s what you’ll see when you click on the Nodequeue tab for a node
Queue selection for node.

Conditional Display of Subqueues

Well, great, we’re done, right? Nope. We still need to not only display our queues in the node page but we also have to only show the subqueue that matches the media type of the review (e.g. csm_tv for TV show reviews). Views to the rescue! We happen to be using Panels so I created this View as a content pane, but you can create it as a block. The two configurations necessary for this to work are Relationships and Contextual Filters.
view_config

Add a Relationship of type Nodequeue: queue to show items that are in a queue
relationship_selector

Select your newly created queue to show only items that are in this queue
relationships

Then, we want to tell the View which of the subqueues to use. That will change based on the context the queue is displayed in so we’ll be passing that value through as a Contextual Filter. Add a new Nodequeue: subqueue reference filter
filter_selector

From here on out, I’ll assume you know how to embed a View in a way that allows you to pass in contextual filters, as it’s different based on whatever layout engine you use (Panels vs Display Suite, etc). In my case, I had the context of the content type of the product node that was referenced by the review node, the panel pane in which my View content pane was embedded. This would also work for a block in Panels. YMMV otherwise. But that’s pretty much it!