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

I've been working on a few Drupal projects recently that required geospatial Solr integration. After numerous failed attempts with patches, outdated modules, and Java Solr plugins, I decided to try a different approach: upgrading the Solr library from 1.4.1 to 3.1.0. I wish I had seen this earlier, but spatial search is core to Solr 3.1, see: http://wiki.apache.org/solr/SpatialSearch

This article is a companion to my configuration documented here: http://ericlondon.com/creating-centos-server-installation-apache-mysql-t..., and assumes you have a similar working environment using the 1.4.1 Solr library.

I started by downloading apache-solr-3.1.0.tgz and replacing my installed Solr library 1.4.1 with the new files:

$ tar -xzf apache-solr-3.1.0.tgz

# copy/rename solr war file into Tomcat webapps directory
$ cp ~/downloads/apache-solr-3.1.0/dist/apache-solr-3.1.0.war /var/lib/tomcat6/webapps/solr.war

# copy solr files
$ cp -r ~/downloads/apache-solr-3.1.0/example/solr/ /var/lib/tomcat6/solr/

I then re-copied the Drupal apachesolr module xml configuration files into my Tomcat Solr directory:

$ cp /var/www/vhosts/example.com/sites/all/modules/contrib/apachesolr/protwords.txt /var/lib/tomcat6/solr/conf/
$ cp /var/www/vhosts/example.com/sites/all/modules/contrib/apachesolr/schema.xml /var/lib/tomcat6/solr/conf/
$ cp /var/www/vhosts/example.com/sites/all/modules/contrib/apachesolr/solrconfig.xml /var/lib/tomcat6/solr/conf/

Unfortunately, the Drupal apachesolr module and provided xml configuration files do not account for the new data types in 3.1.0. I modified my schema.xml file and added the following:

<fieldType name="point" class="solr.PointType" dimension="2" subFieldSuffix="_d"/>
<fieldType name="location" class="solr.LatLonType" subFieldSuffix="_coordinate"/>
<fieldtype name="geohash" class="solr.GeoHashField"/>

<field name="coordinates" type="location" indexed="true" stored="true"/>
<dynamicField name="*_coordinate"  type="tdouble" indexed="true"  stored="false"/>

Next, I added a hook_apachesolr_update_index() function to a custom module to index Location CCK data into the Solr Document.

<?php
function MYMODULE_apachesolr_update_index(&$document, $node) {

 
// check for location data
 
if (empty($node->field_location[0]['latitude']) || empty($node->field_location[0]['longitude'])) {
    return;
  }
   
 
$document->coordinates = $node->field_location[0]['latitude'] . ',' . $node->field_location[0]['longitude'];
 
}
?>

I then created a bunch of nodes that had a Location CCK field, and varied their locations around New England. After re-indexing my Solr index and running cron, I queried Solr directly to ensure the locative data was added to the documents.

Solr Query Coordinates

