background image
HomeRecent PostsDrupalSearchTagsRSSContactAboutAccount
Eric.London's picture

In this article, I'll address an issue that has surfaced on every recent project I've been working on: how to update nodes when a CCK field's allowed values list changes. For my projects, I implemented a variety of batch processes and drush scripts to solve this problem. But for this article, I decided to implement a form submit handler solution to automatically update nodes when the CCK field settings page is submitted. Please be careful when implementing code like this, it has potential to update a lot of nodes, and hit memory limitation issues depending on how many nodes need to be updated. As I mentioned before, I implemented a combination of batch APIs and drush scripts to process production environments; I implemented this code in a development environment. Anyway..

I created a new content type "band", and added a CCK text/select field (genre) with an allowed values list:

Allowed value list before

I used Devel's auto-generate content functionality to create 20 nodes (/admin/generate/content), and then added a simple view to show the node's title and genre:

view of nodes before

Now that I had some working test data, I added some code to a custom module to modify the CCK field property settings page, and add a submit handler update the nodes:

<?php
// define module constants
define('CCK_FIELD_PROCESS', 'field_genre');
define('CCK_TYPE_PROCESS', 'band');

/**
* Implements hook_form_alter()
*/
function helper_form_alter(&$form, &$form_state, $form_id) {
 
 
// check for CCK form, CCK type, and CCK field (see above constants)
 
if ($form_id == 'content_field_edit_form' && $form['type_name']['#value'] == CCK_TYPE_PROCESS && $form['field_name']['#value'] == CCK_FIELD_PROCESS) {
   
   
// get original allowed values
   
$original_allowed_values_string = $form['field']['allowed_values_fieldset']['allowed_values']['#default_value'];
   
   
// explode values, get array of values   
   
$exploded = explode("\n", $original_allowed_values_string);
   
$original_allowed_values_array = array();
    foreach (
$exploded as $data) {

     
// explode data on "|"
     
list($key, $value) = $exploded2 = explode('|', $data);     
     
$original_allowed_values_array[$key] = $value;
     
    }
   
   
// ensure data exists
   
if (empty($original_allowed_values_array)) {
      return;
    }

   
// store original values in form state storage
   
$form_state['storage']['original_allowed_values'] = $original_allowed_values_array;
   
   
// store other information in form state
   
$form_state['storage']['cck_type'] = CCK_TYPE_PROCESS;
   
$form_state['storage']['cck_field'] = CCK_FIELD_PROCESS;

   
// add submit handler
   
$form['#submit'][] = '_helper_form_alter_content_field_edit_form_submit';
       
  }

}

