Just Racing UK - Selling an Event with Drupal, Ubercart, Views, Dates and some glue

12 replies [Last post]
Joined: 11/20/2007
Juice: 72

This month, we launched the new Just Racing UK website, a racing and Triathlon (running, cycling and swimming) event management website. The build process was relatively complicated, involving a fair few modules, some of which were dropped, some of which worked right out the box, and some of which required a good degree of hacking and customisation.

Requirements

Just Racing's first website was created by a different design company and offered the visitor the ability to view up and coming race events, and book races electronically through a third party. As well as bulking out the process for the visitor, Just Racing incurred very high processing fees.

We were asked to take inspiration from the exiting site, but add the ability to: -

Promote and Process events
Offer payment processing online
Vary sponsorships
Create and manage online start-lists (based upon age and swim-times)
Display previous years race results
Manage race-specific newsletter subscriptions
Utilise an existing database
It went without saying, but a fresh new look was also required.

The Core Modules

With it's powerful, open framework and incredible array of modules, Drupal (version 5.x) was the first choice for a base platform (we did want Drupal 6, but at the time, Ubercart was not available for this release). The usual modules such as CCK, Views, FCKEditor and Webform were installed as standard.

The tricky part was finding an appropriate event booking and payment system, that could be developed into a larger database for the start-list tracking and newsletter subscriptions.

We looked at CiviCRM, Event, E-Commerce and Ubercart. After a lot of testing, trial and error, Ubercart was chosen as a base for the payment processing. We just needed to turn an event into a product taking into account product expiry (after the event date), single quantity (each user can only register once) and links to previous years' results and start-lists.

Selling an Event with Ubercart

As with most Drupal concepts, there are many ways to achieve a task. Although there is an Ubercart Event module, after some testing we decided an integration with CCK Date was a better way to go. CCK works so well with views and allows for a much greater level of customisation on the date field that the Event module.

So, a Race Event CCK content type / Ubercart class was created using a combination of CCK Date, CCK Filefield (for results), CCK Imagefield as well as the standard CCK text fields.

By default, Ubercart has loads of physical product fields such as weight, stock level etc.. A custom module was created at this stage to hide these unnecessary inputs using the Form API. After all, an event needs to look like an event for the race editors as well as the visitors.

The final customisation for the race event was an expiry module. Once a race has occurred, we do not want visitors to be able to register for it, although they will revisit the node to view race results. This custom module simply hides the "Add-to-cart" button if a certain date has past and checks again upon payment (for saved carts from the past).

Race events have different pricings depending upon UK club membership discounts as well as single / team registrations. For this reason, Ubercart's Fixed Price and AAC (Ajax Attribute Calculation) contributions were installed and several minor module and theme hacks were required to make it all work as required.

The Sponsors

Just Racing have a variety of sponsors for each event, as well as varying sponsors over time (which are displayed on the home page). A simple view was created the randomly vary the position of the sponsors. The sponsor nodes themselves use CCK Redirection and CCK Image to display a hyperlinked image to the sponsors' website.

Event-Specific Newsletter Subscriptions

Just Racing UK needed to manage E-mail lists of people who have entered a particular event. Most of the events repeat annually, and Just Racing did not need year-specific filtering for the newsletters. This made the task very simple - we just created roles for each race name and created an Ubercart product feature to automatically add purchasers of a race to its particular role. The Simplenews with Simplenews Roles modules can be setup to send newsletters to the race-specific roles - perfect.

The Existing Database

One of the critical task was to import an existing database of over 5000 records. Essentially, the only information required was the name, E-mail address, date-of-birth and the races entered.

As the primary purpose of this database was for the newsletters mentioned above, a simple User Import was all that was required. The User Import module does not support varying roles out-the-box, but modifying the module allowed us to map role based columns from the CSV to roles on the user account.

Finally, users were notified on their new account on the website which greatly promoted the site and resulted in a phenomenal increase of event registrations.

Automated Start-Lists

