Mar 26 2017
Mar 26
Last time we've created a style plugin. Now we will learn how to cache panel panes with custom cache plugins.

In this tutorial we will create a panel cache plugin which will cache panel pane for a given time (5, 10 or 15 seconds). For now we have a module with next file structure:
example_panels_module
  |__ plugins
  |  |__ styles
  |  |  |__ example_panels_module.styles_plugin.inc
  |__ example_panels_module.info
  |__ example_panels_module.module
All that we need to do is put file example_panels_module.cache_plugin.inc into plugins/cache directory with next content:
<?php

/**
 * @file
 * Example panels cache plugin.
 */

$plugin = [
  // Plugin title.
  'title' => t('Example panel cache'),
  // Plugin description.
  'description' => t('Example panel cache.'),
  // Cache get callback.
  'cache get' => 'example_panels_module_cache_get_cache',
  // Cache set callback.
  'cache set' => 'example_panels_module_cache_set_cache',
  // Cache clear callback.
  'cache clear' => 'example_panels_module_cache_clear_cache',
  // Settings form.
  'settings form' => 'example_panels_module_cache_settings_form',
  // Settings form submit.
  'settings form submit' => 'example_panels_module_cache_settings_form_submit',
  // Default values.
  'defaults' => [
    'lifetime' => 5,
  ],
];

/**
 * Get cached content.
 */
function example_panels_module_cache_get_cache($conf, $display, $args, $contexts, $pane = NULL) {
  $cache = cache_get('example_panels_module_cache:' . $pane->pid, 'cache_panels');

  // No cached data available.
  if (!$cache) {
    return FALSE;
  }

  // Cache is expired.
  if ((REQUEST_TIME - $cache->created) > $conf['lifetime']) {
    return FALSE;
  }

  return $cache->data;
}

/**
 * Set cached content.
 */
function example_panels_module_cache_set_cache($conf, $content, $display, $args, $contexts, $pane = NULL) {
 cache_set('example_panels_module_cache:' . $pane->pid, $content, 'cache_panels');
}

/**
 * Clear cached content.
 */
function example_panels_module_cache_clear_cache($display) {
  cache_clear_all('example_panels_module_cache:', 'cache_panels', TRUE);
}

function example_panels_module_cache_settings_form($conf, $display, $pid) {
  // Cache lifetime.
  $form['lifetime'] = [
    '#title' => t('Lifetime'),
    '#type' => 'select',
    '#options' => drupal_map_assoc([5, 10, 15], 'format_interval'),
    '#default_value' => $conf['lifetime'],
  ];

  return $form;
}
So now module structure looks like:
example_panels_module
  |__ plugins
  |  |__ styles
  |  |  |__ example_panels_module.styles_plugin.inc
  |__|__ cache
  |__|__|__ example_panels_module.cache_plugin.inc
  |__ example_panels_module.info
  |__ example_panels_module.module
