Open Atrium (OA) core utilizes Panelizer, Fieldable Panels Panes, and CTools to give content managers (CM) a convenient UI for adding new and existing content to a node. Open Atrium core also includes the Webform contrib module but, unfortunately, neither is it panelized nor is it ready as a CTools content type out-of the box.
This means that (a) core OA does not allow a CM a way to add other panes to a webform node and, thus, no way to add panes native display. In this post, we will outline how to add a new content type to CTools and how to define that new content type.
First, we need to create a module. I’m calling mine:
1 |
tmbridge_webform_panes |
So in my modules/custom directory, I’ve created a directory:
1 |
tmbridge_webform_panes |
and files:
1 2 3 |
tmbridge_webform_panes.module tmbridge_webform_panes.info |
Of course, you’ll need to write your info file but I’m sure you know that already! Now we need to tell CTools where to find our plugin files so my module file looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?php /** * @file * Main file for the module. * * We define the content type plugins directory for this module. */ /** * Implements hook_ctools_plugin_directory(). * * Tell ctools where to look for our plugin files. */ function tmbridge_webform_pane_ctools_plugin_directory($owner, $plugin_type) { if ($owner == 'ctools' && $plugin_type == 'content_types') { return 'plugins/' . $plugin_type; } } |
This is boilerplate code that implements hook_ctools_plugin(). It tells CTools to look in directory “plugins” for plugin files when looking for content types.
Within the tmbridge_webform_panes directory, next to the tmbridge_webform_panes.module, let’s create a “plugins” directory and inside that, let’s create a “content_types” directory. My directory structure now looks like:
1 2 3 4 5 |
tmbridge_webform_panes -- plugins -- content_types tmbridge_webform_panes.info tmbridge_webform_panes.module |
CTools knows to look for include files within the directory that is returned from the call to hook_ctools_plugin_directory() so let’s create an include file within the content_types directory. We’ll call it tmbridge_webform_panes.inc. Our directory structure now looks like:
1 2 3 4 5 6 |
tmbridge_webform_panes -- plugins -- content_types -- tmbridge_webform_panes.inc tmbridge_webform_panes.info tmbridge_webform_panes.module |
The first thing we need in this file is a $plugin array definition that outlines what this plugin does:
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 |
<?php /** * @file * Main file for the the webform content type definition. */ /** * Ctools plugin configuration. * * This $plugin array which will be used by the system that includes this file. */ $plugin = array( 'single' => FALSE, 'title' => t('Rendered Webform'), 'description' => t('Shows a rendered Webform.'), 'category' => t('Webforms'), 'edit form' => 'tmbridge_webform_panes_webform_panes_edit_form', 'render callback' => 'tmbridge_webform_panes_webform_panes_render', 'admin info' => 'tmbridge_webform_panes_webform_panes_admin_info', 'content types' => 'tmbridge_webform_panes_webform_panes_type_content_types', 'defaults' => array( 'selected_forms' => 1, ), ); |
Many of these elements are self-explanatory. The first element, single, tells CTools that this plugin has 0 subtypes. The next three elements are string definitions that will appear in the UI for CMs. The latter four elements define the callback functions that will be called when this plugin’s code is run. Next in this post, we’ll get to the meat-and-potatoes of writing a CTools plugin: callback functions.
We need to determine which types this plugin should be able to render. Since we are concerned with webforms, we’ll query the database for all subtypes that have webform components and build an array containing that set:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
/** * Content types callback. * * This callback returns the list of subtypes available to the content type * plugin. */ function tmbridge_webform_panes_webform_panes_type_content_types() { $types = array(); // Selects the node types that have webform components associated with them. $query = db_select('webform_component', 'w') ->fields('w', array('nid')) ->distinct(); $query->leftjoin('node', 'n', 'n.nid = w.nid'); $query->fields('n', array('type')); // Builds an array with the node types and their content type ctools info. foreach ($query->execute() as $result) { $types[$result->type] = array( 'title' => node_type_get_name($result->type), 'category' => t('Webforms'), ); } return $types; } |
Next, we’ll build the edit form that presents available options to the user by writing the callback function that we defined earlier in the plugin array.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/** * Edit form callback for the content type. * * This edit form presents the options available when adding a webform to a * panel. It allows the user to specify a view mode and the node that is to be * rendered. */ function tmbridge_webform_panes_webform_panes_edit_form($form, &$form_state) { $conf = $form_state['conf']; // Sets the subtype according with the chosen value in the UI. $subtype = $form_state['subtype_name']; $options = array( 'default' => 'Default', ); // Select options with the nodes of the selected subtype which have webforms. $subtype_webforms = _webform_panels_get_all_forms($subtype); $form['selected_forms'] = array( '#title' => t('Webform to embed'), '#description' => t('Used to embed the selected webform in the page.'), '#type' => 'select', '#options' => $subtype_webforms, '#default_value' => $conf['selected_forms'], '#required' => TRUE, ); return $form; } |
We are only concerned with the webform to embed in this plugin, but this function can be extended to include any kind of user-defined settings you may need.
You may notice that in this callback function, we make a call to a helper function called _webform_panes_get_all_forms($subtype). This is a custom auxiliary function defined as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/** * Helper function that retrieves all nodes that have webform components. * * @return options[] * An array of with the returned webforms keyed nid->title */ function _webform_panels_get_all_forms($subtype) { // Select all nodes from the database that have webform components. $query = db_select('webform_component', 'w')->fields('n', array('nid'))->fields('n', array('title'))->distinct(); $query->leftjoin('node', 'n', 'n.nid = w.nid'); $query->condition('n.type', $subtype, '='); // Add node_access tag to show only nodes the user can access. $query->addTag('node_access'); // Builds an array with all the nodes returned by the query. $options = $query->execute()->fetchAllKeyed(); return $options; } |
Something to note in this function is the line:
1 |
$query->addTag('node_access'); |
This is how we limit the select options on the edit form to include only nodes to which the user has access. If our use case called for it, we could instead look here for the node_edit permission and only allow a user to include a webform to which they have edit access. We’d like users to embed any forms to which they have access so all we are looking for is node_access.
Next, we define the form submit callback which saves the form state information:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * The edit form submit handler. * * Saves the submitted form data in the edit form. */ function tmbridge_webform_panes_webform_panes_edit_form_submit($form, &$form_state) { if (isset($form_state['values']['selected_forms'])) { $form_state['conf']['selected_forms'] = $form_state['values']['selected_forms']; } if (isset($form_state['values']['view_mode'])) { $form_state['conf']['view_mode'] = $form_state['values']['view_mode']; } } |
Finally, we can talk about rendering. The render function below creates a block from the selected webform and defines the view mode that will be used to render said node.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
/** * Render callback for the content type plugin. * * Renders the webform in the page using the selected view mode. */ function tmbridge_webform_panes_webform_panes_render($subtype, $conf, $panel_args, $context = NULL) { $block = new stdClass(); $view_mode = 'pane'; // Initialize settings array $settings = array('is_webform' => TRUE); // Add the rendered node in the selected view mode to the block. $node = node_load($conf['selected_forms']); // Use the webform node's title as the block title if not overridden by user. if ($conf['override_title'] == 0) { $block->title = $node->title; } // Check if user has access to the node being rendered. $access = node_access('view', $node); // If the user has access to these webforms, render them. if ($access) { $content = new stdClass(); $render = node_view($node, $view_mode); $content->content = render($render); $block->content = theme('tmbridge_theme_pane', array('content' => $content, 'settings' => $settings)); } return $block; } |
We check here if the pane has an override title defined and, if not, we use the webform node’s title as default. A check for permission is made here as well. We also take this opportunity to populate the $settings variable with a flag that will be used in the corresponding template to easily check if this block is a webform block.
We also define here the theme hook to use to build the block content. This is completely dependent on the theme you are using, I’ve opted for a custom hook defined in my theme but use whatever suits your needs (we’ll talk more about creating a custom theme hook to use here in another post.). You can find more information on the theme() function in drupal.org’s API documentation.
And, with one last function, we define the administrative title that will be used for a webform in a pane:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** * Implements hook_PLUGIN_content_type_admin_title(). * * Returns the administrative title for a webform in a panel. */ function tmbridge_webform_panes_webform_panes_content_type_admin_title($entity_type, $conf, $contexts) { // Get all available view modes for node. $entity_info = entity_get_info('node'); // Get the chosen view mode. $view_mode = $conf['view_mode']; // Get the chosen node. $node = node_load($conf['selected_forms']); if (isset($entity_info['view modes'][$view_mode])) { $view_mode = $entity_info['view modes'][$view_mode]['label']; } return t('Rendered webform "%title" using view mode "@view_mode"', array('%title' => $node->title, '@view_mode' = > $view_mode)); } |
And voilà! We now have a new content type available to CTools to embed a webform within a pane on any Panelized node’s display. The plugin pulls all accessible webforms into a select box and allows the user to select from that list:


Of course, Open Atrium has native functionality to embed blocks and, at least with Webforms 3 and up, we can configure all webform nodes to automatically create an associated block — however, a custom CTools plugin yields a more comfortable user experience for CMs as Webforms are considered a distinct content type. It also allows more customization of and during the rendering a webform.
Another alternative to a custom CTools plugin would be to “Panelize” the entire Webform content type. This would allow CMs to add other panes to a webform node itself. This comes with its own set of pros and cons. I will cover this process along with its pros and cons in another post.
A corollary to creating a plugin for a new CTools content type is to create a new pane style plugin for CTools. Combined together, a content type plugin and a style plugin can yield some great functionality. I plan to write another post on that subject soon.
CTools plugins are incredibly powerful. The above tutorial is merely one use case but the methodology and structure is consistent across the board. Be sure to keep them in your toolchest for when a need may arise.