The start list is the positioning of people at the beginning of the race. These are usually grouped by age and estimated swim time, but can vary upon other factors. A custom module was required here, using TAPIr as a display mechanism. Each race event has a custom SQL order clause which can be applied to a SELECT query which generates the list.

Currently, Just Racing are using their current Excel-based start-list system, but the new one will be live soon!

The Theme

The theme itself was the usual two and three column CSS / XHTML using a PHP template.

As a varying two / three column theme was required, some of the CSS positioning is dependent on the visibility of the sidebars. To make the columns fill to the bottom, we used the overflowing margin trick with a solid back-colour.

The only other point worth mentioning is the primary-links. We wanted to vary the links depending upon whether we have a logged in user, but make sure that they are justified on both sides. We simply set the overflow of the primary-link container to hidden, and positioned the varying items towards the end. This navigation is actually perfect and required no further modifications.

And finally...

The site is doing extremely well attracting a large number of visitors and registrations, and we will continue to promote and optimise the site. An upgrade to Drupal 6 will be required at some point as we very much like to keep things as contemporary as possible.

Still to do it make the automated start-list go live as well as continuing to adapt the site to the evolving requirements of the business.

About NuMedia Advantage

NuMedia Advantage are a design and development company based in the UK specialising in Drupal and PHP development.

Joined: 04/23/2008
Juice: 677

Thanks for taking the time to describe what you've done, and congratulations on a great job!

Please let me know if your cck/event/ubercart module will be available as a contribution at some point. I'd definitely like to try it out, as an alternative to uc_event. Thanks.

Joined: 11/20/2007
Juice: 72

Hi Zeezhao,

Thanks for the compliment - we took a good while to get things right.

I certainly will contribute the module(s) - most of the functionality works by using the CCK Date field on a product class, but things like product expiry, hiding fields etc, is a custom module which needs some tidying. Right now, it works very well, but I expect we will have some feedback from the client with a couple of feature requests and bug fixes. Once we've gone through a couple of rounds, the code should be stable enough to release.

David

Joined: 01/20/2009
Juice: 392

Dubs

I'm keen to see the expiry module too once its released.

Glenn

Joined: 01/20/2009
Juice: 392

David

Any progress on the product expiry module? Are you able to make it available, even in its current state so I can play with it?

Thanks

Glenn

Joined: 11/20/2007
Juice: 72

Hi Glenn,

Sorry for not replying sooner - I'll take a look at the code and see what I can do. Basically, there is a cron hook that checks through the exiry date values, which has a hard-coded CCK field name in it - you can change this to suit your requirements. If the product has expired, then we delete all the products from people's open carts.

The code is wrapped in a module with some other bits and pieces, but I'll take a look.

Dubs

Joined: 01/20/2009
Juice: 392

Dubs

That would be great, thanks.

Glenn

Joined: 03/29/2009
Juice: 11

Dubs,
I'm trying to decide if I can adapt your strategy to a project that I'm working on which will be a training company's website.

Among the snags I'm looking at is that there could be class A 2 times a week, and they are distinctly different, with a different trainer and roster... and there could be class A, B, C, D and E throughout the week.

I'd like to avoid having to have the scheduling admins re-create the class for each iteration.

Also, did you look at some sort of 30 day calendar view for these events?
I'm hoping to make sure I'm starting down the correct road before someone pops up and says 'you fool, you should have used THIS' Smiling

Joined: 11/20/2007
Juice: 72

Hi Eric,

Well, the strategy works well for this site, that's for sure....

As far as the 30-day calendar is concerned, we didn't use this approach because there aren't really many events per month.

I am currently working on another sports site (this time tennis) which will have bookings based on a calendar, and I will certainly write up about this one. You could see if the new strategy works better, and this will be in D6 as well.

Dubs

Joined: 11/20/2007
Juice: 72

Hi Glenn,

Here is some code, which I haven't had time to make Drupal friendly. It works for this particular job, and you could easily adapt it include a settings form for some of the hard-coded values. This particular project was on a very tight deadline, which did not facilitate Drupal-friendly code.

