Tuesday, March 11, 2014

How to send real time notifications to your Drupal site users

Often you would want to notify logged in users of an urgent deployment that is going to take the site down. For these unavoidable deployments, real-time notifications to end users would be God-send as it would help editors save their work (if they are adding content) in adequate time.




Here is a simple attempt at building one such system. HTML5 Server Sent Events is a one way communication technology which we can leverage for this. The caveat is that editors need to use HTML5 browsers or resort to XHR polling.

In this example, we are using Jenkins for deployment and want to notify logged-in site editors when a deployment is about to happen. We'll build a separate notification server for this purpose (on node.js) that will register browser connections and will also listen for notifications from Jenkins. Once it receives a notification regarding a deployment, it will broadcast that to the registered browsers. We'd like this server to support different sites and their notifications (multi-site scenario in Drupal). Jenkins can send build info notifications via the Notifications plugin. This server has been built and hosted on GitHub along with usage instructions.


When each browser gets loaded, it fires off an EventSource() to the server, registering its connection. The server keeps a list of active connections per site. The Notifications plugin on Jenkins posts JSON build information to our server as it happens, with the site information as parameter. The server in turn broadcasts this to the registered browsers for that site, who then get a Growl/Ubuntu like notification. The GitHub repo contains both the server side code (node.js) and client side javascript with the required polyfill, which can be used for any web application.

To get the client-side working in Drupal, read on.

Drupal


Create a module that adds the client JS and CSS on all pages. It would help to create and use a permission to receive notifications for editors and other roles. Some of the crucial hooks are listed below. The JS assets are loaded on hook_init() based on 'receive sse notifications' permission. For the Growl-like notifications, add in some required bit of html in hook_preprocess_page() which invokes a theme function so as to make it extensible/over-writeable.

/** * Implements hook_init(). */ function mymodule_init() { if (user_access('receive sse notifications')) { drupal_add_js(SSECLIENTPOLYFILLPATH, 'external'); drupal_add_js(variable_get('mymodule_eventsource_url', SSECLIENTJSPATH), 'external'); // Jquery notify additions // See http://www.erichynds.com/blog/a-jquery-ui-growl-ubuntu-notification-widget drupal_add_css(drupal_get_path('module', 'mymodule') . '/assets/css/ui.notify.css'); drupal_add_js(drupal_get_path('module', 'mymodule') . '/assets/js/jquery.notify.js'); // Add the notifications server URL for the SSE client JS to use. drupal_add_js(array( 'mymodule' => array( 'server' => variable_get('mymodule_server_address', SSESERVERPATH) ) ), 'setting'); } }
/** * Implements hook_permission. */ function mymodule_permission() { return array( 'receive sse notifications' => array( 'title' => t('Receive HTML5 SSE notifications'), 'description' => t('Receive notifications regarding site builds in real-time.'), ), ); } /** * Implements hook_theme. */ function mymodule_theme() { return array( 'mymodule_jquery_notify' => array( 'variables' => array(), 'template' => 'jquery_notify', 'path' => drupal_get_path('module', 'mymodule') . '/templates', ) ); } /** * Implements hook_preprocess_page(). */ function mymodule_preprocess_page(&$variables) { if (!user_access('receive sse notifications')) { return; } // Append notify container to footer or content. if (!empty($variables['page']['footer'])) { $variables['page']['footer']['jquery_notify']['#markup'] = theme('mymodule_jquery_notify'); } else { $variables['page']['content']['jquery_notify']['#markup'] = theme('mymodule_jquery_notify'); } }

Thursday, March 06, 2014

Improve performance: Organize your files into subfolders - Drupal 7

Having too many files in a single folder can be detrimental to your site's performance. It is advisable to segregate files into multiple folders for various benefits. Your sysadmin would surely thank you later :-)

This Acquia post introduces the problem of file organization and suggests two token based modules. Personally, having tried both, File Entity Paths is my choice as it allows you to create folder structures based on properties of the file per-se and not the parent entity. This is useful if you would not like files from changing their location every time they get reused in another entity and need to be in folders based on that entity's properties. By ensuring a folder structure based on file id (fid) for example, you can also guarantee a more even spread and can limit the number of folders created.

For example, to store up to 1,000,000 files with not more than 1000 in a folder, it means we do not need more than 1000 folders. For an even distribution of files, this folder structure could be: [0-9]/[00-99] where the [0-9] is the third digit from the end of the fid, and the [00-99] digits are based on the last two digits of the fid (left pad fids with '0' for fids < 99). So a file 'image.jpg' having fid 123456 would be in files/4/56/image.jpg.

To implement the above logic, we can modify the helpful Drupal 7 drush script to relocate files based on their fid, instead of the timestamp.

Here's the script (Drupal 7):

For new files uploaded via the UI, it's easy to create a file token based on the above logic, to be used in File Entity Paths settings.

/** * Implements hook_token_info(). */ function mymodule_token_info() { $info['tokens']['file']['fid_split'] = array( 'name' => t('Split based on File id'), 'description' => t('Creates two directories based on fid'), ); return $info; }
/** * Implements hook_tokens(). */ function mymodule_tokens($type, $tokens, $data = array(), $options = array()) { $replacements = array(); if (($type == 'file') && !empty($data['file'])) { $fid = str_pad($data['file']->fid, 10, '0', STR_PAD_LEFT); $len = strlen($fid); // Top level dir to be 0 - 9 $top_dir = $fid[$len - 3]; // Sub level dir to be 00 - 99 (100 per top level dir) $sub_dir = $fid[$len - 2] . $fid[$len - 1]; $value = $top_dir . '/' . $sub_dir; $replacements[$tokens['fid_split']] = $value; } return $replacements; }

At the time of this writing, you need to use the latest dev version of fe_paths module (commit dated 16 Oct 2012).

Monday, January 13, 2014

Alter path alias programmatically for an existing taxonomy term

Here's a method to alter the path alias (if you're using the pathauto module) of an existing taxonomy term programmatically. Here we shall be doing this in a hook_update_N(). It is quite un-intuitive given the lack of proper documentation as to how to alter the alias of an existing term. Here is my trial & error method:

/**
 * Re-alias 'Trailer' term & create a redirect from its old path to the new alias.
 */
function xxxxx_update_7007() {
  // 'Trailer' term.
  $term = taxonomy_term_load(20);
  // Set 'Generate automatic URL alias' to FALSE to allow custom alias setting later.
  $term->path['pathauto'] = FALSE;
  taxonomy_term_save($term);

  // Create a new alias
  $new_path = path_load('taxonomy/term/20');
  $new_path['alias'] = 'trailers';
  path_save($new_path);

  // Setup redirect from the old url 'trailer' to 'trailers'
  $redirect = new stdClass();

  module_invoke(
    'redirect',
    'object_prepare',
    $redirect,
    array(
      'source' => 'trailer',
      'source_options' => array(),
      'redirect' => 'trailers',
      'redirect_options' => array(),
      'language' => LANGUAGE_NONE,
    )
  );

  module_invoke('redirect', 'save', $redirect);
}

In the above example, we are changing the path of a term from '/trailer' to '/trailers' and setting up a redirect from the old path so that users don't get lost (plus it's better for SEO).

We first have to unset the 'Generate automatic URL alias' option and save the term. Then create the new alias, followed by adding the redirect from the old alias.