Oct 02 2018
Oct 02

[embedded content]

Watch other videos on our YouTube channel. Click here to subscribe.

I was recently looking at all the default views that come with Drupal 8. For people who don’t know, the Views module is part of Drupal 8 core. In Drupal 7 and below it’s the most installed module so during Drupal 8’s development it was decided to move Views into core.

During my exploration into all of the default Views, I noticed that in the People (User) view there was a filter called “Combine fields filter”.

Want to learn about Views? Read Build a Blog in Drupal 8: Using Views or watch it as part of our FREE Drupal 8 Site Building course.

Now just a quick side note, if you’re new to Drupal and Views I’d highly recommend you spend time walking through all of the default views and see how they were configured. You can learn a lot just by seeing how things are set up.

The “Combine fields filter” does a pretty cool thing. It allows you to search across multiple fields or put another way, it allows you to combine fields and then filter by their combined value.

How to use “Combine Fields Filter”

Using this filter is relatively straightforward. Just click on Add in the Filter criteria field-set. Search for the filter by name or select Global from the Category drop-down.

When configuring the filter, you can select which fields you want to search from the “Choose fields to combine for filtering” drop-down.

If you want to see what the actual query looks like, turn on “Show the SQL query” from the Settings page (admin/structure/views/settings).

Then in the preview area, you should see the query that gets generated.

The above example is from the “People (User)” view.

Summary

If you want to add basic filtering across fields to your views, then this is the way to go. It’s useful for those custom admin pages which we create to help editors manage content. If you’re looking for something more advanced such as keyword searching, then look at using Search API.

Ivan Zugec

About Ivan Zugec

Ivan is the founder of Web Wash and spends most of his time consulting and writing about Drupal. He's been working with Drupal for 10 years and has successfully completed several large Drupal projects in Australia.

Feb 13 2018
Feb 13

On a recent project, I had to create a custom page which displays content by the logged in user, think of it as a “My articles” or “My blogs” page. I knew how to do it by writing code but I thought I’d try it with Views and see how far I could get without writing any custom code. Long story short, I was able to do it all by using just the Views module.

In this tutorial, you’ll learn how to create a page which will appear as a tab (local task) on the user profile page.

Getting Started

For once there are no extra modules to download and install. In Drupal 8, Views ships with core and will be automatically installed if you installed Drupal using the Standard installation profile.

If it’s not already installed, go to Extend and install Views and “Views UI”.

Create User Profile Page

The first bit of work we need to do is create an actual Views page.

This page will display a table of articles which is owned by the user, we’ll call it “My articles” and the URI to the page will be /user/%user/my-articles the %user argument will be the user ID which will be used by the contextual filter.

The owner of the content is defined by the user added to the “Authored by” field on the content edit page.

1. Go to Structure, Views and click on “Add view”.

2. Fill in the “Add view” form with the values defined in Table 1.0.

Table 1.0: Add view

Option Value View name My Articles Machine name my_articles Show Content (default) Of Type Article Create a page Checked Page title My Articles Path user/%user/my-articles Display format Table

3. If you go to /user/%user/my-articles replace %user with any number, it should return a table of articles.

Create Contextual Filter

The %user argument getting passed through the URI is not being used at this point. Let’s now add a contextual filter which will use the argument and only display articles which are authored by the user ID.

1. While on the Views edit page, click on Advanced then Add next to Contextual filters.

2. Search for “Authored by” in the Content category and click on Add.

3. Select the “Provide default value” radio button and choose “User Id from route context”

4. Further down the page:

  1. Check “Specify validation criteria”.
  2. Select “User ID” from Validator.
  3. Check “Validate user has access to the User”.
  4. Under “Access operation to check” select Edit.

5. Click on Apply to save the contextual filter, then click on Save to save the view.

Now if you go to the page, /user/%user/my-articles make sure you change %user with an actual user ID, you should only see their articles.

Page Access Control

Please make sure you’ve checked “Validate user has access to the User” and have chosen Edit under “Access operation to check”.

This means that only users who have edit access can access the page. This would be users accessing their own accounts or site administrators who can edit other user accounts.

If you do not then any user who knows the URI /user/%user/my-articles could go directly to it and see which articles are owned by the user.

Display Page as Tab (Local Task)

At this point, we’ve created the page and added a contextual filter to only display articles owned by the user.

Now let’s create a menu for the page so it’s accessible via a tab on the user profile page.

1. While on the views edit page, click on “No menu” link in the “Page settings” section.

2. In the “Page: Menu item entry” window, complete the following:

  1. Select “Menu tab” from Type.
  2. Add “My articles” to “Menu link title”.
  3. Select “<User account menu>” from Parent. This is important if you don’t do this then the tab won’t appear.
  4. Add 5 to weight.

3. Now if you go to the “My articles” page it should go from this:

To this:

If you can’t see the tabs but have configured it properly then try rebuilding the site cache. Go to Configuration, Performance and click on “Clear all caches”.

Summary

The ability to create these types of pages is where Views really shines. Often a client will ask for a content specific page such as a “My blog” or “My articles” page and Views makes it very easy to create these types of pages.

FAQs

Q: “My Articles” tab is not appearing.

First, make sure you’ve chosen “<User account menu>” from the Parent drop-down. Second, try rebuilding the site cache (go to Configuration, Performance and click on “Clear all caches”) and see if that fixes it.

Ivan Zugec

About Ivan Zugec

Ivan is the founder of Web Wash and spends most of his time consulting and writing about Drupal. He's been working with Drupal for 10 years and has successfully completed several large Drupal projects in Australia.

May 02 2017
May 02

Custom Video Export/Import Process With Views and Feeds

In the Media Research Center's set of three main Drupal sites, MRCTV serves as our video platform where all videos are created and stored as nodes, and then using Feeds, specific videos are imported into the other two sites (Newsbusters and CNS News) as video nodes. Then, on NB and CNS, we use the Video Embed Field module with a custom VEF provider for MRCTV to play the embedded videos.

There are only specific videos that need to be imported into the destination sites, so a way to map channels between the two sites is needed. All three sites have a Channels vocabulary, a mapping is created between the appropriate channels. This mapping has two parts:

  1. A feed of channels terms on NB and CNS.
  2. A custom admin form that links source channels on MRCTV with target channels on the destination site.

On the receiving site side, in addition to the standard feed content, the following custom elements are needed for the Feeds import:

  1. The nid of the video node on MRCTV. This is used to create the URL that is put into the VEF field.
  2. The taxonomy terms for the Channels vocabulary terms in the destination sites (NB and CNS).

Since these are outside of the standard feed components, they will need to be added custom to the feed items.

I documented my custom Feeds importer on drupal.stackexchange, so you can see the code there.

MRCTV is finally in the process of being updated to D8 from D6 (insert derision here), so both the mapping form and the feed needed to be re-created. The first part of the structure is the channel mapping form. The following file VideoExportForm.php is placed in /modules/custom/video_export/src/Form:

/**
 * @file
 * Contains \Drupal\video_export\Form\VideoExportForm.
 */

namespace Drupal\video_export\Form;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use GuzzleHttp\Exception\RequestException;

class VideoExportForm extends ConfigFormBase {
  
  /**
   * {@inheritdoc}.
   */
  public function getFormId() {
    return 'video_export_settings';
  }
    
  /**
   * {@inheritdoc}
   */
  public function buildform(array $form, FormStateInterface $form_state) {
    $form = array();
    $channels = array();
    
    // Get list of channels.
    $terms =\Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadTree('channels');
    foreach ($terms as $term) {
      $channels[$term->tid] = $term->name;
    }
    
    // Get config data from video_export.settings.yml.
    $config = \Drupal::config('video_export.settings');
    $mapping_config = \Drupal::config('video_export.mappings');
    $sites = $config->get('sites');
    
    foreach($sites as $site => $site_data) {
      // Get channels list.
      try {
        $response = \Drupal::httpClient()->get($site_data['channel_url'], array('headers' => array('Accept' => 'text/plain')));
        $data = $response->getBody();
        if (empty($data)) {
          return FALSE;
        }
      }
      catch (RequestException $e) {
        return FALSE;
      }
  
      $channel_data = new \SimpleXMLElement($data);
      foreach ($channel_data->channel as $channel) {
        $channel_name = $channel->name->__toString();
        $channel_tid = $channel->tid->__toString();
        $target_channels[$channel_tid] = $channel_name;
      }
      // Sort array alphabetically by element.
      asort($target_channels, SORT_STRING);
  
      $target_channel_options = array();
      $target_channel_options[0] = "No Channel";
      foreach ($target_channels as $target_tid => $target_name) {
        $target_channel_options[$target_tid] = $target_name;
      }
  
      //Get mappings from mappings conifg.
      $mappings = $mapping_config->get('sites');
      foreach ($mappings[$site]['mappings'] as $mrctv_channel => $target_channel) {
        $mapping_defaults[$mrctv_channel] = $target_channel;
      }
  
      $form[$site] = array(
        '#type' => 'details',
        '#title' => t($site . ' Channel Mappings'),
        '#description' => t('Map MRCTV channels to ' . $site . ' channels'),
        '#collapsible' => TRUE,
        '#collapsed' => TRUE,
        '#tree' => TRUE,
      );
  
      // Loop through all of the categories and create a fieldset for each one.
      foreach ($channels as $id => $title) {
        $form[$site]['channels'][$id] = array(
          '#type' => 'select',
          '#title' => $title,
          '#options' => $target_channel_options,
          '#tree' => TRUE,
        );
        if (in_array($id, array_keys($mapping_defaults))) {
          $form[$site]['channels'][$id]['#default_value'] = intval($mapping_defaults[$id]);
        }
      }
    }
    
    // Get mapping configs.
    $xml = array();
    $mapping_config = \Drupal::config('video_export.mappings');
    $sites = $mapping_config->get('sites');
    $channel_mappings = $sites[$site]['mappings'];
    // Get video nodes that belong to one of the selected channels.
    $query = \Drupal::entityQuery('node')
      ->condition('status', 1)
      ->condition('type', 'video')
      ->condition('changed', REQUEST_TIME - 59200, '>=')
      ->condition('field_channels.entity.tid', array_keys($channel_mappings), 'IN');
    $nids = $query->execute();
    // Load the entities using the nid values. The array keys are the associated vids.
    $video_nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($nids);

    foreach ($video_nodes as $nid => $node) {
      $host = \Drupal::request()->getSchemeAndHttpHost();
      $url_alias = \Drupal::service('path.alias_manager')->getAliasByPath('/node/' . $nid);
      // Get channels values.
      $channel_tids = array_column($node->field_channels->getValue(), 'target_id');
      $create_date = \Drupal::service('date.formatter')->format($node->getCreatedTime(), 'custom', 'j M Y h:i:s O');
      $item = array(
        'title' => $node->getTitle(),
        'link' => $host . $url_alias,
        'description' => $node->get('body')->value,
        'mrctv-nid' => $nid,
        'guid' => $nid . ' at ' . $host,
        'pubDate' => $create_date
      );
      // Check for short title and add it if it's there.
      if ($node->get('field_short_title')->value) {
        $item['short-title'] = $node->get('field_short_title')->value;
      }
      foreach ($channel_tids as $ctid) {
        $item[$site . '-channel-map'][] = $ctid;
      }
      $xml[] = $item;
    }

    return parent::buildForm($form, $form_state);
  }
  
  /**
   * {@inheritdoc}.
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
  
  }
  
  protected function getEditableConfigNames() {
    return ['video_export.mappings'];
  }
  
  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $values = $form_state->getValues();
    $config = $this->config('video_export.mappings');
    $sites = array();

    foreach($values as $site => $mappings) {
      if (is_array($mappings)) {
        foreach ($mappings['channels'] as $mrctv_channel => $target_channel) {
          if ($target_channel != 0) {
            $sites[$site]['mappings'][$mrctv_channel] = $target_channel;
          }

        }
        $config->set('sites', $sites);
      }
    }
    $config->save();
  
    parent::submitForm($form, $form_state);
  }
}

The setting for the channels feeds on NB and CNS are stored in /modules/custom/video_export/config/install/video_export.settings.yml:

sites:
  newsbusters:
    channel_url: 'http://www.newsbusters.org/path/to/channels'
  cnsnews:
    channel_url: 'http://www.cnsnews.com/path/to/channels'
list_time: 24
        

Since this is an admin settings form, I extend the ConfigFormBase class. This adds some additional functionality over the standard FormBase class, similar to the way the system_settings_form() function does in D7 and older (see the change record for details).

As mentioned above the form does the following things:

  1. Reads the channels feed from the destination sites
  2. Creates a fieldset for each site with a select list for each MRCTV channel where the user can select the destination channel.
  3. Saves the mappings in config.

The next thing that is needed is the feed of video nodes that are available to be imported. After trying unsuccessfully to create a custom REST API endpoint, I ended up going with a Feeds display in Views. Out of the box I can create my feed, but I still need to add my custom elements. In D6, I used hook_nodeapi($op = 'rss item') to add my custom elements. In other feeds on D7 sites I've been able to use the Views RSS module with its provided hooks to add custom RSS elements, but as of now it is currently unusable for D8 due to one major issue.

Finally, since everything in D8 is based on OOP, I knew there had to be a way to override a Views class at some level, so after some searching, I decided to override the display plugin. I poked around in the Views code and found the RssFields class that is used for the field level display for a Feeds display, so I overrode that.

namespace Drupal\video_export\Plugin\views\row;

use Drupal\views\Plugin\views\row\RssFields;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;

/**
 * Renders an RSS item based on fields.
 *
 * @ViewsRow(
 *   id = "mrctv_rss_fields",
 *   title = @Translation("MRCTV Fields"),
 *   help = @Translation("Display fields as RSS items."),
 *   theme = "views_view_row_rss",
 *   display_types = {"feed"}
 * )
 */
class MRCTVRssFields extends RssFields {
  
  /**
   * Override of RssFields::render() with additional fields.
   *
   * @param object $row
   *
   * @return array
   */
  public function render($row) {
    $build = parent:: render();
    $item = $build['#row'];
    
    // Add MRCTV nid
    $item->elements[] = array(
      'key' => 'mrctv-nid',
      'value' => $row->nid,
    );
  
    // Add channels and their target nids. We can get them from $row->_entity.
    $site = $this->view->args[0];
    // Get MRCTV nids from view.
    $channel_tids = array_column($row->_entity->field_channels->getValue(), 'target_id');
    // Now, get destination tids from config.
    $mapping_config = \Drupal::config('video_export.mappings');
    $all_mappings = $mapping_config->get('sites');
  
    foreach($channel_tids as $mrctv_channel) {
      if(in_array($mrctv_channel, array_keys($all_mappings[$site]['mappings']))) {
        $item->elements[] = array(
          'key' => $site . '-channel-map',
          'value' => $all_mappings[$site]['mappings'][$mrctv_channel],
        );
      }
    }
    
    // Re-populate the $build array with the updated row.
    $build['#row'] = $item;
    
    return $build;
  }
}

As you can see, the override is fairly simple; all I needed to do was override the render() method. This method returns a render array, so all I do is get the built array from the parent class, add my custom elements to the #row element in the array, and return it.

One thing that I couldn't do simply in the views UI was select the nodes that should be in the feed based on the associate Channels vocabulary terms. These are dynamic, based on the mappings selected in the admin form, so I can't pre-select them in the view settings. This is where hook_views_query_alter() comes to the rescue.

/**
 * Implements hook_views_query_alter().
 */
function video_export_views_query_alter(Drupal\views\ViewExecutable $view, Drupal\views\Plugin\views\query\Sql $query) {
  if ($view->id() == 'video_export' && $view->getDisplay()->display['id'] == 'feed_1') {
    // First, we need to get the site parameter from the view.
    $site = $view->args[0];
    
    // Next, we need to get the saved config for the channel mapping.
    $mapping_config = \Drupal::config('video_export.mappings');
    $all_mappings = $mapping_config->get('sites');
    $tids = array_keys($all_mappings[$site]['mappings']);
   
    // Modify query to get nodes that have the selected nids, which are the array keys.
    $query->addWhere(NULL, 'node__field_channels.field_channels_target_id', $tids, 'IN');
  }
}

All I do here is get the saved mappings from config and add them to the views query as a WHERE condition to limit the feed items to the appropriate nodes.

One issue I ran into with the results was duplicate records. Since field_channels (the entity reference field for the Channels vocabulary) is multiselect, the query returns multiple records for each node if there are multiple Channels terms selected. There are display settings to show multiple items in one row, but they don't take effect here. I didn't dig far enough into the code to know for sure, but my guess is that the grouping happens at a higher layer in the views rendering process, so they don't take effect in this situation.

To get around this, I implemented hook_views_pre_render(). At this point in the process, the results have been built, so I just loop through them and remove duplicates.

/**
 * Implements hook_views_query_pre_render().
 */
function video_export_views_pre_render(Drupal\views\ViewExecutable $view) {
  $unique_nids = $new_results = array();
  
  // Loop through results and filter out duplicate results.
  foreach($view->result as $index => $result) {
    if(!in_array($result->nid, $unique_nids)) {
      $unique_nids[] = $result->nid;
    }
    else {
      $new_results[] = $result;
    }
  }
  // Replace $view->result with new array. Apparently views requires sequentially keyed
  // array of results instead of skipping keys (e.g. 0, 2, 4, etc), so we can't just
  // unset the duplicates.
  $view->result = $new_results;
}

As noted in the code comment, views seems to require a sequentially numbered array, so you can't just unset the duplicate keys and leave it as is, so I chose to just add each item to a new array. In retrospect, I could have just used PHP functions like array_splice() and array_filter(), but this method works just as well.

It should also be noted that the views hooks need to go in a *.views_execution.inc file, so this one is in /modules/custom/video_export/video_export.views_execution.inc.

All I do at this point is use the Job Scheduler module with Feeds in the destination sites to schedule the import at the desired interval, and the process runs by itself. 

Sep 07 2016
Sep 07

A recent project involved the use of the Simple Hierarchical Select module to input category data for a particular content type. Simple Hierarchical Select provides a clean way of browsing hierarchical vocabularies to easily add taxonomy terms to nodes.

An initially tricky user interface problem to utilise this module with Search API and Views exposed filters was solved using a couple of Drupal 8 plugins and a bit of smart thinking!

A recent project involved the use of the Simple Hierarchical Select module to input category data for a particular content type. Simple Hierarchical Select provides a clean way of browsing hierarchical vocabularies to easily add taxonomy terms to nodes.

The module works great and does exactly what it says on the tin, however it created something of a head-scratcher as the same project, which was using Apache Solr via Search API, required this data to be searchable by the end user. Creating a view of the indexed data and then exposing the various filters to be included in the search was the obvious answer and initially it was felt that it should be pretty straightforward.

For a number of the exposed filters, it was straightforward; although some required the use of hook_form_alter to make some alterations to the output of some exposed filters such as changing boolean filters from a True/False selection to Yes/No.

Unfortunately, the simple hierarchical select filter was not available as an exposed filter for Search API index views. A few approaches were made to try and resolve this including using hook_form_alter and trying to force the exposed filter to make use of simple hierarchical select. In the end, a solution was found that was actually more simple but perhaps not quite immediately obvious and required creating two plugins; a Search API processor plugin and a Views filter plugin.

Because the taxonomy that required to be searchable was hierarchical in nature, it is quite possible that the user making a search may want to do a search on higher level categories and return results that included sub-categories. For example, the top level categories could be Business and Community and below Business may be sub categories such as Manufacturing, Transport and Retail and then perhaps below Retail would be child categories such as Food, Clothing, Consumer Electronics. A user searching on Retail would expect results to be returned that have been categorised with Food, Clothing or Consumer Electronics rather than simply those categorised as Retail.

The Search API processor plugin is needed to add the parent terms of each taxonomy term to the index so that the views filter would be able to refer to those parent terms when the search query is generated.

It looks something like this:

namespace Drupal\search_api_demo\Plugin\search_api\processor;

use Drupal\search_api\Processor\ProcessorPluginBase;
use Drupal\taxonomy\TermStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Adds an additional field containing the term parents.
 *
 * @SearchApiProcessor(
 *   id = "term_parents", 
 *   label = @Translation("Term parents"),
 *   description = @Translation("Adds all term parents for directory field."),
 *   stages = {
 *     "pre_index_save" = -10,
 *     "preprocess_index" = -30
 *   }
 * )
 */
class TermParents extends ProcessorPluginBase {

