Overriding Views user name filter

Recently I had to create a view that lists all nodes of a certain node type and allow website visitors to use an autocomplete filter to filter nodes based on the user name of the author.

Pretty straightforward task, I created the view, added a filter for the required node type and added the node author as a relationship and exposed the default "User: Name" filter of Views, which provides the handy autocomplete feature.

However there was an issue with this solution: the client wanted to only show a user name in the autocomplete filter if the user is the author of at least one node of the content type the view was filtered for.

I've spent some time trying to find a solution, but no contrib module seemed to solve this problem. However, writing a custom module for this problem seemed to be an interesting task, so I gave up searching for ready-made solutions and created a custom module.

You can download the entire code from my GitHub account: here.

1. Identifying the problem

Finding out where the user autocomplete filter feature is located

The first task was to find out which Views handler is used for the autocomplete user name filter. There are two ways to do this:

  • You can go to the Views module directory and start scanning through the files and take a deeper look at the ones that has a promising filename. Sooner or later you'll probably end up finding out that modules/user/views_handler_filter_user_name.inc is the one you're looking for.
  • The other way is to use hook_views_data_alter to print out all Views data and find the handler name there.

This is how I implemented the second option:

/**
 * Implements hook_views_data_alter().
 */
function viewsfilterusernodetype_views_data_alter(&$data) {
  dpm($data); 
}

Next time you flush the cache devel print out a huge array, your job is to find the required class name ( = needle ) in this haystack.

Search Krumo module comes to the rescue. This module allows you to search in the info printed out with dpm.

1. Take a look at the filter that we're going to use:

User name filter

2. Copy the description of the filter: "The user ID". Now go to the array devel printed out and paste it into the search field and click submit:

Using search krumo

Instead of scanning through that large array, there are only two results we need to look at and they are already highlighted.

Now it's really easy to find the info we need:

Search Krumo result

As you can see the name of the handler class we need is views_handler_filter_user_name. Hopefully you are using an IDE that helps you quickly locate the file that defines this class (e.g. NetBeans).

How the autocomplete feature is actually implemented?

It's the value_form method of this class that is called when a visitor actually starts to type a user name into the search field. (It's easy to find this out based on the description of the form element.)

    $form['value'] = array(
      '#type' => 'textfield',
      '#title' => t('Usernames'),
      '#description' => t('Enter a comma separated list of user names.'),
      '#default_value' => $default_value,
      '#autocomplete_path' => 'admin/views/ajax/autocomplete/user',
    );

Now that we have the autocomplete path, we just have to take a look at the hook_menu implementation in views.module to find out what function that path is mapped to.

It turns out that it's the views_ajax_autocomplete_user function in includes/ajax.inc. This is the actual database query that is run when you start typing in the user name field:

    $result = db_select('users', 'u')
      ->fields('u', array('uid', 'name'))
      ->condition('u.name', db_like($last_string) . '%', 'LIKE')
      ->range(0, 10)
      ->execute()
      ->fetchAllKeyed();

This is a quite simple query, I couldn't find a reasonable way to hook into this and modify it as I needed.

The plan

Instead of trying to hook into the query above, I decided to override the views_handler_filter_user_name class and:

  • allow the user to select certain node types on the Views config UI (see picture below)
  • if no node types were selected I do nothing, however if the user selected at least one node type I'll change the autocomplete path to run my own database query

Select node type

Based on my selection above, the user autocomplete should only return user names if they have authored at least one News node.

2. Writing the module

Overriding the Views core class

Now that we have found out which Views class has almost the same functionality we need, we can override that class:

class viewsfilterusernodetype_handler_filter extends views_handler_filter_user_name {
  // put methods here
}

We need to override the filter configuration form to let the user select on or more node types. The options_form() method is used to show the configuration form, this is how to override it:

  function options_form(&$form, &$form_state) {
    parent::options_form($form, $form_state);
    $form['use_node_type'] = array(
      '#type' => 'checkbox',
      '#title' => t('Filter by node type'),
      '#description' => t('Filter only users that are authors of nodes of selected types.'),
      '#default_value' => $this->options['use_node_type'],
      '#weight' => 49,
    );
    $form['selected_node_types'] = array(
      '#type' => 'checkboxes',
      '#title' => t('Select node types'),
      '#options' => _viewsfilterusernodetype_node_types(),
      '#default_value' => $this->options['selected_node_types'],
      '#weight' => 50,
      // Only show the checkboxes if the "Filter by node type" option is selected.
      '#dependency' => array('edit-options-use-node-type' => array(TRUE)),
    );
  }

