Drupal 6: Programmatically Create Panels and Nodes in Panels

This tutorial provides the wherewithal to create a new node and insert that node in the top pane on a programmatically created Panel. Looking around the net I was surprised that nobody has done this before. The context for my requirements comes from a not-for-profit client of mine - they wanted to be able to create a new type of charitable donation dependent upon a campaign's remit (this would be the node) and then to place that campaign information (node) in the top pane of the panel. The staff working at the not-for-profit are very talented Drupal-ers and would be able to place supporting static HTML blocks around the node on the panel once they've created the node and the panel.

The inspiration on how to do this comes from analysing the output of the Bulk Exporter utility. To see how this works, create a test panel using your chosen layout and place a test node of type Story on the uppermost pane, go to admin/build/bulkexport, click on the panel you just created, give it a module name, then export. The most significant part of the exported code is the hook_default_page_manager_pages - this does the heavy lifting, creating the handler, page, display and pane classes. So any module we develop in this tutorial will have to implement this hook.

For the Impatient - the Code

donation_campaign.info

name = "Donation Campaign"
description = "Donation Campaign based around Panels"
core = 6.x
php = 5.2
dependencies[] = "panels"
dependencies[] = "page_manager"

donation_campaign.install
<?php
/**
* Implementation of hook_install().
*/
function donation_campaign_install() {

   
drupal_install_schema('donation_campaign');

}



/**
* Implementation of hook_uninstall().
*/
function donation_campaign_uninstall() {

   
drupal_uninstall_schema('donation_campaign');
}



/**
* Implementation of hook_schema().
*/
function donation_campaign_schema() {

   
$schema['donation_campaign'] = array(
       
'fields' => array(
           
'vid' =>            array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
           
'nid' =>            array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
           
'panel_data' =>     array('type' => 'text', 'not null' => FALSE),
            ),
       
'unique keys' => array(
           
'nid_vid' =>        array('nid', 'vid'),
            ),
       
'indexes' => array(
           
'nid' =>            array('nid'),
        ),
       
'primary key' => array('vid'),
    );

    return
$schema;
}
?>

donation_campaign.module
<?php
/**
* Implements hook_node_info().
*/
function donation_campaign_node_info() {

    return array(
       
'donation_campaign' => array(
           
'name' => t('Donation Campaign'),
           
'module' => 'donation_campaign',
           
'description' => t("Donation Campaign based on the Panels module technology"),
           
'has_title' => TRUE,
           
'title_label' => t('Donation Campaign Title'),
           
'has_body' => TRUE,
           
'body_label' => t('Donation Preamble'),
            ),
        );

}




/**
* Implementation of hook_form().
*/
function donation_campaign_form(&$node) {

   
$type = node_get_types('type', $node);

    if (
$type->has_title) {
       
$form['title'] = array(
       
'#type' => 'textfield',
       
'#maxlength' => 250,
       
'#title' => check_plain($type->title_label),
       
'#required' => TRUE,
       
'#default_value' => check_plain($node->title),
       
'#weight' => -5,
        );
    }

    if (
$type->has_body)
       
$form['body'] = node_body_field($node, $type->body_label, $type->min_word_count);


    return
$form;

}




/**
* Implements hook_perm()
*/
function donation_campaign_perm() {

    return array(
       
'create donation_campaign',
       
'edit donation_campaign',
       
'delete donation_campaign',
       
'view donation_campaign',
    );
}



/**
* Implements hook_access()
*/
function donation_campaign_access($op, $node, $account) {

    switch (
$op) {
        case
'create':
            return
user_access('create donation_campaign', $account);
        case
'update':
            return
user_access('edit donation_campaign', $account);
        case
'delete':
            return
user_access('delete donation_campaign', $account);
        case
'view':
            return
user_access('view donation_campaign', $account);

    }
}