/**
* Implements custom form submit handler
*/
function _helper_form_alter_content_field_edit_form_submit($form, &$form_state) {

 
// get new allowed values list
 
$new_allowed_values_string = $form_state['values']['allowed_values'];
 
 
// ensure data exists
 
if (empty($new_allowed_values_string)) {
    return;
  }

 
// explode values, get array of values   
 
$exploded = explode("\n", $new_allowed_values_string);
 
$new_allowed_values_array = array();
  foreach (
$exploded as $data) {

   
// explode data on "|"
   
list($key, $value) = $exploded2 = explode('|', $data);     
   
$new_allowed_values_array[$key] = $value;
   
  } 

 
// ensure data exists
 
if (empty($new_allowed_values_array)) {
    return;
  }
 
 
// NOTE: the next few lines gets the key values from the allowed values
  // and checks to see which are different.
  // This code assumes any change to the keys (including changing the order) will update the nodes.
  // Update to your heart's content
 
$new_keys = array_keys($new_allowed_values_array);
 
$old_keys = array_keys($form_state['storage']['original_allowed_values']);
 
 
$key_diffs = array();
  foreach (
$old_keys as $key => $value) {
   
    if (
$new_keys[$key] != $value) {
     
$key_diffs[$value] = $new_keys[$key];
    }
   
  }
 
 
// ensure data has changed
 
if (empty($key_diffs)) {
    unset(
$form_state['storage']);
    return;
  }
 
 
// get data from form storage
 
$cck_field = $form_state['storage']['cck_field'];
 
$cck_type = $form_state['storage']['cck_type'];
 
 
// loop through key diffs
 
foreach ($key_diffs as $old => $new) {
 
   
// get a list of node ids that need to be updated
    // NOTE: this sql assumes the field is NOT shared
   
$sql = "
      select n.nid
      from {node} n
      join {content_type_%s} b on b.nid = n.nid and b.vid = n.vid
      where n.type = '%s' and b.%s_value = '%s'
    "
;
   
$resource = db_query($sql, $cck_type, $cck_type, $cck_field, $old);
   
$node_ids = array();
    while (
$row = db_fetch_object($resource)) {
     
$node_ids[] = $row->nid;
    }
   
   
// ensure results exist
   
if (empty($node_ids)) {
      continue;
    }
   
   
// loop through node ids
    // NOTE: got a lot of nodes? you might need a batch API instead
   
foreach ($node_ids as $nid) {
     
     
// load node
     
$node = node_load($nid, NULL, TRUE);
     
     
// ensure node loaded
     
if (!is_object($node)) {
        continue;
      }
     
     
// update node properties
     
$node->{$cck_field}[0]['value'] = $new;
     
$node->revision = TRUE;
     
node_save($node);
     
     
// remove cck cache for node
     
db_query("DELETE FROM {cache_content} WHERE cid LIKE 'content:%d%'", $node->nid);
     
    }
 
  }
 
 
// remove form storage
 
unset($form_state['storage']);

}
?>

To test this code, I went back to the CCK field settings page (example: /admin/content/node-type/band/fields/field_genre), updated the allowed value list, and saved.

updated allowed value list

The form submit handler is triggered and the nodes are updated. Refreshing my view shows the updated genre fields on my test nodes:

Updated node list

In this tutorial I'll show how you to create a custom Lucene Search facet from taxonomy and integrate into the search form block. A great first step would be to review the Drupal Lucene API documentation (see luceneapi.api.php PHP file in the luceneapi module folder).

I assigned the search form block to a region in my theme (admin/build/block), which generates this form:

Search form block

I added a new vocabulary called "Topics", and added a few terms (admin/content/taxonomy).

Topic and terms

The goal of this code is to integrate the Topic vocabulary into the search form block to allow the user to select a taxonomy term as they search. To start, you need to implement a hook_luceneapi_facet_realm() and a callback function in your custom module.

<?php
/**
* Implements hook_luceneapi_facet_realm()
*/
function MYMODULE_luceneapi_facet_realm() {

 
$realms = array();

 
$realms['form'] = array(
   
'title' => t('Search form block'),
   
'callback' => 'MYMODULE_luceneapi_facet_realm_callback_search_form_block',
   
'callback arguments' => array(),
   
'allow empty' => TRUE,
   
'description' => t('Displays facets in the search form block.'),
  );
 
  return
$realms;
 
}

/**
* Implements hook_luceneapi_facet_realm() callback function
*/
function MYMODULE_luceneapi_facet_realm_callback_search_form_block($facets, $realm, $module) {

 
$form = array();

 
// loop through facets
 
foreach ($facets as $name => $facet) {

   
// NOTE: luceneapi_facet_to_fapi_convert() converts a Lucene facet to Drupal Form API data
   
$form = array_merge_recursive($form, luceneapi_facet_to_fapi_convert($facet));

  }

  return
$form;

}
?>

At this point, if you go to the facets admin page (admin/settings/luceneapi_node/facets), you can see the newly created realm. I assigned the taxonomy vocabulary "Topic" to this realm.

Assigning facets to realms

Up next is implementing a hook_form_alter() to integrate the facet into the search form.