  /**
   * Term storage.
   *
   * @var \Drupal\taxonomy\TermStorageInterface
   */
  protected $termStorage;

  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $plugin = parent::create($container, $configuration, $plugin_id, $plugin_definition);
    $plugin->setTermStorage($container->get('entity_type.manager')->getStorage('taxonomy_term'));
    return $plugin;
  }

  protected function setTermStorage(TermStorageInterface $storage) {
    $this->termStorage = $storage;
  }

  public function preprocessIndexItems(array &$items) {
    foreach ($items as $item) {
      foreach ($this->filterForPropertyPath($item->getFields(), 'field_category) as $field) {
        foreach ($field->getValues() as $tid) {
          foreach ($this->termStorage->loadAllParents($tid) as $term) {
            $field->addValue($term->id());
          }
        }
      }
    }
  }

  public function calculateDependencies() {
    parent::calculateDependencies();
    $this->addDependency('config', 'field.storage.node.field_category);
    $this->addDependency('config', 'taxonomy.vocabulary.category');
    return $this->dependencies;
  }
}

​The most interesting part of this class is the preprocessIndexItems method. When Search API indexes the data, the processor plugin iterates through each item to be indexed, filtering out fields other than field_category, which contains the term id stored by the particular node and the field we are interested in working with. It loads the parents of the term, by term id, and adds the id of each parent to the index using the addValue method. This method is basically storing each term id value in the item field, in this case field_category.

Note that the indexed item mentioned above isn’t an entity, it is referring to a Search API Item. This item could as easily be a user as a node, taxonomy term, file, comment or any other piece of data that Search API is able to index and it represents data that is being indexed or returned as a search result.

Incidentally, this functionality is already available in the Drupal 7 version of Search API and progress is being made on porting this to Drupal 8.

The second plugin is the Views filter itself. This is in two parts, the plugin itself and a hook that lets Views know about the filter. Firstly, the hook:

/**
 * Implements hook_views_data_alter().
 */ 
function search_api_demo_views_data_alter(array &$data) {
  $data['search_api_index_default']['shs'] = [
    'title' => t('Category filters'), 
    'help' => t('SHS filter for category'), 
    'filter' => [
      'field' => 'field_category’, 
      'id' => search_api_demo_shs_taxonomy_index_tid_depth',
    ],
  ];
}

The array key search_api_index_default refers to the Default Search API index or if you were creating a custom filter for node data, you would want to use node_field_data to refer to the database table instead. The field key is the important item in this array and refers to the actual field, field_category, we want to be able to filter on. shs is a reference in Views to the field_category field and can be set to anything. Lastly, the id key is the plugin we want to use.

Alternatively if you simply wanted to create a filter that would provide a text filter for a particular database, or Search API, field, this id could be set to String which would use the core Views string filter plugin and you’d be finished now. As we have a specific requirement for our filter, we need to create a plugin, which has been given the id search_api_demo_shs_taxonomy_index_tid_depth.

The plugin code is simple and looks as follows:

namespace Drupal\search_api_demo\Plugin\views\filter; 

use Drupal\search_api\Plugin\views\filter\SearchApiFilterTrait; 
use Drupal\search_api\UncacheableDependencyTrait; 
use Drupal\shs\Plugin\views\filter\ShsTaxonomyIndexTidDepth; 

/**
 * Filter handler for taxonomy terms with depth. 
 * 
 * @ingroup views_filter_handlers 
 * 
 * @ViewsFilter("search_api_demo_shs_taxonomy_index_tid_depth") 
 */ 
class SearchAPIDemoFilter extends ShsTaxonomyIndexTidDepth { 

  use UncacheableDependencyTrait; 
  use SearchApiFilterTrait; 

  public function query() {
    if ($value = reset($this->value)) {
      $this->getQuery()
        ->addCondition('field_directory_type', $value);
    }
  }
}

The magic happens in two places. Firstly the class extending Simple Hierarchical Select’s ShsTaxonomyIndexTidDepth views filter, which does all the hard graft of creating the hierarchical select widget, and secondly SearchApiFilterTrait, a trait which gives access to Search API filter methods and lets us add our filter as a condition to the search query, as can be see in the query() method in the code above.

All this results in turning the default select list that only works on the current term id...

...to something more flexible and easier to use, and that respects taxonomy hierarchy:

Search API Drupal 8 Plugins Views
Jun 27 2016
Jun 27

On a recent Drupal 8 client project our client was building listing pages using views exposed filters and adding these to the menu.

This resulted in several menu URLs pointing to the same base path, but with the query arguments determining the difference.

However Drupal 8's default menu-trail calculation was resulting in the menu highlighting all instances when one of them was viewed.

Luckily the active trail calculation is done in a service and it was simple to modify the default behaviour.

Read on to see how we did it.

The problem

So the site included a view that displayed all of the different Venues the client managed, with exposed filters that allowed filtering the listing into groups.

The client used the URL generated by the filters to add different menu entries. For example there was a list of 'Community centres' in one section of the menu, linking to a pre-filtered view. In another section of the menu there was a link to 'Outdoor art spaces', also a link to a pre-filtered view.

However Drupal 8's default menu active trail calculation uses the \Drupal\Core\Menu\MenuLinkManager::loadLinksByRoute() method to calculate the active trail. As indicated by the name, this only loads matches based on the route name and parameters, but doesn't consider query arguments such as those used by Views exposed filters.

The solution

Luckily, the menu active trail calculation is handled in a service. This means we can override the definition and inject an alternate implementation or arguments.

Now there are two points we could override here, we could inject a new menu link manager definition into the menu active trail service, and change the way that loadLinksByRoute works to also consider query arguments - however the active trail service is heavily cached, and this would result in the first one to be cached and any subsequent ones to not work.

Instead we need to run our code after the values are fetched from the cache, so the logical point is to override Drupal\Core\Menu\MenuActiveTrail::getActiveTrailIds() method to filter out matches and their parents that don't match the current query arguments.

So to do this we need an implementation of \Drupal\Core\DependencyInjection\ServiceModifierInterface. Ours looks something like this:

<?php

namespace Drupal\my_module;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceModifierInterface;
use Symfony\Component\DependencyInjection\Reference;

class MyModuleServiceProvider implements ServiceModifierInterface {

  /**
   * {@inheritdoc}
   */
  public function alter(ContainerBuilder $container) {
    // Get the service we want to modify.
    $definition = $container->getDefinition('menu.active_trail');
    // Inject an additional service, the request stack.
    $definition->addArgument(new Reference('request_stack'));
    // Make the active trail use our service.
    $definition->setClass(MyModuleMenuActiveTrail::class);
  }
}

For more information, see our previous blog post on overriding Drupal 8 service definitions.

Filtering on query parameters

Now we have our new active trail service, we need to filter out the links that match on route, but not on query arguments.

To do this, we need to get the query arguments from the current request. In our service alter above you'll note we injected an additional service into our active trail class, the request stack.

This allows us to get the current request and therefore the query arguments.

So first we need a constructor to handle the new argument, and a class property to store it in.

<?php

namespace Drupal\my_module;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Menu\MenuActiveTrail;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Defines a class for menu active trail that considers query parameters.
 */
class MyModuleMenuActiveTrail extends MenuActiveTrail {

  /**
   * Current request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

  /**
   * {@inheritdoc}
   */
  public function __construct(MenuLinkManagerInterface $menu_link_manager, RouteMatchInterface $route_match, CacheBackendInterface $cache, LockBackendInterface $lock, RequestStack $request_stack) {
    parent::__construct($menu_link_manager, $route_match, $cache, $lock);
    $this->requestStack = $request_stack;
  }

}

Now we have the pieces in place, we just need to add the code to filter out the links and their parents that don't match on query parameters.

/**
 * {@inheritdoc}
 */
public function getActiveTrailIds($menu_name) {
  // Get the existing trail IDs from the core implementation.
  $matching_ids = parent::getActiveTrailIds($menu_name);
  // If we don't have any query parameters, there's nothing to do here.
  if (($request = $this->requestStack->getCurrentRequest()) && $request->query->count()) {
    // Start with the top-level item.
    $new_match = ['' => ''];
    // Get all the query parameters.
    $query = $request->query->all();
    // Get the route name.
    $route_name = $this->routeMatch->getRouteName();
    if ($route_name) {
      $route_parameters = $this->routeMatch->getRawParameters()->all();

      // Load all links matching this route in this menu.
      $links = $this->menuLinkManager->loadLinksByRoute($route_name, $route_parameters, $menu_name);
      // Loop through them.
      foreach ($links as $active_link) {
        $match_options = $active_link->getOptions();
        if (!isset($match_options['query'])) {
          // This link has no query parameters, so cannot match, ignore it.
          continue;
        }
        if ($match_options['query'] == $query) {
          // This one matches - so we add its parent trail to our new match.
          if ($parents = $this->menuLinkManager->getParentIds($active_link->getPluginId())) {
            $new_match += $parents;
          }
        }
      }
    }
    // Replace the existing trail with the new trail.
    $matching_ids = $new_match;
  }
  return $matching_ids;
}

Wrapping up

Drupal 8's service based architecture gives us new levels of flexibility, personally I'm really enjoying building client projects with Drupal 8. I hope you are too.

Drupal 8 Menu active trail Views Request Stack Service Modifier
Jan 31 2016
JK
Jan 31

In previous articles (here and here), we have seen a method to add custom views and data in MyModule.

With Drupal 8 there is a very easy and practical way to add this custom view as a configuration that will be installed with the module.

1) extract the configuration data

Navigate to "/admin/config/development/configuration/single/export".

On this page, select configuration type 'view' and configuration name 'My module list' that was created earlier.

Single export

2) create configuration install file

You will obtain from the above export a list of configuration data that you can copy and paste into a file called for instance "views.view.mymodule-list.yml";

Simply place this file into the install folder :

Install folder

Upon installation of the module, the view will be automatically created.

We hope this demonstration is helpful to you. You can view as well another demo in our custom module address book , part of EK management tools that use the same technique.

If you have comments or want to add techniques to improve views of custom data, feel free to do so.

Jan 17 2016
JK
Jan 17

In our EK management tools suite we have custom designed lists of items like for instance list of management documents.

Those lists are build with custom codes and templates which is somehow more convenient to manage with complex data, links, menus and filters as in the example below.

Example of documents list

However for simple list, the views module is very useful and can be integrated in a custom module as well to automatically create the list.

Here is an example with companies list in the system address book module showing the company name as link and a field about the type of record plus a simple filter box.

List companies

To achieve this, you need first to reference the data into your module called for instance MyModule.