/**
* implements hook_insert().
*/
function donation_campaign_insert($node) {

   
db_query(
       
'INSERT INTO {donation_campaign} (vid, nid) '
       
."VALUES (%d, %d)",
           
$node->vid,
           
$node->nid);

   
// create the panel if everything looks ok
   
if ($node->path and (strpos($node->path, '/') === FALSE)) {

       
// now construct and save the panel data
       
$page = new stdClass;
       
$uscore_title = str_replace(' ', '_', $node->title);
       
$page->disabled = FALSE;
       
$page->api_version = 1;
       
$page->name = $uscore_title;
       
$page->task = 'page';
       
$page->admin_title = $node->title;
       
$page->admin_description = $node->title;
       
$page->path = 'donate/' . $node->path;
       
$page->access = array();
       
$page->menu = array();
       
$page->arguments = array();
       
$page->conf = array();
       
$page->default_handlers = array();


       
$handler = new stdClass;
       
$handler->disabled = FALSE; /* Edit this to true to make a default handler disabled initially */
       
$handler->api_version = 1;
       
$handler->name = 'page_' . $uscore_title . '_panel_context';
       
$handler->task = 'page';
       
$handler->subtask = $uscore_title;
       
$handler->handler = 'panel_context';
       
$handler->weight = 0;
       
$handler->conf = array(
           
'title' => '',
           
'no_blocks' => 0,
           
'pipeline' => 'standard',
           
'css_id' => '',
           
'css' => '',
           
'contexts' => array(),
           
'relationships' => array(),
        );

       
$display = new panels_display;
       
$display->layout = 'twocol';
       
$display->layout_settings = array();
       
$display->panel_settings = array(
           
'style_settings' => array(
               
'default' => NULL,
               
'left' => NULL,
               
'right' => NULL,
            ),
        );
       
$display->cache = array();
       
$display->title = '';
       
$display->content = array();
       
$display->panels = array();

       
$pane = new stdClass;
       
$pane->pid = 'new-1';
       
$pane->panel = 'left';
       
$pane->type = 'node';
       
$pane->subtype = 'node';
       
$pane->shown = TRUE;
       
$pane->access = array();
       
$pane->configuration = array(
           
'nid' => "{$node->nid}",
           
'links' => 0,
           
'leave_node_title' => 0,
           
'identifier' => '',
           
'build_mode' => 'full',
           
'link_node_title' => 0,
           
'override_title' => 0,
           
'override_title_text' => '',
        );
       
$pane->cache = array();
       
$pane->style = array(
           
'settings' => NULL,
        );
       
$pane->css = array();
       
$pane->extras = array();
       
$pane->position = 0;

       
$display->content['new-1'] = $pane;
       
$display->panels['left'][0] = 'new-1';

       
$display->hide_title = PANELS_TITLE_FIXED;
       
$display->title_pane = 'new-1';
       
$handler->conf['display'] = $display;
       
$page->default_handlers[$handler->name] = $handler;

       
db_query("UPDATE {donation_campaign} SET panel_data = '%s' WHERE vid = %d", serialize($page), $node->vid);

       
menu_rebuild();

    }
}



/**
* Implementation of hook_default_page_manager_pages().
*/
function donation_campaign_default_page_manager_pages() { 

   
$ret = db_query("SELECT panel_data FROM {donation_campaign} INNER JOIN {node} USING (vid) WHERE `status` = 1");
    while(
$res = db_fetch_object($ret)) {
       
$page = unserialize($res->panel_data);
       
$pages[$page->name] = $page;
    }

    return
$pages;

}



/**
* implements hook_update().
*/
function donation_campaign_update($node) {

    if (
$node->revision)
       
donation_campaign_insert($node);
   
//else
    //    db_query("UPDATE {donation_campaign} SET WHERE vid = %d",
    //        $node->vid);

}



/**
* Implementation of hook_ctools_plugin_api().
*/
function donation_campaign_ctools_plugin_api($module, $api) {

    if (
$module == 'page_manager' && $api == 'pages_default')
        return array(
'version' => 1);
}





/**
* Implements hook_delete().
*/
function donation_campaign_delete($node) {

   
db_query('DELETE FROM {donation_campaign} WHERE nid = %d', $node->nid);
}




/**
* This implementation just handles deleting node revisions.
* Implements hook_nodeapi().
*/
function donation_campaign_nodeapi(&$node, $op, $teaser, $page) {

    if (
$op == 'delete revision')
       
db_query('DELETE FROM {donation_campaign} WHERE vid = %d', $node->vid);
}





/**
* Implementation of hook_load().
*/
function donation_campaign_load($node) {

   
$result = db_query('SELECT * FROM {donation_campaign} WHERE vid = %d', $node->vid);

    return
db_fetch_object($result);
}
?>

For the Relaxed - the Explanation

Those who develop Drupal modules which create new content types should be very familiar with the basic layout of the code, and there is little to be gained from a step-by-step analysis of that aspect of the code. Those wishing to find out more should check out Learning Drupal 6 Module Development or Drupal 7 Module Development.

In the installation source code you will note I have created a field panel_data - this will become a holding area for the serialized page class that contains all the panel information. In reality, there would be additional fields in the table for other functionality, but since this is just a proof of concept test, I have omitted this meta data.

Now look at the module file. After we insert the new node into the database we create the panel using the same sequence of class instantiation as we saw from the bulk exporter. This is saved in the field we just discussed. Note that the layout is hard coded in the $display as two columns. If you need to use a different layout, you should create a sample panel the normal way, then export it using bulk exporter, then copy and paste the $display code over what I have here. In addition, you will need to change the $pane->panel value.

Once the panel_data is saved we need to rebuild the menus for what we have to take effect so we issue a menu_rebuild();

We need to implement hook_default_page_manager_pages - but instead of the hard coded data we see in the example from the bulk exporter, we retrieve the page classes from the panel_data field and unserialize, saving in a array to be returned once all the pages have been retrieved.

Finally, hook_update will need to be changed dependent upon your circumstances. You will need to add your own meta information to the commented out database update call.