I then used the new spatial query syntax (documented here: http://wiki.apache.org/solr/SpatialSearch) to pass in Boston coordinates. The results were limited to 3 matches within the specified distance.

Example query syntax, with coordinates and distance, searching for "ma"

?q=ma&fq={!geofilt sfield=coordinates pt=42.346617,-71.098747 d=10}

Solr Query GeoSpatial Search

The next challenge was integrating with the Drupal apachesolr module. The spatial query syntax has an augmented "fq" value, but the apachesolr Drupal module sets the fq parameter automatically (via: $query->get_fq()):

<?php
# snippet from file: apachesolr.module
function apachesolr_modify_query(&$query, &$params, $caller) {
 
// ...snip...

  // TODO: The query object should hold all the params.
  // Add array of fq parameters.
 
if ($query && ($fq = $query->get_fq())) {
   
$params['fq'] = $fq;
  }
 
// ...snip...
?>

After much struggle, I changed one line in the apachesolr.module file to allow the fq value to be modified (I normally refuse to patch ANY contrib module… please, if anyone knows a way to do this without patching, please let me know! :)

<?php
function apachesolr_modify_query(&$query, &$params, $caller) {
 
// ...snip...

  // TODO: The query object should hold all the params.
  // Add array of fq parameters.
 
if ($query && ($fq = $query->get_fq())) {
   
$params['fq'] = array_merge($fq, $params['fq']);
  }
 
// ...snip...
?>

Actual diff:

1264c1264
<     $params['fq'] = $fq;
---
>     $params['fq'] = array_merge($fq, $params['fq']);

I then add a hook_apachesolr_modify_query() function to my custom module to override the Solr query params.

<?php
function MYMODULE_apachesolr_modify_query(&$query, &$params, $caller) {

  if (
$caller != 'apachesolr_search') {
    continue;
  }

 
// NOTE: hardcoded Boston area coordinates: 
 
$coordinates = '42.346617,-71.098747'

 
$distance = 10;
 
 
// modify params
 
$params['fq'][] = "{!geofilt sfield=coordinates pt=$coordinates d=$distance}";
 
}
?>

I then search for "ma", in Drupal this time (!), and my results reflected the nodes within the Boston area.

Drupal Solr GeoSpatial Results

In this article, I'll explain one way to embed a GMAP view into your search results.

I downloaded and installed the following modules:

content (cck)
location_cck
gmap
gmap_location
location
location_search
views
views_ui

Configuring the GMap and Location modules:

  • Location main settings (admin/settings/location), ensure "Enable JIT geocoding" is enabled.
  • Location map links (admin/settings/location/maplinking), ensure "Google Maps" is enabled for Unites States.
  • Location Geocoding options (admin/settings/location/geocoding), enable "Google Maps" for United States.
  • GMap (admin/settings/gmap), enter your Google Maps API key.

I then imported US zip codes into my Drupal database:

$ cd sites/all/modules/contrib/location
$ mysql db_drupal < zipcodes.us.mysql

I created a content type with a single Location CCK field for address. I made this field required and set my desired collection settings. I added some sample nodes, and verified the location coordinates were being added dynamically.

Next, I added a new view with the following settings:

  • Name: search_gmap
  • Type: Node
  • Style: GMap; macro: [gmap behavior=+autozoom]; Data source: Location.module;
  • Fields: Node: Title
  • Filters: Node: Published
  • Arguments: Node: Nid; Action to take if argument is not present: display empty text; Enabled: Allow multiple terms per argument.

Lastly, I added some code to my module:

<?php
function MYMODULE_preprocess_search_results(&$vars) {
 
 
// get nodeids from search results
 
$node_ids = array();
  foreach (
$vars['results'] as $result) {
    if (!
is_object($result['node'])) {
      continue;
    }
   
$node_ids[] = $result['node']->nid;
  }

 
// ensure node ids exist
 
if (!count($node_ids)) {
    return;
  }

 
// generate views output
 
$view_output = views_embed_view('search_gmap', 'default', implode('+', $node_ids));
  if (
$view_output) {   
   
$vars['search_results'] = $view_output . $vars['search_results'];
  }

}
?>

After flushing my caches, and running cron to index my nodes, I ran a search for "nashua":

GMap search results

I created a Drupal site to host my photography in CCK Imagefield nodes and used Lucene to enhance my search functionality. By default Drupal's search results are text-based so I decided to add some code to show image thumbnails in my search results. I checked out Drupal Lucene's hooks and decided to implement a hook_luceneapi_result_alter() function in my existing module.

<?php
function MYMODULE_luceneapi_result_alter(&$result, $module, $type = NULL) {
 
 
// check for node results
 
if ($type == 'node') {
 
   
// check node type
   
if ($result['node']->type == 'image') {
   
     
// define an imagecache image path for image thumbnail
     
$imagecache_path_thumbnail = file_directory_path() . '/imagecache/thumbnail' . str_replace(file_directory_path(),'',$result['node']->field_image[0]['filepath']);     
     
     
// define an imagecache image path for image (large)
     
$imagecache_path_large = file_directory_path() . '/imagecache/large' . str_replace(file_directory_path(),'',$result['node']->field_image[0]['filepath']);
   
     
// define theme_image() variables
     
$alt = check_plain($result['node']->title);
     
$title = check_plain($result['node']->title);
     
// add rel=lightbox to enable lightbox2 module
     
$attributes = array(
       
'rel' => 'lightbox',
      );
     
// let imagecache define the size
     
$getsize = FALSE;
     
// generate the image hml
     
$image_html = theme('image', $imagecache_path_thumbnail, $alt, $title, $attributes, $getsize);     
   
      if (
$image_html) {
               
       
// define lightbox link
       
$image_link = l(
         
$image_html,
         
$imagecache_path_large,
          array(
           
'html' => true,
           
'attributes' => array(
             
'rel' => 'lightbox',
            )
          )
        );

       
// add data to the result variable, passed by reference
       
$result['image_thumbnail'] = $image_link;
       
      }
   
    }
 
  }

}
?>

The above code adds additional data to my search results variables. I then implemented a hook_preprocess_search_result() function in my theme's template.php file to pass this data to the search-result.tpl.php template file.

<?php
function MYTHEME_preprocess_search_result(&$variables) {

 
// ...snip...

  // check for lucene node search results
 
if ($variables['type']=='luceneapi_node') {

   
// check for image
   
if ($variables['result']['image_thumbnail']) {   

     
// pass additional data to theme template file
     
$variables['image_thumbnail'] = $variables['result']['image_thumbnail'];

    }
   
  }

}
?>

And in my theme's search-result.tpl.php template file, I added the following PHP to show the new variable.

<div class="search-result <?php print $search_zebra; ?>">

  <?php if($image_thumbnail): ?>
    <?php print $image_thumbnail; ?>
  <?php endif; ?>

  <!-- ...snip... -->

I also added a few lines of CSS in my theme's style.css file to tidy up the layout.

.search-results.luceneapi_node-results .search-result {
  clear: both;
}

.search-results.luceneapi_node-results .search-result img {
  float: left;
  margin: 0px 20px 20px 0px;
}

The visual results can be seen here on my photo gallery.

Visual search results

In this tutorial I'll show you how to upload an image using the Forms API, create a new node, and attach the image to the CCK (filefield/imagefield) field. I wrote this code to work with the modules I primarily use for image processing: cck, filefield, imageapi, imagecache, imagefield, mimedetect, and transliteration.

After I installed those modules, I created a new node type (admin/content/types/add) called "Image" and added a single imagefield field.

Image node fields

Next, I created a custom module with a hook_menu() implementation:

<?php
// NOTE: this variable is used through the code,
// so I thought it would be better to put it in a constant
define('IMAGE_UPLOAD_CONTAINER', 'image_upload');

/**
* Implements hook_menu()
*/
function helper_menu() {

 
// create a blank array of menu items
 
$items = array();
 
 
// define page callback for upload form
  // NOTE: you'll want to restrict permission better [see: access arguments]
 
$items['upload'] = array(
   
'title' => t('Upload'),
   
'description' => t('Upload'),
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('helper_page_callback_upload_form'),
   
'access arguments' => array('access content'),
   
'type' => MENU_CALLBACK,
  );
 
 
// return menu items
 
return $items;

}
?>

I defined the form function page callback:

<?php
/**
* Implements page callback for upload form
*/
function helper_page_callback_upload_form() {

 
// create an empty form array
 
$form = array();
 
 
// set the form encoding type
 
$form['#attributes']['enctype'] = "multipart/form-data";
 
 
// add a file upload file
 
$form[IMAGE_UPLOAD_CONTAINER] = array(
   
'#type' => 'file',
   
'#title' => t('Upload an image'),
  );
  
 
// add a submit button
 
$form['submit'] = array(
   
'#type' => 'submit',
   
'#value' => 'Submit',
  );
 
 
// return form array
 
return $form;

}
?>

This page callback function results in the following form:

Image node form

Then I added the form validation and submit handler functions:

<?php
/**
* Implements form validation handler
*/
function helper_page_callback_upload_form_validate($form, &$form_state) {

 
// if a file was uploaded, process it.
 
if (isset($_FILES['files']) && is_uploaded_file($_FILES['files']['tmp_name'][IMAGE_UPLOAD_CONTAINER])) {

   
// validate file extension
    // NOTE: you can ellaborate on this code and add additional validation
   
if ($_FILES['files']['type'][IMAGE_UPLOAD_CONTAINER] != 'image/jpeg') {
     
form_set_error(IMAGE_UPLOAD_CONTAINER, 'Invalid file extension.');
      return;
    }

   
// attempt to save the uploaded file
   
$file = file_save_upload(IMAGE_UPLOAD_CONTAINER, array(), file_directory_path());

   
// set error if file was not uploaded
   
if (!$file) {
     
form_set_error(IMAGE_UPLOAD_CONTAINER, 'Error uploading file.');
      return;
    }
      
   
// set files to form_state, to process when form is submitted
   
$form_state['storage'][IMAGE_UPLOAD_CONTAINER] = $file;
      
  }
  else {
   
// set error
   
form_set_error(IMAGE_UPLOAD_CONTAINER, 'Error uploading file.');
    return;  
  }

}

/**
* Implements form submit handler
*/
function helper_page_callback_upload_form_submit($form, &$form_state) {
 
 
// create new node object
 
$new_node = (object) array(
   
'type' => 'image',
   
'uid' => $GLOBALS['user']->uid,
   
'name' => $GLOBALS['user']->name,
   
'title' => t('YOUR NODE TITLE'),
   
'status' => 1,
   
'field_image' => array(
      (array)
$form_state['storage'][IMAGE_UPLOAD_CONTAINER],
    ),
  );
   
 
// save node
 
node_save($new_node);
 
 
// clear form storage, to allow form to submit
 
$form_state['storage'] = array();
 
 
// redirect user, set message, etc!

}
?>

After using the form to upload an image, the following node was created:

New image node

Syndicate content