<?php
/**
* Implementation of hook_form_FORM_ID_alter().
*/
function MYMODULE_form_search_block_form_alter(&$form, &$form_state) {
 
 
// get default search module (IE: luceneapi_node) 
 
$module = luceneapi_setting_get('default_search');

 
// check if default search module is defined in lucene searchable module list
 
if (array_key_exists($module, luceneapi_searchable_module_list())) {

   
// get index type (IE: node)
   
$type = luceneapi_index_type_get($module);

   
// fetch realm facets
   
$elements = luceneapi_facet_realm_render('form', $module, $type);

   
// if facet form elements exist, recursively merge with current form object
   
if (!empty($elements)) {
     
$form = array_merge_recursive($form, $elements);
    }

  }

}
?>

The search from block should now show an empty "Topic" facet.

Search form block, empty topic

The last piece of code implements hook_luceneapi_facet_postrender_alter() which gives you the opportunity to modify the facet, and in the this case, add its options.

Immediately after implementing this hook, if you krumo() or dsm() the $items argument, you'll see the form element has no options.

Using krumo to see empty form element

The next section of code copied the contrib module code in "Lucene Node". [See file: luceneapi/contrib/luceneapi_node/luceneapi_node.module; function: function luceneapi_node_luceneapi_facet_postrender_alter()]

<?php
/**
* Implements hook_luceneapi_facet_postrender_alter()
*/
function MYMODULE_luceneapi_facet_postrender_alter(&$items, $realm, $module, $type = NULL) {

  if (
$realm == 'form' && $module == 'luceneapi_node' && $type == 'node' && is_array($items['category'])) {
   
   
// get taxonomy form data
   
$taxonomy = module_invoke('taxonomy', 'form_all', 1);
   
   
// get enabled facets
   
$facets_enabled = luceneapi_facet_enabled_facets_get($module, $realm);
   
   
// loop through enabled facets, validate, and fetch weight
   
$weights = array();
    foreach (
$facets_enabled as $name => $value) {
   
     
// check for "category" facet
      // FORMAT: category_{VOCABID}
     
if (preg_match('/^category_(\d+)$/', $name, $match)) {
     
       
// load taxonomy vocabulary     
       
if ($vocabulary = taxonomy_vocabulary_load($match[1])) {
       
         
// ensure category and vocab id is enabled for this module and realm
         
if (luceneapi_facet_enabled($match[0], $module, 'form')) {
                    
           
// fetch weight
           
$variable = sprintf('luceneapi_facet:%s:%s:%s:weight', $module, $realm, $name);
           
$weights[$vocabulary->name] = variable_get($variable, 0);
         
          }
       
        }
     
      }
   
   
// end foreach
   
}
   
   
// gets weighted taxonomy array
   
asort($weights);
   
$taxonomy_weighted = array();
    foreach (
$weights as $vocab_name => $weight) {
     
$taxonomy_weighted[$vocab_name] = $taxonomy[$vocab_name];
    }

   
// create array of fapi data to override
   
$category_data = array(
     
'#prefix' => '<div class="criterion">',
     
'#suffix' => '</div>',
     
//'#size' => 10,
     
'#options' => $taxonomy_weighted,
     
'#multiple' => TRUE,
     
'#default_value' => luceneapi_facet_value_get('category', array()),
     
'#title' => NULL,
     
'#description' => NULL,
    );

   
// merge data
   
$items['category'] = array_merge($items['category'], $category_data);
   
   
// sets weight as the lowest weight of all taxonomy facets
   
if (is_array($items['category']['#weight'])) {
     
$items['category']['#weight'] = min($items['category']['#weight']);
    }   
         
 
// end if
 
}

}
?>

Reloading the page will now show the search form with a completed facet.

Search form with facet

Submitting the search form block with a selected taxonomy term will now take the user to the search results page with the taxonomy facet pre-selected!