<?php

function uc_product_availability_nodeapi(&$node, $op, $arg3, $arg4){
    switch ($op){
      case 'view':
  $available=_uc_product_availability_is_product_available($node);
  $node->content['sell_price']['#access'] = $available;
        $node->content['add_to_cart']['#access'] = $available;
      break;
    }
}

function _uc_product_availability_is_product_available($node) {
$exp_date = $node->field_expiry_date[0]['value'];
$todays_date = date("Y-m-d");

$today = strtotime($todays_date);
$expiration_date = strtotime($exp_date);

$available = TRUE;

if (!$exp_date=='') {
if ($expiration_date < $today) {
$available = FALSE;
}
if ($expiration_date == $today) {
$time = date("H:i:s");
$expire_time = strtotime("12:00:00");
$now_time = strtotime($time);
/*drupal_set_message("n=".$time." ".$now_time." e=".$expire_time);*/
if ($now_time > $expire_time) {
$available = FALSE;
}
}
}
return $available;
}

function uc_product_availability_cron() {
$result = db_query("SELECT nid FROM {node} WHERE type='race_event'");
while ($row = db_fetch_array($result)) {
$node = node_load($row['nid']);
$available = _uc_product_availability_is_product_available($node);
if (!$available) {
$reference = db_query("DELETE FROM {uc_cart_products} WHERE nid = %d", $node->nid);
if (db_affected_rows()) {
watchdog("Product Availability", $node->title . " has expired. ");
}
}
}
}

Joined: 01/20/2009
Juice: 392

Dubs

Great, thanks heaps.

Where does $exp_date come from? A hook_form_alter or a CCK field?

Cheers

Glenn

Joined: 11/20/2007
Juice: 72

Hi Glenn,

It comes from a CCK field, although you could use a hook_form_alter instead.

$exp_date = $node->field_expiry_date[0]['value'];

Dubs.

Joined: 01/20/2009
Juice: 392

Dubs

I'm trying to take a slightly different approach, I simply want registrations to close 1 week before my event (course) starts. I currently have:

<?php

function course_reg_expiry_nodeapi(&$node, $op, $arg3, $arg4){
    switch (
$op){
      case
'view':
 
$available=_course_reg_expiry_is_product_available($node);
 
$node->content['sell_price']['#access'] = $available;
       
$node->content['add_to_cart']['#access'] = $available;
      break;
    }
}

function

_course_reg_expiry_is_product_available($node) {
$exp_date_start = $node->event_end;
$exp_date = $exp_date_start - 604800;
$todays_date = date("d-m-Y");

$today = strtotime($todays_date);
$expiration_date = strtotime($exp_date);

$available = TRUE;

if (!

$exp_date=='') {
if (
$expiration_date < $today) {
$available = FALSE;
}
if (
$expiration_date == $today) {
$time = date("H:i:s");
$expire_time = strtotime("12:00:00");
$now_time = strtotime($time);
/*drupal_set_message("n=".$time." ".$now_time." e=".$expire_time);*/
if ($now_time > $expire_time) {
$available = FALSE;
}
}
}
return
$available;
}

function

course_reg_expiry_cron() {
$result = db_query("SELECT nid FROM {node} WHERE type='rbtpa_course'");
while (
$row = db_fetch_array($result)) {
$node = node_load($row['nid']);
$available = _course_reg_expiry_is_product_available($node);
if (!
$available) {
/* $reference = db_query("DELETE FROM {uc_cart_products} WHERE nid = %d", $node->nid); */
if (db_affected_rows()) {
watchdog("Product Availability", $node->title . " has expired. ");
}
}
}
}
?>

This is having no effect at all on my product node, Add to cart is still available. What I would like to do is replace the 'Add to Cart' button with, "Registrations for this course have closed."

This product has a specific node.tpl.php file, do I need to do something in there with $available?

Cheers

Glenn