Custom Drupal Event Registration Module for Ubercart

I've had a number inquiries about the custom Ubercart event registration module I wrote to register bicycle racers for local races. Rather than answer each of them individually I figured it was about time to do a write-up about how I did it.

The problem

I know quite a few bicycle race promoters in the local area. They’re all in need of some way to register people on-line for the events they promote. There are plenty of registration sites around, but none are locally owned and operated. Because of this they also don’t work very hard to help promoters out with their own individual event’s needs. They also charge quite a bit for their services.

Bike racing and its registration process is somewhat unique. There are a few special things that need to be collected at registration such as a license number and emergency contact information in addition to money. Inventory is also a unique challenge. A bike race is an event made up of several individual events. Bike racers compete against other racers in their class. These classes, or fields, may be defined by ability level and/or age. Each field will have its own limit to how many may enter. In some bike races competitors may choose to enter multiple races. Typically the promoter will offer a lower price for a second or third entry.

All events will have a registration period. While event information must be available at all times, registration may only take place during a specific time period. Additionally, promoters may want to provide an incentive to register early by discounting fees before a particular date or increasing fees after a certain date.

Lastly, all registration data must be compiled into formats needed to run a race. Promoters need a spreadsheet from which they can assign numbers and against which they can report results. They also need every entrant's registration information merged and printed into liability release forms competitors must sign before being allowed to compete on the day of the race.

The objective

At a bare minimum this event registration module must do the following:

  1. Allow registration only during a specific window of time
  2. Collect all pertinent registration information from competitors/participants
  3. Keep track of how many have registered by field and only allow registrations up to the limit
  4. Allow for the export of registration data after registration closes

Let's take a look at each of the above in turn, and I'll show you how I implemented each one.

Allow registration only during a specific window of time

and

Collect all pertinent registration information from competitors/participants

My approach was to present each overall bike race event as a standard Ubercart product, but modify the product such that it cannot be added to the shopping cart unless the required registration data were present. The product details themselves would always need to be presented. The solution was to set up a CCK date field to hold registration open and close dates and times, and to compare the current time with the open/close times when the product is rendered in the user's browser.

The first two requirements can therefore be met by using the ubercart hook, hook_uc_form_alter(). We'll change the form according to our data collection and registration time period rules.

<?php
/**
* Implementation of hook_form_alter().
*/
// This code alters the "add_to_cart" form, adding in registration details, thereby associating
// the product being ordered with an individual
function bike_race_uc_form_alter(&$form, $form_state, $form_id) {
  global
$user;
 
// This if block sets up the form for node type = bike_race
  // If we have a uc_product_add_to_cart_form rendering on a  bike_race node_type, we need to either
  // show the registration form or not, depending on the time. So first we'll check to see if we're
  // rendering an ubercart form, and if we are then is it on a node of type bike_race
 
if (strlen($form_id) >= 27 && substr($form_id, 0, 27) == 'uc_product_add_to_cart_form' && $form['nid'] && $form['nid']['#value'] && $form['node']['#value']->type == 'bike_race') {
   
// If the above conditions are met we'll need to get the registration window out of the node's CCK date fields
   
$reg_open = strtotime($form['node']['#value']->field_dates_registration[0]['value']);
   
$reg_close = strtotime($form['node']['#value']->field_dates_registration[0]['value2']);
   
$reg_time = time();
   
// Now we'll check to see if registration is open or not
   
if ($reg_time < $reg_open or $reg_time > $reg_close) {
     
$form['releasedata'] = array(
       
'#value' => t('Registration is not available at this time'),
      );
     
$form['submit'] = '';
      return;
// Registration is not open, so we just replaced the entire form with a message stating as much
   
} // Otherwise we'll make no changes to the form and let it be presented to the user

       
$nid = $form['nid']['#value'];
       
$form['releasedata'] = array(
         
'#type'         => 'fieldset',
         
'#title'        => t('Registration information'),
         
'#collapsible'  => TRUE,
         
'#collapsed'    => FALSE,
         
'#tree'         => TRUE,
         
'#weight'       => -10,
        );
        
$form['releasedata']['fname'] = array(
       
'#type'           => 'textfield',
       
'#title'          => t('First name'),
       
'#default_value'  => '',
       
'#size'           => 15,
       
'#maxlength'      => 20,
       
'#weight'         => -10,
       
'#required'       => TRUE,
        );

       
// I have abbreviated this portion of the code listing
        // Other form items not shown here include: lname, address, city, state, zip,
        // emergency contact info, license number, gender, team name, etc.
   
}
}
?>

Keep track of how many have registered by field and only allow registrations up to the limit

My approach for this aspect was to simply alter the form presented in the browser to reflect whether any of the individual race fields had become fully subscribed. So I implemented logic in the form presentation itself

<?php
$form
['releasedata']['start_group'] = array(
   
// This is the form item which allows competitors to select which field he/she will compete in
 
'#type'           => 'radios',
 
'#attributes'     => array('class' => 'start-group-radios'),
 
'#title'          => t('Start Group'),
 
'#default_value'  => '',
 
'#weight'         => -11,
   
// put logic in place to check inventory and do not display a particular option if it is sold out
 
'#options'        => check_group_capacity_options($start_group_options, $nid),
 
'#required'       => TRUE,
 
'#description'    => t('Note: All P/1/2 category riders must start in the 10:01 to 11:00am group.'),
);
?>

The function check_group_capacity_options() returns an array of key->value pairs which will become the options available in this radio button select group.