Special thanks to Chris Pliakas for all of his great Lucene API work! (I miss working with you Chris)

Recently I was working on a project that had a view with an exposed filter for country (via location module) and the dropdown list by default showed the entire country list, which was very large. I decided to write a code snippet that would reduce the select options to show only the countries that have been assigned to the nodes.

In my sandbox, I create a simple node type for "person" that used the location module to capture the user's country. I then created a view to show the people's names and countries, and added an exposed filter for country.

People & Countries View

Clicking on the exposed filter showed all of the available countries, which was a very large list.

Country dropdown

I added the following code to my module to alter the exposed filter form and reduce the country list. (Please note, an alternative approach would be to modify the views object, I wanted to show how this could be accomplished in hook_form_alter())

<?php
/**
* Implements hook_form_alter()
*/
function MYMODULE_form_alter(&$form, $form_state, $form_id) {

 
// define variables
 
$view_name = 'people_places';
 
$filer_name = 'country';

 
// modify country drop down
 
if ($form_id == 'views_exposed_form' && is_object($form_state['view']) && $form_state['view']->name==$view_name && is_array($form[$filer_name])) {
        
   
// define SQL to fetch nodes that have countries
   
$sql = "
      select distinct
        l.country
      from
        {node} n
      join
        {location_instance} li on li.nid = n.nid and li.vid = n.vid
      join
        {location} l on l.lid = li.lid
      where
       n.status = 1 and n.type = 'person'
    "
;
  
   
// fetch results
   
$resource = db_query($sql, $tid);
   
$countries = array();
    while(
$row = db_fetch_object($resource)) {
     
$countries[] = $row->country;
    }
  
   
// filter country list
   
foreach($form[$filer_name]['#options'] as $key => $value) {
    
     
// allow "All"
     
if ($key == 'All') {
        continue;
      }
    
      elseif (!
in_array($key, $countries)) {
        unset(
$form[$filer_name]['#options'][$key]);
      }
    
    }
  
  }
}
?>

Now the view only shows countries that match the resulting nodes.

Filtered exposed filter

In this snippet, I'll show you how you can submit a webform programmatically using drupal_execute(). The first thing you'll need to do is figure out what the $form_state data looks like when the webform is submitted, so you can recreate that structure and pass it into drupal_execute().

One way to accomplish this is to add a validation/submit handler to the form using hook_form_alter() and then output the contents of the submitted data (using krumo, print_r, etc).

The following code will prepend a validation handler to the webform $form, so we can dump the submitted data to the screen:

<?php
function MYMODULE_form_alter(&$form, $form_state, $form_id) {
 
 
// define node id of webform
 
$webform_node_id = 146;
 
 
// check for form id of webform
 
if ($form_id == 'webform_client_form_' . $webform_node_id) {
 
   
// prepend a validation callback to the form
   
array_unshift($form['#validate'],
     
'_MYMODULE_form_alter_webform_' . $webform_node_id . '_validate');
 
  }
 
}
?>

And here is the validation handler which will dump the submitted data to the screen. NOTE: krumo() is available from the devel module; you could use print_r() as well:

<?php
function _MYMODULE_form_alter_webform_146_validate($form, &$form_state) {
 
// debug
 
krumo($form_state);
  die;
}
?>

Now, if I populate the webform with some data:

And submit the form, I will get the following debug output:

You'll need to mirror the structure of the submitted data when creating your $form_state variable, which will be passed into drupal_execute().

In the following function, I show how you can submit a webform programmatically. There is even additional code in there which loads a user's previously submitted data, if you care.

