Adding New Fields to File Uploads

Error message

The spam filter installed on this site is currently unavailable. Per site policy, we are unable to accept new submissions until that problem is resolved. Please try resubmitting the form in a couple of minutes.

Handling file uploads have always been a bit of a challenge with Drupal. Uploads work, so long as you want them to work exactly the way they have been implemented through CCK or the upload module. Changing this default functionality, in versions going back to 4.6, has always been something of a pain, involving hacks to core or building one-off upload fields to handle things the way you want.

The introduction of CCK has made this a bit easier, through widgets, but there are still challenges. We built a site recently that allowed users to upload multiple images into a photo gallery, and it needed to collect more information about each than the fields collected through imagefield (in this case, besides a file and title, we needed an attribution, a description, a caption, a date, and some other stuff to help the client keep track of things).

Adding fields to a CCK upload field is surprising easy, but it is a little tricky figuring it out the first time. Here is what we did to make it all work.

First off, we decided to use the imagefield CCK widget as a basis to get started. Imagefield has it's own form handler (imagefield_widget) and a textfield for uploading the document and storing a description, along with AJAX controls for reordering files. So it has all the basic elements we needed to handle the additional fields.

The challenge is, when the form is generated, there is only one element for each imagefield and no way to add our own fields to the form. So, a simple call to hook_form_alter would not allow us to alter the form elements themselves. We got around this issue using custom #process and #element_function callbacks. Each callback expands our original form element after other parts of the form_alter have fired, giving us a way to change the final construction of the element before the form is rendered.

Let's say you want to add custom field called 'color'. Our callback would add a field called 'color' as a textfield people can fill out. For imagefield there are two important callbacks - filefield_widget_process and imagefield_widget_widget_process. We use these callbacks to add original handlers for rendering fields and validating form submissions.

Assume there is a custom node type called 'Color Image' and it contains an imagefield 'field_image'. In this example, the call to form_alter would look like:

<?php
function image_color_form_alter(&$form, $form_state, $form_id) {
  if (
$form_id == "color_image_node_form") {
   
// each field can have multiple values, we need to add custom process function to every upload field
   
foreach (element_children($form['field_image']) as $key) {
      if (
$form['field_image'][$key]['#type'] == 'imagefield_widget') {
       
// we have to include original process functions otherwise field wont be build correctly
       
$form['field_image'][$key]['#process'] = array('filefield_widget_process', 'imagefield_widget_widget_process', 'image_color_widget_process');
       
// same with validation function
       
$form['field_image'][$key]['#element_validate'] = array('filefield_widget_validate', 'imagefield_widget_validate', 'image_color_widget_validate');
      }
    }
  }
}
?>

Notice we are not trying to change the structure of the form element, we are simply adding validation and processing hooks to get the form to look the way we want. The processing function is what will render the additional fields we need to process file uploads. It will be fired before the form is rendered and will allow us to add new fields to the data array.

In the case of the 'field_color' example, it would look like this:

<?php
function image_color_widget_process($element, $edit, &$form_state, $form) {
 
$file = $element['#value'];

 
$element['data']['color'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Color'),
   
'#value' => $file['fid'] ? $file['data']['color'] : ''
 
);

  return
$element;
}
?>

Pretty simple. All we are doing is delaying the processing of the form element until just before the form is rendered, to ensure we have all the fields we may need (and also, to ensure that additional upload fields will be rendered properly through the AHAH interface).

In our validation function, we set our own rules for verifying form submissions.

<?php
function image_color_widget_validate($element, &$form_state) {
  if (
$element['fid']['#value']) {
    if (
$element['data']['color']['#value'] && is_numeric($element['data']['color']['#value'])) {
     
form_set_error(implode('][', $element['data']['color']['#array_parents']), t('Color cannot be number'));
    }
  }
}
?>

Notice that we are accessing information through the element object, not a node or form array. This is because the function will be used to validate individual form elements when the form is submitted.

Also notice we do not have to use a submit handler to store the data associated with the element. We could put the data in another table if we wanted, but with imagefield there is no need. All of the data associated with the uploaded file is stored as a serialized array, and becomes available to Drupal anytime the node is loaded. You can call the color value any time you are displaying the node, as seen below:

<?php
  $node
->field_image[0]['data']['color'];
?>

And that is it! I wish they would have had something this easy back in the days of Flexinode...