<?php
/**
* function check_group_capacity_options($start_group_options, $nid)
* Returns an array of options for group selector form item
*/
function check_group_capacity_options($start_group_options, $nid) {
 
// Note: I have replaced the code which retrieves field limit information from the node's CCK field
  // and replaced it with a simple array of values for brevity
 
$group_capacities = array(76, 60, 60);
 
$rows = array();
 
// Create an SQL query to extract registration data for our race
 
$query = "
    SELECT uc_order_products.data as 'regdata'
    FROM uc_order_products
    LEFT JOIN uc_orders USING (order_id)
    WHERE uc_order_products.nid = '%d' AND order_status = '%s'
    "
;
 
// Query the database
 
$result = db_query(db_rewrite_sql($query), $nid, $order_status = 'completed');
 
// Cycle through our result to get an array of entries and their start groups
 
while ($data = db_fetch_object($result)) {
   
$regdata = unserialize($data->regdata);
   
$rows[] = $regdata['start_group'];
  }
 
$used_capacity = array_count_values($rows);
 
// and finally formulate the option group's key->value pairs based on whether the number
  // of competitors registered in each category is below the limit
 
foreach ($start_group_options as $key => $value) {
    if (
$used_capacity[$key] < $group_capacities[$key]) {
     
$checked_group_capacity_options[$key] = $value;
    }
  }
  return
$checked_group_capacity_options;
}
?>

Allow for the export of registration data after registration closes

The first thing we want to do here is restrict permissions for accessing the data export function. For this we'll implement hook_perm()

<?php
/**
* Implementation of hook_perm().
*/
function bike_race_perm() {
 
// This will present a checkbox per role in our user permissions' screen in
  // our bike race registration module's area. The roles selected will be able to
  // download a .csv file of registered competitors. This is useful if we want to
  // define a role for race promoters.
 
return array('access csv download');
}

/**
* Enables csv download of registration data.
*/
function bike_race_csv_access($node) {
 
// This function checks to see if our node type and permissions line up. If
  // they do, then the condition for downloading a .csv file is met.
  // Note: This is a greatly simplified condition check. A more robust
  // implementation would check ownership of the node and only
  // allow the node's owner to download the data.
 
return ($node->type == 'bike_race' && user_access('access csv download'));
}
?>

Now we need to implement the menu item which will call the .csv download functionality.

<?php
/**
* Implementation of hook_menu().
*/
function bike_race_menu() {
 
$items = array();
 
$items['node/%node/view/csv'] = array(
   
'title'             => 'CSV Download',
   
'file'              => 'includes/bike_race_csv_handler.inc',
   
'page callback'     => 'bike_race_registration_download_csv',
   
'page arguments'    => array(1),
   
'access arguments'  => array(1),
   
'access callback'   => 'bike_race_csv_access',
   
'weight'            => 9,
   
'type'              => MENU_LOCAL_TASK,
  );
  return
$items;
}
?>

This will present a menu tab on the far right (after the view and edit tabs) which, when clicked, will initiate the download of this node's associated registration data. The bike_race_registration_download_csv() function performs this task and is located in the includes/bike_race_csv_handler.inc file.

<?php
/**
* This is file: ./includes/bike_race_csv_handler.inc
*/
function bike_race_registration_download_csv($node) {
 
drupal_set_header('Content-Type: application/csv');
 
drupal_set_header('Content-disposition: attachment; filename=reglist-'.$node->path.'.csv');
  print 
bike_race_create_csv($node);
 
module_invoke_all('exit');
}

function
bike_race_create_csv($node) {
 
$csv_output = '';
  if (
$node->type != 'bike_race') {
    return;  
// do nothing if we have the wrong node type
 
}
 
$query = "
    SELECT
      uc_order_products.order_id,
      uc_order_products.title as 'event',
      uc_order_products.data as 'regdata',
      uc_order_products.price as 'amt_paid',
      uc_orders.created as 'created',
      order_status,
      uc_orders.uid AS uid,
      users.mail AS 'email'
    FROM
      uc_order_products
    LEFT JOIN uc_orders USING (order_id)
    LEFT JOIN users on users.uid = uc_orders.uid

    WHERE uc_order_products.nid = '%d' AND order_status = '%s'
    "
;
  unset (
$header);
 
$header = array('Event', 'Start Group', 'Entry Timestamp', 'Name', 'Category', 'Team', 'Address',
 
'City', 'State', 'Zip', 'Racing Age', 'License', 'Phone', 'email',
 
'Emergency Contact', 'Emergency Contact Ph.', 'Amount Paid');

 
$result = db_query(db_rewrite_sql($query), $node->nid, $order_status = 'completed');
 
$rows[] = $header;
  while (
$data = db_fetch_object($result)) {
   
$regdata = unserialize($data->regdata);
   
$rows[] = array(
     
$node->title,
     
$regdata['start_group'],
     
$data->created,
     
$regdata['last_name'].', '.$regdata['first_name'],
     
$regdata['category'],
     
$regdata['team'],
     
$regdata['address'],
     
$regdata['city'],
     
$regdata['state'],
     
$regdata['zip'],
     
$regdata['race-age'],
     
$regdata['license'],
     
$regdata['phone'],
     
$regdata['email'],
     
$regdata['e-contact'],
     
$regdata['ec-phone'],
     
$data->amt_paid,
      );
  }
 
// now output csv
 
foreach ($rows as $row) {
    foreach (
$row as $index => $column) {
     
$row[$index] = '"'. str_replace('"', '""', $column) .'"';
    }
   
$csv_output .= implode(',', $row) ."\n";
  }
  return
$csv_output;
}
?>

That's pretty much all it took to get this working. The only concern I had throughout the process of developing the technique had to do with performance. A couple of comments I saw elsewhere expressed concern about storing this data in a serialized array in the orders table. While this technique may not suitable for a high volume application, even a very large bike race would only see a few hundred people register. With demands this low, I was confident site performance would not be impacted. While I have not benchmarked any of this, I can say site performance has never been an issue.

Powered by Drupal, an open source content management system