<?php
function MYMODULE_some_function() {

 
// define webform node id
 
$webform_node_id = 146;

 
// define user submitting the webform
 
$user_id = 2;

 
// load webform node
 
if ($node = node_load($webform_node_id)) {

   
// load module include file, per loading previously submitted webform data
   
module_load_include('inc', 'webform', 'webform_report');

   
// get submissions for user
   
$submissions = webform_get_submissions($node->nid, NULL, $user_id);

   
// get submission data ($submission) and submission id ($sid)
   
if (FALSE !== ($sid = key($submissions))) {
     
$submission = $submissions[$sid];
    }
    else {
     
$submission = NULL;
     
$sid = NULL;
    }

   
// create array of $form_state data
    // NOTE: be sure to use the debug output as a guide to make this array match!
   
$form_state = array(
     
'submitted' => true,
     
'values' => array(
       
'submission' => $submission,
       
'submitted' => array(
         
'test_field_1' => 'abc',
         
'test_field_2' => '123',
         
'test_field_3' => 'def',
        ),
       
'details' => array(
         
'email_subject' => $node->webform['email_subject'],
         
'email_from_name' => $node->webform['email_from_name'],
         
'email_from_address' => $node->webform['email_from_address'],
         
'nid' => $webform_node_id,
         
'sid' => $sid,
        ),
       
'op' => t('Submit'),
       
'submit' => t('Submit'),
       
'form_id' => 'webform_client_form_'. $webform_node_id,
      ),
    );

   
// Saves the webform data submited prior to login.
   
drupal_execute('webform_client_form_'. $webform_node_id, $form_state, $node, $submission, TRUE, FALSE);
   
  }

}
?>

In this tutorial, I'll show you how you can expose your search form on another site using jQuery. At first, I thought about scraping the form's html using AJAX.. and quickly remembered you cannot easily do that. Which lead me to review the AJAX functionality included in jQuery. Bingo, one of my favorites: jQuery.getJSON. To summarize this code, I create a callback function to display the form's json-ified html which can then be easily embedded on another site.

First I defined the menu hook:

<?php
function MYMODULE_menu() {

 
$items = array();

 
// add a page callback for the url: "external-search.js"
 
$items['external-search.js'] = array(
   
'page callback' => '_MYMODULE_external_search',
   
'type' => MENU_CALLBACK,
   
'access arguments' => array('search content'),
  );
   
  return
$items;
   
}
?>

Then I created the callback function for the menu callback:

<?php
function _MYMODULE_external_search() {

 
// create a json string of the search form html
 
$json = drupal_to_js(drupal_get_form('search_form'));
   
 
// format the json as a callback function
  // see: http://docs.jquery.com/Ajax/jQuery.getJSON for more information
 
if ($_GET['jsoncallback']) {
   
$json = $_GET['jsoncallback'] . "(" . $json . ");";
  }
   
 
// output the json
 
print $json;

 
// stop the script, so the theme layer is not applied
 
die;
}
?>

One problem though, the form submits locally. That can be fixed using a form_alter function:

<?php
function MYMODULE_form_alter(&$form, $form_state, $form_id) {
   
 
// check for external search form and set form action to be full path
 
if ($form_id == 'search_form' && arg(0)=='external-search.js') {
   
// change the form action to be the full path
   
$form['#action'] = 'http://' . $_SERVER['HTTP_HOST'] . $form['#action'];
  }
}
?>

Now, if you clear your cache and go to http://YOURSITE/external-search.js, you should see the JSON (and nothing else).

Lastly, you can embed the code on another site using a few lines of jQuery. You can even pull the jQuery from your site if the external site does not have jQuery included.

<!-- Include jQuery (as necessary) -->
<script type='text/javascript' src='http://YOURSITE/misc/jquery.js' ></script>

<!-- create a div container to contain the search form -->
<div id='embedded_search'></div>

<!-- add the jQuery to embed the form -->
<script type='text/javascript'>
$(document).ready(function(){
  // make the ajax request
  $.getJSON("http://YOURSITE/external-search.js?jsoncallback=?",
    function(data){
      // append the form to the container
      $('#embedded_search').append(data);           
    }
  );
});
</script>

Now people should be able to access your site's search form from another site!

Syndicate content