The sample table structure containing the data is as follow:

    `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(100) NOT NULL DEFAULT '',
    `type` VARCHAR(5) NULL DEFAULT NULL,
    PRIMARY KEY (`id`)

The field `type` in our case is a numeral from 1 to 3 that maps to defined description (Here we will use 1 => blue, 2 => green and 3 => red).


In MyModule.module file in our custom module, we reference those data to be accessible in views with MyModule_views_data() function.

In this function, we declare the following information:

/**
 * @file
 * MyModule module .
 */

function MyModule_views_data() {
  // This write hook_views_data() for the main table

  // First, the entry $data['mymodule_tb']['table'] describes properties of
  // the actual table – not its content.

  $data['mymodule_tb']['table']['group'] = t('My Module');

  // Define this as a base table
  $data['mymodule_tb']['table']['base'] = array(
    'field' => 'id', // This is the identifier field for the view.
    'title' => t('My Module'),
    'help' => t('My Module contains some data.'),
    'database' => 'external_db',
    'weight' => -10,
  );

  // This table references the {_tb_2} table. The declaration below creates an
  // 'implicit' relationship to the _tb_2 table
  $data['mymodule_tb']['table']['join'] = array(
    'database' => 'external_db',
    'mymodule_tb_2' => array(
      'left_field' => 'mid',
      'field' => 'id',
      'database' => 'external_db',
    ),
  );

  // Next, describe each of the individual fields in this table to Views.
  //  ID table field.
  $data['mymodule_tb']['id'] = array(
    'title' => t('mymodule_tb id'),
    'help' => t('mymodule_tb id.'),
    'relationship' => array(
      'base' => 'mymodule_tb_2', // The name of the table to join with
      'field' => 'mid', // The name of the field to join with
      'id' => 'standard',
      'label' => t('linked table to mymodule_tb'),
    ),
        'field' => array(
      'id' => 'numeric',
    ),
    'sort' => array(
      'id' => 'standard',
    ),
    'filter' => array(
      'id' => 'numeric',
    ),
  );

  // Example plain text field.
  $data['mymodule_tb']['name'] = array(
    'title' => t('name'),
    'help' => t('entry name.'),
    'field' => array(
      'id' => 'standard',
    ),
    'sort' => array(
      'id' => 'standard',
    ),
    'filter' => array(
      'id' => 'string',
    ),
    'argument' => array(
      'id' => 'string',
    ),
  );
 
  $data['mymodule_tb']['type'] = array(
    'title' => t('type'),
    'help' => t('type: 1 blue, 2 green, 3 red'),
    'field' => array(
      'id' => 'numeric',
    ),
    'sort' => array(
      'id' => 'standard',
    ),
    'filter' => array(
      'id' => 'numeric',
    ),
  );   
      

// This write hook_views_data() for the linked table

  $data['mymodule_tb_2']['table']['group'] = t('My Module table 2');

  $data['mymodule_tb_2']['table']['base'] = array(
    'field' => 'id', // This is the identifier field for the view.
    'title' => t('My Module table 2'),
    'help' => t('My Module tb_2 contains linked data to mymodule_tb.'),
    'weight' => -10,
    'database' => 'external_db',
  );

  $data['mymodule_tb_2']['table']['join'] = array(
    'mymodule_tb' => array(
      'left_field' => 'id',
      'field' => 'mid',
      'database' => 'external_db',
    ),
  );

  //  ID table field.
  $data['mymodule_tb_2']['id'] = array(
    'title' => t('tb_2 id'),
    'help' => t('tb_2 id.'),
       'field' => array(
       'id' => 'numeric',
    ),
       'sort' => array(
       'id' => 'standard',
    ),
       'filter' => array(
       'id' => 'numeric',
    ),
  );
 
  $data['mymodule_tb_2']['mid'] = array(
    'title' => t('mymodule_tb id'),
    'help' => t('mymodule_tb id ref.'),
    'relationship' => array(
      'base' => 'mymodule_tb',
      'field' => 'id',
      'id' => 'standard',
      'label' => t('mymodule_tb entry'),
    ),
       'field' => array(
       'id' => 'numeric',
    ),
       'sort' => array(
       'id' => 'standard',
    ),
       'filter' => array(
       'id' => 'numeric',
    ),
  );


  $data['mymodule_tb_2']['comment'] = array(
    'title' => t('comment'),
    'help' => t('linked comment.'),
    'field' => array(
      'id' => 'standard',
    ),
    'sort' => array(
      'id' => 'standard',
    ),
    'filter' => array(
      'id' => 'string',
    ),
    'argument' => array(
      'id' => 'string',
    ),
  );
  return $data;
}

Few remarks about the above information:

  • In this example, we have a table linked to our main table which is described by $data['mymodule_tb']['table']['join'] and  $data['mymodule_tb_2']['table']['join']
  • The dabase containing the data is specified as 'external_db'. In our configuration, we do not use the default database of Drupal installation (this database must be defined in settings.php).

If we navigate to "/admin/structure/views/add", we can now create a view based on our main table content:

My Module view

In the next article we will describe how to create the page similar to our address book list view with specific rewrite results for "type" field and filter criterion.

Feel free to add your own comments or suggestions.

Nov 08 2015
Nov 08

Nothing gives me more professional satisfaction than seeing my work being used for practical applications. In 2014, I was commissioned to write and maintain a few Views-related modules to add interesting functionality to that ecosystem:

Recently, the original sponsor of this work notified me that he had finally launched Vizala, his site that utilizes these modules. Vizala "aims to be the internet's most useful database for country, demographic, social, and economic information. Instead of just providing answers, [its] robust analytics allow for in-depth analysis and provide a complete picture of your topic of interest. Vizala only uses data from trusted sources and includes links to the original source for maximum transparency."

Below is a screenshot of an economic report provided by the site. Under the hood, this report is a view using the Flipped Table style, to show data entries as columns, and Field Tooltip on header cells to provide more context to the information presented. The "Share" menu item invokes the Views Share functionality.

Vizala in action

The site uses many more Views modules, including Views Save to save filter settings. Congratulations to Vizala for the launch and thanks for sponsoring useful modules that the whole Drupal community can reuse!

Attachment Size screenshot-vizala.com_.png 97.38 KB
Jan 16 2015
Jan 16

Now that Drupal 8 is in beta, I’ve been trying to spend some more time with it. Reading articles and watching presentations are good ways to keep up with where things are (or are going), but nothing beats actually using it. Simplytest.me, Pantheon, and Acquia Cloud all now provide free ways to spin up an instance of the latest version (beta 4 as of this writing), so there’s no excuse not to try it out, even if a local setup seems daunting.

After clicking around a bit and admiring some of the administration interface improvements, I set to work on putting a test site together.

Arguably the most essential site building tool, Views in now part of Drupal 8 core. In being integrated, the module has also been leveraged to power most of the default lists and blocks (think content admin page, front page, taxonomy term pages, user admin page, recent content block, etc.). You can use your Views knowledge to modify these site elements or use them as starting points for your own creations.

Credit goes to the VDC (Views in Drupal Core) team for doing an excellent job of porting the module and converting to the new core plugin system. Although VDC wasn’t one of the original initiatives, it was one of the first ones ready, and the team was then able to use what it learned in the process to help out on other initiatives too.

The Views refactoring has brought many improvements, but in this post I’m going to focus on some new Displays functionality. A common task when putting a new site together is to customize the out-of-the-box pages (particularly the home page and content admin page), so I headed to Structure -> Views to copy a default view and get started.

After realizing that everything was mostly the same, one of the first differences I spotted was that you can now clone a display as a different type, so the block you’ve been working on can easily be turned into a page. Each display has its own “View” button, that now also allows you to “duplicate as”, which is slightly different from the old way of doing things. Technically, Views still uses the concept of a “Master” display that can be overridden. You can see it if you create a view with no display type, but it goes away after you create your first display. It pretty much disappears into the UI and is only present in the various settings’ “Apply” buttons — where you can save your changes by display or universally (“this display” vs “all displays”).

d8-views-display-optionsExamining the “duplicate as” options in my test view, I noticed three new display types:

Embed

In the Views module settings, you can choose to “Allow embedded displays”, and they can be used in code via views_embed_view().

Entity Reference

In your Entity Reference field settings, you can choose to “filter by an entity reference view” and use a view with this display type to determine what’s referenceable.

REST export (with RESTful Web Services enabled)

You can convert the output of a view into whatever format is requested, such as JSON or XML, and easily create a REST API for an application.

These Views improvements represent a few differences coming in D8, but are just a small taste of some of the exciting new functionality we have to look forward to in the near future. What Drupal 8 updates interest you the most?

Resources:

Jun 29 2014
Jun 29

The Drupal community web site has a profile field to list "My mentors"

For example, on my profile I say I was mentored by:
  • robbiethegeek - how to appreciate Drupal awesomeness and its limitations
  • Alex UA - how to run a business providing Drupal services
  • forestmars - how to be involved in the Drupal community
  • smerrill - how to be an engineer with platform tools like Jenkins, Vagrant, Redis
  • snugug - how to make web sites responsive
  • ericduran - how to experiment with new doodads like HTML5, Android
  • zroger - how to use Drupal hooks and APIs in code

I started thinking about my dumb luck picking Drupal as a tool about 9 years ago. I was looking for a Content Management System that made sense.

I was awfully interested in a project called PAWS (PHP Automatic Web Site) -- and it's a good thing I didn't ride that horse, which was long ago put out to pasture.

A client asked me to convert his static PHP site so that he could manage the content in the include files without editing code. I built my first Drupal 4.x site, with the crazy hack of creating a node for every include, and then printing the includes/nodes inside a main node (Panels, sort of, which did not exist in Drupal then). I also customized the front end of the TinyMCE wysiwyg editor to add buttons to apply his brand's pink and blue colors. The client smoked a lot of pot, drifted away, came back a year or two later for more work -- without a database. Oh well, not the first - or last - time the db was lost by a client.

That experience convinced me that a lot could be done with Drupal that I had not been able to do without a lot of custom coding just to build the base web application. Other projects with early versions of WordPress and Mambo (predecessor to Joomla) left me unimpressed with their extensibility. I have often said since then that "WordPress is like the smaller sibling of Drupal, but Joomla is the evil cousin."

Then Earl Miles conjured up his merlinofchaos wizardry for Sony Music, creating Views and Panels and Ctools, and that was around the time that a lot of developers took notice of Drupal. I was profoundly convinced that Drupal had outgrown being a CMS enabling writers to (more or less) easily edit content without (much) coding, and had become a Content Management Framework that could perform elegant and dynamic manipulations of the content in its database.

So I had to add dumbluck to my mentors - not just for my early experiment hacking the node system, but for each solution that I was able to implement afterwards, because my choice of Drupal provided me with an extensible framework allowing complex algorithms for presentation of content, and the Drupal project improves with every contributor's enhancements.

I think I'm dumb, maybe just happy

[embedded content]

I noticed in preparing this post that some Drupal user profiles are accessible by username, eg. https://www.drupal.org/u/decibel.places and https://www.drupal.org/u/robbiethegeek, while others, like merlinofchaos and smerrill, are only accessible by their UIDs https://www.drupal.org/user/26979 and https://www.drupal.org/user/77539 respectively.

Jun 26 2014
Jun 26

One of my long-standing gripes with Views is the inability to alter the behaviour of existing Views handlers (e.g. fields, filters, etc.) without having to subclass the desired handlers to add new functionality. While the subclassing approach is fine when the functionality targets a new field type, it is not ideal if the change required should affect existing fields, across different types of handlers.

I was recently commissioned to create a module that displays tooltips on field headers, regardless of field type. This is an example of the latter case above, and my solution, Views Label Tooltip, exemplifies my technique to achieve field alterations that are orthogonal to handler types. Following is an explanation of how I did it.

We want to extend Views field settings with additional options and modify their rendering and/or behaviour based on these options. In our case, each field has an additional "Tooltip" setting that gets rendered on the field's label.

This is the key to the technique. We need to store the custom field settings such that they behave just like the native ones:

  • Get imported/exported with standard view import/export
  • Get overridden when a field is overridden

For this, we use a Views display extender. The official documentation for this plugin type is that "Display extender plugins allow scaling of views options horizontally. This means that you can add options and do stuff on all views displays. One theoretical example is metatags for views." The key word is horizontal: it applies to all display plugins. What we're trying to achieve here is similar, but for field handlers. So until the Views maintainers decide to generalize the concept of extenders to other Views objects, we can use (some would say abuse) display extenders to hold the additional settings for us.

Here's the implementation of our display extender:

class views_label_tooltip_plugin_display_extender extends views_plugin_display_extender {
  function options_definition_alter(&$options) {
    $options['tooltips'] = array('default' => array(), 'unpack_translatable' => 'unpack_tooltips');
  }

  function unpack_tooltips(&$translatable, $storage, $option, $definition, $parents, $keys = array()) {
    $tooltips = $storage[$option];
    if (!empty($tooltips)) foreach ($tooltips as $field => $tooltip) {
      $translation_keys = array_merge($keys, array($field));
      $translatable[] = array(
        'value' => $tooltip,
        'keys' => $translation_keys,
        'format' => NULL,
      );
    }
  }
}

Here, we define the new tooltips option, and since tooltips hold translatable text, we instruct Views on how to export this data structure. (Note: unpack_translatables don't work correctly for display extenders in the current version of Views, but I submitted a patch to fix that.)

Now this option gets imported/exported along with the all other Views settings, which fulfills our first storage requirement. But since there's only one copy of it in each display, we will use this option as an array of tooltips, one entry per field. The tooltips option will be manipulated on each field's admin UI, as is shown below. There is no need for the display extender to have its own admin UI.

We need to alter the Views UI views_ui_config_item_form in order to inject our new options. Here's the code from Views Label Tooltip:

/**
 * Implements hook_form_FORM_ID_alter() for `views_ui_config_item_form`.
 */
function views_label_tooltip_form_views_ui_config_item_form_alter(&$form, &$form_state) {
  if ($form_state['type'] != 'field') return;

  $form_state['tooltips'] = views_label_tooltip_get_option($form_state['view']);
  $form['options']['element_label_tooltip'] = array(
    '#type' => 'textarea',
    '#title' => t('Tooltip'),
    '#description' => t('Place your tooltip text here. HTML allowed.'),
    '#default_value' => @$form_state['tooltips'][$form_state['id']],
    '#attributes' => array(
      'class' => array('dependent-options'),
    ),
    '#dependency' => $form['options']['element_label_colon']['#dependency'],
    '#weight' => $form['options']['element_label_colon']['#weight'] + 1,
  );
  $form['buttons']['submit']['#submit'][] = 'views_label_tooltip_form_views_ui_config_item_form_submit';
}

Note how we need to explicitly add the CSS class dependent-options to our element. Here's how the form looks like after alteration: Tooltip setting in field UI

To place the new form element below an existing one, we set the #weight attribute to follow the latter. Note that we're using a custom function views_label_tooltip_get_option() to get the option's value, we'll see why below. Here's the implementation of the form submit handler:

/**
 * Submit function for `views_ui_config_item_form`.
 */
function views_label_tooltip_form_views_ui_config_item_form_submit($form, &$form_state) {
  // Set the tooltip in our display extender.
  $display_id = $form_state['values']['override']['dropdown'];
  $tooltips = $form_state['tooltips'];
  $form_state['view']->set_display($display_id);
  if ($form_state['values']['options']['element_label_tooltip']) {
    $tooltips[$form_state['id']] = $form_state['values']['options']['element_label_tooltip'];
  }
  else {
    unset($tooltips[$form_state['id']]);
  }
  $form_state['view']->display_handler->set_option('tooltips', $tooltips);

  // Write to cache.
  views_ui_cache_set($form_state['view']);
}

in this submit handler, we detect whether the user is overriding the field on the current display, or altering the default fields. Based on this, we decide to save the option to the current display, or to the default (master) display, respectively. That's why we need a custom function to read back the options: we need to emulate the standard Views overriding logic by choosing the correct display to read the options:

/**
 * Helper function to get tooltips setting.
 */
function views_label_tooltip_get_option($view) {
  if (isset($view->display_handler->display->display_options['fields'])) {
    // Fields are overridden: use this display's tooltips.
    $tooltips = @$view->display_handler->display->display_options['tooltips'];
  }
  else {
    // Fields are default: use default display's tooltips.
    $tooltips = @$view->display['default']->display_options['tooltips'];
  }
  return $tooltips;
}

We have now fulfilled the second storage requirement. On to rendering.

To render the tooltip on top of field labels that are generated by the Views theming system, we use JavaScript. We inject our JS code during hook_views_pre_render(). Our hook implementation calls a theme function to generate each tooltip - theme functions can be overridden, which allows theme developers to customize the tooltip markup. The hook implementation also marks each target field label with a special class that our JavaScript can recognize:

/**
 * Implements hook_views_pre_render().
 */
function views_label_tooltip_views_pre_render(&$view) {
  $tooltips = views_label_tooltip_get_option($view);
  if (empty($tooltips)) return;

  // Theme tooltip and add our label class before rendering.
  $themed = array();
  foreach ($tooltips as $field => $tooltip) {
    if (!empty($view->field[$field]) && empty($view->field[$field]->options['exclude'])) {
      $field_css = drupal_clean_css_identifier($field); 
      $themed[$field_css] = theme('views_label_tooltip', array(
        'view' => $view, 
        'field' => $field, 
        'tooltip' => t($tooltip),
      ));

      $label_class =& $view->field[$field]->options['element_label_class'];
      if ($label_class) {
        $label_class .= ' ';
      }
      $label_class .= 'views-label-tooltip-field-' . $field_css;
    }
  }

  // Bail early if nothing to do.
  if (empty($themed)) return;

  // Add our JS files.
  drupal_add_js(drupal_get_path('module', 'views_label_tooltip') . '/js/views_label_tooltip.js');
  drupal_add_js(array(
    'viewsLabelTooltip' => array(
      $view->name => array(
        $view->current_display => array(
          'tooltips' => $themed,
        ),
      ),
    ),
  ), 'setting');
}

/**
 * Theme function for `views_label_tooltip`.
 */
function theme_views_label_tooltip(&$variables) {
  return theme('image', array(
    'path' => drupal_get_path('module', 'views_label_tooltip') . '/images/help.png',
    'attributes' => array(
      'title' => $variables['tooltip'],
      'class' => array(
        'views-label-tooltip',
      ),
    ),
  ));
}

Finally, the JavaScript code is responsible for adding the theme for each tooltip to the appropriate field label:

(function ($) {

  Drupal.behaviors.viewsLabelTooltip = {
    attach: function(context) {
      $.each(Drupal.settings.viewsLabelTooltip, function(view, displays) {
        $.each(displays, function(display, settings) {
          $.each(settings.tooltips, function(field, tooltip) {
            $('.view-id-' + view + '.view-display-id-' + display + ' .views-label-tooltip-field-' + field + '.views-field-' + field)
              .once('views-label-tooltip')
              .append(tooltip);
          });
        });
      });
    }

})(jQuery);

And here's the result (additionally rendered with the qTip jQuery plugin): Field labels with tooltips

Attachment Size views_label_tooltip_ui.png 24.07 KB views_label_tooltip_0.png 54.11 KB
Jun 23 2014
Jun 23

In this article we will continue exploring the powers of Views and focus on how to use relationships, contextual filters and rewrite field outputs. In a previous tutorial I showed you how to create a new View and perform basic customizations for it. We’ve seen how to select a display format, which fields to show and how to filter and sort the results.

In this article we will go a bit further and see what relationships and contextual filters are – the two most important options found under the Advanced fieldset at the right of the View edit page. Additionally, we’ll rewrite the output of our fields and combine their values into one.

To begin with, I have a simple article View that just shows the titles. Very easy to set up if you want to follow along. And there are three things I want to achieve going forward:

  1. Make it so that the View shows also the username of the article author
  2. Make is so that the View shows only articles authored by the logged in user
  3. Make it so that the author username shows up in parenthesis after the title

Relationships

First, let’s have the View include the author of the articles. If the View is displaying fields (rather than view modes or anything else), all we have to do is find the field with the author username, right? Wrong. The problem is the following: the node table only contains a reference to the user entity that created the node (in the form of a user ID – uid). So that’s pretty much all we will find if we look for user related fields: Content: Author uid.

What we need to do is use a relationship to the user entity found in the user table. Relationships are basically a fancy way of saying that table A (in our case node) will join with table B (in our case user) in order to retrieve data related to it from there (such as the name of the user and many others). And the join will happen in our case on the uid field which will match in both tables.

So let’s go ahead and add a new relationship of the type Content: Author. Under Identifier, we can put a descriptive name for this relationship like Content Author. The rest we can leave as default.

Now if you go and add a new field, you’ll notice many others that relate to the user who authored the content. Go ahead and add the User: Name field. In its settings, you’ll see a Relationship select list at the top where the relationship identifier we just specified is automatically selected. That means this field is being pulled in using that relationship (or table join). Saving the field will now add the username of the author, already visible in the View preview.

relationships

You can also chain relationships. For instance, if the user entity has a reference to another table using a unique identifier, you can add a second relationship. It will use the first one and bring in fields from that table. So the end result will be that the View will show fields that relate to the node through the user who authored the node but not strictly from the user table but somewhere else connected to the author. And on and on you can join tables like this.

Contextual filters

Contextual filters are similar to regular filters in that you can use mainly the same fields to filter the records on. Where contextual filters differ greatly is that you do not set the filtering value when you create the View, but it is taken from context.

There are many different contexts a filter value can come from, but mainly it comes from the URL. However, you can instruct Views to look elsewhere for contexts as well – such as the ID of the logged in user.

What we’ll do now is add a contextual filter so that the View shows only the articles authored by the logged in user. So go ahead and add a new contextual filter of the type Content: Author uid. Next, under the WHEN THE FILTER VALUE IS NOT IN THE URL fieldset, select the Provide default value radio. Our goal here is to have Views look elsewhere if it does not find the user ID in the URL.

contextual filters

You then have some options under the Type select list, where you should choose User ID from logged in user. This will make Views take the ID of the user that is logged in and pass it to the View as a filter. The rest you can leave as is and save the filter. You’ll immediately notice in your preview that only articles authored by you show up. The filtering is taking place dynamically. If you log in with another user account, you should see only the articles authored by that user account.

A great thing about contextual filters is that if you are displaying a View programatically in a custom module, you can pass the filtering value in code, which opens the door to many possibilities.

Rewriting fields

The last thing we will do in this tutorial is look at rewriting fields in order to concatenate their values. We will illustrate this technique by changing the title field to include the author username in parenthesis.

We’ll start by rearranging the order of the fields and move the title to be the last one showing. The reason we want to do this is that when you rewrite fields, you can use tokens that get values only from fields that are added before the one being rewritten. And since we want to rewrite the title field, we want the token for the username value to be present so we need to move it before the title field.

Now that the title field is last, edit the author username field and uncheck the box Create a label and then check the box Exclude from display. You can now save the field. The reason we are excluding this field from being displayed in our View is so that we don’t duplicate it once we concatenate it to the title field.

rewriting fields

Next, edit the title field and under REWRITE RESULTS, check the box Rewrite the output of this field. A new textarea should appear below where we will write the new contents of this field. If you write some gibberish in there and save the field, you’ll notice the title gets replaced by that gibberish.

Below this textarea, you’ll notice also some REPLACEMENT PATTERNS. These represent tokens of all the fields in the View loaded before this one (and including this one as well). So if you followed along, you’ll see there [name] and [title], among others.

What we need to do now is put these tokens in this box, wrapped with the text or markup we want. Having said that we want the username to be in parenthesis after the node title, we can add the following to the text box to achieve this:

[title] ([name])

Save the field and check out the result. Now you should have the author user in parenthesis. However, it’s still not perfect. We left the title field’s Link this field to the original piece of content box checked and this is breaking the output for us a bit due to also the username having a link to the user profile page. What we want is a clean link to the node title and in parenthesis (which themselves do not link to anything), the username linking to the user profile page.

So first up, add a new field called Content: Path (the path to the node). Make sure you exclude it from display, remove its label and move it before the title field. Then, edit the title field, uncheck the Link this field to the original piece of content box and replace the REWRITE RESULTS text with this:

 href="[path]">[title] ([name])

The [path] token is available from the new field we just added. And after you save, you should see already in the preview a much cleaner display of title nodes and usernames in parenthesis.

Conclusion

In this tutorial we’ve looked at three main aspects of building Views in Drupal 7: relationships, contextual filters and rewriting fields. We’ve seen how with the use of relationships we can use information also from related entities, not just those on the base table a View is built on. Contextual filters are great for when the View needs to display content dynamically depending on various contextual conditions (such as a URL or logged-in user). Lastly, we’ve learned how to rewrite fields and build more complex ones with values taken from multiple fields. As you can see, this technique is very powerful for theming Views as it allows us to output complex markup.

Views is pretty much the most popular Drupal module and it is highly complex. Despite its complexity, building views as a site administrator is very easy. All you need to understand is a few basic concepts and you are good to go. Developing for Views to extend its functionality or expose data to it is also an enjoyable experience. If you’d like to know more about that, you can read my tutorial on exposing your own custom module table to Views right here on Sitepoint.com.

Jan 02 2014
Jan 02

I took over a Drupal 7 project building a web application for college students to upload original videos about their school, and for schools to manage, group, and share the videos.

It's a startup privately funded by the principal, and we are literally working on a shoestring. My previous experience with media in Drupal led the principal to contact me via LinkedIn.

When it came time to build a video playlist in Drupal Views for JW Player version="6.7.4071" (formerly known as Longtail Video), I found very little useful documentation. In fact, someone suggested that those who know how are not interested in sharing their knowlege. -- but not me smiley

There are a couple of videos on YouTube by Bryan Ollendyke for Drupal 6. But a lot has changed in Drupal since then.

The Goal:

Back to the playlist: Site admins can mark a video featured by ticking a checkbox on the custom video content type. Now I want to display those featured videos as a playlist.

JW Player provides clear documentation about how to embed the player, and how to construct and load the playlist via a RSS feed.

The structure of the RSS file:

<rss version="2.0" xmlns:jwplayer="http://rss.jwpcdn.com/">
<channel>

  <item>
    <title>Sintel Trailer</title>
    <description>Sintel is a fantasy CGI from the Blender Open Movie Project.</description>
    <jwplayer:image>/assets/sintel.jpg</jwplayer:image>
    <jwplayer:source file="/assets/sintel.mp4" />
  </item>

</channel>
</rss>

There are various threads on drupal.org discussing video playlists for JW Player. None of them were particularly useful for me.

I constructed my Drupal View to to create a page display with a URL path and display fields:

jwplayer aml playlist drupal view

I tried using the Views Data Export module (a Drupal 7 successor of the Views Bonus Pack export functions) but it was difficult to create the proper syntax, particularly the xml namespace tag 

xmlns:jwplayer="http://rss.jwpcdn.com/"

I found the Views Datasource sub-module views_xml.module workable - mostly. I could add the namespace tag. The old videos by Bryan Ollendyke suggested the strategy to use the Views field labels for the xml tags like <title> and <description>.

I added the xml namespace in Format: XML data document: Settings:

jwplayer drupal view xml namespace

and set up the root and child syntax there too:

jw player xml root and child settings

But there were still two problems:

  1. The Video module does not expose the derivative transcoded video filenames to Views
  2. The JW Player syntax for the file source <jwplayer:source file="/assets/sintel.mp4" /> does not follow the convention of an opening and closing tag.

I used the Views PHP module to locate the filename from the node nid that was available in the the View. Yes, I could have written custom Views plugins and handlers, but remember: shoestring budget. It only took 3 queries:

$video_fid = db_query('SELECT field_video_fid FROM `field_revision_field_video` WHERE entity_id = ' . $data->nid)->fetchColumn();

$video_output_fid = db_query('SELECT output_fid FROM `video_output` WHERE original_fid = ' . $video_fid . ' ORDER BY output_fid DESC')->fetchColumn();

$video_uri = db_query('SELECT uri FROM `file_managed` WHERE fid = ' . $video_output_fid)->fetchColumn();

$video_source = str_ireplace("public://",$base_url,$video_uri);

return $video_source;

  1. Locate the original video_fid in the field_revision_field_video table
  2. Locate the video_output_fid (a different number generated by the Video module) in the video_output table
  3. Get the video_uri in the file_managed table

(yes we could use SQL JOIN - but we're only listing a few videos; TODO: rewrite the queries with placeholders for better security)

I considered creating php template files for these Views fields. But since I planned to embed the player in a custom block, I decided the parsing and processing could take place there.

Now we have the video file uri wrapped in tags <php></php> (from the label of the Views php field - it could be any arbitrary label, and we will be replacing it later).

The result of this view is something like:

<?xml version="1.0" encoding="utf-8"?> <rss version="2.0" xmlns:jwplayer="http://rss.jwpcdn.com/"> <channel> <item> <title>Who doesn&#039;t like free dessert?! </title> <description>Manhattan College</description> <jwplayer:image>http://[domain]/sites/default/files/[path]/thumbnail-690_0004.jpg</jwplayer:image> <php>path/690/MVI_4592_mp4_1384800425.mp4</php> </item> </channel>

Notice that it is not a valid XML or RSS document because there are no closing tags for the xml or the rss - but we will fix that later. Also I have used the value of the school field to rewrite the description field; but I might restore the description text in the playlist.

At first I thought I might use the view to write a file to the filesystem so I set the path to sites/default/files/playlists/featured_videos_playlist.rss.xml

Next, I had to get the playlist into the player.

I created a block with PHP code (or you can do it with a custom module and hook_block_info - but you already know I was working fast and cheap):

<?php

global $base_url;

$url = $base_url . "/sites/default/files/playlists/featured_videos_playlist.rss.xml";

$xml = fopen($url, "r");  

$playlist = stream_get_contents($xml);

$playlist = str_ireplace('<php>','<jwplayer:source file="/sites/default/files/',$playlist);

$playlist = str_ireplace('</php>','"/>',$playlist);

$playlist .= '</rss>';

file_put_contents('public://playlists/featured_videos_playlist.rss',$playlist);

?>

<div id="myElement">Loading the player ...</div>

<script type="text/javascript">

jwplayer.key="[put your key here if you have one]";

jwplayer("myElement").setup({

    playlist: "<?php print $base_path . '/sites/default/files/playlists/featured_videos_playlist.rss' ?>",

    listbar: {

        position: 'bottom',

        size: 80

    },

    height: 380,

    width: 420

    });

</script>

The block goes through 7 steps:

  1. Load the JW Player Javascript library
  2. Read the View output with PHP's fopen function (yes it works even though there is no actual file at the uri). I tried file_get_contents, but that failed because there was no actual file.
  3. Create the specialized jwplayer:source file tag from the file uri we placed within the <php></php> labels
  4. Close the rss tag (no need to close the xml tag)
  5. Write the playlist file to the filesystem (tip - use a different filename than the path in the View!!!)
  6. Embed the player in a div in the block
  7. Load the playlist we created

The generated playlist file (for the first item):

<?xml version="1.0" encoding="utf-8"?>

<rss version="2.0" xmlns:jwplayer="http://rss.jwpcdn.com/">

<channel>

  <item>

    <title>Who doesn&#039;t like free dessert?! </title>

    <description>Manhattan College</description>

    <jwplayer:image>http://[domain]/sites/default/files/[path]/thumbnail-690_0004.jpg</jwplayer:image>

    <jwplayer:source file="/sites/default/files/[path]/MVI_4592_mp4_1384800425.mp4"/>

  </item>

</channel>

</rss>

Finally, I used Context to place the block on the desired (home) page. Some additional CSS was required. JW Player is also fully skinnable.

The result may be viewed on the Proud Campus home pageOn that page, and the user pages and video upload page, we are using a simple responsive theme Ember selected via the Themekey module, to be more mobile-friendly. The web site is under active development and has some "rough edges."

Debriefing:

The project I inherited uses the "premium" GoVideo theme by ThemeSnap and the Video module.

If I were to start fresh, I would not have chosen to use them. GoVideo is a good example of how not to build a Drupal application all in the theme templates. I have started to move many of the functions into custom modules. One benefit of purchasing the GoVideo theme is that it includes a key for the licensed version of JW Player. Also, the Video module makes some assumptions and decisions that become tricky to customize. I will replace them with a responsive theme and component media modules, to better configure the file management, transcoding and display of the videos.

However, I think that much of my solution can be useful in general, without depending on the Video Module or the GoVideo theme. Also, we are not using the Drupal JW Player module - GoVideo includes the player's JavaScript library within its theme, and we are loading it in the block from the preferred location in Libraries.

I'm not going to into detail on how the content type is created, it uses the video field provided by the Video module, but the video could have been attached to the nodes in other ways.

I'm also not going to elaborate on the transcoding process; we are using avconv (formerly ffmpeg) on our server. I have used Encoding.com in the past at a client's request, and the Brightcove and Ooyala video management services as well. Eventually we will use Amazon AWS as a CDN for the videos - when we get more funding wink

 

<?php

global $base_url;

$url = $base_url . "/sites/default/files/playlists/featured_videos_playlist.rss.xml";

$xml = fopen($url, "r");  

$playlist = stream_get_contents($xml);

$playlist = str_ireplace('<php>','<jwplayer:source file="/sites/default/files/',$playlist);

$playlist = str_ireplace('</php>','"/>',$playlist);

$playlist .= '</rss>';

file_put_contents('public://playlists/featured_videos_playlist.rss',$playlist);

?>

<div id="myElement">Loading the player ...</div>

<script type="text/javascript">

jwplayer.key="[put your key here if you have one]";

jwplayer("myElement").setup({

    playlist: "<?php print $base_path . '/sites/default/files/playlists/featured_videos_playlist.rss' ?>",

    listbar: {

        position: 'bottom',

        size: 80

    },

    height: 380,

    width: 420

    });

</script>

The block goes through 7 steps:

  1. Load the JW Player Javascript library
  2. Read the View output with PHP's fopen function (yes it works even though there is no actual file at the uri). I tried file_get_contents, but that failed because there was no actual file.
  3. Create the specialized jwplayer:source file tag from the file uri we placed within the <php></php> labels
  4. Close the rss tag (no need to close the xml tag)
  5. Write the playlist file to the filesystem (tip - use a different filename than the path in the View!!!)
  6. Embed the player in a div in the block
  7. Load the playlist we created

The generated playlist file (for the first item):

<?xml version="1.0" encoding="utf-8"?>

<rss version="2.0" xmlns:jwplayer="http://rss.jwpcdn.com/">

<channel>

  <item>

    <title>Who doesn&#039;t like free dessert?! </title>

    <description>Manhattan College</description>

    <jwplayer:image>http://[domain]/sites/default/files/[path]/thumbnail-690_0004.jpg</jwplayer:image>

    <jwplayer:source file="/sites/default/files/[path]/690/MVI_4592_mp4_1384800425.mp4"/>

  </item>

</channel>

</rss>

(the school tag is ignored

Finally, I used Context to place the block on the desired (home) page. Some additional CSS was required. JW Player is also fully skinnable.

The result may be viewed on the Proud Campus home page. On that page, and the user pages and video upload page, we are using a simple responsive theme Ember selected via the Themekey module, to be more mobile-friendly. The web site is under active development and has some "rough edges."

Debriefing:

My project uses the "premium" GoVideo theme by ThemeSnap and the Video module. If I were to start fresh, I would not have chosen to use them. Govideo is a good example of how not to build a Drupal application all in the theme templates. I have started to move many of the functions into custom modules. One benefit of purchasing the Govideo theme is that it includes a key for the licensed version of JW Player. Also, the Video module makes some assumptions and decisions that become tricky to customize. I would have used a nice responsive theme and component media modules, to better configure the file management, transcoding and display of the videos. However, I think that much of my solution can be useful in general, without depending on the Video Module or the Govideo theme. Also, we are not using the Drupal JW Player module - GoVideo includes the player's JavaScript library within its theme, and we are loading it in the block from the preferred location in Libraries.

I'm not going to into detail on how the content type is created, it uses the video field provided by the Video module, but the video could have been attached to the nodes in other ways.

I'm also not going to elaborate on the transcoding process; we are using avconv (formerly ffmpeg) on our server. I have used Encoding.com in the past at a client's request, and the Brightcove and Ooyala video management services as well. Eventually we will use Amazon AWS as a CDN for the videos - when we get more funding wink

Nov 26 2013
vp
Nov 26

Our library is always open…*almost always.  Things happen (like Thanksgiving, Christmas, New Years) and sometimes we even have extended hours and we stay open even longer. Altogether, we have ~ 14 weeks/year that have “non-standard” hours.

In the past, we had to manually managed our weekly hours by updating a single, static piece of panel content.

wysywighours

You can probably imagine the problems we had trying to maintain this accurately – every week that we had deviated hours the desk supervisor had to submit a request for us to change them. We might be busy with other projects, might not get to it right away, the hours on the home page might be wrong, the desk supervisor might have to submit another request, and then pretty soon they might have to ask us to change it back to the regular hours…

Too much hassle.

We knew we needed dynamic content, and because our homepage was built as panel page, we were ready to pump a view into the hours pane. The question was how to set up the content so that we wouldn’t have to touch it…at all.

We landed on a combination of the office hours and scheduler modules.

Office hours gives us an “Office hours” field type. Since we want to display hours on our home page in weekly chunks, we create a “Weekly Hours” content type and add that field.

weekly hours content type

And we set the default values for this field to the library’s standard opening hours – usually when we have a week with weird hours, it’s only going to affect a couple of days so this helps cut down on data entry.

default office hours field settings

Then the scheduler module kicks in – the content type must be configured to allow nodes to be scheduled for publication. Check a couple boxes and start adding content.

scheduler options of content type

Since the goal here is for us not have to touch the hours (at all) we enlisted the desk supervisor to enter the data – with a couple of tweaks to the permissions table and a 5 minute demo (add node >> toggle hours >> set publish date for sunday evening before given week >> set unpublish date for sunday evening of given week >> click save) he was good to go. In all, he added 10 “Weekly Hours” nodes, including one with the library’s standard opening hours, because that was as far in advance as the library had planned its opening hours.

scheduling data

With the content added, we were able to start manipulating views so that we could display it on the homepage. Since all the nodes (except for the one with standard hours) had an unpublish date, we could configure a view to output only nodes of the type “Weekly Hours” that are published, sorted by the post date descending, displaying one at a time. That way, when a week of non-standard hours is published, it’s displayed in place of the standard hours, which were published previously but are never unpublished.

view for current hours

By creating a block display and setting it to only output the “Office hours” field, we should be all set. If there’s any simple formatting, re-ordering, etc. that you want to do to the hours, you should check in the field settings in the view. The office hours module has substantial integration with views, and gives you a lot of flexibility in how you want to display the hours.

highly flexible field settings for office hours

After that, we view block into a panel and it takes care of itself – here’s the final result on our homepage:

finalhours

One cool feature that we may implement for mobile is the current hours display. In the view configuration for the office hours field we can set the number of days to show – by setting only the next open day to be displayed, you’ll get a view that can be formatted something like, “The library is open today from: [*]”

current hours today display office hours module
Nov 22 2013
vp
Nov 22

Ever since we launched our site re-design, we’ve had a pretty steady flow internal change requests – add links, change fonts, re-style buttons, adjust layouts, modify permissions, etc.

We were trying to process these requests through a ticketing system that we built on our staff intranet in Drupal 6 – it basically allowed us to toggle tickets between open/closed and send emails when we were finished. With requests piling up (some of which overlapped with one another and/or were vaguely articulated and/or were out of our control anyways) this system wasn’t cutting it. We decided to scrap it and develop a new workflow in Drupal 7 with three questions in mind:

1) How can the requestor  clearly communicate what it is they actually want us to do
2) How can we clearly communicate what it is we’re actually doing
3) And how can we, as the general put it, start “hitting a small nail with an awfully big hammer”

The first two issues were solved easily enough: we enabled comments on the “Web Request” content type. When someone creates a node of that type, we can sniff out what they need with some probing in the comments area. The comments allow for transparent dialogue, so long as you have an effective method for distribution.

Enter the awfully big hammer…

Using a combination of rules, views, display suite and user reference fields, we gave node authors the ability to create email lists specific to the requests they create. With the click of a button they can message as many of their colleagues as they choose, giving them the gift of email at every step of the request.

Here’s how we did it:

First we created a form – we set up a content type for “Web Requests” – we were going to use entityforms but couldn’t since we needed commenting. We added fields for the data we wanted to collect (request title, details, link, and screenshots). We also created administrative fields (for things like UX data, project tagging, etc.) that are only visible to the web team, and therefore don’t appear on the node/add form that requestors use.

requestform

The most important field (for the email list) is the user reference field – what we termed “Stakeholders” is a selection of users of a given role formatted as checkboxes, with no restriction on the number of values.

users

The stakeholders appear on the form listed by name – that works for the users filling out the form, but not for rules, which will need their email addresses. Using display suite, we can configure a custom output for the stakeholders field when it is used as a token. In the “Manage Display” screen, you can specify custom display settings. Since we’ll be using tokens in the rules and views configurations we want to customize that to get the user’s email.

displaysuiteconfig1

So the token display for the field needs to output [user:mail]

displaysuiteconfig2

With the content type and fields configured, we’re ready for rules.

We want “Stakeholders” to be notified when a new request is created. So, we add a rule that reacts on saving new content and triggers an email message. By adding a condition that states that the content has the stakeholder field, we can get the token (mentioned above) and use that as one of the recipients for the email. But the email action will only pass through one of the values. That means only one person will be emailed, and that’s not good enough – we need everyone getting these emails. Rules allows us to loop actions, but not without a view that contextually displays content, which in this case is a list of stakeholder email address.

So we create a new view of content – the only field we need is a stakeholders field that has a custom formatter of tokenized text and outputs [user:mail]

viewsfield

The next step is to set a contextual filter that acts on the content node id (nid) – this should generate a list of stakeholder email address per node. Preview it to make sure by passing the view a nid.

viewsruledisplay

In order to call the view in rules, we have to give it a rules display. This also allows you to specify row variables – the important thing in this case is that we use the rendered result and the data type is text.

viewsrulevariable

With the view in place, we can go back to the rule we created and call it as a views loop.

viewsloop

Add the email action to that loop and you’re set.

rule-2

Each event that triggers an email will have its own rule, but you can use the same view/view loop structure for all of them, except comments. To get the stakeholder addresses to work in an email that’s set off by a comment, you have to create another view. This will be a view of comments that is configured almost identically to the stakeholder view, but with a relationship that joins comments to content so that you can use the nid filter.

commentsview-2
Nov 13 2013
Nov 13

One of the technologies that made a lasting impression on me, as a young programmer, was Microsoft OLE. To give my own applications the ability to embed documents created in other applications, and vice-versa, was mind-blowing! But the even bigger thrill came when I understood how the API had been architected to achieve this.

Then came the Web and we had to rebuild everything from scratch - not a bad thing really, since we're still very new at this coding thing :-)

Document sharing and embedding is one of the cornerstones of the modern Web. That's why I was happy to accept a commission to write a new module that allows views to be shared - thanks herd45!. The result is Views Share, and I am proud to release version 1.0 today. Here are some interesting bits from the code:

Views Share dialog in action

The module was originally inspired by the architecture of Share This Thing, which accomplishes a similar function for nodes. The functionality is packaged as a Views area handler, which allows to add a link to the view's header or footer - the link that opens up the sharing dialog. The dialog shows the view's original URL for sharing as well as an embed code for the view. The embed code is an IFRAME tag that points to a special URL that the module catches to render the embedded view.

The embedded view needs to be rendered in an undecorated page: no sidebars, header, footer, or any other Drupal component, except for the view itself. To do this, a new theme function is introduced. This theme renders a full page, complete with HEAD and BODY, and only incorporates the Drupal parts that are absolutely needed. Here's the code for the theme preprocessor and its template file:

function template_preprocess_views_share(&$variables) {
  global $base_url, $language;

  $variables['content'] = /* render the view here */
  $variables['title'] = $view->get_title();
  $variables['base_url'] = $base_url;
  $variables['language'] = $language;
  $variables['language_dir'] = $language->direction == LANGUAGE_RTL ? 'rtl' : 'ltr';
  $variables['head']     = drupal_get_html_head();
  $variables['styles']   = drupal_get_css();
  $variables['scripts']  = drupal_get_js();
}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">
<html lang="<?php print $language->language; ?>" dir="<?php print $language_dir; ?>">
<head>
<?php print $head; ?>
  <title><?php print $title; ?></title>
  <base href="http://thereisamoduleforthat.com/content/introducing-views-share/<?php print $base_url; ?>">
  <?php print $styles; ?>
  <?php print $scripts; ?>
</head>
<body class="views-share">

  <?php print $content; ?>

</body>
</html>

To prevent Drupal from rendering its own fully decorated pages, I call drupal_exit() right after printing the theme above.

One of the requirements of the module was to support embed previewing, based on the way Google Maps does the same: Previewing the view embedding

This was fun to implement, and there are a couple of interesting JavaScript tricks:

Opening a link in a new window

To emulate the Google Maps previewing model (and to accommodate an arbitrary IFRAME size), I needed to open the preview dialog in a different browser window. By listening to a link's click event, I was able to make it open a new window that I specified:

$('a.views-share-preview').live('click', function(e) {
    e.preventDefault();

    // Open popup in a new window center screen and listen to its messages.
    var width = 1024,
        height = 768,
        left  = ($(window).width()-width)/2,
        top   = ($(window).height()-height)/2,
        popup = window.open(
          $(this).attr('href'), 
          'views-share-preview',
          'scrollbars=1,width='+width+',height='+height+',top='+top+',left='+left
        );
});

Updating share dialog embed code with values from the preview window

Now when the user adjusts the embedding values in the preview window, I need to update the embed code in the underlying share dialog. window.postMessage is the API that allows browser windows to communicate. In my case, the preview window sends an update message to its opener:

      // http://tutorialzine.com/2013/07/quick-tip-parse-urls/
      var a = $('', { href:location.href } )[0];
      $('#edit-embed-width, #edit-embed-height').change(function() {
        var embedCode = Drupal.settings.viewsSharePreview.embedCode
          .replace('%width', $('#edit-embed-width').val())
          .replace('%height', $('#edit-embed-height').val());
        $('#edit-embed-share').val(embedCode); // update embed code
        $('iframe').replaceWith(embedCode); // update preview
        window.opener.postMessage({embedCode: embedCode}, a.baseURI); // update parent window
      });

The opener window (which contains the share dialog) receives and processes the new embed code:

    // http://stackoverflow.com/questions/8822907/html5-cross-browser-iframe-po...
    var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
    var eventWindow = window[eventMethod];
    var eventMessage = eventMethod == "attachEvent" ? "onmessage" : "message";
    eventWindow(eventMessage, function(event) {
      $('#edit-share-embed').val(event.data.embedCode);
    }, false);

Another fun bit was adding support for oEmbed. This was done in two parts: first, add the oEmbed discovery tags on the regular view page, and then respond to oEmbed calls on our endpoint.

Discovery

The oEmbed standard specifies that a page can make its oEmbed content discoverable, by adding LINK tags to its HEAD. This is what our Views area handler does in case the oEmbed option is chosen. The endpoints specified in the LINK tags are also handled by our module, just like in the regular embed case.

Rendering

An oEmbed structure is created by the module in response to an oEmbed request. Its most important payload is the embed code that can be used by the oEmbed consumer. The consumer can ask for the structure in JSON or XML format. To render JSON, drupal provides the drupal_json_output() function, but to convert an object to XML, I was able to find a simple class on PHPClasses.org that does the job:

// @see http://www.phpclasses.org/package/4657-PHP-Generate-XML-from-values-of-o...
if (!class_exists('ObjectToXML')) {
  class ObjectToXML {
     private $dom;

     public function __construct($obj) {
        $this->dom = new DOMDocument("1.0", "UTF8");
        $this->dom->xmlStandalone = true;
        $root = $this->dom->createElement(get_class($obj));
        foreach($obj as $key=>$value) {
          $node = $this->createNode($key, $value);
          if($node != NULL) $root->appendChild($node);
        }
        $this->dom->appendChild($root);
     }

    private function createNode($key, $value) {
        $node = NULL;
        if(is_string($value) || is_numeric($value) || is_bool($value) || $value == NULL) {
          if($value == NULL) $node = $this->dom->createElement($key);
        else $node = $this->dom->createElement($key, (string)$value);
        } else {
        $node = $this->dom->createElement($key);
        if($value != NULL) {
          foreach($value as $key=>$value) {
            $sub = $this->createNode($key, $value);
            if($sub != NULL)  $node->appendChild($sub);
          }
        }
        }
      return $node;
    }

    public function __toString() {
      return $this->dom->saveXML();
    }
  }
}

You can see a demo of Views Share in action on my Feeds+Views demo site. I had lots of fun building that module, now go on and use it to make your views shareable!

Attachment Size views-share-preview.png 69.66 KB
Nov 05 2013
Nov 05

Today we will look at creating a module to allow you to export your views to code. While this concept has been explored on other blogs previously, several important end steps that I find particularly useful have been left out of those discussions. I would like to remedy that here, detailing an entire process for setting up the module, displaying use scenarios, and also showing the changes that this module will make to your views administration process.

The first thing to discuss is why you would even want to export your views to code. There are a few good reasons to do this, but the most obvious one (and the one I personally find the most important) is "protection." Protection of your views and your site.

Standard views don't work until after they're saved on the site. However, since there is no such thing currently as "View Revisions," if the view ends up not functioning (or worse yet, breaks the entire site!) after saving the view, you cannot just go back to a previous version. You typically have to scrap the entire view and start from scratch. If you're just trying to make a list of content, that's not a huge deal, but if you have extremely complicated views with contextual filters, relationships, and accessing multiple parts of your site, this can at best be a headache, and at worse cost you many hours of work.

Storing your views in code allows you to have a safe place to keep a working "backup" of the view. Obviously you can just export your view and save it in a notepad file if you choose, but the method we're going to use not only backs it up, but it uses the backup as a module to display the view.

The Drupal 7 and Drupal 6 methods for this are both extremely similar. I will note the one difference below, but to be clear, this blog is speaking as though you are using Drupal 7 and Views 3.

This blog assumes you know how to create a basic custom module to use as a foundation. If you do not know how to create a basic custom module, check out our quick tutorial on the subject.

1. Hooking into views

Hooks in Drupal are just ways of modifying the website page's results.
Let's create one in our new module.

There are two hooks we will be using in this exercise: hook_views_api and hook_views_default_views. These links will take you to the drupal api pages that describe each hook.

Now let's edit the custom_example.module, and make its contents the following: <?php /* * Implementation of hook_views_api() */ function custom_example_views_api() { return array('api' => 3.0); }

This is all you need to do inside the module file. Note the api of "3.0". Here is where you will make the one change I mentioned above if you are using Drupal 6. Since Views 3.0 was never backported to Drupal 6, you will instead name "2.0" as the api. Here is the code: <?php /* * Implementation of hook_views_api() */ function custom_example_views_api() { return array('api' => 2.0); }

That's it. Now comes the fun part.

2. Providing our views

Now we will hook into 'hook_views_default_views' - basically, the purposes of this hook is to allow a module to provide views that can be read and understood by drupal. This is the main part of the blog that diverges from most other blogs on this subject. Typically, descriptions of how to export views to code end with this section, and the view is input into the hook function. However, that leaves this function very large (with a lot of view information in the function), and also makes it much more difficult to have multiple views. So the steps I will take here will be for the purpose of leaving the function clean, and providing us with a method for having as many views as you want.

Create a new file titled 'custom_example.views_default.inc' and place it in the same folder as your module. Then post the following into the file and look over the comments: <?php /** * Implements hook_views_default_views(). **/ function custom_module_views_default_views() { //Finds all files that match a given mask in a given directory //In our case, looks for any files named *.view in the /views directory $files = file_scan_directory(drupal_get_path('module', 'custom_module'). '/views', '/.view/'); foreach ($files as $filepath => $file) { require $filepath; if (isset($view)) { $views[$view->name] = $view; } } //Check that there are views in the directory //This keeps the site from throwing errors if there are no views to return if ($views) { return $views; } }

What this new hook does is it tells drupal to look into this directory that we've named (/views) and see if there are any files inside the directory using the format *.view. If there are, it takes the name of that view and sets it as the default version of that view. This will make more sense once we get a little further along, but just note that basically, this hook is telling drupal to look for your views in code.

3. Create the /views directory and start a template

Now you need to create the /views directory that is shown in the hook above. Inside your module directory, create a new folder called "views." Now create a new file and place in that directory named "view_template.view." This is an optional step, but one I find useful. Inside the template, simply paste the following: <?php //Paste Exported views Code here //Name file according to the "views_name" in exported view //Example: this_is_my_view_name.view

I use this template in a very simple way - anytime I export a new views code, I copy the exported information and paste it over my comments in the template file. Then I save-as the file with the appropriate view name.

4. An example and the "Revert" option

Let's make a quick example. You just need a list of all content on the site (note, your site already does this, but this is an example that you could literally copy from here and paste into your code and it will work, because I am using no custom cck fields, arguments, roles, etc). Open your template file created above (view_template.view), and copy the following code into the file (you can copy over the commented instructions if you like), and save the file as "all_content.view" (note that the file name is identical to value in the key-value pair on the 2nd line - $view-name = 'all_content'): $view = new view(); $view->name = 'all_content'; $view->description = ''; $view->tag = 'default'; $view->base_table = 'node'; $view->human_name = 'all_content'; $view->core = 7; $view->api_version = '3.0'; $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ /* Display: Master */ $handler = $view->new_display('default', 'Master', 'default'); $handler->display->display_options['use_more_always'] = FALSE; $handler->display->display_options['access']['type'] = 'perm'; $handler->display->display_options['cache']['type'] = 'none'; $handler->display->display_options['query']['type'] = 'views_query'; $handler->display->display_options['exposed_form']['type'] = 'basic'; $handler->display->display_options['pager']['type'] = 'full'; $handler->display->display_options['style_plugin'] = 'default'; $handler->display->display_options['row_plugin'] = 'fields'; /* Field: Content: Title */ $handler->display->display_options['fields']['title']['id'] = 'title'; $handler->display->display_options['fields']['title']['table'] = 'node'; $handler->display->display_options['fields']['title']['field'] = 'title'; $handler->display->display_options['fields']['title']['label'] = ''; $handler->display->display_options['fields']['title']['alter']['word_boundary'] = FALSE; $handler->display->display_options['fields']['title']['alter']['ellipsis'] = FALSE; /* Sort criterion: Content: Post date */ $handler->display->display_options['sorts']['created']['id'] = 'created'; $handler->display->display_options['sorts']['created']['table'] = 'node'; $handler->display->display_options['sorts']['created']['field'] = 'created'; $handler->display->display_options['sorts']['created']['order'] = 'DESC'; /* Filter criterion: Content: Published */ $handler->display->display_options['filters']['status']['id'] = 'status'; $handler->display->display_options['filters']['status']['table'] = 'node'; $handler->display->display_options['filters']['status']['field'] = 'status'; $handler->display->display_options['filters']['status']['value'] = 1; $handler->display->display_options['filters']['status']['group'] = 1; $handler->display->display_options['filters']['status']['expose']['operator'] = FALSE;

Now here is an important step. Your new view will not show up until the views cache on your site has been cleared. In Drupal 7, go to '/admin/structure/views/settings/advanced' and click the "Clear views' cache" button. In Drupal 6, go to '/admin/build/views/tools' and click the "Clear views' cache" button. Now, when you go back to your list of views, you will see your new view. You should notice one new change as well. If you hover over the options for what you can do with that view, you will see a new option, called "Revert." It will have taken the place of the "Delete" option on any views you had the option for deleting. By clicking this button, it will delete any changes that have been made to the view on the site, and will revert the view back to its default position, which will be the view as it is in code (such is the power of the hook_views_default_view hook). This means, once you have a view that is working, you can save it to code, flush your views' cache, and then edit that view to your heart's content, knowing that if you ever mess anything up too terribly, you can always "Revert" the view back to its working glory. Or for that matter, on a production site, you can allow your clients with limited knowledge of views to attempt to create and edit their own, knowing that if something is messed up, you can easily have it fixed.

I have included two images so you can see what the view would have looked like on the views list page before flushing caches, and what it looks like afterward. Note the difference between "Delete" and "Revert":

Websmiths.co Views Tutorial - Delete Option Before Flushing Cache

Websmiths.co Views Tutorial - Delete Option After Flushing Cache

(Note: When you click "Revert," on the confirmation page that follows, it will still say "Are you sure you want to delete this view?" - this is okay, it is just the wording that was used. If on the views list page, it said "Revert" instead of "Delete," you are safe to revert.)

And that's all there is to do.

Jun 07 2013
Jun 07

I'm working with the Private Message module and applied a patch for Views integration from dawehner (yay!). The patch works pretty well so I was able to create an admin dashboard that shows private messages and lets you filter the messages by subject text, body text, has been read, etc.

Then, I noticed that messages with the same message id were showing up in the results. This is because you get one private message sent to the recipient and one "sent" to the author (i.e. you get a copy for yourself when sending private messages). Ok, easy enough (I thought). I enabled Distinct in Views but that didn't work. Then I enabled Pure Distinct but that didn't work either. Bummer.

Ok, so then I remembered something about using aggregation in Views so I enabled Aggregate. Then all my results were gone except for one :/ I started googling and eventually found an issue comment that says you can't use Aggregate with Pure Distinct. Oh!

So... I turned off of Pure Distinct and low and behold I only had one result for each message id. Great! Until I looked more carefully. The "From" (author) and "To" (recipient) fields were the same for some results. Which is what I was trying to get rid of in the first place. Doh!

Then, I decided to scratch the "distinct" method altogether and do a hook_views_query_alter to add the where clause I needed:

where pm_index.recipient != pm_message.author

Should be easy, right? I tried to use the:

$query->add_where()

but couldn't figure out how to add my where clause. It appears that add_where is similar to DBTNG's condition() which is good for adding where clauses like:

where pm_index.recipient != 43


If you know how to add this where clause in Views, PLEASE LEAVE A COMMENT :) Or, better yet, if you know how to configure Views in the UI to do field1 != field2 that would be awesome!

Then I decided I would use hook_query_alter to add my where clause after Views was done with it. I've used condition() (which isn't what I needed) and looked through the docs and googled couldn't find what to do (maybe I was too tired by this point and claiming defeat :P). Then I had the recollection that I had used addExpression() before so I went down that path (whoops!). That was a dead end.

Meanwhile, I sent my plea to the twitterverse and a wonderful Fox replied with:


outside of views, api.drupal.org/api/drupal/inc…?

Well... silly me. <facepalm> There is a where() in DBTNG too. How did I miss that??? Thanks, Fox!!!

I figured my woes were solved and celebrated in advance :)

Then, I added the wonderfully simple code to my hook_query_alter:

$query->where('pm_index.recipient != pm_message.author');

and this is what ended up in the where clause:

where (11 is NULL) and (pm_index.recipient != pm_message.author)

What the?

I assumed I must have had some query alters left over... I searched and didn't. I assumed there was something wrong with my filters so I looked and all seemed fine (they were all exposed filters so none were in the query string by default). Then I used one of the exposed filters and the "11 is NULL" went away from the query.

I googled the "11 is NULL" thing and funnily enough I see it on some sites that have their query strings exposed on the page (maybe a coincidence?). But, no mention to how it gets there. I checked the Views issue queue for "11 is NULL" and "11" (though that is probably too short) and didn't notice anything obvious.

No worries! I just added a filter (not exposed) that I knew would always be okay. I set a filter so that it only grabbed messages if the body text was not null.

Voila :) Happy dance! After lots of pain and suffering, my view has my where clause added. It only needed 5 simple lines of code:

function mymodule_query_alter($query) {
  if ($query->hasTag('mytag')) {
    $query->where('pm_index.recipient != pm_message.author');
   }
}

I'm writing this up so that maybe it will help someone even if that someone is just my future self ;)

Note that I removed that body filter to try to reproduce the "11 is NULL" issue and it no longer shows up. So... my guess is that this post will be high in google for "11 is NULL"... if you are seeing this issue, my suggestion is to add a Views filter and then remove it and see the "11 is NULL" goes away ;)

Happy Friday!

Adding some keywords so that people might find this when they are running into a similar problem:

  • compare fields in views
  • compare columns in views
  • field comparison
  • column comparison
  • comparing two fields
  • comparing two columns
Attachment Size views-field-comparison.png 49.21 KB
Jun 02 2013
Jun 02

Last week, I described a technique to query and display nodes in all available translations. This worked well enough, but a performance-minded reader pointed out that the query generated by Views (that includes N self-joins for N enabled languages) would not scale to a large number of nodes.

My usual approach when implementing new ideas is to ensure the logic works first, and only handle optimization when needed. It's a strategy that has worked well for me in the past. So I set out to test this hypothesis, and to optimize the query if it was needed. Here's what happened:

The first obstacle was to generate a large set of nodes and their translations. Devel Generate, the Devel sub-module that generates Drupal objects for development purposes, does not support content translation at the time of writing. I submitted a D7 patch to the 2 years old feature request to achieve this. I tested it with 10K nodes, and it seems to work well. Your review is appreciated!

Having generated 10K nodes and their translations to Arabic and French (30K nodes in total), I cloned the Proverbs view from last time to query and display this content. The result was quite explicit: the view page never finished loading! Clearly, the Views-generated query was not scaling. And for good reason: 3 SQL JOINS of 30,000 records each is a performance black hole. Optimization was needed.

My goal for optimizing the query was to retain all the advantages that Views offers in terms of theming query results, integrating into Drupal pages, etc. - these are indispensable features when creating real-world applications. In short, I wanted to transparently override the Views-generated query. To do so, I needed to:

  • Remove the peformance-killing JOINs from the query
  • Perform an optimized query to find node translations
  • Re-insert the results from the optimized query into the Views results, to allow it to proceed with the display

The code I used follows. I will explain the important parts below.

Remove the peformance-killing JOINs from the query

The function demo_i18n_views_query_alter() removes from the Views query object all references to the SQL JOINs, which are called "relationships" in Views parlance. Views core invokes this hook just before converting the query object into an SQL statement. The resulting query that Views will execute looks like this:

SELECT node.nid AS nid, node.created AS node_created
FROM {node} node
WHERE (( (node.status = '1') AND (node.type IN  ('multilingual_node')) AND (node.tnid = node.nid OR node.tnid = 0) ))

Perform an optimized query to find node translations

The query as modified above will only return nodes that are translation sources. It's now up to me to query the node translations, by waiting for Views to execute the modified query, and then gathering the nids to find their translations (as stored in {node}.tnid). This is a simple query using the SQL IN operator. I call this hand-made query in the demo_i18n_views_post_execute() function, which is invoked by Views after it executes its own query.

Re-insert the results from the optimized query into the Views results

The challenge with the new query is that it returns one node translation per row, as opposed to the original query which returned all translations on the same row. In addition, the results need to be copied into the view::result object, with the right key names that Views expects. In order to find the right key names, I first displayed the results from the unmodified Views query and noted the result keys. With this information, I then proceeded to loop over the optimized query results, and find the corresponding entry in the Views result array that would receive them. This loop is also implemented in the demo_i18n_views_post_execute() function.

The results were impressive! The view page loaded in very acceptable time (ApacheBench reports a mean time of ~1350ms, against ~650ms in the case of a view with just 4 nodes), and Views happily themed the translated nodes as if it had queried them itself. You can see this code in action on my i18n demo site.

The approach of hand-crafting Views queries has been on my mind for a long time, and I'm glad I took the first step. So far, I am not sure that a generic module can be created out of this, mainly due to the necessity to transform the result set after the optimized query is run. In any case, I'll be applying this technique in my projects!

May 25 2013
May 25

Here's a little puzzle: display a table of nodes, each row containing the same content in all available translations.

Then, a couple of days ago, someone asked me if I had solved it. I hadn't thought of that puzzle since then, but I would have felt bad answering no. So, with 3 years of i18n work under my belt, I decided to give it another go. I did find a solution this time, but it's not optimal, and it required coding. You can find a demo of the solution online. Demo of solution

The basic idea is to select the nodes in their source language, then relate each node to all its translations. To do this, the view is built by filtering on Content translation: Source translation, then adding one Content translation: Translations relationship per language. Nodes and translations

Now this view works pretty well, except for nodes that are not translated: although they are picked up by the SQL statement, the related nodes in each language are empty, since the tnid is not set for untranslated nodes. That's where I had to write a new join handler that not only joins the source language node to its translation, but also joins it to itself in case there are no translations. The following code silently replaces the standard join handler for Content translation: Translations with this new one:

The resulting query will look like the following - note the JOIN clauses:

SELECT node_node_1.title AS node_node_1_title, 
            node_node_1.nid AS node_node_1_nid, 
            node_node_1.language AS node_node_1_language, 
            node_node_2.title AS node_node_2_title, 
            node_node_2.nid AS node_node_2_nid, 
            node_node_2.language AS node_node_2_language, 
            node_node.title AS node_node_title, 
            node_node.nid AS node_node_nid, 
            node_node.language AS node_node_language, 
            node.created AS node_created
FROM {node} node
LEFT JOIN {node} node_node 
           ON (node.nid = node_node.tnid OR (node_node.tnid = 0 AND node.nid = node_node.nid)) AND node_node.language = 'ar'
LEFT JOIN {node} node_node_1 
           ON (node.nid = node_node_1.tnid OR (node_node_1.tnid = 0 AND node.nid = node_node_1.nid)) AND node_node_1.language = 'en'
LEFT JOIN {node} node_node_2 
           ON (node.nid = node_node_2.tnid OR (node_node_2.tnid = 0 AND node.nid = node_node_2.nid)) AND node_node_2.language = 'fr'
WHERE (( (node.status = '1') AND (node.type IN  ('proverb')) AND (node.tnid = node.nid OR node.tnid = 0) ))

The careful reader will have noticed that there's one extra database JOIN in my solution: the one that joins the source language node to itself. If you have a suggestion to remove it, please let me know!

AttachmentSize 104.25 KB 22.44 KB
May 09 2013
May 09

Average: 5 (3 votes)

I rencently spent a few quality hours with the Views interface trying to figure out how to add an Organic Groups Group ID contextual filter to a Views display and have the display's title overridden based on the value of the contextual filter. Actually, it's easy to do if you don't mind having the actual Group ID integer in the title. But, like most people, I actually wanted the Group name in the title of my display.

It took me more time that I'd care to admit, as well as some guidance from the most excellent maintainer of the Organic Groups module, Amitai Burstein, but eventually, I discovered a simple solution that didn't involve additional relationships, contextual filters, fields, or trickery. Well - maybe not the "trickery" part. The solution involved what I consider to be a previously undocumented feature (at least to me!) of the Views module.

original contextual filter settings

When I first set up the view, I knew that the incoming values of the contextual filter (Group ID) were going to be integers (they are IDs, and this is Drupal after all). I needed to figure out how to override the title so that the Group ID wasn't used in the title, but rather the Group name was displayed. After all, I didn't want the resulting title to be "12341 Happenings" (where "12341" is the Group ID), rather I wanted the title to be "Miami Happenings" (where "Miami" is the Group name whose Group ID is "12341"). I was hoping that this would happen automatically, but it did not.

I spent several hours toying with various potential solutions - everything from using the display's Format|Grouping functionality to adding an additional relationship and using the "Content: title" field as the contexual filter instead of the Group ID (while the second option basically worked, it could have led to issues if there were multiple groups with the same name or funky characters in group names). Nothing I tried worked as well, or was bullet-proof enough to my liking.

It wasn't until Amitai told me that if I use Group ID as the contextual filter, I can override the title in the contextual filter's settings and Views will automatically substitute the Group ID with the Group title as long as the contextual filter is validated! That is, if I select the "Specify validation criteria" option in the contextual filter's settings and validate it using the (in my case) "OG group" validator, and Views will magically make the substitution. Seriously!

original contextual filter settings

When I was first informed of this, I was both overjoyed (that my issue had been solved) and aghast (at the incredibly non-obviousness of the solution). I've been around Drupal for quite some time, and this was one "feature" that had snuck past me. There isn't a clue in the Views interface about this functionality (in the maintainers' defense I can't think of a good way to add this to the description of the "Override title" setting without adding at least three paragraphs), nor could I find any indication of it using Advanced Help (low-hanging fruit for a patch!) Ultimately, I went to the relevant Views documentation page and didn't see anything about it there as well. It's there now, I just added it a few minutes ago (I love open-source).

Views is a seriously complex module with lots of moving parts, written by volunteers that are much smarter than I am. I'm sure there's more "sneaky" functionality that I'll discover in the future, and I'm looking forward to it.

Trackback URL for this post:

http://drupaleasy.com/trackback/589

May 06 2013
May 06

I've needed to build a regular expression filter for a view I'm working on, so I'm sharing the code here because it might be helpful to other people as well. My specific case is that I am building a Blocks administration VBO. I'd like to let the administrator filter on block body content, and allow them to enter a regular expression as a filter.

I first declare the relevant field in the Views schema for the block table:

// @file views_block.views.inc
function views_block_views_data() {
  [..]
  // Body
  $data['block_custom']['body'] = array(
    'title' => t('Body'),
    'help' => t('The block body.'),
    'field' => array(
      'handler' => 'views_handler_field',
    ),
    'filter' => array(
      'handler' => 'views_handler_filter_regex',
    ),
  );
  [..]
  return $data;
}

Now adding the regex handler is a matter of implementing the views_handler_filter_regex class. I want my handler to support MySQL, PostgreSQL, and any other database system that supports regular expressions. Here's some minimal code to achieve this:

// @file views_handler_filter_regex.inc
class views_handler_filter_regex extends views_handler_filter {
  var $always_multiple = TRUE;

  function operator_options() {
    // Return placeholders that will be expanded at query creation time.
    return array(
      'match' => t('Matches regex'),
      'nomatch' => t('Does not match regex'),
    );
  }

  function admin_summary() {
    if (!empty($this->options['exposed'])) {
      return t('exposed');
    }
    return parent::admin_summary();
  }

  function value_form(&$form, &$form_state) {
    $form['value'] = array(
      '#type' => 'textfield',
      '#title' => t('Value'),
      '#size' => 30,
      '#default_value' => $this->value,
    );
  }

  function query() {
    // Find actual regex operators depending on database type.
    $db_type = Database::getConnection()->databaseType();
    switch ($db_type) {
      case 'mysql':
        $match = 'REGEXP';
        $nomatch = 'NOT REGEXP';
        break;
      case 'pgsql':
        $match = '*~'; // case insensitive match
        $nomatch = '!*~'; 
        break;
      default:
        // Allow other modules to define these operators.
        $operators = &drupal_static(__METHOD__);
        if (empty($operators)) {
          // hook_views_regex_operators($db_type)
          // @param $db_type - the type of database engine being used ('mysql' and 'pgsql' will not be called).
          // @return array('match' => operator for matching, 'not match' => operator for negative matching);
          $operators = module_invoke_all('views_regex_operators', $db_type);
        }
        if (empty($operators)) {
          watchdog('views_regex', 'No regex operators found for database type %type. Using operator LIKE instead.', array('%type' => $db_type));
          $match = 'LIKE';
          $nomatch = 'NOT LIKE';
        }
        else {
          $match = $operators['match'];
          $nomatch = $operators['not match'];
        }
    }
    // Replace placeholder with actual operator.
    $this->operator = $this->operator === 'match' ? $match : $nomatch;
    parent::query();
  }
}

That's it! Short and sweet.

Apr 29 2013
Apr 29

Average: 4.8 (8 votes)

FarmersMarkets.com

At Florida DrupalCamp 2013, I presented a session that demonstrated how to utilize the Feeds, Feeds Tamper, Address field, Geofield, and other modules to create a fully-functional website for searching for Farmers Markets anywhere in the United States. While the session's intent was to inspire people as to what Drupal can do in a very short amount of time, this blog post will focus on the details of the process.

A few years ago, I built a similar presentation using world-wide earthquake data, importing into a Drupal 6 site using Table Wizard and displaying the data using the Mapstraction module. I must have given that presentation about half-a-dozen times over the course of a year or so at various meetups and camps, so I thought now was a good time to bring it up-to-date with modern (relatively-speaking) Drupal tools.

StopwatchBefore we get started, let me point out that the title is a lie. It's actually going to be more than 7,000 records, but I like the way the "5,000" and "45" play off each other. The first time I did this demonstration in front of an audience, it actually took me only 25 minutes, 26 seconds - the rest of the presentation time was taken up with some initial slides and furious betting on how long it would actually take me (the winner got a copy of Mapping with Drupal).

The Farmers Markets Source Data

Data.gov logoThe first step in a project like this is to find some good clean source data. I'm a big fan of the seemingly infinite supply of publically available data found on Data.gov, the United States' public repository of federal government data. After poking around for a bit of time (I'm embarassed to say exactly how much!), I stumbled upon the Farmers Markets Geographic Data - a Microsoft Excel-formatted dataset containing data on over 7,000 Farmers Markets all over the United States. The dataset contains names, descriptions, addresses, websites, and other details - most importantly, it contains the latitude and longitude for each location. While not mandatory, having the latitude and longitude data sure does make the process easier.

Farmers Markets spreadsheetInspecting the data in a spreadsheet, things looked pretty clean. Since I knew I needed to save the file in comma-separted-values (.csv) format, I did some very minor cleanup on it by doing the following:

  • Removed the top 3 descriptive header rows. For the import, all we actually need is a column/field name header row and the data. Any anciliary header rows need to be removed.
  • Removed the bottom 2 descriptive footer rows. For this dataset, there was a row at the bottom of the dataset that contained information about when the dataset was last updated. This wasn't needed for the import, so I manually deleted it.

Additionally, I took note of the following things:

  • The FMID field (I'm assuming this is an acronym for "farmers market identifier") appears to be a unique integer for each record in the dataset. This will come in handy during import.
  • There was no "country" field. This isn't unexpected since this was data for United States' farmers markets, but I did take note of it because the Address Field module will be looking for country data. I could have simply added a new "country" field (with all values set to "United States") to the dataset prior to exporting it as a .csv file, but I prefer to keep the dataset as "pure" as possible, so I decided to leave it alone for now and deal with the country stuff as part of the import process (see below).
  • The data in the "State" column included full state names, not the standard 2-letter abbreviations. I knew that this would need some tampering (via the Feeds Tamper) module to convert it so that it would import cleanly.
  • The "Schedule" field for some records is longer than 255 characters. This means that we'll have to use a "Long text" field type in the content type to handle the data in this field.
  • The data also included 20 "category"-type fields indicating the types of goods available at each farmer's market (eggs, cheese, soap, trees, etc...) Ideally, each of these 20 fields should be mapped to a single Drupal vocabulary. This would require a custom Feeds Tamper plugin.
  • The "lastUpdated" field mostly contains well-formatted dates, but because there are some records where the data is not well-formatted ("2009" instead of "mm/dd/yyyy") and it is just informational, its probably best just to use a text field in the content type for this data.

Once I was satisfied that the data was clean and I had a good understanding of it, I saved it as a .csv file and moved onto getting Drupal ready to import it.

Setting Up the Basic Site

As with most of the sites DrupalEasy builds, we started out with our own custom Drush make file that automatically downloads a bunch of standardish modules we use on every site as well as our own custom installation profile that does some initial site configuration (turning off the Overlay, enabling the Administration Menu module, etc...) This enables us to get a basic site up-and-running in just a few mintues.

Next, we need to download and enable the modules that we're going to need:

  • Geofield - be sure to use a version of the 7.x-2.x branch dated later than 2013-Apr-07

Depending on whether or not you start with our custom make file, there may be other modules that are dependencies of the ones listed above that will also need to be downloaded and enabled.

If you use Drush, the following command will enable all the necessary modules:

drush en addressfield feeds_ui feeds_tamper_ui geofield 
geofield_map job_scheduler feeds geophp rules_admin openlayers_ui

Creating the Farmers Market Content Type

Once the site is up-and-running, the first step is to set up something for all of the data to be imported into. In this case, hopefully it is obvious that we need to create a new content type with fields that roughly match the fields in our source file. By creating a node for each Farmers Market, once imported we can leverage all of the tools in the Drupal universe to interact with them as we build out the site.

Create a new content type (admin/structure/types/add) with the following properties (throughout this post, any properties/attributes/settings not specifically mentioned can be left at their default values):

  • Name: Farmers Market
  • Disable "Promoted to front page"
  • Disable "Main menu" from "Available menus"

Moving on to the fields (admin/structure/types/manage/farmers-market/fields):

  • Delete the "Body" field
  • Add "Address" field: type=Postal Address, Available countries=United States, enable "Hide the country when only one is available"
  • Add "Lat/Long" field: type=Geofield, Widget=Latitude/Longitude
  • Add "URL" field: type=Link, Link Title=No Title
  • Add "Location details" field: type=Text
  • Add "Schedule" field: type=Long text
  • Add "Last updated" field: type=Text

Farmers Markets content type

One thing to note is that once the import is complete, we're going to go ahead and enable the Geocoder module so that any Drupal-side address updates to any Farmers Market nodes will be automatically updated with the proper latitude/longitude coordinates. We don't want to enable this functionality prior to import otherwise the module will attempt to geocode each address from the source file during import. This is completely unecessary since the source file already includes latitude/longitude data. Plus, Google Geocoder limits non-paid users to 2,500 requests per day - unless you pay for more.

Creating the Importer

At this point, we have the source data (the .csv file) and the destination (the "Farmers Market" content type). The next step is to create the mechanism that will actually transfer the data from the source to the destination. We'll use the Feeds module to do this. The Feeds module is designed to take data from a variety of sources (most commonly RSS feeds and .csv files) and map it to Drupal entities (usually nodes, but not always).

Add a new importer (admin/structure/feeds) named "Farmers Markets Importer". The "Edit" page for importers has 4 major sections. Let's look at each one in detail.

Basic settings

This section consists of the general configuration of the importer. For this project, use the following settings:

  • Attach to content type = Use standalone form. Note that this isn't referring to were the content is going to go, it is referring to the importer itself. In our case, since we only have a single data source, a standalone form is fine. If were were planning on importing data from multiple sources, a custom importer content type might be necessary.
  • Periodic import = Off. This setting is primarily used for automatically checking a feed for new data. For a one-time .csv import, it is not necessary.
  • Import on submission = Enabled. This triggers the import to start whenever a new .csv file is uploaded on the main import (/import) page.

Fetcher

This section sets the mechanism that actually interacts with the source data. We need to change the Fetcher from "HTTP Fetcher" (commonly used for RSS feeds) to "File upload". Looking at the settings for the "File upload" fetcher, all the default values are fine, so no changes are necessary.

Parser

This section sets the process that will be used to parse the source data into a format that the "Processor" (next step) can understand. In our case, we need to change the Parser from "Common Syndication parser" to "CSV parser". Again, the default settings for the "CSV parser" are fine as-is. It is interesting to note that the Feeds module is easily extensible. Custom Fetchers, Parsers, and Processors can be written to handle virtually any type of incoming data.

Processor

This final section is were the parsed source data is mapped to the proper place in Drupal. In our case, the default "Node processor" is what we want (since we're mapping the data into our new "Farmers Market" content type). The settings for the Node processor are as follows:

  • Update existing nodes = Update existing nodes (slower than replacing them). This is a big win for us, and is only possible because of the FMID field in the source data. This means that when an updated source dataset is available, (assuming the field structure hasn't changed) we can simply re-run our importer on the new file and upload only the records that have changed. In other words, we won't have to delete (including any user comments and Drupal-side updates) and re-import Farmers Market nodes. This will allow us to keep the site up-to-date with a minimum of work.
  • Content type = Farmers Market. This is where we tell the importer that we're going to be generating nodes of the Farmers Market content type with the imported records. This is the first link between the source and destination.
  • Author = admin (or any user on your site). It's fine to leave it as "anonymous", but I'd rather have my nodes "owned" by an actual user.

The final (and most tedious) step is to set up the mapping of fields between the source and destination. In other words: data from each source fields needs to know which destination field it will go into. It is important to note here that the source field names must be entered exactly as they appear in the source data file. The mappings for this importer are (Source = Target):

  • MarketName = Title
  • FMID = GUID - set this field to "Unique" then be sure to click to "Save" the mapping.
  • Street = Address: Thoroughfare
  • City = Address: Locality
  • State = Address: Administrative area
  • Zip = Address: Postal code
  • x = Lat/Long Longitude
  • y = Lat/Long Latitude
  • Website = URL: URL
  • Location = Location details
  • Schedule = Schedule
  • updateTime = Last updated

Be sure to double-check that the "FMID" field is set to unique!

Farmers Markets content type

Massaging the Data

As I indicated in the "Farmers Markets Source Data" section above, there are a couple of things we need to do in order to get the dataset to import cleanly: set the default country ("United States") and translate the full state name to the 2-letter abbreviation ("New York" to "NY", for example).

Setting the Default Country

Setting the default country field for every record on import is actually a fairly simple operation to set up - assuming you're aware that the Feeds module exposes a "Before saving an item imported via [importer name]" event for each Feeds importer. This allows us to step in the middle of the import process and set a data value as we wish.

From the main Rules configuration page (admin/configure/workflow/rules), add a new rule named "Add default country for imported markets" that reacts on the "Before saving an item imported via Farmers Markets Importer" event.

Next, add an "Entity has field" condition with the Data selector=node and Field=field_address. This ensures that the country field exists (it is part of field_address) and (more importantly) is available for us to set its value in the next step.

Finally, add an "Set a data value" action with a Data selector=node:field-address:country and a Value=United States. Click to save everything it's done!

Add default country for imported markets rule

Translating the State Names

The second data issue that we need address during data import is that of the "State" field. We need a mechanism were we can automatically translate full state names into their 2-letter abbreviation. I turned to the Feeds Tamper module for this, as it is relatively straight-forward for a developer to create a custom plugin that can be assigned to any field via the Feeds Tamper interface. The source data is then run through the plugin code to make any necessary changes. Unfortunately, a plugin had to be written for this application - I have contributed it back to the community, but the module author has not acted on it as of April, 2013.

If you're not familiar with applying patches, feel free to download the state_to_abbrev_inc.zip file, uncompress it, and place it in your feeds_tamper/plugins directory.

Once the plugin is installed, it needs to be assiged to the "State" field. This is done by clicking "Tamper" link for our importer from the main "Feed importers" page (admin/structure/feeds). Then, add the "Full U.S. state name to abbrev" plugin to the "
State -> Address: Administrative area" field.

Tamper with the State field

Import!

At this point, everything is ready to proceed with the import. Navigate to the main "Import" page (/import) via the Navigation menu and click the "Farmers Markets Importer". Select the file to upload and click to "Import".

I like to test things with a small version of the main source file - one with only a handful of records. This is helpful in making sure everything is being imported correctly without having to wait for all 7,000+ records to be processed. I check things by inspecting a few of the newly created Farmers Market nodes, ensuring fields are populated as expected. If I'm satisfied, then I go ahead and run the import with the full data set.

On my particular local machine, importing all 7,000+ records took about 6 minutes.

Setting Up the Proximity Search

One of the features that really makes location-based content useful is proximity searches: being able to allow the user to "show me all the things near a particular location". For this example, we're going to use the built-in proximity functionality of the 7.x-2.x version of the Geofield module. We'll create a view that exposes a proximity filter that incorporates geocoding by allowing the user to enter any valid location data into the "origin" textfield. That is, the user can query for farmers markets within 10 miles of "Sacramento, CA", "06103", or "1600 Pennsylvania Ave, Washington, DC" - any text that the active Geocoder (usually Google Geocoder) can parse.

Exposed Views proximity filter

Once the import is complete, enable the Geocoder module. Then, create a new view named "Proximity search". On the initial views wizard page, "Show Content of type Farmers Market" (sorting doesn't matter yet). Create a page with a Display format" of "Geofield Map". Set the "Items to display" to 100 (just to make sure we never overwhelm the map with points), and disable the pager. On the main views interface:

  • Click to add the "Content: Lat/Long" field - exclude from display, Formatter=Latitude/Longitude
  • Click to add the "Content: Lat/Long - proximity" field - exclude from display, Source of Origin Point=Exposed Geofield Proximity Filter, Unit of Measure=Miles
  • Click to add the "Content: Lat/Long - proximity" filter - Expose this filter to visitors, enable "Remember the last selection", Operator=Is less than or equal to, Source of Origin Point=Geocoded Location. Be sure to add a default value for the exposed filter ("10 miles from New York, NY") or you may see a nasty little bug (http://drupal.org/node/1871510)
  • Remove the "Content: Post date" sort criteria
  • Edit the settings of the Geofield Map format: Data source=Lat/Long, Popup Text=title

Once complete, save the view, then navigate to the /proximity-search (or whatever URL you set for the page display of the view) and give it a whirl!

Proximity search

Pimping the Display (and Functionality) of Farmers Market Nodes

At this point, if you click on a Farmers Market pin, then click through to a particular Farmers Market node, the display of the node is less-than-impressive.

Initial Farmers Market node display

With just a little bit of effort, this can be greatly improved. We'll rearrange the order of fields, tweak the display a little bit, add a map, and incorporate Geocoder functionality for address updates.

OpenLayers Map

To keep things interesting, we're going to use the OpenLayers module for the map display on the individual Farmers Market nodes. First, we'll need to edit the OpenLayers map that we're going to utilize. Go to the main OpenLayers "Maps" page (admin/structure/openlayers/maps), and click to edit the geofield_formatter_map (the description of the map should explain why we're using this one - it is designed to handle the display of Geofield output). There's lots of available settings for each map, we'll only make a few small configuration changes:

  • Basics section: Width=auto, Height=250px
  • Layers and Styles section: only the "OMS Mapnik" layer should be enabled and set to default, set the styles for "Placeholder for Geofield Formatter" to "Marker Black Small"
  • Behaviors section: Point Zoom Level=14

Once the map is configured, we can utilize it on the Lat/Long field of our Farmers Market content type. Go to the "Manage Display" page (admin/structure/types/manage/farmers_market/display) and change the format of the "Lat/Long" field to OpenLayers. Click to save and test.

Reordering the Display Fields

While we're on the "Manage Display" page of the Farmers Market content type, rearrange the fields as follows:

  1. Lat/Long: Label=Hidden
  2. Location details: Label=Hidden
  3. Address: Label=Hidden
  4. URL: Label=Hidden
  5. Schedule: Label=Inline
  6. Last updated: Label=Inline

With these changes, things improve quite a bit.

Pimped Farmers Market node display

Enabling the Geocoder for Address Updates

Finally, now that all the data is imported, we can go back and modify the Lat/Long field to automatically be updated by the Geocoder module whenever the node is updated (in case the address changes). From the "Manage Fields" page for our content type (admin/structure/types/manage/farmers_market/fields), click the "Latitude/Longitude" widget for the "Lat/Long" field, change the widget to be "Geocode from another field", then continue to click to edit the field configuration and ensure the "Geocode from field" option is set to "Address". Click to save.

Are We Done?

At this point, we have a fully functional site where users can search for farmers markets near them, then click to view the details on ones that interest them. Since the farmers markets are nodes, we can leverage all the great modules available from Drupal.org to futher extend and enhance the site.

With just a few additional modules, a contribued (responsive) theme with just a few extra lines of CSS, and some publically available imagery, it's quite simple to produce a usable site - just like FarmersMarketsNow.com!

Extra Credit - Utilizing the Source Dataset's Category Fields

Still reading? Congrats - you're in it for the long haul. Wondering how we can leverage the category data from the source file? Here are the steps:

  1. Before creating the Farmers Market content type, create a new "Categories" vocabulary.
  2. Add a "Categories" term reference field to the Farmers Market content type and set the "Number of values" to "Unlimited".
  3. Install the taxonomy_inc.zip Feeds Tamper plugin in the feeds_tamper/plugins directory. This is a custom pluging that is specific to this particular source file. It takes data from all the category-type fields in the source file and imports them to the new "Categories" vocabulary.
  4. Add a new mapping field to the importer. The way the Feeds Tamper plugin was created, only one needs to be added. Use "Bakedgoods = Categories".
  5. Utilize the custom "Taxonomy Y/N" Feeds Tamper plugin on the "Bakedgoods -> Categories" field.

Rerun the import and see the magic! Note that the extra processing for the categories really slows down the import quite a bit. I'm sure that there are other ways of importing the category-type fields to a single vocabulary, let me know in the comments if you know of an easier method.

Trackback URL for this post:

http://drupaleasy.com/trackback/575

AttachmentSize 1.21 KB 1.34 KB 763 bytes
Mar 30 2013
Mar 30

Few month ago I developed  a simple module called Commerce Order2pdf and then I also added custom tokens as you can read from this post Custom token creation for Drupal 7 and now I reviewed this module a bit and planned to add views field handler (This means I can create custom view with download links). 

So how did I implemented this?

First step was to edit commerce_order2pdf.module file and add hook_views_api. So I could start working with views.

/**
 * Implements hook_views_api().
 */
function commerce_order2pdf_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'commerce_order2pdf') . '/includes/views',
  );
}

Next one was to create subdirectories includes/views into my commerce_order2pdf folder (module folder).

After this I had to tell views about my data structure.
So I did created new file /includes/views/commerce_order2pdf.views.inc and added hook_views_data.

/**
 * Implements hook_views_data()
 */
function commerce_order2pdf_views_data() {
  $data = array();
  $data['commerce_order']['order2pdf_link'] = array(
    'field' => array(
      'title' => t('Download pdf order'),
      'help' => t('Provide a simple link to download the order.'),
      'handler' => 'commerce_order2pdf_handler_field_order_pdf_link',
    ),
  );
  return $data;
 }

In this array I did extended commerce_order group and added field handler class callback.

Now we will add a new file commerce_order2pdf_handler_field_order_pdf_link.inc to includes/views folder.

/**
 * Field handler to present an order pdf download link.
 */
class commerce_order2pdf_handler_field_order_pdf_link extends commerce_order_handler_field_order_link {
  function construct() {
    parent::construct();
  }

  function render($values) {
    $order = commerce_order_new();
    $order->order_id = $this->get_value($values, 'order_id');
    // Add hash key for a download link.
    $hash = hash('md5', $order->order_id . $values->commerce_order_created);

    $text = !empty($this->options['text']) ? $this->options['text'] : t('Download');
    return l($text, "order2pdf/{$order->order_id}/{$hash}");
  }
}

My last step is to say Drupal about these files in commerce_order2pdf.info

For this step I will just add two rows to my .info file. 

files[] = includes/views/commerce_order2pdf.views.inc
files[] = includes/views/commerce_order2pdf_link_handler.inc

Now you can add views download field to your view. (If you make thses changes after you enabled your module then you can just clear caches and it will work).

Edit 13. April 2013 
Just found a good videos

[embedded content]

[embedded content]

Mar 15 2013
Mar 15

Rather remarkably we’ve managed to avoid the top xxx module list for Drupal 7… however to recap the presentation yesterday at ACCSVa.org here it goes….

A Drupal Roadmap with Rich Gregory – Look in to some critical dev tools like Drush and other things to get you going.

1.  Display Suite (putting Views at the top almost redundant….) – thanks to Display Suite and it’s buddy Field Group Drupal 7?s core content creation kit is a flexible dashboard delivery tool.  With a few clicks you can now turn a lengthy and unintuitive form into a dashboard – i’m seeing hope for a wordpress like content adding area.

and after

and after DS + FG

Forms before Display Suite and Field Group

Forms before Display Suite and Field Group

2. Views – it should go without saying, and now that it’s going to be a part of Drupal 8 core I’m going to leave it at that… you need views to do anything worth doing.  We’ve got a half dozen or more tutorials on views here, so dig in.

3. Context – this is your logic layout tool – pick conditions and reaction.  There are numerous modules to extend context as well – in the presentation I mentioned http://drupal.org/project/context_reaction_theme however this only has a D6 option.   You’ll probably need to use http://drupal.org/project/context_addassets to do essentially the same thing.  Also note that Mobile Tools allows you to use contexts to do dramatic theming changes based on the mobile device.

First up choose the conditions for your layout

First up choose the conditions for your layout

The choose your reactions

The choose your reactions

4.Rules: Rules allows your site to become a dynamic workflow management intranet style workhorse. The amount of flexibility here, much like Views, extends beyond the scope of a simple “short stack” review, however in essence you’re taking events that happen within the site, or custom crontab events, setting conditions and triggering actions. Coupled with modules like Views Rules the possibilities are amazing.

5. Entity reference - extending CCK (part of drupal 7 core) the up-and-coming successor to References. Allow content to reference other content, and as mentioned this allows View Relationships to create a SQL JOIN on your content – get more information about your Content Author, and many more options…this post here is particularly fun with references referencing references…

6. Honorable mention: Feeds – this is the bulk content migration tool of choice for folks like myself.  It’s intuitive and lets you harvest content from various sources and ingest it in to your content types, user tables, etc.. we have a few tutorials on feeds that may help you with some specifics – it’s a powerful tool, and coupled with tools like feeds tamper there are a lot of options.

7. Honorable mention: Flag.  Give your users buttons and they’ll push them.  Flags allow your users to have simple on/off buttons – categorize content, flag spam, etc…  they of course work with views, rules, and the rest of the gang :)

So there’s my short stack for Drupal 7 – I’m sure entities and entity forms probably belong on there, however for most basic sites I think this is a good start… heck probably need to talk wysiwyg editors too…. so many modules!  Thanks again to ACCSVA.org for the conference, Rich Gregory for the great tunes and the lift, and  Blue Cat Networks – the hat is bangin.

Mar 13 2013
Mar 13

Episode Number: 

127

Update: If you want the labels to show up, install the jquery_update Drupal module and inside the configuration options select jQuery version 1.8. This will fix the label issue.

The Drupal 7 FooTable Module makes it easy to build responsive tables using the Drupal 7 Views module. The FooTable module makes a table responsive by allowing the user to select which fields should be hidden at various responsive breakpoints for mobile and tablet devices. Any hidden fields are then displayed below the table in an expandable section. The module still needs a little work, but is a great way to get started with responsive design and responsive layouts in Drupal.

In this episode you will learn:

  • How to install and configure the FooTable Drupal module
  • How to create a responsive Drupal views table using the FooTable Drupal module
  • How to test the various responsive breakpoints using the Web Developer toolbar chrome extension

Thanks to BuildAModule.com for sponsoring this episode of the Daily Dose of Drupal.

DDoD Video: 

Mar 04 2013
Mar 04

Our use case seemed pretty straightforward.  When users register on the site they select terms from a taxonomy list based on a field in their profile2 profile.  As content is added to the site the same taxonomy is used to categorize content.  We wanted a view that would let users see any content that matched any of their taxonomy term selections.

I’m on the drupal4lib mailing list and want to thank all those who responded, specifically Kelly Lucas aka krlucas who is quoted below

Start with a base View that shows Users (not Content).
1) Set a Contextual Filter for User ID, provide a default value and set it the
logged-in User’s ID
2) Create a relationship from the user to the user’s profile
3) Create a relationship from the field containing the select terms on the
user/user profile to the taxonomy terms
4) Create a relationship from the taxonomy terms to the nodes that have that
taxonomy term.

You should now be able to add the relevant node fields to the View.

walkthrough of a view

walkthrough of a view

using the query settings "distinct" to limit the values instead of one per term

using the query settings "distinct" to limit the values instead of one per term

whatcha get from all these relationships

whatcha get from all these relationships

What didn’t work: So while this seemed incredibly straightforward getting this many-to-many relationship to work caused me endless grief.  I tried views global filter thinking that might be a good solution – according to their documentation any fields pulled from the profile are used to override the values.  Using the multiselect widget this simply failed all over the place – values didn’t save at all, weirdness reigned, and general misery ensued.*

Also in the “fail” category – making a view based on the content table and the taxonomy table.  Either of these might have worked with a few more relationships, however at this point I think we’re stocked up.

Thanks to all involved who helped on this – Cary Gordon, Brazos**, and of course Kelly Lucas

*fwiw we read the entire issue queue and documentation, however this statement really hurt Rik’s feelings.  Would someone please hug him down under?

**Brazos added another interesting option of using page manager to override the user page and pass variables along that way – I’m not experienced in this line of development to implement, but it seems interesting.

Mar 04 2013
Mar 04

Episode Number: 

120

The Views Data Export Module provides an easy way to export your views data in many different formats (CSV, XLS, DOC, TXT, XML, etc).

In this episode you will learn:

  • How to export Drupal 7 views data as a CSV file
  • How to export Drupal 7 views data as an XLS file
  • How the Drupal views page display is handled independently of what is actually exported in the Views data export

Thanks to Drupalize.me for sponsoring this episode of the Daily Dose of Drupal.

DDoD Video: 

Feb 25 2013
Feb 25

In the Drupal community, we always recommend using the Drupal API, and best practices for development, management and deployment. This is for many reasons, including modularity, security and maintainability.

But it is also for performance that you need to stick to these guidelines, refined for many years by so many in the community.

By serving many clients over many years and specifically doing Drupal Performance Assessments, we have seen many cases where these guidelines are not followed, causing site slowdowns and outages.

Here are some examples of how not to do things.

Logic in the theme layer

We often find developers who are proficient in PHP, but new to Drupal misuse its API in many ways.

In extreme cases, they don't know they should write modules to house the application logic and doing data access, and leave only presentation to be done in the theme layer.

We saw a large site where all the application logic was in the theme layer, often in .tpl.php files. The logic even ended with an exit() statement!

This caused Drupal page caching mechanism to be bypassed, resulting in all page accesses from crawlers and anonymous users to be very heavy on the servers, and complicating the infrastructure by over-engineering it to compensate for such a development mistake.

Using PHP in content (nodes, blocks and views)

Another common approach that most developers start using as soon as they discover it, is placing PHP code inside nodes, blocks or views.

Although this is a quick and dirty approach, the initial time savings cause lots of grief down the road through the life cycle of the site. We wrote an article specifically about that, which you will find a link to below.

Heavy queries in the theme layer, when rendering views

In some cases, the logic for rendering individual nodes within a view is complex, and involves code in the view*.tpl.php file that has SQL queries, or calls to heavy functions, such as node_load() and user_load().

We wrote an article on this which you can find the link to below.

Conclusion

Following Drupal's best practices and community guidelines is always beneficial. Performance is just one of the benefits that you gain by following them.

Further reading

Feb 23 2013
Feb 23

Episode Number: 

114

The Drupal 7 Views Calc module provides an easy way to do calculations on the numeric data that is displayed within a Drupal 7 View.

In this episode you will learn:

  • How to create a Drupal 7 Views Calc view
  • How to use Views Calc to perform calculations such as SUM and AVG on a Drupal 7 field within a View

Thanks to Drupalize.me for sponsoring this episode of the Daily Dose of Drupal.

DDoD Video: 

Feb 13 2013
Feb 13

Episode Number: 

107

The Drupal 7 Views Accordion module allows you to build a Drupal View with results displayed in jQuery accordion tabs.

In this episode you will learn:

  • How to install and configure the Drupal Views Accordion module
  • How to build a simple Drupal View using the Drupal Views Accordion module
  • How to change the Drupal Views settings to alter how the Views Accordion module works

Another big thanks to Drupalize.me for sponsoring this episode of the Daily Dose of Drupal.

DDoD Video: 

Feb 04 2013
Feb 04

Episode Number: 

101

The Drupal 7 Menu Badges module is a neat little module that allows you to add numbered badges next to menu items on a Drupal 7 website. It integrates nicely with views so you can easily add your own numbered icons to any menu link. You will also need the Link Badges module for this to work.

In this episode you will learn:

  • How to add a Menu Badge to a Drupal PrivateMsg link
  • How to create your own custom Menu Badge using Views and apply it to a Drupal menu link

Today's episode was sponsored by Drupalize.me

DDoD Video: 

Jan 09 2013
Jan 09

This week we are finally finishing up our Coding for Views series with the last two lessons, which lagged a bit for us. We're happy to round things out by providing another example of writing a Views handler which creates a Views area handler, and with a look at theming Views. In addition to wrapping up that series, we also have a free video that looks at working with secondary menus. While this lesson is using Drupal 6, the concepts and processes used are exactly the same in Drupal 7, and is still applicable there. The lesson looks at how to take a menu with child items, and turn those children into a separate, but contextually related menu, in relation to the parent menu items. We also open up our theme, and look at how to move the secondary menu around, and manipulate the HTML that is being output, so that we can make the menu look the way we expect. The main difference between Drupal 6 and 7 with this lesson, is that the theme code to print the secondary menu looks different, but functions in the same way.

Next week, we'll be taking a look at some more community tools and processes to help people accelerate their Drupal experience. We're also hammering away at some cool new series on topics like Entity API, Git, and SASS. To see what we have in the hopper, to add to our list, and vote on topics, check out our Suggestion Box.

Dec 14 2012
Dec 14

Episode Number: 

67

Note: In this video I do not mention that you need the jQuery Cycle plugin installed to get this working. I already had it installed in my video. You can see information on how to do that in steps 3 through 7 on http://drupal.org/node/903244

The Drupal 7 Views Slideshow module makes it easy to add a slideshow to display rotating content on any Drupal 7 website.

In this episode you will learn:

  • How to create a simple Drupal content type with an image field to use for the Views Slideshow
  • How to create a block that displays as a Drupal 7 views slideshow
  • How to rewrite the Drupal views output so you can more easily style the slideshow with CSS

DDoD Video: 

Dec 12 2012
Dec 12

Most Popular blocks are a pretty common requirement. One nice solution for Drupal is the Drupal Most Popular module. The Most Popular module provides several sources for your blocks, such as Drupal core Statistics and Comment modules, as well as Google Analytics (see the issue regarding Google Analytics/Reports), Disqus and AddThis, allowing you to create different block types like Most Viewed, Most Commented and Most Shared. The Most Popular module also provides a lot of nice themeing options and allows you to set up tabbed blocks without writing any code. I won’t go into documenting how the module works, as that has already been done. You may decide you just want to use blocks from Views. If you try to create a Most Commented block using the Disqus module, you quickly discover it currently only provides a field for views and not a sort option. Fortunately, this is not difficult to remedy. A simple module can be created to provide this functionality. (To be really creative, going to it: disqus_most_commented.) First, implement hook_schema in the module .install file to add a new table to hold the comment counts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**  
 * Implements hook_schema().  
 */
function disqus_comment_count_schema() {
  $schema = array();
  $schema['disqus_comment_count'] = array(
    'description' => 'Stores counts from disqus',
    'fields' => array(
      'nid' => array(
        'type' => 'int',
        'not null' => true,
        'description' => 'nid of related node',
      ),
      'count' => array(
        'type' => 'int',
        'not null' => true,
        'description' => 'number of reads',
      ),
    ),
    'indexes' => array(
      'disqus_comment_count_nid' => array('nid'),
      'disqus_comment_count_count' => array('count'),
    ),
    'primary key' => array('nid'),
  );
  return $schema;
}

The meat of the .module file could look something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function disqus_comment_count_views_api() {
  return array(
    'api' => 3,
  );
}
 
/**  
 * Implements hook_cron().
 */
function disqus_comment_count_cron() {  
  // Assumes the Disqus module is installed   
  // Could be made into module admin settings if it is not   
  $secret_key = check_plain(variable_get('disqus_secretkey', ''));  
  $forum = check_plain(variable_get('disqus_domain', ''));
   
  // According to Disqus api: disqus.com/api/docs/threads/listPopular/   
  // acceptable interval options are: 1h, 6h, 12h, 1d, 3d, 7d, 30d, 90d   
  $interval = '1d'; // hard-coding one day, but could make this an admin setting   
 
  // Using the Disqus php api downloaded to sites/all/libraries from   
  // github.com/disqus/disqus-php   $path = libraries_get_path('disqusapi');    
  require($path . '/disqusapi/disqusapi.php');  
  $disqus = new DisqusAPI($secret_key);  
  $data = array(); //will hold return data    
 
  try {
    $data = $disqus->threads->listPopular(array(      
      'forum' => $forum,        
      'interval' => $interval,    
    ));  
  }  
  catch (Exception $e) {    
    // Log or throw exception   
  }    
 
  if (!empty($data)) {    
    // Clear out the table and insert new rows     
    db_query('delete from {disqus_comment_count}');      
    foreach ($data as $comment_info) {
      $nid = str_replace('node/', '', $comment_info->identifiers[ 0 ]);      
      $record = array('nid' => $nid, 'count' => $comment_info->posts);      
      drupal_write_record('disqus_comment_count', $record);    
    }  
  }
} 

Finally, add a Disqus count sort option for Views in the module’s .views.inc file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * Implements hook_views_data(). 
 */
function disqus_comment_count_views_data() {
  $data = array();
  $data['disqus_comment_count']['table']['group'] = t('Disqus Comments');  
  $data['disqus_comment_count']['table']['join'] = array(
    'node' => array(      
      'table' => 'disqus_comment_count',
      'left_field' => 'nid',
      'field' => 'nid',
      'type' => 'left outer',    
    )  
  );    
  $data['disqus_comment_count']['count'] = array(    
    'title' => t('Comment Count'),    
    'help' => t('Number of Disqus posts on a node.'),    
    'sort' => array(      
      'handler' => 'views_handler_sort',    
     ),  
  );    
  return $data;
}

The above functions are relatively simple and can certainly be made more robust, but they illustrate a straight-forward means of adding a Disqus comment count sort option for Views.

Nov 28 2012
Nov 28
Lullabot logo

Lullabot has trained thousands of Drupal developers & guided the development of some of the largest Drupal websites.

Nov 28 2012
Nov 28

In this lesson, we use the taxonomy that we have created for our videos to create a view of our videos, with an exposed filter that lets users limit the view by taxonomy terms.

Ooyala is a paid video delivery service, which manages your videos and handles video delivery to your site. You will need to have an Ooyala account in order to use this module on your Drupal site.

Nov 21 2012
Nov 21

Hello everyone and welcome to another Daily Dose of Drupal. I’m Shane, as always you can follow me on Twitter @smthomas3, you can also go to codekarate.com website and sign up for the newsletter over hare.

Today we’re going to continue on learning about Views Bulk Operations and you can see if you haven’t already watched Episode Number 52, I would recommend watching that first, that’s going to go over the basics and also if you have not watched any of the episodes on the Rules Module which looks like episodes number 24, 25 and 26. They may be helpful, you’ve probably get through this without it but afterwards you may want to go back and watch those as well.
However; today we’re going to look at Views Bulk Operations and how to actually use Views Bulk Operations to execute or trigger a Rule and this obviously is very useful for building out very complex workflows and administration pages as we went over yesterday’s Views Bulk Operations allows you to create a View of … in our example we use Nodes but it can by any type of Entity; Users or any other entity that’s declared and create actions or excuse me, execute actions on multiple of those nodes users, etc and performs something.
So in yesterday’s example we were able to publish multiple post set one time or unsticky or sticky multiple nodes at one time using the Views Bulk Operations module. So let’s go ahead and pull up that View and we take a look at where we left off.

So we have a Views Bulk Operations test view that we created yesterday, as you can see it has the Bulk Operations field and then a couple of other fields, you can see an example down below, we’ll go ahead and we’ll go to the page so you can see what it looks like, you can see you have the Operations Field set up here, you can select multiple items from the View and execute actions on this.

What we’re going to do is we’re going to show you how to create a simple Rule, we’re not going to go too far on the Rules because we have that other episodes, I go on to that but how to execute a simple Rule from here. So instead of just selecting unpublished content we could say for Test Article 1 and Test Article 2 we want to run this Rule that may … let’s say you have guest post and you want to send an e-mail to all the authors using Rules, you could do something like that or you could … any type of workflow you could build with Rules or any type of actions you can tie in with the Views Bulk Operations module.

So the sky is really the limit and I guess you can pretty much figure out everything that your imagination might be able to come up with for multiple different scenarios of how this can be useful. So the first step; we’re going to download the Rules module using Drush, we go ahead and get that downloaded, I’m going to hop over to the modules page and you can see we have rules, I’m going to go ahead and install Rules and Rules UI, I will click Save, you need the Entity Tokens module as well so I’ll go ahead and let that be installed and now I’m going to go ahead and go to the Rules Configuration page and create a simple Rule, excuse me; we’re actually going to start … we’re going to go back here, we’re going to go ahead and look at the components, we’re going to add a new component.

So these components are stand alone sets of rules that can be used by Rules and other modules on your site so we’re going to add a new Component and we’re going to go ahead and say we want this to be an Action Set.

The Variable; we’re going to go ahead and say this is going to be a Node, we’ll go ahead and call this vbo_node, this is going to be a parameter, I’ll go ahead and hit Continue. Now that we have this Action Set created we’re going to go ahead and add a couple actions, we can for instance … we’ll say we’ll publish the Content and we’re going to … I’m going to go back and select Node here, we’re also going to go ahead we can send mail.

So we can go ahead and find … just keep this basic, we’ll just use the site e-mail address and we will say test e-mail, test message, we’re just going to going to go through and show some simple ways to get this setup and are working and obviously you can do whatever you want here, this one doesn’t really make sense, I guess you could say if someone is publishing content that wasn’t site owner and you wanted an e-mail to go out every time content was published from this interface you could use this rule, so we’re going to save it and it’s nothing in the settings that … we’ve already configured that, we’re going to save changes and now that’s saved we’re going to go back to our View and if everything is setup correctly you won’t notice any differences yet but we’ll come in and edit the View, we should be able to hopefully find that Actions Set in here.

You’ll notice when I go ahead and click on the Bulk Operations, that’s the checkbox field that we added and you’ll notice in here that there’s a Rules Action Set and you can choose to … and queue the operations which would be the queue it up and execute it later, you can skip the Confirmation step, you can override the label and we’ll go ahead and override and say publish an e-mail, so that’s what’s going to show up, we’ll go ahead and hit Apply All Displays, save it and we’ll give this a quick test.

So now if I go here you’ll notice there’s a Publish an E-mail, I’m going to go ahead and first make sure I unpublished this and I will hit Execute, it’s going to ask me to confirm it, now if I open this up you’ll notice it looks like my homepage is blank. I will now go ahead and run publish an E-mail, I will go ahead and execute that, there we go, and I go ahead and hit Confirm and now it’s going to run through this Rule and it might take a little bit of time because that’s what few more Operations, If I run to the homepage you can see both these articles are now published, I’m going to go ahead and open up my e-mail quick just to make sure that I got that and obviously this isn’t a very useful used case because I’m going to get multiple e-mails so it really … you’ve probably not use this exact use case because as you can see I had two e-mails that say test message, they all both had the test e-mail subject.

So you can obviously figure out your own use cases but what this will allow you to do is build out and it really expand on what Views Bulk Operations can do to pretty much build out any type of workflow you want without actually having to write any custom code.

So it’s super easy to get started and I recommend you try it out because this can make building an administration back end of your site, incredibly easy and incredibly flexible and I highly recommend trying it out. That’s it for this time on the Daily Dose of Drupal; we’ll be back again tomorrow with another exciting episode. Thank you for watching.

Nov 20 2012
Nov 20

Hello everybody and welcome to another Episode of the Daily Dose of Drupal, we’re on Episode Number 52 today. I’m Shane, you can follow me on Twitter @smthomas3, you can also got o codekarate.com and sign up for the newsletter and check out everything else on the website.

Today we’re going to be going over the Views Bulk Operations Module. I’m just going to over to the very basics of it because you can do a lot with module and it integrates in with Rules and a couple of other things that you can see here that play nicely with the Views Bulk Operations module but we’ll go ahead and get started and I’ll show you simple example and then you can take it from there.
First thing I’m going to do is hop over to my command line and I’m going to use Drush to download the module. You can see it has two modules; Actions Permissions and Views Bulk Operations. Actions Permissions I believe it creates a permission for each of the various Views Bulk Operations so you can separate it out who can use which operation.

So I have my test site here, I’m going to hop over to the module’s page and I already have Views installed so you’ll need to make sure you have views installed if you do not already. You’ll also notice the Views Bulk Operations module requires the Entity module.

So I’ll go ahead and search for Drupal Entity and you can see that the Entity API module is here so you’ll need to download that as well, so go ahead and do that and I’m just going to go Drush Enable Views Bulk Operations and Entity, go ahead and click Yes and if I refresh the page on my modules page you’ll see that Views Bulk Operations module has been turned on, it looks like I’m using 7.x-3.0 and we’re going to go ahead and create a very simple view to show you what you can do here.

So I’m going to add a new view; I’m just going to call it VBO Test and we’re going to show content of type, we’ll go ahead and show all and we’ll go ahead and leave the rest of that the same. Over here we’re going to go ahead and just click on Continue and Edit, we’ll just leave it at most of the defaults. Now the trick here is going to be changing the format I believe, we’re going to change this to a table and we are going to go ahead and keep the defaults here, go ahead and apply.

So now we have a very basic View, the next step is of course to get the Views Bulk Operations filters, I guess you can call them Setup. I guess it’s not really filter, it’s more of like a dropdown to perform Bulk Actions on the view and just a quick step back so you can understand what this does.

What this module actually does is it allows you to … let’s say you have a list of content and you want to maybe perform one action on that entire lists at once. This is going to create checkboxes along the left side of the View, allow you to select Multiple and then perform one action on multiple selections at the same time.

The other thing that you might want to take a look at. I’m just going to go over the basics today so may want to look at the documentations so there’s a Read Documentation page on drupal.org and this is going to walk you through some actions and snippets if you need to add that, looks like there’s not much there but VBO for Drupal 7 is going to come over and give some Rules Integration, some Aggregation things and just some basic information.

So you may want to also look at that for a little bit on how to get started but we’re going to go ahead and create this or finish creating this view here. So right now it just shows the title, we need to go ahead and add a new field, we’re going to click on Bulk Operations content and that’s going to add the checkbox to select the row for the Bulk Operations. I can of course add other fields, let’s say we want to show the comment counts, comment status and let’s see if we want anything else here, the post, the date and we’ll go ahead and show content sticky as well just so we can look at that, go ahead and hit Apply.

You can see the Bulk Operations; it allows you to select which Bulk Operations you want to be available. So let’s go ahead and select a couple of them so you can override the label, you can skip the confirmation step, we’re just going to add a couple and we’ll come back and look at this. So we’re also going to make Content Sticky and make Content Unsticky and we’ll go ahead and say we want to skip the confirmation step on those.

The other thing is you’ll notice they say In queue the operations instead of executing it directly and I believe what that does is it just queues up that process so it doesn’t immediately take action and if have a … if you’re doing actions on a lot of nodes at one time, that may be useful to make sure that you’re not trying to do too much and then I believe it will get run on the next Cron run and then maybe broken up depending on how many different pieces of content you’re making a change too.

We can also unpublished contents and published contents and we’ll go ahead and hit Apply to All Displays and we’ll the rest of these at their default just so we can get a look at how it’s going to work, I’m going to click Save and I’m going to click on View Page here and you’ll see … obviously it’s not an order and probably moved this checkbox way over here to the left but you can see that it shows that none of these are sticky.

So I’m going to go ahead and select two of them, you can see now I have this Operations Field set up here, choose Operation and I’ll make the contents sticky, I’ll execute it and you’ll notice that now I have two of them that are sticky, I’m also going to go ahead and … let’s say we wanted to unpublished everything, I’m going to click and say Unpublished Content, execute, you’ll notice this one asks me to confirm it so I’ll go ahead and I’ll hit Confirm and now you’ll notice that nothings shows up here because if you go into the View it’s only showing contents that’s published, so you know that that work, we’re going to remove this so we’ll show All Content, save it, come back to the page and you’ll notice that now they’re all showing again, I’m going to go ahead and republished those.

So as you can see this can really be used for administration pages for changing lots of different pieces of contents at once. So if you want to turn comments off for a lot of different pieces of content, you want to publish a bunch of content that wants unpublished and make them sticky, you know all those different operations that you saw, it’s very easy to do with Views, it’s point and click and pretty much makes it easy for you to control the actions or control the workflow on your site and control the content in a much more refined way.

So we’ll take a look at in one more time, you can see there’s a whole bunch of different Bulk Operations, now of course it can’t do everything but it is extensible and that’s where reading it up on how it can work with Rules module and how you can create your own actions, your own Bulk Operations can be used to extend the system and basically create various extensible Views, I can do any type of administration actions that you want.

So that’s it for this time, it’s really pretty simple but this module is extremely powerful and I recommend everyone to take a look at it if they haven’t already and we’ll be back again tomorrow with another exciting topic, Drupal related of course and I’ll see you again next time. Thanks for watching.

Nov 08 2012
Nov 08

Welcome to another Daily Dose of Drupal, we’re on Episode Number 46 today. As always; I am Shane and you can follow me on Twitter @smthomas3 or you can hop over to Code Karate website and check out all the other things that we have going on. A while back on the Daily Dose of Drupal episode I believe it was on the 19th of September I did an episode on the Profile 2 Module and this is going to extend that a little bit and just show you how you can build a view … basically a membership directory using information from those fields that you created with the Profile 2 Module.

Just a quick overview if you don’t remember if you don’t watch that video in the Profile 2 Module will basically allow you to create a separate profile which can be a collection of fields, maybe it’s date of birth, biography, your social media profiles, things like that and have an actual separate profile that’s attached to your user account on a Drupal website.

So this is going to be useful for any type of membership website or any type of social networking website that’s using Drupal where you need to list the actual members of the site. So the first thing that I’m going to do is to make sure that I have Profile 2 and Views installed, which I do and well actually if you look at it let’s come to the modules page and let’s go ahead and confirm that I have the Profile 2 Module installed, Profile 2 is turned on and it looks like I do not have the Views Module.

So we’re going to go ahead and download the Views Module, so I’ll go ahead and use Drush from the command line to just download Views and we will come back into the modules page and use the filter to find and turn on Views and Views QI, so I’ll go ahead and save that and what we’re going to do in this example is we’re just going to create a very basic views page that lists all the members of the site and it also show their profile information if they have any.

So if we go ahead and look at the profile module or the Profile 2 Module we click on Configure, you’ll notice that we have two different profiles; we have a Main Profile and we have a Reviewer Profile. We’re only going to be concerned with the Main Profile here. All this profile has is a Full Name and a Bio Field.

So nothing too difficult, there’s a couple of fields. What we’re going to do now is we’re going to create this View and we’re going to make sure that it links to this Main Profile. Let’s go ahead and look at the users that we have on the site; so it looks like we have a couple of different users so we should have be good to go as far as some basic content for the view.

The View is not going to look at the end but it’s going to be workable and you’re going to be able to use that to build any type of view that you need. If you haven’t used Views before, this probably isn’t the starting point you want to look at, there’s probably a bunch of other tutorials you can go and learn the very basics. This one is going to show you how to build a basic … a fairly basic View but using a relationship. So I’m going to create a View called Site Members, we’re going to show Users and we’ll go ahead and show the newest first, that’s fine, we’ll create a page called Site Members, the path will be test3.codekarate.com/sitemembers and in this case we could show a table format if you wanted a listing and you wanted to be able to easily administer this or a grid format or on format if you just wanted the format with CSS.

I’m going to go ahead and use a grid and we’ll show 12 items and we will have it use a pager and go ahead and hit Continue and Edit. So the first thing we’re going to do is we’re going to add a couple of extra fields here; so we selected the fields and I’m going to go down to user and we already have the user name as you can see, I want to show the user create a date. If this was an administrative page you could show the user e-mail address and user edit link but depending on if you’re showing this to your average user if you want a site membership directory that any user can see, you probably don’t want to show any of those fields, the edit link would only show up of course if they have access to that but you don’t want any user to be able to see user’s e-mail addresses most likely.

So we could add user’s picture if they had one and we’ll go ahead and leave at that. I don’t believed any of the users on this site have a picture so we don’t have anything to show there but if you did have uploaded pictures you would of course see an image there.

So it be come down here, it shows a label of a picture of the user name and the created date, I’m going to go ahead and get rid of the label on the image even though there are no images, just imagine that there would be a user profile picture there if any of these users have actually uploaded the image and for the grid settings we can say maybe we only wanted to be 3 columns, so if we apply that you could see if 3 columns but what we really want is we want to show those extra fields that we have in our profile that we created with Profile 2. So this main profile we really want to show this Bio and this Full Name field.

Now your first thought would be … well let’s just try to add this field but since it’s actually a separate entity you can’t get to it from just a normal fields interface. You actually have to first add a relationship and this relationship is going to reference this original user entity to that profile entity that you created, in this case this Main Profile. So we go and we click on Add Relationships and we’re going to select User Profile and this shows up because of the Profile Module has been turned on, we’ll go ahead and click Apply and you could require this relationship.

What this will do is if any user does not have a profile, they will not show up in this listing, if you check it ... if you leave it uncheck then it will still show the user but these fields will be blank because it won’t actually link to anything, you can select which profile you want to use, in this case I’m only going to show the Main Profile and I’m going to click Apply.

Now that requires if you’re familiar with SQL or Databases it’s kind of like of inter joined versa left joined sequel statement if you’re not familiar with that then that may not quite make sense but basically all the views is just a very fancy user interface for making a specific types of database queries in structuring the results just a way to build simple list and of course it gets pretty complex when you get into the contextual filters and the relationships but what this will allow us to do is it will now allow us to add those fields that we wanted.

So you can see there’s another option here for profile, if you scroll down you can see those or I can just go ahead and limit it so I only see the Profile fields. So now I can select Bio and Full Name, go ahead and hit Apply, I can leave the label, you of course want to use the relationship that you created so you need that to show up there and I will leave … I’ll go ahead and remove the label from this one and then I can of course rearrange them however I want.

So user name, full name, created date and bio, now if I hit Apply and I scroll down I can see that this user of course they must not have profile, they have an old bio, this one has a bio that says Test User but they’re … and this is the user’s actual full name and down here here’s the full name and a bio as well.

So we can go ahead and save this, click on View Page and will take us to Site Members page and you can see I now have a grid of the site members of the website, it has my fields from the profile pulled in and you can do a whole bunch of cool things; you could add different types of filters so you can search for site members, you can do a whole bunch of stuff, so you can come in to the view, you can add a filter; for instance we’ll say full name, go ahead and hit Apply and we’ll go ahead and click Expose here, I’ll just a name, we’ll go ahead and put Member Name and we’re going to use contains … keep in mind this is depending on how many users you have, this could make it relatively slow search query but it will work and we’ll go ahead and hit Apply.

What we did here is we’re adding a filter and we’re exposing it so the user is going to be able to input what they want to search by. So I will show you how that works; so I’ll hit Save and now you can see there’s basically a search box so I can search by member name. If I go ahead and type User you can see that I get just this Test User because it’s searching by this field only, if I search for the first letters of Shane you can see I get this member.

Keep in mind it’s only searching by this field but you can add of course different dropdowns if you had all different types of options for various members and you could use the view to search and filter through these different members. So this is good for building out User Administration interfaces for you if the default user administration interface isn’t good enough for your needs or if you need to build out a membership directory for your other site users.

That’s really all we’re going to go over today, just some Views concepts that we went over here looking at Relationships and Exposed Filters and Views and also using that with the Profile 2 Module so you can help or start building out your membership directory. That’s it for this time on the Daily Dose of Drupal and we will be back again tomorrow with another exciting episode. Thanks for watching.

Pages

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web