Clear cache (on enable module if it isn't enable yet) and navigate to panel pages settings (for this tutorial I've created a test panel page for displaying a single node). Choose a panel pane to cache and set up a cache type as Example panel cache (for this tutorial I've created a panel pane that shows a REQUEST_TIME value and put it into node/view panel page):

Set up cache for a given panel pane

Then chose cache lifetime:

Cache lifetime

Save settings and create some test node. For example if you set up cache life time as 5 seconds then value will be static for 5 seconds and only after this time will be refreshed.

Key notes:


Source code of all examples are available here.

Feb 25 2017
Feb 25
Panels style plugins are made for wrapping panel panes and panel regions into extra markup. 99% of your needs are covered by a "Panels Extra Styles" module. Please look at that module if you need extra styles for panels. But if you need some specific style you can easily implement it.

In this tutorial we will create plugin style for region or pane. It will allow site builders to wrap region or pane into a custom markup entered in settings form.

1. Create a directory example_panels_module and put there a file example_panels_module.info:

name = Example panels module
description = Provides example implementation of panels plugins
core = 7.x

; Since we're using panels for creating
; plugins we have to define this
; dependency.
dependencies[] = panels
2. Tell ctools where it has to search plugins (file example_panels_module.module):
<?php

/**
 * Implements hook_ctools_plugin_directory().
 *
 * Integrate our module with Ctools. Tell where
 * Ctools has to search plugins. Please note that
 * we should return path to plugins directory only
 * if $module equals 'panels'.
 */
function example_panels_module_ctools_plugin_directory($module, $plugin) {
  if ($module == 'panels' && !empty($plugin)) {
    return "plugins/$plugin";
  }
}
3. Create directory plugins/styles and put there a file example_panels_module.style_plugin.inc:
<?php
/**
 * @file
 * 'Example panel style' style.
 */

$plugin = [
  // Plugin title.
  'title' => t('Example panel style'),
  // Plugin description.
  'description' => t('Raw HTML wrapper.'),
  // Render region callback.
  'render region' => 'example_panels_module_raw_wrapper_render_region',
  // Render pane callback
  'render pane' => 'example_panels_module_raw_wrapper_render_pane',
  // Region settings form.
  'settings form' => 'example_panels_module_raw_wrapper_region_settings_form',
  // Pane settings form.
  'pane settings form' => 'example_panels_module_raw_wrapper_pane_settings_form',
];

/**
 * Region settings form callback.
 */
function example_panels_module_raw_wrapper_region_settings_form($settings) {
  // Define a settings form with prefix and suffix text areas
  // for region style.
  $form['wrapper_region_prefix'] = [
    '#type' => 'textarea',
    '#title' => t('Region wrapper prefix'),
    '#default_value' => !empty($settings['wrapper_region_prefix']) ? $settings['wrapper_region_prefix'] : '',
  ];

  $form['wrapper_region_suffix'] = [
    '#type' => 'textarea',
    '#title' => t('Region wrapper suffix'),
    '#default_value' => !empty($settings['wrapper_region_suffix']) ? $settings['wrapper_region_suffix'] : '',
  ];

  return $form;
}

/**
 * Region render callback.
 *
 * Please note that it's a theme function
 * and has to start with 'theme_' prefix.
 */
function theme_example_panels_module_raw_wrapper_render_region($vars) {
  $output = '';

  // Variable $vars['panes'] contains an array of all
  // panel panes in current region. Collect them into
  // variable.
  foreach ($vars['panes'] as $pane) {
    $output .= $pane;
  }

  // Variable $vars['settings'] contains settings
  // entered in settings form. Wrap region content
  // into custom markup.
  return $vars['settings']['wrapper_region_prefix'] . $output . $vars['settings']['wrapper_region_suffix'];
}

/**
 * Pane settings form callback.
 */
function example_panels_module_raw_wrapper_pane_settings_form($settings) {
  // Define a settings form with prefix and suffix text areas
  // for pane style.
  $form['wrapper_pane_prefix'] = [
    '#type' => 'textarea',
    '#title' => t('Pane wrapper prefix'),
    '#default_value' => !empty($settings['wrapper_pane_prefix']) ? $settings['wrapper_pane_prefix'] : '',
  ];

  $form['wrapper_pane_suffix'] = [
    '#type' => 'textarea',
    '#title' => t('Pane wrapper suffix'),
    '#default_value' => !empty($settings['wrapper_pane_suffix']) ? $settings['wrapper_pane_suffix'] : '',
  ];

  return $form;
}

/**
 * Pane render callback.
 *
 * Please note that it's a theme function
 * and has to start with 'theme_' prefix.
 */
function theme_example_panels_module_raw_wrapper_render_pane($vars) {
  // Variable $vars['settings'] contains settings
  // entered in settings form. Variable
  // $vars['content']->content is a renderable array
  // of a current pane. Wrap pane content
  // into custom markup.
  return $vars['settings']['wrapper_pane_prefix'] . render($vars['content']->content) . $vars['settings']['wrapper_pane_suffix'];
}
For now we have a module with next structure:
example_panels_module
  |__ plugins
  |  |__ styles
  |  |  |__ example_panels_module.styles_plugin.inc
  |__ example_panels_module.info
  |__ example_panels_module.module
When it's enabled you will be able to setup style for a region or a pane. We've created style for both of them but you can define style only for region or only for pane.

Let's test our style. I want to edit node/%node page: wrap content region into div tag with a class "content-region-wrapper". And wrap node_beind_viewed pane into span tag with id "pane-node-view-wrapper". That's how can I do that:

Change style for a region

Select newly created style

Set up prefix and suffix for a region

The same way I've set up prefix and suffix for a panel pane and that's what I've got:

Result markup

Key notes:

Feb 14 2017
Feb 14
Relationships plugins are "bridge" between existing context (that is already set up in a panel) and a context which you want to get from existing one. Let's say your panel contains "Node" context and you want to get a node author (user from node:uid property). To do that you can just set up "Node author" relationship in a panel (under a "contexts" tab) and that's all. That's why relationships plugins are so important - they provide easy way for getting context from existing contexts. Please have a look at this post before continue reading - there is described how to create module integrated with ctools API which allows us to define own plugins.

As you remember we've created a custom server_info context plugin that provides information from $_SERVER superglobal php variable. All that you can grab from that context are strings. Ctools comes with a simple context plugin string. It provides three ways of string representation: html_safe, raw and uppercase_words_html_safe. In this tutorial we will create a relationship plugin that will convert a string from server_info to a string context.

Let's add new plugin to our existing example_module which for now looks like:

example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |  |  |__ example_module.example_access_plugin.inc
  |  |__ contexts
  |  |  |__ example_module.example_context_plugin.inc
  |  |  |__ example_module.example_context_plugin.node_status.inc
  |  |__ arguments
  |     |__ example_module.example_argument_plugin.inc
  |__ example_module.info
  |__ example_module.module
1. Create file example_module/plugins/arguments/example_module.example_argument_plugin.inc:
<?php


/**
 * @file
 *
 * Plugin to provide a string relationship from server_info context.
 */

/**
 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
 */
$plugin = [
  // Plugin title.
  'title' => t('String from "Server info" context'),
  // Plugin description.
  'description' => t('Adds a string context from existing server_info context.'),
  // Keyword.
  'keyword' => 'string_from_server_info',
  // We want to create string context from server_info context.
  // It means that server_info has to be already set up into
  // a panel page. So server_info context is required for
  // this relationship.
  'required context' => new ctools_context_required(t('Server info'), 'server_info'),
  // Context builder function.
  'context' => 'example_relationship_plugin',
  // Settings form. We will provide a property from
  // server_info context we want to present as string context.
  'edit form' => 'example_relationship_plugin_settings_form',
  // Default values for settings form.
  'defaults' => [
    'server_info_property' => 'HTTP_HOST',
  ],
];

/**
 * Return a new context based on an existing context.
 */
function example_relationship_plugin($context = NULL, $conf) {
  $string_context = NULL;

  // If empty it wants a generic, unfilled context, which is just NULL.
  if (empty($context->data)) {
    $string_context = ctools_context_create_empty('string', NULL);
  }
  else {
    if (!empty($conf['server_info_property'])) {
      // Create the new string context from server_info parent context.
      $string_context = ctools_context_create('string', $context->data[$conf['server_info_property']]);
    }
  }

  return $string_context;
}

/**
 * Settings form for the relationship.
 */
function example_relationship_plugin_settings_form($form, $form_state) {
  $conf = $form_state['conf'];
  $keys = array_keys($_SERVER);

  $form['server_info_property'] = [
    '#required' => TRUE,
    '#type' => 'select',
    '#title' => t('"Server info" property'),
    '#options' => array_combine($keys, $keys),
    '#default_value' => $conf['server_info_property'],
  ];

  return $form;
}

/**
 * Configuration form submit.
 */
function example_relationship_plugin_settings_form_submit($form, &$form_state) {
  foreach ($form_state['plugin']['defaults'] as $key => $default) {
    $form_state['conf'][$key] = $form_state['values'][$key];
  }
}
Module structure:
example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |  |  |__ example_module.example_access_plugin.inc
  |  |__ contexts
  |  |  |__ example_module.example_context_plugin.inc
  |  |  |__ example_module.example_context_plugin.node_status.inc
  |  |__ arguments
  |  |  |__ example_module.example_argument_plugin.inc
  |  |__ relationships
  |     |__ example_module.example_relationship_plugin.inc
  |__ example_module.info
  |__ example_module.module
2. Clear cache. After that you will see a new relationship on panel settings page under contexts tab (please note that you need to set up server_info context first before be able to set up a relationship based on server_info):

New relationship is available for this panel

3. Set up newly created relationship:

Choose a value from server_info context you want to represent as a string context

4. At this point you will see additional context on a panel - "String from 'Server info' context".

A string context available from server_info context

5. It means you can use, for example, %string_from_server_info:uppercase_words_html_safe token as a substitution for a page title. Let's try to set up title as "%server_info:HTTP_HOST : %string_from_server_info:uppercase_words_html_safe" and see what will happen:
Set up page title from contexts values

My host is "drpual.loc" so page title looks like "drupal.loc : Drupal.loc" where first part (before ":") is a value grabbed from server_info context and second part is a value from string_from_server_info context in "uppercase_words_html_safe" format.
Page title consists of context and relationship values

The above is true for:

  • Ctools: 7.x-1.12
  • Panels: 7.x-3.8

Key notes:


Feb 12 2017
Feb 12
This time we will consider an argument plugin. Arguments are pretty similar to contexts. Actually arguments are context objects loaded from url. By default ctools provides a full set of needed arguments such as "Node: ID", "User: ID", "User: name" etc. But what if you've created a custom context? You might need to create a custom argument for your context (if you want to use your context as an argument of course). I advise you to read previous articles from "Ctools custom plugin" series such as "Ctools: custom access plugin" and "Ctools: custom context plugin". It's also required to read "Ctools: custom content type plugin" before reading this post because there I've described how to create a module integrated with ctools API which can contain ctools plugins.

For this tutorial we need a custom context (because there is no sense to copy paste default ctools argument plugins for creating default ctools context plugins - let's be creative) so I've created one: node_status. It's pretty simple and similar to default 'node' context by it provides extra node properties: 'status' and 'promote' (default node context doesn't provide them). It behaves similar to 'node' context. I mean it has a 'nid' option in settings form. Once you've entered a node id you will be able to use %argument_name:status and %argument_name:promote values. Context definition looks like:
<?php

/**
 * @file
 *
 * Plugin to provide a node_status context.
 */

/**
 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
 */
$plugin = [
  // Plugin title.
  'title' => t('Node status'),
  // Plugin description.
  'description' => t('Node status.'),
  // A function that will return context.
  'context' => 'example_context_node_status_plugin',
  // Context keyword to use for
  // substitution in titles.
  'keyword' => 'node_status',
  // Context machine name.
  'context name' => 'node_status',
  // Settings form callback.
  'edit form' => 'example_context_node_status_plugin_settings_form',
  // Default values for settings form.
  'defaults' => [
    'nid' => 1,
  ],
  // Array of available context values.
  'convert list' => [
    'status' => t('Node status'),
    'promote' => t('Node promote'),
  ],
  // A function gets data from a given
  // context and returns values defined
  // in 'convert list' property.
  'convert' => 'example_context_node_status_plugin_convert',
];

/**
 * Context callback.
 */
function example_context_node_status_plugin($empty, $data = NULL, $conf = FALSE) {
  // Create context object.
  $context = new ctools_context('node_status');

  // This property should contain file name where
  // plugin definition is placed.
  $context->plugin = 'example_module.example_context_plugin.node_status';

  if (empty($empty)) {
    // Define context data.
    // Variable $data can be an array or an object.
    // It depends on how this context is created.
    // If it's created by putting context directly to
    // a panel page then $data - array containing
    // settings from form.
    if (is_array($data) && !empty($data['nid'])) {
      $node = node_load($data['nid']);
    }

    // If context is created by
    // an argument then $data - object.
    if (is_object($data)) {
      $node = $data;
    }

    if (!empty($node)) {
      $context->data = $node;
      $context->title = $node->title;

      // Specify argument value - node id.
      $context->argument = $node->nid;
    }
  }

  return $context;
}

/**
 * Returns property value by property type.
 */
function example_context_node_status_plugin_convert($context, $type) {
  $result = '';

  // Return node property (status or promote).
  if (!empty($context->data)) {
    $result = $context->data->{$type};
  }

  return $result;
}

/**
 * Settings form for cookies context.
 */
function example_context_node_status_plugin_settings_form($form, &$form_state) {
  // Node id option.
  $form['nid'] = [
    '#type' => 'textfield',
    '#title' => t('Node nid'),
    '#default_value' => $form_state['conf']['nid'],
    '#element_validate' => [
      'element_validate_integer_positive',
    ],
  ];

  return $form;
}

/**
 * Settings form submit.
 */
function example_context_node_status_plugin_settings_form_submit($form, &$form_state) {
  // Save submitted value.
  $form_state['conf']['nid'] = $form_state['values']['nid'];
}
I won't describe what goes here because we've already discussed custom context theme last time. Just put this code into example_module/plugins/contexts/example_module.example_context_plugin.node_status.inc file. Make sure that your module file structure now looks like this one:
example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |  |  |__ example_module.example_access_plugin.inc
  |  |__ contexts
  |     |__ example_module.example_context_plugin.inc
  |     |__ example_module.example_context_plugin.node_status.inc
  |__ example_module.info
  |__ example_module.module
Ok, finally we've got a context which we want to use as argument. For this tutorial I've created a custom page with a "node-view-page/%nid" url.

Page with required argument

Let's move on. Now we will create an argument plugin.

1. Create file example_module/plugins/arguments/example_module.example_argument_plugin.inc:

<?php

/**
 * @file
 *
 * Plugin to provide an argument handler for a node_status context.
 */

/**
 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
 */
$plugin = [
  // Plugin title.
  'title' => t('Node status argument'),
  // Plugin description.
  'description' => t('Creates a node status context from a node ID argument.'),
  // Keyword.
  'keyword' => 'node_status_argument',
  // Context builder function.
  'context' => 'example_argument_plugin',
];

/**
 * Discover if this argument gives us the node we crave.
 */
function example_argument_plugin($arg = NULL, $conf = NULL, $empty = FALSE) {
  $context = FALSE;

  // If empyt it wants a generic, unfilled context.
  if ($empty) {
    $context = ctools_context_create_empty('example_module.example_context_plugin.node_status');
  }
  else {
    // We can accept either a node object or a pure nid.
    if (is_object($arg)) {
      $context = ctools_context_create('example_module.example_context_plugin.node_status', $arg);
    }
    elseif (is_numeric($arg)) {
      $node = node_load($arg);

      if (!empty($node)) {
        $context = ctools_context_create('example_module.example_context_plugin.node_status', $node);
      }
    }
  }

  return $context;
}
2. Clear cache and navigate to panel page settings: arguments. Assign "Node status argument" to %nid.

Assigned argument

3. Navigate to panel page settings: contexts. You will see that argument provides properties from defined context.
Context loaded from an argument

4. Now you can use context properties for example for page title:
Context properties are used in a page title

Now if you open node-view-page/1, for example, you will see:
Custom argument plugin in action

If you unpublish a node then title will be "Status: 0 Promoted: 1". That's all.

The above is true for:

  • Ctools: 7.x-1.12
  • Panels: 7.x-3.8

Key notes:

Feb 11 2017
Feb 11
In previous post we created an access ctools plugin which can be used as a selection or visibility rule in panels. It's time to learn how to create another important custom plugin - a context. It provides additional information for a panel page. For example if you've put a node context to the page you will be able to use node properties as substitutions for a page title. Moreover you will be able to put node fields as panes to a page. By default ctools module provides useful contexts (node, user, taxonomy_term, entity etc) but you can define your own. Please, read first post of "Ctools custom plugins" series before continue reading this. There we've created a module integrated with ctools.

In this tutorial we will create a context plugin which will provide information about a server from superglobal php variable $_SERVER. As you remember for now we have an example_module with next file structure:
example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |     |__ example_module.example_access_plugin.inc
  |__ example_module.info
  |__ example_module.module
To create a context plugin we need to create contexts directory and put there a file with plugin definition. Let's start:

1. Create example_module/plugins/contexts/example_module.example_context_plugin.inc file:

<?php

/**
 * @file
 *
 * Plugin to provide a server_info context.
 */

/**
 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
 */
$plugin = [
  // Plugin title.
  'title' => t('Server info'),
  // Plugin description.
  'description' => t('Available information about a server.'),
  // A function that will return context.
  'context' => 'example_context_plugin',
  // Settings form callback.
  'edit form' => 'example_context_plugin_settings_form',
  // Default values for settings form.
  'defaults' => [
    'decode_request_uri' => FALSE,
  ],
  // Context keyword to use for
  // substitution in titles.
  'keyword' => 'server_info',
  // Context machine name.
  'context name' => 'server_info',
  // A function returns a list of
  // available values from a context.
  // By the way it can be just an array.
  'convert list' => 'example_context_plugin_get_convert_list',
  // A function gets data from a given
  // context and returns values defined
  // in 'convert list' property.
  'convert' => 'example_context_plugin_convert',
];

/**
 * Context callback.
 */
function example_context_plugin($empty, $data = NULL, $conf = FALSE) {
  // Create context object.
  $context = new ctools_context('server_info');

  // This property should contain file name where
  // plugin definition is placed.
  $context->plugin = 'example_module.example_context_plugin';

  if (empty($empty)) {
    // Define context data.
    $context->data = $_SERVER;

    // Decode property if selected.
    // Just for demonstration how to use
    // plugin settings.
    if (!empty($conf) && !empty($data['decode_request_uri'])) {
      $context->data['REQUEST_URI'] = urldecode($context->data['REQUEST_URI']);
    }
  }

  return $context;
}

/**
 * Returns available properties list.
 */
function example_context_plugin_get_convert_list() {
  $list = [];

  // Get all $_SERVER properties and return them.
  foreach ($_SERVER as $property_name => $property_value) {
    $list[$property_name] = t('$_SERVER["!name"]', [
      '!name' => $property_name,
    ]);
  }

  return $list;
}

/**
 * Returns property value by property type.
 */
function example_context_plugin_convert($context, $type) {
  // Return $_SERVER property value.
  return $context->data[$type];
}

/**
 * Settings form for server_info context.
 */
function example_context_plugin_settings_form($form, &$form_state) {
  // Demo setting.
  $form['decode_request_uri'] = [
    '#type' => 'checkbox',
    '#title' => t('Decode $_SERVER["REQUEST_URI"] value'),
    '#default_value' => $form_state['conf']['decode_request_uri'],
  ];

  return $form;
}

/**
 * Settings form submit.
 */
function example_context_plugin_settings_form_submit($form, &$form_state) {
  // Save submitted value.
  $form_state['conf']['decode_request_uri'] = $form_state['values']['decode_request_uri'];
}
Updated module structure looks like:
example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |  |  |__ example_module.example_access_plugin.inc
  |  |__ contexts
  |     |__ example_module.example_context_plugin.inc
  |__ example_module.info
  |__ example_module.module
2. Clear cache and set up Server info context for a panel page:

Newly created context

Plugin settings form

Available context properties

Let's test context and set up page title as %server_info:REQUEST_URI. By the way it's a node/%nid page on screeshot so to test it we should open a node view page (but you can set up this context to any panel page you want):
Page title will contain value from $_SERVER['REQUESTED_URI'] variable

Request uri as a page title

The last thing we have to test is an option "Decode $_SERVER["REQUEST_URI"] value". When it's checked uri will be decoded. Check this checkbox and open node view page with a query parameter such as "?test=parameter to decode" and update page. Browser will encode spaces in url and it will look like "?test=string%20to%20decode". But page title will contain original (decoded) value.

The above is true for:

  • Ctools: 7.x-1.12
  • Panels: 7.x-3.8

Key notes:

Feb 05 2017
Feb 05
Last time we've learned how to create custom ctools content type plugin. In that post we've already created a module example_module where we defined the plugin. This time we will learn how to create custom ctools access plugin. This type of ctools plugins can be used as a selection rule for a panel variant or as a visibility rule for a panel pane. Please, read previous post before continue reading this. There is described how to create a module and integrate it with ctools.

Let's say we have a node/%node panel page where are placed "Powered by Drupal" and "Node being viewed" panes.

Node view panel page with "Powered by Drupal" and "Node being viewed" panes

And we want first panel pane be shown only if node title equals some specific string. For example "Test article". Well, for this case we have to create an access plugin and apply it to the panel pane. Let's start.
For now if you remember we have an example_module with next structure:
example_module
  |__ plugins
  |  |__ content_types
  |     |__ example_module.example_content_type_plugin.inc
  |__ example_module.info
  |__ example_module.module
1. Create file example_module/plugins/access/example_module.example_access_plugin.inc:
<?php

/**
 * @file
 * File with access plugin definition.
 */

/**
 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
 */
$plugin = [
  // Plugin title.
  'title' => t('Example access rule'),
  // Plugin description.
  'description' => t('Example access rule'),
  // Access rule callback. This function has to return
  // TRUE of FALSE.
  'callback' => 'example_access_plugin',
  // A required context for this pane. If there is no required
  // context on a panel page than you will not be able to apply
  // this access rule to a pane or page variant. In this example
  // we need a 'Node' context.
  'required context' => new ctools_context_required(t('Node'), 'node'),
  // Settings form constructor callback. If you need custom submit/validate
  // callback you can define it by 'settings form submit' and
  // 'settings form validate' properties.
  'settings form' => 'example_access_plugin_settings_form',
  // Default values for edit form inputs.
  'default' => [
    'example_option' => 'example_value',
  ],
  // Summary callback. Returns plugin summary text which is visible
  // in drop-down setting menu of a pane.
  'summary' => 'example_access_plugin_summary',
];

/**
 * Settings form a pane.
 */
function example_access_plugin_settings_form(array $form, array $form_state, $conf) {
  // Please note that all custom form elements
  // should be placed in 'settings' array.
  $form['settings']['example_option'] = [
    '#type' => 'textfield',
    '#title' => t('Example text option'),
    '#default_value' => $conf['example_option'],
    '#required' => TRUE,
  ];

  return $form;
}

/**
 * Access rule callback.
 */
function example_access_plugin($conf, $context) {
  $access = FALSE;

  // Variable $context contains a context grabbed by a panel page.
  // In this case it contains a node object in a 'data' property.
  if (!empty($context->data)) {
    $node_from_context = $context->data;

    // Access will be granted only if node title equals a string
    // set up in settings form.
    $access = $node_from_context->title == $conf['example_option'];
  }

  return $access;
}

/**
 * Summary callback.
 */
function example_access_plugin_summary($conf, $context) {
  return t('Example access plugin');
}
Now our module structure looks like:
example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |     |__ example_module.example_access_plugin.inc
  |__ example_module.info
  |__ example_module.module
2. Clear cache. Apply newly created access rule for the 'Node being viewed' pane in node view panel page:

Set up visibility rule for a pane

Select 'Example access rule'

Set up string to match with a node title

It's time to test our access plugin. Create a node with title "Test" and look at it's page. There will be no "Powered by Drupal" widget because node title doesn't equal "Test article":

Node view page without "Powered by Drupal" widget

And now edit node and change title to "Test article". See what will happen:
Node view page with "Powered by Drupal" widget

Access plugin works correctly.

P.S.
As you could notice there is a little difference between content_type and access plugin definition:

  • 'edit form' VS 'settings form' properties.
  • 'defaults' VS 'default' properties.
  • you have to define custom inputs for access plugin settings form inside 'settings' array. At the same time there is no so strict rule for defining settings inputs for content type plugin.
  • in a form callback for content type plugins configuration is available in $form_state variable, but for access plugins it isn't there. It available in an extra variable $conf.


The above is true for:

  • Ctools: 7.x-1.12
  • Panels: 7.x-3.8

Key notes:

Feb 04 2017
Feb 04
Ctools content types are an alternative to standard Drupal blocks. They are more comfortable and powerfull than blocks. Ctools content type plugins also known as panel panes. In this post you will learn how to create a configurable ctools content pane plugin.

For example we will create a custom content type plugin that will render a user name and user email. Let's start.

1. Create a module which will contain all defined plugins. File example_module/example_module.info:

name = Example module
description = Provides ctools content type plugin
core = 7.x

; Since we're using ctools api for creating
; ctools plugins we have to define this
; dependency.
dependencies[] = ctools
File example_module/example_module.module:
<?php

/**
 * Implements hook_ctools_plugin_directory().
 *
 * Integrate our module with Ctools. Tell where
 * Ctools have to search plugins. Usually a place
 * where developers store all defined plugins is
 * "module_name/plugins" directory. Variable
 * $plugin contains a name of a plugin type. It can be
 * content_types, context, argument, cache, access. So all
 * of listed plugin types should be located in corresponding
 * subdirectories (plugins/content_types if you create a content
 * type plugin).
 */
function example_module_ctools_plugin_directory($module, $plugin) {
  if ($module == 'ctools' && !empty($plugin)) {
    return "plugins/$plugin";
  }
}
2. Create a file which will contain plugin definition. File example_module/plugins/content_types/example_module.example_content_type_plugin.inc:
<?php

/**
 * @file
 * File with plugin definition.
 */

$plugin = [
  // Pane title.
  'title' => t('Example pane'),
  // Pane description. 
  'description' => t('Example pane'),
  // Tell ctools that it's not a child of another pane.
  'single' => TRUE,
  // You can categorize all defined panes by
  // defining a category name for a pane.
  'category' => [t('Example panes')],
  // A machine name of your pane.
  'content_types' => ['example_content_type_plugin'],
  // Function that will render markup of this pane.
  'render callback' => 'example_content_type_plugin_render',
  // A required context for this pane. If there is no required
  // context on a panel page than you will not be able to add
  // this pane to the panel page.
  'required context' => new ctools_context_required(t('User'), 'user'),
  // Edit form constructor callback.
  'edit form' => 'example_content_type_plugin_edit_form',
  // Default values for edit form inputs.
  'defaults' => [
    'example_option' => 'example_value',
  ],
];
A file name is just a combination of a module name and plugin name joined by a dot. But actually you can name it as you want. Please note that you can omit 'required context' property if you want to be able to add this pane to all existing panel pages. If you want to add your pane only to pages that have a context just use required context like described above. If you want to add your pane to all pages (but in some cases you need a context) just define it as optional:
'required context' => new ctools_context_optional(t('User'), 'user'),
Let's define plugin configuration form:
/**
 * Configuration form for a pane.
 */
function example_content_type_plugin_edit_form($form, $form_state) {
  $form['example_option'] = [
    '#type' => 'textfield',
    '#title' => t('Example text option'),
    '#default_value' => $form_state['conf']['example_option'],
    '#required' => TRUE,
  ];

  return $form;
}

/**
 * Configuration form submit.
 */
function example_content_type_plugin_edit_form_submit($form, &$form_state) {
  foreach ($form_state['plugin']['defaults'] as $key => $default) {
    $form_state['conf'][$key] = $form_state['values'][$key];
  }
}
The last thing we shoud define is render callback:
/**
 * Render callback.
 */
function example_content_type_plugin_render($subtype, $conf, $args, $context) {
  $block = new stdClass();

  // Variable $context contains a context grabbed by a panel page.
  // In this case we will put this pane on user/%uid page which
  // has a user context.
  if (!empty($context->data)) {
    $user_from_context = $context->data;

    // Just output user's name and email.
    $block->content = 'User name: ' . $user_from_context->name . '</br>User mail: ' . $user_from_context->mail . '</br>Example option: ' . $conf['example_option'];
  }

  return $block;
}
3. Clear cache. Edit user view panel page and put this pane into some region:

Example ctools content type pane

Enter settings for a pane:

Configuration form

Open user/1 page and here's a result:

Rendered example ctools content type pane

The above is true for:

  • Ctools: 7.x-1.12
  • Panels: 7.x-3.8

Key notes:

Apr 16 2015
Apr 16

One of the major disadvantages of entities in Drupal 7, is the lack of support for built-in comments. This is due to the model of the core comments module, which is heavily tied to nodes -node comments are essentially settings of each content type-, and not available for other entity types. In Drupal 8 this has changed already, and comments are considered fields that can be attached to any entity type.

For Drupal 7, in the meantime, there’s been a similar solution for a while, which is the reply module. The reply module leverages the Entity API to define a “reply” entity type, and offers a UI to create “reply” bundles. Then, via the Field API, you can add a “Reply” field to any entity type, choosing the reply bundle that should be used by that particular field. The flexibility that gives is huge, since it means that you’re not restricted to just a comment type for your application.

I’ve been using it in a project for some time, and have to say the module works pretty well, although there’s not an official stable release yet. One of the problems I came across when I started to use it, was the lack of support for the panels module, so I decided to write a ctools “content_type” plugin to add that support myself. Here is the process I followed.

First, let ctools know where the ctools plugins live:

/** * Implements hook_ctools_plugin_directory() */ function reply_ctools_plugin_directory($module, $plugin) { if ($module == 'ctools') { return 'plugins/' . $plugin; } } * Implements hook_ctools_plugin_directory()functionreply_ctools_plugin_directory($module,$plugin){  if($module=='ctools'){    return'plugins/'.$plugin;

Then, create the plugin in the relevant folder. Let’s name it reply_add_form. Following the code above, in this case it would be under “{module_folder}/plugins/content_types/reply_add_form”. Let’s have a look at the $plugin declaration array:

/** * Plugins are described by creating a $plugin array which will be used * by the system that includes this file. */ $plugin = array( 'single' => TRUE, 'title' => t('Reply - Add form'), 'description' => t('Form to add a new reply to an Entity'), 'category' => t('Reply'), 'defaults' => array(), 'render callback' => 'reply_add_form_content_type_render', 'edit form' => array( 'reply_add_form_select_entity_type' => t('Reply form - Entity Type'), 'reply_add_form_select_reply_field' => t('Reply form - Reply Field'), ), 'required context' => array( new ctools_context_required(t('Entity being viewed'), 'any'), ), ); * Plugins are described by creating a $plugin array which will be used* by the system that includes this file.$plugin=array(  'single'=>TRUE,  'title'=>t('Reply - Add form'),  'description'=>t('Form to add a new reply to an Entity'),  'category'=>t('Reply'),  'defaults'=>array(),  'render callback'=>'reply_add_form_content_type_render',  'edit form'=>array(    'reply_add_form_select_entity_type'=>t('Reply form - Entity Type'),    'reply_add_form_select_reply_field'=>t('Reply form - Reply Field'),  'required context'=>array(    newctools_context_required(t('Entity being viewed'),'any'),

No magic in there if you’re familiar with ctools (if you aren’t, you can install the advanced_help module, which has plenty of documentation on how to create your own ctools plugins). One point to highlight, is the fact that the plugin has two edit forms instead of one: in the first one, we’ll choose the entity type for which we’re adding the form, and in the second one, we’ll choose the reply field used, from the available within the selected entity type (could be more than one).

Also, note that the required context bit is accepting “any” context available within the panels page. I had to do it that way because, unlike with nodes, it’s impossible to know in advance all the entity types that will be available in the system how the user will name the arguments and contexts in panels and offer just the relevant options. Instead, all contexts are accepted, and the user (which will be usually a developer or a site-builder, anyway), is responsible for choosing the right one in the settings form.

Let’s have a look at the settings forms:

/** * Returns an edit form for custom type settings. */ function reply_add_form_select_entity_type($form, &$form_state) { $entities = entity_get_info(); $options = array(); // Get all existing entities. foreach ($entities as $entity_type => $entity) { $options[$entity_type] = $entity['label']; } $form['config']['reply_entity_type'] = array( '#type' => 'select', '#title' => t('Entity Type'), '#options' => $options, '#default_value' => isset($form_state['conf']['reply_entity_type']) ? $form_state['conf']['reply_entity_type'] : NULL, ); return $form; } /** * Returns an edit form for custom type settings. */ function reply_add_form_select_reply_field($form, &$form_state) { $options = array(); // Get entity type chosen in previous step. $entity_type = $form_state['conf']['reply_entity_type']; // Get all the field instances for the given entity type, and add the 'reply' // ones as options. $field_map = field_info_field_map(); $reply_fields = array_filter($field_map, '_reply_add_form_filter_reply_fields'); foreach ($reply_fields as $field_name => $fields_info) { if (!empty($fields_info['bundles'][$entity_type])) { $options[$field_name] = $field_name; } } $form['config']['reply_field'] = array( '#type' => 'select', '#title' => t('Reply field'), '#options' => $options, '#default_value' => isset($form_state['conf']['reply_field']) ? $form_state['conf']['reply_field'] : NULL, ); return $form; } /** * Submit handler for the custom type settings form. */ function reply_add_form_select_entity_type_submit($form, &$form_state) { $form_state['conf'] = array_merge($form_state['conf'], array_filter($form_state['values'])); } /** * Submit handler for the custom type settings form. */ function reply_add_form_select_reply_field_submit($form, &$form_state) { reply_add_form_select_entity_type_submit($form, $form_state); } * Returns an edit form for custom type settings.functionreply_add_form_select_entity_type($form,&$form_state){  $entities=entity_get_info();  $options=array();  // Get all existing entities.  foreach($entitiesas$entity_type=>$entity){    $options[$entity_type]=$entity['label'];  $form['config']['reply_entity_type']=array(    '#type'=>'select',    '#title'=>t('Entity Type'),    '#options'=>$options,    '#default_value'=>isset($form_state['conf']['reply_entity_type'])?$form_state['conf']['reply_entity_type']:NULL,  return$form;* Returns an edit form for custom type settings.functionreply_add_form_select_reply_field($form,&$form_state){  $options=array();  // Get entity type chosen in previous step.  $entity_type=$form_state['conf']['reply_entity_type'];  // Get all the field instances for the given entity type, and add the 'reply'  // ones as options.  $field_map=field_info_field_map();  $reply_fields=array_filter($field_map,'_reply_add_form_filter_reply_fields');  foreach($reply_fieldsas$field_name=>$fields_info){    if(!empty($fields_info['bundles'][$entity_type])){      $options[$field_name]=$field_name;  $form['config']['reply_field']=array(    '#type'=>'select',    '#title'=>t('Reply field'),    '#options'=>$options,    '#default_value'=>isset($form_state['conf']['reply_field'])?$form_state['conf']['reply_field']:NULL,  return$form;* Submit handler for the custom type settings form.functionreply_add_form_select_entity_type_submit($form,&$form_state){  $form_state['conf']=array_merge($form_state['conf'],array_filter($form_state['values']));* Submit handler for the custom type settings form.functionreply_add_form_select_reply_field_submit($form,&$form_state){  reply_add_form_select_entity_type_submit($form,$form_state);

Pretty simple stuff. As mentioned, the first one allows to select the entity type for which the reply form will be added. The second one, gets a simple map containing all the fields in the system, filters them out to keep only reply fields, and then filters them out again to show only the ones available for the entity type selected in the first settings form. The forms look like this:

panels_reply_1panels_reply_2

Finally, the render function, which simply takes care of loading the entity passed in the plugin config, and calls the appropriate function of the reply module, to let it create the reply form.

/** * Output function for the 'reply_add_form' content type. */ function reply_add_form_content_type_render($subtype, $conf, $panel_args, $context) { if (!$context[0]->data) { return; } $entity = $context[0]->data; $reply_field_instance = field_info_instance($conf['reply_entity_type'], $conf['reply_field'], $conf['reply_entity_type']); $block = new stdClass(); $block->title = ''; $form_options = array( 'entity_id' => $entity->identifier(), 'instance_id' => $reply_field_instance['id'], ); $output = drupal_get_form('reply_form', (object) $form_options); $block->content = array( '#markup' => render($output), ); return $block; } * Output function for the 'reply_add_form' content type.functionreply_add_form_content_type_render($subtype,$conf,$panel_args,$context){  if(!$context[0]->data){    return;  $entity=$context[0]->data;  $reply_field_instance=field_info_instance($conf['reply_entity_type'],$conf['reply_field'],$conf['reply_entity_type']);  $block=newstdClass();  $block->title='';  $form_options=array(    'entity_id'=>$entity->identifier(),    'instance_id'=>$reply_field_instance['id'],  $output=drupal_get_form('reply_form',(object)$form_options);  $block->content=array(    '#markup'=>render($output),  return$block;

I’ve uploaded the plugin file as a gist to github, so you can download it from here. Also, it’s worth noting that there’s an issue in the issue queue of the reply module to get the panels support implemented, with this plugin supplied as a patch, so hopefully you won’t need to keep it as a separate plugin, as it should make it into the next alpha release soon, and in the dev branch in the next few days.

Dec 03 2014
Dec 03

Panelizer is a great module for being able to modify the layout of a page on a per-node basis. However, its greatest strength can sometimes be its greatest weakness. We found this out the hard way when a client asked us to help them add a block on every single page of their site directly beneath the h1 page title. Read on for how we approached this issue.

 Introduction

One of our clients with a pretty content-heavy site asked us to help them place a new block on every single page of their site. A pretty straightforward request, sure. But they wanted it to appear directly beneath the h1 page title on every page. At first glance, this looked like something core’s Block module could handle. Except that the page content is grouped together, so there was no way in the UI to place the new Block directly beneath the page title.

There are many possible approaches to this, but we had to work within the existing structure of the site and there are also multiple modules handling the layout of pages. For instance the Panelizer module is being used for some content types and standard nodes/templates for others. They are also using Views pages and one or two Page manager pages. So we needed a solution that would cover all these bases.

The trickiest part, and the focus of this article was the Panelizer pages. The Panelizer module is much like its parent, Panels, except Panelizer allows you to “Panelize” a page on a per-node basis. So with Panels we might create a Panels page, and apply the use of that page to a content-type. This means that we need only change the layout of the Panels page and it automatically applies to any page based on that.

Panelizer, however, works by doing this on a per node basis. So, a user could modify the layout of a single node page. For example, let’s say node/56 is a page called “Company Expenses”. The user wants to show some content taken from the company report, but we wants it laid out in a very custom and specific way. The layout is a little too complex or cumbersome for just using the WYSIWYG editor, and perhaps it is not intended to be reused anywhere else. This is where Panelizer is great. It gives users the flexibility to make changes to the layout AND content of a page, without having to touch any code. But...


“its greatest strength can sometimes be its greatest weakness.”


When a user does change the layout or the content, the page is then in an “Overridden” state. This means that any changes to the default Panelizer template, will not be reflected on overridden pages. In this case of our client, there were hundreds of pages where their layout/content was overridden. We could have just given them instructions on how to manually place the block using the Panelizer interface, but it would have been a lot of work to manually edit hundreds of pages, so we decided to look at a way to automate it.

Option 1: Preprocess page

The first place I looked was preprocessing of the page. I hoped that by using a general hook_preprocess_page I could simply add an additional element to the page array that would get rendered. Unfortunately, the Panels content of the page is already rendered to markup by the time it gets to hook_preprocess_page, so that wasn't going to work for us.

Option 2: Pre render/alter hook

The next road I went down was trying to do the same thing as above, but with a more targeted hook. Sometimes modules will provide the pre render or alter hook for you. I did some Googling and looked through the Panelizer documentation, the first function I came across was hook_preprocess_panels_pane(). I tried it out but found that it was preprocessing the individual panes within a region. I needed to actually provide a new pane, so it wasn't going to help us.

I then started to dig around in the actual Panelizer code. That is when I found calls to panelizer_pre_render(). This seemed to be perfect as it specifically targeted the exact thing I was trying to modify. One of the arguments that is passed to this function is the $display object of the entity. The display has a $content property which is an array of "panes". I thought this was going to be perfect. I could insert my own pane into this array and it should just work. It did work to some degree in that I could insert a pane, but for some reason I could not modify the order of the panes. The requirement was to have this new block appear directly beneath the h1 page title. I tried re-ordering the array elements. I tried using the 'position' property but nothing I did could change the order of elements at this point. At first I was just adding in an array but later I found the function panels_add_pane() function. I hoped this would help but unfortunately I still could not modify the order of the panes.

Option 3: Update hook/programmatic load/save

After some more research I came across the interesting approach outlined in this blog post. Basically it outlines how you can programmatically load a Panelizer entity and add in a new pane. It does in code what you would do through the Panels interface by clicking the gear icon then clicking 'Add Content', placing it in a region and then clicking 'Save'. I decided to write an update hook that would do something very similar. Here is my code:

First I wanted to grab all the Panelized nodes that are overridden. This function handles querying for them:

function example_block_pane_update() {
// Load all overridden nodes.
$panelizer_nodes = db_select('panelizer_entity', 'e')->fields('e', array('entity_id'))->condition('entity_type', 'node')->condition('did', 0, '<>')->groupBy('e.entity_id')->execute()->fetchCol('entity_id');
// Pass to function which will add my_block block.
example_add_block_pane_nodes($panelizer_nodes);
}

Notice that I am querying the panelizer_entity table looking for any entities that have the column “did” set to zero. This “did” column is a foreign key to an entry in the panels_display table, or the "display object" essentially. What I found was that all of our overridden nodes had their did set to zero. So this was how I queried the database for the entity id’s that I wanted to update. Note: I did find some anomalies with this approach. There were a handful of entities which had zero for did but were not listed as overridden. I was unable to determine why this was the case. It could be that I am making the wrong assumption about the did in this case but I wasn't able to find a more definitive answer at this stage. If you are aware of the answer please comment to let me know.

Now that I have queried and found all the overridden Panelizer nodes, I want to loop through these and add in my new block. Here is my function that handles it.

function example_add_block_pane_nodes($nids) {
$nodes = node_load_multiple($nids);
foreach ($nodes as $nid => $node) {
// Set region based on layout.
switch ($node->panelizer['page_manager']->display->layout) {
case 'landing_page':
$region = 'primary';
break;

case 'sub_landing_page':
$region = 'main';
break;

case 'onecol':
$region = 'middle';
break;
}
// Check if matching type.
if ($region) {
$panes = array();
$my_block_pane_exists = FALSE;
// Load the display.
if ($display = panels_load_display($node->panelizer['page_manager']->display->did)) {
// Get this display's panes.
$panes = $display->content;
// Reset the panes.
$display->content = array();
$display->panels[$region] = array();
// Loop through and add in our new pane.
foreach ($panes as $pid => $pane) {
// If my_block pane already exists we can skip this.
if ($pane->type == 'block' && $pane->subtype == 'my_block') {
$my_block_pane_exists = TRUE;
break;
}
// Set content.
$display->panels[$region][] = $pid;
$display->content[$pid] = $pane;
// Add a new pane after the node_title pane.
if ($pane->type == 'node_title' || $pane->type == 'page_title') {
$new_pane = panels_new_pane('block', 'my_block', TRUE);
$new_pane->panel = $region;
$display->panels[$region][] = $new_pane->pid;
$display->content[$new_pane->pid] = $new_pane;
}
}
// Finished reordering pane's, now save.
if (!$my_block_pane_exists) {
panels_save_display($display);
}
}
}
}
}


Some things to take note of here:

  • I set the $region value based on the layout for this entity. This is because the regions within the layouts can have different names.
  • The main foreach loop was modelled after the submit function panels_edit_display_form_submit() which is in panels/includes/display-edit.inc
  • Since we want this new pane to appear directly beneath the page title, we simply check as we loop through each pane if it is the page/node title and if it is we create the new pane using panels_new_pane() and apply it as the next element in the panels and content arrays on the display object.

The above functions were placed into an update hook and so we updated a large number of Panelized nodes in one go. I then wrote some tests that would test creating a new Panelizer node (using the default) and to look at some of the existing to ensure that my_block was present. 

Conclusion

Panelizer is a great module, so long as you are aware of this kind of potential issue. It can be particularly confusing to users who probably wouldn't consider the issue that even though you have the flexibility to change the layout/structure/content of a page, it then means that it can be incredibly difficult to automatically make changes to the layout/structure/content of those pages in the future. I think this kind of issue can be mitigated by careful design and site architecture. Perhaps instead of allowing the entire page to be Panelized, only certain parts? During the planning phase of a project you should consider whether users really need the flexibility to change the layout of every single page and again this should be weighed against the ability to later maintain those pages.

Panelizer Panels Layout
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.

Aug 29 2013
vp
Aug 29

Over the past few months our site has undergone a total re-design // upgrade from Drupal 6 => 7.   The priority was to rebuild functionality and migrate content quickly and cleanly – permissions schemes and editing privileges for our content contributors took a backseat. Now that the site has been launched and stabilized, we’ve begun to look at some of the tools we used to try to figure out how and if we can train our librarians to use them.

On our old site, we made pretty heavy use of blocks (particularly for sidebar items) – since we had to rebuild these pieces of content anyways, we tried to put them in more flexible containers.  We started looking at panels – we found Panopoly.  This tool worked really well for us.  We could (and did) use panel pages, custom panel layouts, views as panel panes, mini panels as blocks, blocks as mini panels, etc.  But when we started to turn over content editing responsibilities to our librarians, we discovered that the default settings were way too powerful for what they needed to do.  The interface was overwhelming and privileges were set too high – our content editors had too many options.  We had to scale back.

toomuch

The first step was to lock down Panelizer privileges on the home page – we were clued into this one when one of the librarians told us that she was seeing a “Change Layout” button on the site’s front page.  That meant she (or any other content editor) could have changed the layout of the home page with two button clicks.  Not good.

We probably could have done this a few different ways – we chose to change the renderer of the home page (built as a panel page) from “In-Place Editor” to “Standard”. Of course this means that we (admins) can’t use the groovy Panelizer interface when we want to edit content on the home page – but that’s cool since we know that those content regions are mini-panels and can be edited elsewhere.

homepagerenderer

That took care of the home page, but the librarians were still seeing too many options on the other pages (see first screenshot) – we could get rid of the In-Place Editor on all pages, but we’d have to make those configurations on each panel page (or page that had been panelized) and we would lose the slick, drag-and-drop editing interface. So we hit the permissions table.

We found that the permissions were set way too high. In the screenshots that follow, you’ll see what we left on – note that we have 11 roles in the table. The 3rd column from the left is admin, the 4th is editor (librarians) and the last one on the right is portal manager, which we made for development purposes. When we apply these changes to the production site, we’ll set editor privileges to the same as the portal manager on development. So just pay attention to the one on the far right – these are the only permissions we need for our use case: giving librarians permission to to edit layout and add panel content to basic web pages.

permissionstable2 permissionstable3 permissionstable1 permissionstable7

All other panel, panel pane, panelizer, etc. privileges in the table need to be locked down. Note that some of the permissions we turned on were specific to a content type (our “Web Page” content) and that this will vary depending on your needs.

Restricting these permissions reduces access to the editing interface.  Our librarians will no longer see “gear” buttons – they’ll only see “Customize This Page” and “Change Layout” .

buttons

But when they click “Customize This Page” and try to change panel content, they still get bombarded with too many editing options (see the first screenshot) – we can fix that. The Panelizer configuration allows you to adjust settings for allowed content per content authoring method.

allowedcontent1

Since we’re locking down Panelizer settings for the “Web Page” content type, that’s where we’re headed in the configuration table.

allowedcontent2

This is where we want to be – lots of boxes to uncheck, lots of buttons to push.

That’s cool – all our librarians need to do is add lists of links, images and text, and (ideally) to be able to reuse the content they create elsewhere.

Here are the settings we used:

allowedcontent3 allowedcontent4 allowedcontent5

The result?  Content editors can now…

…choose layout:

editing1

…add, edit, move, delete content to the regions within this layout:

editing2

…add links, images, text or reusable content:

editing3

Note that if they do want to reuse content, they have to specify that in the editor:

editing4
Jul 02 2013
Jul 02

For as long as I've been working with Drupal, there's been a wonderful tension on how to make Drupal not look like Drupal. People have taken great pride in being able to recognize a Drupal site both from the rendered page in a browser, and by looking at the code. How frustrating for graphic designers who want to create an experience, not just decorate Drupal! With a greater understanding of the contributed module space, it becomes infinitely easier to make Drupal look like your solution, instead of making your solution look like Drupal.

Each major revision of Drupal has seen a new point-and-click tool introduced into the contrib space. In Drupal 5 we got Panels. In Drupal 6 we got Display Suite and in Drupal 7 we got Omega. It's been fascinating to watch the evolution of our layout tool kit…and frustrating for new-to-Drupalers to know which one to pick. If we oversimplify what these UI-based layout tools do, we can divide the tools out as follows:

  1. Breaking down and altering the node.tpl.php file (e.g. Display Suite).
  2. Breaking down and altering the page.tpl.php file (e.g. Omega and Context ).
  3. Building up new layouts by pulling in specified Drupal components at the page or region level (e.g. Panels).

As you can see, the approaches are somewhat complementary, except when they're not. This can make it difficult to know which suite of modules (and themes) you should choose for your particular project. I'm an early adopter of Panels. I know it. I love it. (I helped write the D6 book on it.) And I'm delighted to see some of the concepts developed in Panels going into Drupal 8. BUT! I also know it's not the right tool for every site builder and themer. So where do you start?

Display Suite

First up: Display Suite. This is a beautiful little module. It fits politely into Drupal's existing administrative interface for managing content type fields. The very first time I installed Display Suite I used it to break my Blog display into the classic two column WordPress display: one side for the content, and one side with the date. It was a little bit revolutionary for me that I could just click a few things and never have to open a text editor to make a two-col layout. Yum!

I also feel in love with Drupal's built-in View Modes after playing with Display Suite. Instead of perverting Views I was able to get fine grained control of my nodes through Display Suite. (Don't pretend like you haven't used Views to "fix" the Default layout of your content type. We've all done it at least once.) If it sounds like the breath of fresh air you've been looking for, check out our free video series on using Display Suite.

Panels

Next up: Panels. I won't lie, there's a learning curve associated with this very powerful module. If you just want to shuffle some fields around in your content type display, this is not the module for you. You should use Panels, however, when you need to: display a block in multiple locations for different contexts; alter the layout of core Drupal pages, such as the taxonomy page; or provide different layouts under different conditions, such as the homepage for an authenticated or anonymous visitor. Yes, bits and pieces of these elements can also be found in other modules, such as Context (we'll get to that in a minute), but in most other cases you are spread across multiple configuration screens, instead of having a single control interface. Before rushing out to install Panels, take a look at our free video series on using Panels by the TWIG initiative lead, Jen Lampton. I hope that you end up loving Panels as much as I do, but we can still be friends if you decide it's overkill for your needs.

Omega

Finally we come to my nemesis, the base theme Omega. Ages ago I recorded a little video of myself unpacking Omega for the first time. Back then it was hard to use, undocumented, and very frustrating for anyone who was used to doing things "The Drupal Way". The video (shouldn't) exist any more because Omega has grown up a lot. It's become easier to use, and very well loved by a lot of Drupalers.

The 3.x branch of Omega was essentially its own theming system. Where most themes encourage you to take a series of Regions and sub-divide them using Blocks, Omega goes the other way, allowing you to collect Regions into Zones and Sections. Omega, as far as I know, was the first theme to integrate with CTools, allowing you to export your point-and-clicked theme settings to code. We cover both of these features in our video series Introduction to Omega 3.x. Watching the videos is a great way to explore Omega without the commitment. (And if someone else has already committed you to Omega, the videos are a great way to get up to speed.)

Be careful though! The Omega 4.x series takes OUT the pointy-clicky tools and puts them back in code, so make sure you're grabbing the right version when you download the theme. Or, if you want to move forward the the latest-and-greatest version of Omega, consider combining it with Panels for your layout. The base theme offers good integration with my favorite layout module.

So there you have it: three different approaches to altering the layout of your Drupal site. With these three powerhouse tools there's no reason for your Drupal sites to ever look Drupally again.

Apr 04 2013
Apr 04

Episode Number: 

136

Another episode covering the Drupal 7 Panels module. This time we continue learning about Panels module contexts, but focus on using Panels module contexts relationships with an entity reference field.

In this episode you will learn:

  • How to set up and use Panels context relationships
  • How to include related node information from an entity reference field

DDoD Video: 

Apr 03 2013
Apr 03

Episode Number: 

135

Another episode on the Drupal 7 Panels module, but this time we cover Drupal Panels Contexts.

In this episode you will learn:

  • What are Drupal Panels module contexts
  • How can Drupal Panels module contexts be used to load in additional information in a panel page
  • How can Drupal Panels module contexts be used to load in dynamic information based on a page argument

DDoD Video: 

Apr 02 2013
Apr 02

Episode Number: 

134

In this episode we continue learning about the Drupal 7 Panels module and go into how visibility rules can be used to control the display of individual Panels items within your Drupal panels content.

In this episode you will learn:

  • What Drupal 7 Panels module visibility rules are
  • How Panels visibility rules differ from Panels selection rules
  • How to use Drupal Panels visibility rules to hide and show content based on URL path
  • How to use Panels visibility rules to hide and show content based on user role or user permission

DDoD Video: 

Apr 01 2013
Apr 01

Episode Number: 

133

This episode continues with the Drupal 7 Panels Module and goes over how to use variants and selection rules within a custom panel page.

In this episode you will learn:

  • How to create multiple variants for a single Drupal 7 Panel page
  • How to use selection rules to display a different Drupal 7 Panel variant based on a URL path
  • How to use Drupal 7 Panels module selection rules to display a different variant based on a user's role on the Drupal site

DDoD Video: 

Mar 20 2013
Mar 20

Episode Number: 

130

In this episode we continue learning more about the Drupal 7 Panels module and go over how to use the panels flexible layout builder to easily build complex layouts using the panels module.

In this episode you will learn:

  • How to use the Panels flexible layout builder
  • How to convert a standard Panels layout to a flexible layout

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

DDoD Video: 

Mar 19 2013
Mar 19

Episode Number: 

129

This continues with the Drupal 7 Panels module and goes into more detail on how the Panels module can be used to override the display of the node view (or content type) pages.

In this episode you will learn:

  • How to use the Panels module to override the node view page on a Drupal 7 website
  • How to use Panels to completely customize the layout of content type pages for specific content types using selection rules and variants

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

DDoD Video: 

Mar 14 2013
Mar 14

Episode Number: 

128

The Drupal 7 Panels module allows the creation of customized page layouts using an easy to use drag and drop page builder. Panels can be used to build custom pages, change the layout of node (or content type) pages, and even modify the Drupal user profile or account page.

In this episode you will learn:

  • An overview of what the Drupal Panels module is and how it can be used
  • A quick overview of Drupal 7 Panels options and configurations
  • How to create a custom Drupal 7 Panel page

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

DDoD Video: 

Oct 24 2012
Oct 24

My last week at DoSomething I spent some time working on getting better metrics on which panel pages are slow. One half of that was to use New Relic's PHP API to provide better transaction names that included the node type and panel name:

<?php
/**
* Implements hook_page_alter().
*
* We want to provide more detail to New Relic on the transaction and late in
* the page build seemed like the simplest place.
*/
function example_page_alter(&$page) {
  if (!
extension_loaded('newrelic')) {
    return;
  } 
$name = NULL// Look for a panel page...
 
$panel_page = page_manager_get_current_page();
  if (isset(
$panel_page['name'])) {
   
// If it's a node page put the argument's node type into the transaction
    // name.
   
if ($panel_page['name'] == 'node_view') {
      if (isset(
$panel_page['contexts']['argument_entity_id:node_1']->data)) {
       
$node = $panel_page['contexts']['argument_entity_id:node_1']->data;
       
$name = 'page_manager_node_view_page/' . $node->type;
      }
    }
   
// If it's a page_manager page use the panel name.
   
else if ($panel_page['task']['task type'] == 'page') {
     
$name = 'page_manager_page_execute/' . $panel_page['name'];
    }
  }
  else {
   
$menu_item = menu_get_item();
    if (
$menu_item['path'] == 'node/%') {
     
// Looks like panels didn't have a variant and it's falling back to
      // node_page_view.
     
$name = 'node_page_view/' . $menu_item['page_arguments'][0]->type;
    }
  }  if (
$name) {
   
newrelic_name_transaction($name);
  }
}
?>

So once you know which panels are slowing down your site you can use the new Panels, Why so slow? module to put the blame on the specific panes.

Oct 17 2012
Oct 17

Though Panels comes with several built-in layotus for you to choose from, you’ll find that these don’t always suit your needs. Fortunately, there’s also a layout designer that anyone can use to create a new layout with panels. In this lesson you will learn how to use the Panels layout designer for rapid prototyping.

Oct 17 2012
Oct 17

Drupal site builders have long wanted to rearrange the display of each piece of content. The page manager module provides us with a default node view context we can use to accomplish just this. In this lesson you will learn how to break an article into two columns.

Oct 03 2012
Oct 03

Now that you’ve manually created a new variant for your front page, this lesson will teach you a much faster way to duplicate an existing variant. This technique is useful if your variants are very similar to one another.

Oct 03 2012
Oct 03

Panels and the Page manager module allow you to create different versions of the same page (called variants) under different circumstances. In this lesson you will learn how to build two different versions of the content on your home page. People who are logged in to your site will see a different home page than people who are not.

Sep 28 2012
Sep 28

We've launched a great new series by Jen Lampton, Building Websites in Drupal 7 Using Panels. The Panels module is a very powerful tool for site builders. Jen covers everything from the Panels interface to using variants for multi-scenario layouts. The first two videos are available for free, and cover getting started with Panels, creating a multi-column home page, and working with Panels panes.

Setting up a Multi-column Home Page

Adjusting the Settings for Each Panel Pane

Sep 26 2012
Sep 26

Each piece of content placed into a panel has it’s own configuration settings, covering everything from display style to access control. In this lesson we will take a tour through the settings for each pane.

Sep 26 2012
Sep 26

Drupal would like to assume that every page on your website will use the same layout. As it turns out, this is often not the case. The home page of your site, in particular, regularly uses a different layout. In this lesson we will demonstrate how you can use Panels to build a unique home page layout for your site.

Sep 26 2012
Sep 26

In this first series of Panels videos on Drupalize.me you will learn how to use the panels module to take your Drupal website to the next level.  When you reach the limits of what Drupal alone allows you to do with it’s layouts, adding Panels to your site enables you to create more sophisticated displays of your content.  

Here are just a few things the Panels module makes easy:

  • Divide the display of your content into multiple columns
  • Place blocks into the center of your pages
  • Use different layouts on the same page, under different circumstances

Because Panels works with the Page Manager module in ctools, you get all the benefits of the page manager, as well as complex control of your layouts. Page manager allows you to take over many of the ‘default’ displays in Drupal, including the display of content (nodes), profiles (users), category listings (taxonomy), and also the edit forms for each of these entities.

Jun 04 2012
Jun 04

Posted Jun 4, 2012 // 2 comments

Perhaps you already know Panelizer, the new-ish module that lets you turn any content type into a Panel, allowing a content editor to customize the layout of individual pieces of content as they are created.

Panelizer is a pretty cool tool that I've only recently started to explore on a new project. One of the tasks this new project needed was for the default Panelizer settings and configuration to be put into code and enabled as part of the site install process.

Panelizer defaults are a combination of CTools exportables and a handful of variables, which means if your site is already using Features and Strongarm, you can just bundle up those defaults into a custom Feature and you should be good to go. On this particular project, however, we aren't using Strongarm, so we needed to do this the "old fashioned" way.

In case you do, too, whether its because you just love using just Drupal core functionality or because for whatever reason you aren't or can't use Strongarm and Features, here's how to export your default Panelizer configuration into a simple custom module:

To get started let's create our own custom module for Drupal 7: my_panelizer

Create a my_panelizer.info file and enter the following into the file:

1
2
3
4
5
6
7
name = My Panelizer Defaults
description = Default settings for Panelizer customizations
dependencies[] = panelizer
package = custom
core = 7.x
 
files[] = my_panelizer.panelizer.inc

If you notice in the installation instructions for Panelizer it says the following:

"Visit the Page Manager administer pages page and enable the node template system page (node_view) if it is not already enabled. Panelizer won't work without this enabled!"

Well, we don't want to have to have such a manual step when installing these defaults, so let's do this in code instead.

Create my_panelizer.install with the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
/**
 * Imlpements hook_install().
 */
function my_panelizer_install() {
 
  // Set up variables and actions for Panelizer configuration
  my_panelizer_setup_panelizer();
}
 
function my_panelizer_setup_panelizer() {
  // Enable the node/%node page manager plugin
  variable_set('page_manager_node_view_disabled', FALSE);
}

Now, when this module gets installed it will enable the node_view template in Page Manager for us.

Let's move on to the actual Panelizer defaults. After configuring your default panelizer settings via the admin UI for a content type, we can export the panelizers.

Enable the Bulk Export module that is part of CTools, go to its administration page at Administration -> Structure -> Bulk Exporter and select the Panelizer defaults you'd like to export. Enter my_panelizer into the Module Name field and press Export.

You'll now need to create two new files: my_panelizer.module and my_panelizer.panelizer.inc Copy and paste from the textareas on the Bulk Exporter page into those respective files and you've now captured a good portion of your default Panelizer settings.

What's still missing, however, are four variables for each content type for which you've enabled Panelizer. To get these values you can either head into MySQL or use a handy tool like Drush to help you. I prefer Drush so that's what these instructions will use.

The first variable that we need to get is panelizer_defaults_node_<content-type> where <content-type> is the machine name of the content type.

For the remainder of this example, we will assume that your content type's machine name is 'page'

In your command line get to your Drupal docroot and then type:

drush vget panelizer_defaults

You should get back something like this:

1
2
3
4
5
6
panelizer_defaults_node_page: Array
(
    [status] => 1
    [default] => 1
    [choice] => 
)

Now, in your my_panelizer_setup_panelizer function in my_panelizer.install add the following:

1
2
  // Enable Panelizer on Basic Pages
  variable_set('panelizer_defaults_node_page', array('status' => TRUE, 'default' => TRUE, 'choice' => FALSE));

At this point, you have enough to install this module and set up this content type with a Panelizer default that will allow all layout choices and all content options. If you don't need to restrict layouts or content options, you can stop here.

If, however, you do need to restrict those things, we have a bit more work to do. There are three more variables that we need to set to restrict layouts and content options. One of these variables, however, stores an Object of type panels_allowed_layouts so setting it with variable_set() is not as easy as it could be.

If you go to your Panelizer default settings and configure the Allowed Layouts to only allow the built in "Flexible" layout and then save the settings, we can find the variable with Drush:

1
drush vget panelizer_node:page_allowed_layouts

Drush should return something like:

1
panelizer_node:page_allowed_layouts: "O:22:"panels_allowed_layouts":4:{s:9:"allow_new";b:1;s:11:"module_name";s:19:"panelizer_node:page";s:23:"allowed_layout_settings";a:10:{s:8:"flexible";b:1;s:14:"twocol_stacked";b:0;s:13:"twocol_bricks";b:0;s:6:"twocol";b:0;s:25:"threecol_33_34_33_stacked";b:0;s:17:"threecol_33_34_33";b:0;s:25:"threecol_25_50_25_stacked";b:0;s:17:"threecol_25_50_25";b:0;s:6:"onecol";b:0;s:8:"flexgrid";b:1;}s:10:"form_state";N;}"

This isn't super useful -- it's a serialized object and is hardly legible. Moreover, when I was attempting to just take this serialized string and set it during the install process, it wasn't working. The trick lies in that this is a special type of object from Panels, and luckily there is still a way we can create it programatically.

Add the following into my_panelizer_setup_panelizer():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  ctools_include('common', 'panels');
  $allowed_layouts = new panels_allowed_layouts();
  $allowed_layouts->allow_new = TRUE;
  $allowed_layouts->module_name = 'panelizer_node:page';
  $allowed_layouts->allowed_layout_settings = array(
    'flexible' => TRUE,
    'twocol_stacked' => FALSE,
    'twocol_bricks' => FALSE,
    'twocol' => FALSE,
    'threecol_33_34_33_stacked' => FALSE,
    'threecol_33_34_33' => FALSE,
    'threecol_25_50_25_stacked' => FALSE,
    'threecol_25_50_25' => FALSE,
    'onecol' => FALSE,
    'flexgrid' => FALSE,
  );
  $allowed_layouts->save();

The above code includes a file from Panels which allows us to create a new object of type panels_allowed_layouts set some values to it and then call its save() method, which does the job of saving this to the variables table for us.

Last, but not least, is how to configure whether your Panelizer will allow all new content options or only specific values. The variable panelizer_node:page_default stores an array of which content option types allow all items added after this configuration is set and you'll set it in my_panelizer_setup_panelizer() like so:

1
2
3
4
5
6
7
8
9
10
  variable_set('panelizer_node:page_default', array(
    "token" => FALSE,
    "entity_form_field" => FALSE,
    "entity_field" => FALSE,
    "entity_field_extra" => FALSE,
    "custom" => FALSE,
    "block" => FALSE,
    "entity_view" => FALSE,
    "other" => FALSE,
  ));

Then, if you've allowed only specific content options, you need to set panelizer_node:page_allowed_types. This is a very large array of options and you'll set it something like this (array snipped for brevity):

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
  variable_set('panelizer_node:page_allowed_types', array(
    ...
    "node_form_author-node_form_author" => 0,
    "node_form_buttons-node_form_buttons" => 0,
    "node_form_comment-node_form_comment" => 0,
    "node_form_log-node_form_log" => 0,
    "node_form_menu-node_form_menu" => 0,
    "node_form_path-node_form_path" => 0,
    "node_form_publishing-node_form_publishing" => 0,
    "node_form_title-node_form_title" => 0,
    "node_attachments-node_attachments" => 0,
    "node_author-node_author" => 0,
    "node_body-node_body" => "node_body-node_body",
    "node_comment_form-node_comment_form" => 0,
    "node_comments-node_comments" => 0,
    "node_content-node_content" => 0,
    "node_created-node_created" => 0,
    "node_links-node_links" => 0,
    "node_terms-node_terms" => 0,
    "node_title-node_title" => 0,
    "node_type_desc-node_type_desc" => 0,
    "node_updated-node_updated" => 0,
    "node-node" => "node-node",
    "form-form" => 0,
    "panelizer_form_default-panelizer_form_default" => 0,
  ));

And that's it! With these four variables and your exported Panelizers, you should now be able to enable this module (perhaps as part of your install profile) and your content type should be set up with your configured Panelizer defaults.

One last final note: I was having some trouble getting Panelizer 7.x-2.0 to read the defaults from code, however when I updated to Panelizer 7.x-2.x-dev everything started working just fine.

Happy coding!

Brian is the foremost authority on all things Mobile at Phase2 Technology. Brian frequently speaks about topics like front-end web performance, jQuery Mobile, and RaphaelJS. He has been working with Drupal since 2005 and has presented at ...

May 03 2012
May 03

Listen online: 

Kris Vanderwater talks about the Drupal 8 Initiative Blocks & Layouts Everywhere. Kris gives a brief overview of what this is and explains how he is working on the usability side, bringing together designers and javascript experts to ensure this will be a great experience for all users. Kris goes so far as to refer to it as "Panels in Core" and is looking for contributors, so if that strikes your fancy, take a listen to what Kris has to say.

This Drupal Voices was recorded at the 2012 DrupalCon in Denver.

Release Date: May 3, 2012 - 10:00am

Album:

Length: 8:00 minutes (5.56 MB)

Format: mono 44kHz 97Kbps (vbr)

Apr 23 2012
Apr 23

Ran into a views and panels issue today that was very simple to fix, but difficult to track down. Here is my situation in case you run into a similar views/panels issue.

I have one Drupal 7 panel page that does a lot of visibility rule filtering based on the path of the panel. Depending on the path, I have different pieces of content that show up. In some situations, this may be a view (othertimes it is static content, custom coded blocks, etc).

I wanted some of my views that are displayed in the this panel to have an exposed filter to allow easy searching. As I normally would with a view, I simply turned on the exposed filter on the views admin interface and went to make sure it was working... no luck. No exposed filter was being displayed.

I began searching for a solution and it turns out to be very simple. I have my views set up as block displays. In order for exposed filters to work with block views, you need to have Ajax enabled in the view. All I had to do to get this working, was go to the views admin and switch "use AJAX" over to "YES". After that, my exposed filters began showing up in my panel page.

A few things to note. This isn't really an issue with panels, it is only related to views being displayed as blocks, however in my situation, panels is what led me to encounter the problem. Any views block will need AJAX set to YES in order to see the exposed filters.

I also know that it is possible to set up Views Content Panes to use in my panels pages, instead of using Views Blocks. However a year or so ago, back in Drupal 6 I ran into a lot of restrictions with how Views Content Panes handled arguments and panels context, so I stopped using them. I have not used them recently so I can't say if those issues have been fixed or not yet. If anyone has arguments or opinions on why using Views Content Panes are better than using Views Blocks for displaying inside a Panel, I would be happy to hear you out.

Happy Drupaling.

Jan 27 2012
Jan 27

Today we are releasing the final videos in the Page Manger series! And, yes, these are FREE videos brought to you in conjunction with NodeOne.

We will explore:

TGIF (Thank goodness it's FREEday)!!

Jan 18 2012
Jan 18
Lullabot logo

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

Jan 18 2012
Jan 18
Lullabot logo

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

Jan 13 2012
Jan 13

As default, Omega uses the h1.title in region--content.tpl.php.
When using panels, the panel title is used in $title. But what will happen if you want to use your headline inside your panel-layout? A use case might be a project where you have both sidebars in some parts of the project and panels in another part "simulating" a sidebar, for instance with block-styled content in the left panel pane, while main content goes into the right one. Now you will have the headline "within" the main content area on a sidebar display, while it's above the main content area on a panel display. If classic blocks are situated in a sidebar and block-styled content in a sidebar-like panel pane, they will now have a different upper margin relatively to the site header and the headline might also "flip" over the blocks on the left hand side.

As this is difficult to explain only using words, we have a little scheme here:

Our goal is that the H1 title headline "optically" behaves the same in the panel context as if we would just have a normal sidebar. So how to do this?

First, we have to add the title to the panel template.
If you use one of Omegas default panel layouts, copy and paste the layout from Omega to yourtheme/panels/layouts/ and open the *.tpl file. Add the h1 to the position where you want ot to appear (normally the main content part).

<h1 class="title" id="page-title"><?php print $display->get_title(); ?></h1>

($display->get_title() will catch the title that is defined in the panel.)

For custom templates, just add this code snippet to the place you want the title to be displayed.

Now the title should be displayed at the correct position. Or, at least, one of two, as now it is probably displayed twice, since it's still in region--content.tpl.php
However, we can't just remove it from there, because it should still be displayed if we are on a non panel page.

To check if the page is displayed with panels, you can use this function.

function has_panel() {
if (panels_get_current_page_display()) {
return true;
}
return false;
}

Put this in your template.php

Copy and paste the region--content.tpl from omega to your themes template folder and add the has_panel condition to the title request.

This should look like this:

<?php if ($title): ?>

Transform it to:

<?php if ($title && !has_panel()): ?>

Now it should work! Don't forget to set the titles in Panels now.

Jan 05 2012
Jan 05

One major problem with using panels for all pages is
that many modules doesn't have built in integration
for panels. A solution to this problem is to write an
integration for the module to get the full control but
this sure needs some time. So Swentel wrote a
module which allows to take over any existing page, yes any!

To use it start on admin > structure > pages
and "add an existing page". Let's put the Panels configuration page into a panel, so input something into the search paths, see image.

Once you have done this go back to admin > structure > pages,
there you see "pm_existing_pages-{name you choosed above}"; enable it
and configure the panel.

One important pane you should know is under "existing page" -> "existing page":

That's all, see

AnhangGrösse 33.9 KB 15.19 KB 44.74 KB
May 09 2011
May 09

Open Academy is an easy to use Drupal product from Chapter Three specifically designed to create university department websites. Over the past few years, we have worked closely with the nation's leading universities - including Stanford, UC Berkeley, NYU, USC, and UCSF - to help them build great Drupal websites.  We now distilling down that knowledge down and offering it as a fully functional Drupal 7 powered "Departmental Website in a Box" package. Open Academy is currently being refined in a closed alpha/beta with an initial group of users, but will be rolling out more widely this summer. If you are interested in participating in our beta process, sign-up for our beta-list and follow @chapter_three for more updates. 

 

As a best practice departmental website, Open Academy lets you quickly and simply create a new departmental website by entering some basic information about your site in our installation wizard which sets everything up for you. For starters, we baked in critical functionality around departmental news, faculty profiles, publications and presentations, events and calendaring, courses, resources and links, video, social media, and degrees and programs.

In addition, we have built in a state of the art administration dashboard, basic SEO rules, WYSIWYG editing functionality, 508 Accessibility Compliance, and drag-and-drop page layout from the always amazing Panels module. All of this has been developed on a cleanly extendible Drupal 7 install profile with Features-basedKit Compliant functionality and a well documented starter theme you can modify and extend to meet your particular needs. 

We know that the university department is at the center of academic life and Open Academy lets an academic department get an amazing website online easily that communicates their teaching and research to the world using the power of Drupal.

Over the next few months, we will be talking more about Open Academy and highlighting the different universities already using it to build their departmental websites. If you are interested in participating in our beta or interested in helping us build even more features, get in touch with us on our beta-list and watch for our public beta release this summer!

Apr 08 2011
Apr 08

I wanted to create a node view containing both the original node and its translation, sort of like this neat Meedan.net page. I decided to build this page as a Panels node view template, but to reach the translation(s) I had to write a new CTools relationship plugin that I am sharing here:

<?php
// @file mymodule.module/**
* Implementation of hook_ctools_plugin_directory().
*
* It simply tells panels where to find the .inc files that define various
* args, contexts, content_types.
*/
function mymodule_ctools_plugin_directory($module, $plugin) {
  if (
$module == 'ctools' && !empty($plugin)) {
    return
"plugins/$plugin";
  }
}
?>

Place the following file in a plugins/relationships subfolder of your module:

<?php
// @file node_translation.inc/**
* Plugins are described by creating a $plugin array which will be used
* by the system that includes this file.
*/
$plugin = array(
 
'title' => t('Node translation'),
 
'keyword' => 'translation',
 
'description' => t('Creates the translation of a node as a node context.'),
 
'required context' => new ctools_context_required(t('Node'), 'node'),
 
'context' => 'ctools_translation_from_node_context',
 
'settings form' => 'ctools_translation_from_node_settings_form',
 
'defaults' => array('language' => 'en', 'fallback' => FALSE),
);
/**
* Return a new context based on an existing context.
*/
function ctools_translation_from_node_context($context, $conf) {
 
// If unset it wants a generic, unfilled context, which is just NULL.
 
if (empty($context->data)) {
    return
ctools_context_create_empty('node', NULL);
  }  if (isset(
$context->data->nid)) {
   
$original = node_load($context->data->nid);
   
$tnids = translation_node_get_translations($original->tnid);
    if (empty(
$tnids[$conf['language']])) {
      return
$conf['fallback'] ?
       
ctools_context_create('node', $original) :
       
ctools_context_create_empty('node', NULL);
    }
   
$translation = node_load($tnids[$conf['language']]->nid);    // Send it to ctools.
   
return ctools_context_create('node', $translation);
  }
}function
ctools_translation_from_node_settings_form($conf) {
 
$form['language'] = array(
   
'#type' => 'select',
   
'#title' => t('Language'),
   
'#options' => locale_language_list(),
   
'#default_value' => $conf['language'],
  );
 
$form['fallback'] = array(
   
'#type' => 'checkbox',
   
'#title' => t('Fallback to original node'),
   
'#description' => t('Use original node if desired translation is not found.'),
   
'#default_value' => $conf['fallback'],
  );
  return
$form;
}
?>

To use this plugin, create a new variant of the Panels node view template. In the Contexts page, add a Node translation relationship for every language that you want to support. You can specify the desired language in the relationship settings. In your Content layout, you will then be able to use those relationships to access the node translations.

To go back to the original Meedan.net example above, we want the English version on the left and the Arabic on the right. To achieve this, I created two node translation relationships, one for English and the other for Arabic. I placed a node content panel in each of my two columns, and set the left panel to refer to the English translation and the right panel to the Arabic translation. Note that I didn't use the original "Node being viewed" content because there would be no way of telling if it's Arabic or English, given that this node template could be accessed via either versions.

What's missing is to replicate the bilingual comments view, and I'll share my solution once I implement it :-)

Apr 10 2010
Apr 10

This video shows you how to create a custom page for your site using the Panels module. You can use Panels to help you make pages (including a front page) that include a variety of content from your site including content that exists in views, blocks and nodes. The Panels module is dependent on the Chaos Tool Suite module.

Note: Click the 'full screen' icon (to the left of the volume control) in order to watch online at full 1280x720 resolution.

Video Links

Flash Version

Feb 24 2010
Feb 24

They will load faster, it's easier for deployment, and you'll be able have them under version control.

We all know that you can load views from code, and it is even recommended. But what about Panels pages? It is also possible. And actually, it's quite easy.

Let's say that you have reached a beta stage for these pages, and you are ready to start having these under version control so you can sleep better at night.

1. Create the module that will load these pages

panels_default_pages_file_structure.png
We'll call our module ctools_defaults. On the right we see the structure and files that we will need in our module.
Inside of the pages directory, we'll be inserting each of our exported pages.

2. Implement hook_ctools_plugin_api()

This is done in order to let ctools know that our module has something to say. We'll do this inside ctools_defaults.module.

<?php
/**
  * Implementation of hook_ctools_plugin_api().
  */
function ctools_defaults_ctools_plugin_api($module, $api) {
  if (
$module == 'page_manager' && $api == 'pages_default') {
    return array(
'version' => 1);
  }
}
?>

3. Implement hook_default_page_manager_pages()

The name of the file, very important, will be MODULENAME.pages_default.inc - and in our case ctools_defaults.pages_default.inc. Because we are clever, in it we'll do a little trick, to facilitate our lives, and have each page in its separate file. This will allow us to have an independent version control per page, and also make the re-exporting/editing of existing pages easier.

<?php
/**
* Implementation of hook_default_page_manager_pages().
*/
function ctools_defaults_default_page_manager_pages() {
 
$pages = array();
 
$path = drupal_get_path('module', 'ctools_defaults') . '/pages';
 
$files = drupal_system_listing('.inc$', $path, 'name', 0);
  foreach(
$files as $file) {
    include_once
$file->filename;
   
$pages[$page->name] = $page;
   }
  return
$pages;
}
?>


So what's special? not much, this code looks for .inc files in the directory called pages within our module, and for each of the files found, it tells panels that we have a default page. Each new file that we place in there will be loaded automatically.

4. Export our panel pages

The process is pretty simple, but in case someone gets lost, the button is right here when you are editing a panel page:
panel_page_export_button.png

Which will get you to a page with all the code ready to be copied, like:
panel_page_export_code.png

5. Create our PAGENAME.inc

We create a file called PAGENAME.inc inside our pages directory, within our module's directory.

<?php
// paste below the code exported from the panels UI
?>


Inside this file, we'll open php, and paste the code copied in the previous screen. Just like that, don't be shy and save the file.

6. Empty the cache

Panels caches (thankfully) the default pages that third party modules provide, so we must clear the cache when we create a new default page, and when we modify the code of an existing one.

Conclusion

This technique let's us sleep better at night. If someone ever touches the panel page, and breaks things, we can always revert to the default code. We'll be able to create pages based on existing ones if they are very similar, just by copying and modifying the original code, reducing our development time, and improving our personal relationships as an unexpected bonus.

If you have any comments, you know what to do. If you see a mistake in my technique, please do let me know and I'll fix it right away.

Attachment Size ctools_defaults.tar.gz 926 bytes

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