First the parent method is called to display all the usual options and then we append our custom checkbox and node type list to the end of the form.

In order to save the values of the new form elements and to give them default values, the option_definition() method has to be overridden too:

  function option_definition() {
    $options = parent::option_definition();
    // This is required to store our custom values and set their defaults.
    $options['use_node_type'] = array('default' => FALSE);
    $options['selected_node_types'] = array('default' => array());
    return $options;
  }

Again the parent method is called, then we add our custom elements and give them a default value.

The final step is to change the autocomplete url of the filter. We have already found out, that it's the value_form() method we have to override:

  function value_form(&$form, &$form_state) {
    parent::value_form($form, $form_state);

    // Override the autocomplete path if we want to filter by node type.
    if ($this->options['use_node_type']
        && array_filter($this->options['selected_node_types'])) {
      $node_types = implode(variable_get('viewsfilterusernodetype_separator', '-'),
          array_keys(array_filter($this->options['selected_node_types'])));
      $form['value']['#autocomplete_path']
          = 'viewsfilterusernodetype/autocomplete/user/' . $node_types;
    }
  }

If the user has selected to filter by node type and has selected at least one node type, we'll change the autocomplete url.
E.g. if two content types were selected: news and page (these are machine names) then the autocomplete url will be this: viewsfilterusernodetype/autocomplete/user/news-page

Map the new autocomplete url to a function

Now we need to implement hook_menu so that the new autocomplete url will make our custom database call:

/**
 * Implements hook_menu().
 */
function viewsfilterusernodetype_menu() {
  $items = array();
  $items['viewsfilterusernodetype/autocomplete/user'] = array(
    'page callback' => 'viewsfilterusernodetype_autocomplete_user',
    'theme callback' => 'ajax_base_page_theme',
    'access callback' => 'user_access',
    'access arguments' => array('access user profiles'),
    'type' => MENU_CALLBACK,
    'file' => 'includes/viewsfilterusernodetype.inc',
  );
  return $items;
}

The url defined here is viewsfilterusernodetype/autocomplete/user, so the node types (in the example above it's the news-page part of the url) will be passed as an argument.

Implementing the page callback

And finally our callback function that queries the database:

function viewsfilterusernodetype_autocomplete_user($node_types, $string = '') {
  // ...
// This is where we find out what node types we'll need to filter for.
  $types = explode(variable_get('viewsfilterusernodetype_separator', '-'), $node_types);
//...
// The actual database query. $query = db_select('users', 'u'); $query->join('node', 'n', 'n.uid = u.uid'); $query->fields('u', array('uid', 'name')) ->condition('n.type', $types, 'IN') ->condition('n.status', 1) ->condition('u.name', db_like($last_string) . '%', 'LIKE') ->orderBy('u.name') ->range(0, 10); $query->distinct(); $result= $query->execute()->fetchAllKeyed(); // ... }

This is just a rewrite of views_ajax_autocomplete_user, I've only included the parts that were changed. We only modified the database query a bit and took care of the extra argument that contains the node types.

Last but not least, tell Views to use our class

The only thing left is to ask Views to use our custom viewsfilterusernodetype_handler_filter instead of the default views_handler_filter_user_name class. This is done by implementing hook_views_data_alter:

/**
 * Implements hook_views_data_alter().
 */
function viewsfilterusernodetype_views_data_alter(&$data) {
  $data['users']['uid']['filter']['handler'] = 'viewsfilterusernodetype_handler_filter';
}

How do I know that it's $data['users']['uid']['filter']['handler'] that should be changed? Remember that earlier we've used hook_views_data_alter to print out the entire $data array and then Search Krumo to find the entry we needed? Here is the picture again:

Search Krumo result

You can simply click on "Get path" and Search Krumo will display the path to that variable: $var['users']['uid']['filter']['handler'], you just need to replace $var with $data.

Basically this is it. You can download the entire code from my GitHub account: here.

Services

Drupal theming and sitebuild

  • PSD to Drupal theme
  • Installing and configuring modules (Views, CCK, Rules, etc.)
  • Theming modules' output

Drupal module development

  • Almost every site needs smaller modules to modify HTML output or override certain default functionalities
  • Developing custom modules

From the blog