You are here

merci.module in MERCI (Manage Equipment Reservations, Checkout and Inventory) 6

Same filename and directory in other branches
  1. 8.2 merci.module
  2. 6.2 merci.module
  3. 7.2 merci.module

MERCI - Managed Equipment Reservation Checkout and Inventory

File

merci.module
View source
<?php

/**
 * @file
 * MERCI - Managed Equipment Reservation Checkout and Inventory
 */

// Item default availability options.
define('MERCI_AVA_F', 1);
define('MERCI_UNA_F', 2);
define('MERCI_AVA_S', 3);
define('MERCI_UNA_S', 4);

// Reservation status options.
define('MERCI_STATUS_UNCONFIRMED', 1);
define('MERCI_STATUS_PENDING', 2);
define('MERCI_STATUS_CHECKED_OUT', 3);
define('MERCI_STATUS_CHECKED_IN', 4);
define('MERCI_STATUS_CANCELLED', 5);
define('MERCI_STATUS_DENIED', 6);
define('MERCI_STATUS_NO_SHOW', 7);

// Bucket/resource status options.
define('MERCI_STATUS_ACTIVE', 1);
define('MERCI_STATUS_INACTIVE', 2);

// Bucket/resource sub-types.
define('MERCI_SUB_TYPE_ITEM', 1);
define('MERCI_SUB_TYPE_RESERVATION', 2);

// Item status for reservations.
define('MERCI_ITEM_STATUS_AVAILABLE', 0);
define('MERCI_ITEM_STATUS_RESERVED', 1);
define('MERCI_ITEM_STATUS_CHECKED_OUT', 2);
define('MERCI_ITEM_STATUS_CHECKED_IN', -1);

/**
 * Implementation of hook_init().
 */
function merci_init() {
  drupal_add_css(drupal_get_path('module', 'merci') . '/merci.css');
  drupal_add_js(drupal_get_path('module', 'merci') . '/merci.js');
}

/**
 * Implementation of hook_perm().
 */
function merci_perm() {
  return array(
    'create reservations',
    'create confirmed reservations',
    'membership discount',
    'suspend MERCI access',
    'administer MERCI',
    'create reservations outside hours of operation',
  );
}

/**
 * Implementation of hook_form_alter().
 */
function merci_form_alter(&$form, $form_state, $form_id) {

  // Node add/edit forms.
  if (isset($form['type']) && isset($form['#node'])) {
    $type = $form['type']['#value'];
    if ("{$type}_node_form" == $form_id && $form_id != 'merci_reservation_node_form') {
      $merci_settings = merci_load_content_type_settings($type);

      // Only active MERCI node types get processed.
      if ($merci_settings->type_setting != 'disabled') {

        // Cast to object here, as it can come in both ways.
        $node = (object) $form['#node'];

        // Make the data representation consistent.
        if (isset($node->merci_sub_type)) {
          $sub_type = $node->merci_sub_type;
          $default_availability = $node->merci_default_availability;
          $late_fee_per_hour = $node->merci_late_fee_per_hour;
          $rate_per_hour = $node->merci_rate_per_hour;
          $fee_free_hours = $node->merci_fee_free_hours;
          $min_cancel_hours = $node->merci_min_cancel_hours;
          $autocheckout = $node->merci_autocheckout;
          $autocheckin = $node->merci_autocheckin;
          $selfcheckout = $node->merci_selfcheckout;
        }
        elseif (isset($node->merci['sub_type'])) {
          $sub_type = $node->merci['sub_type'];
          $default_availability = $node->merci['default_availability'];
          $late_fee_per_hour = $node->merci['late_fee_per_hour'];
          $rate_per_hour = $node->merci['rate_per_hour'];
          $fee_free_hours = $node->merci['fee_free_hours'];
          $min_cancel_hours = $node->merci['min_cancel_hours'];
          $autocheckout = $node->merci['autocheckout'];
          $autocheckin = $node->merci['autocheckin'];
          $selfcheckout = $node->merci['selfcheckout'];
        }
        else {
          $sub_type = MERCI_SUB_TYPE_ITEM;
          $default_availability = MERCI_AVA_F;

          // Only resource types have individual pricing data.
          if ($merci_settings->type_setting == 'bucket') {
            $rate_per_hour = 0;
            $late_fee_per_hour = 0;
            $fee_free_hours = 0;
            $min_cancel_hours = 0;
            $autocheckout = 0;
            $autocheckin = 0;
            $selfcheckout = 0;
          }
          else {
            $rate_per_hour = $merci_settings->rate_per_hour;
            $late_fee_per_hour = $merci_settings->late_fee_per_hour;
            $fee_free_hours = $merci_settings->fee_free_hours;
            $min_cancel_hours = $merci_settings->min_cancel_hours;
            $autocheckout = $node->merci_autocheckout;
            $autocheckin = $node->merci_autocheckin;
            $selfcheckout = $node->merci_selfcheckout;
          }
        }

        // New nodes are always sub type item.
        $form['merci_sub_type'] = array(
          '#type' => 'value',
          '#value' => $sub_type,
        );
        if (user_access('administer MERCI')) {
          $form['merci'] = array(
            '#type' => 'fieldset',
            '#title' => t('MERCI settings'),
            '#collapsible' => TRUE,
            '#collapsed' => TRUE,
          );
          $form['merci']['merci_default_availability'] = array(
            '#title' => t('Default booking availability'),
            '#type' => 'radios',
            '#options' => merci_item_status(),
            '#description' => t('If no availability information is defined for a given time, the resource falls back onto this setting.'),
            '#default_value' => $default_availability,
          );

          // Bucket item nodes have no individual pricing, so just zero these values out.
          if ($merci_settings->type_setting == 'bucket' && $sub_type == MERCI_SUB_TYPE_ITEM) {
            $form['merci_rate_per_hour'] = array(
              '#type' => 'value',
              '#value' => $rate_per_hour,
            );
            $form['merci_late_fee_per_hour'] = array(
              '#type' => 'value',
              '#value' => $late_fee_per_hour,
            );
            $form['merci_fee_free_hours'] = array(
              '#type' => 'value',
              '#value' => $fee_free_hours,
            );
            $form['merci_min_cancel_hours'] = array(
              '#type' => 'value',
              '#value' => $min_cancel_hours,
            );
            $form['merci_autocheckout'] = array(
              '#type' => 'value',
              '#value' => $autocheckout,
            );
            $form['merci_autocheckin'] = array(
              '#type' => 'value',
              '#value' => $autocheckin,
            );
            $form['merci_selfcheckout'] = array(
              '#type' => 'value',
              '#value' => $selfcheckout,
            );
          }
          else {
            $form['merci']['merci_rate_per_hour'] = array(
              '#type' => 'textfield',
              '#title' => t('Rate per hour'),
              '#size' => 10,
              '#default_value' => $rate_per_hour,
              '#element_validate' => array(
                'merci_is_numeric_validate',
              ),
              '#description' => t('The per hour rental fee for the item.'),
            );
            $form['merci']['merci_late_fee_per_hour'] = array(
              '#type' => 'textfield',
              '#title' => t('Late fee per hour'),
              '#size' => 10,
              '#default_value' => $late_fee_per_hour,
              '#element_validate' => array(
                'merci_is_numeric_validate',
              ),
              '#description' => t('The per hour fee for returning the item late.'),
            );
            $form['merci']['merci_fee_free_hours'] = array(
              '#type' => 'textfield',
              '#title' => t('Fee free hours'),
              '#size' => 10,
              '#default_value' => $fee_free_hours,
              '#element_validate' => array(
                'merci_is_numeric_validate',
              ),
              '#description' => t('The number of hours the item can be used before fees are charged.'),
            );
            $form['merci']['merci_autocheckout'] = array(
              '#type' => 'checkbox',
              '#title' => t('Auto checkout'),
              '#default_value' => $autocheckout,
              '#description' => t('Automatically check this item out when the Reservation starts.'),
            );
            $form['merci']['merci_autocheckin'] = array(
              '#type' => 'checkbox',
              '#title' => t('Auto checkin'),
              '#default_value' => $autocheckout,
              '#description' => t('Automatically check this item in when the Reservation ends.'),
            );
            $form['merci']['merci_selfcheckout'] = array(
              '#type' => 'checkbox',
              '#title' => t('Self checkout'),
              '#default_value' => $autocheckout,
              '#description' => t('Manage checkout with additional code.'),
            );
          }
        }
        else {
          $form['merci_default_availability'] = array(
            '#type' => 'value',
            '#value' => $default_availability,
          );
          $form['merci_rate_per_hour'] = array(
            '#type' => 'value',
            '#value' => $rate_per_hour,
          );
          $form['merci_late_fee_per_hour'] = array(
            '#type' => 'value',
            '#value' => $late_fee_per_hour,
          );
          $form['merci_fee_free_hours'] = array(
            '#type' => 'value',
            '#value' => $fee_free_hours,
          );
          $form['merci_min_cancel_hours'] = array(
            '#type' => 'value',
            '#value' => $min_cancel_hours,
          );
          $form['merci_autocheckout'] = array(
            '#type' => 'value',
            '#value' => $autocheckout,
          );
          $form['merci_autocheckin'] = array(
            '#type' => 'value',
            '#value' => $autocheckin,
          );
          $form['merci_selfcheckout'] = array(
            '#type' => 'value',
            '#value' => $selfcheckout,
          );
        }
      }
    }
  }
  switch ($form_id) {

    // Add check availability button inside date selector.
    case 'merci_reservation_node_form':

      //Hide member cost and commerical cost.  These are only here to make Views easier.
      unset($form['field_merci_member_cost']);
      unset($form['field_merci_commercial_cost']);

      //Users without administer MERCI permission can only alter Unconfirmed Reservations.
      if (!user_access('administer MERCI') && $form['merci']['merci_status']['#default_value']) {
        unset($form['buttons']['preview']);
        unset($form['buttons']['submit']);
      }
      if (user_access('suspend MERCI access') && !user_access('administer MERCI')) {
        form_set_error('merci_status', t('Your access to make new Reservations or edit existing Reservations has been suspended.'));
      }
      else {
        $form['field_merci_date'][0]['merci_date_filter'] = array(
          '#type' => 'submit',
          '#value' => t('Check availability'),
          '#weight' => 10,
          '#submit' => array(
            'merci_date_filter',
          ),
        );

        // Since hook_validate is broken in 6.x, we add our own
        // custom validation here.
        $form['#validate'][] = 'merci_node_validate';
      }
      break;

    // Node settings form.
    case 'node_type_form':

      // Reservation content type can't used for other MERCI functionality.
      if (isset($form['#node_type']->type) && $form['#node_type']->type == 'merci_reservation') {
        return;
      }
      $warning = '<div>' . t('<strong> WARNING:</strong> changing this setting has no effect on existing reserved items.') . '</div>';
      $type = $form['old_type']['#value'];
      $settings = db_fetch_object(db_query("SELECT * FROM {merci_node_type} WHERE type = '%s'", $type));
      $options = array(
        'disabled' => t('Disabled'),
        'bucket' => t('Bucket'),
        'resource' => t('Resource'),
      );
      $form['#validate'][] = 'merci_node_type_save_validate';
      $form['#submit'][] = 'merci_node_type_save_submit';
      $form['merci'] = array(
        '#type' => 'fieldset',
        '#title' => t('MERCI settings'),
        '#collapsible' => TRUE,
        '#collapsed' => TRUE,
      );

      // If any nodes have already been created, lock the type setting.
      if (merci_check_existing_bucket_items($type)) {
        $form['merci']['merci_type_setting'] = array(
          '#type' => 'value',
          '#value' => $settings->type_setting,
        );
        $form['merci']['merci_type_setting_display'] = array(
          '#type' => 'item',
          '#title' => t('Reservable item type'),
          '#value' => $options[$settings->type_setting],
          '#description' => t('The setting can not be changed because content already exists for this type.'),
        );
      }
      else {
        $description_items = array(
          t('Resource:  Use this content type to create unique items that can be reserved.'),
          t('Bucket:  Use this content type to create interchangable items that can be reserved (ex. Camera). Buckets reference interchangable items. The actual item does not have to be chosen until the reservation is checked out.'),
        );
        $form['merci']['merci_type_setting'] = array(
          '#type' => 'radios',
          '#title' => t('Reservable item type'),
          '#options' => $options,
          '#default_value' => $settings ? $settings->type_setting : 'disabled',
          '#description' => theme('item_list', $description_items),
        );
      }
      $status = array(
        MERCI_STATUS_ACTIVE => t('Active'),
        MERCI_STATUS_INACTIVE => t('Inactive'),
      );
      $form['merci']['merci_status'] = array(
        '#type' => 'radios',
        '#title' => t('Status'),
        '#options' => $status,
        '#default_value' => isset($settings->status) && $settings->status ? $settings->status : MERCI_STATUS_ACTIVE,
        '#description' => t('Set to active to allow this type to be reserved.'),
      );

      // This setting is only valid for buckets.
      if (!$settings || $settings->type_setting == 'bucket') {
        $form['merci']['merci_spare_items'] = array(
          '#type' => 'textfield',
          '#title' => t('Spare items'),
          '#size' => 10,
          '#default_value' => $settings ? $settings->spare_items : 0,
          '#element_validate' => array(
            'merci_is_numeric_validate',
          ),
          '#description' => t("Set this to the number of items of this type that should always be available even when it's fully reserved.") . $warning,
        );
      }
      else {
        $form['merci']['merci_spare_items'] = array(
          '#type' => 'value',
          '#value' => 0,
        );
      }

      //Grouping
      $items = array();
      $terms = taxonomy_get_tree(variable_get('merci_equipment_grouping_vid', 37));
      $groupings[0] = 'None';
      foreach ($terms as $term) {
        $groupings[$term->tid] = $term->name;
      }
      $form['merci']['merci_grouping'] = array(
        '#type' => 'select',
        '#title' => t('Grouping'),
        '#default_value' => $settings ? $settings->grouping : 0,
        '#options' => $groupings,
        '#description' => t('This will alter order the content types are displayed to users reserving items from buckets.  Terms added to the MERCI Equipment Groupings taxonomy will appear here.'),
      );
      $form['merci']['merci_max_hours_per_reservation'] = array(
        '#type' => 'textfield',
        '#title' => t('Maximum hours per reservation'),
        '#size' => 10,
        '#default_value' => $settings ? $settings->max_hours_per_reservation : 0,
        '#element_validate' => array(
          'merci_is_numeric_validate',
        ),
        '#description' => t('The maximum hours the item can be reserved for in one reservation. Set to zero for no limit.') . $warning,
      );
      $form['merci']['merci_allow_overnight'] = array(
        '#type' => 'checkbox',
        '#title' => t('Allow overnight reservation'),
        '#default_value' => $settings ? $settings->allow_overnight : 0,
        '#description' => t('Allow a reservation to continue over multiple days.') . $warning,
      );
      $form['merci']['merci_allow_weekends'] = array(
        '#type' => 'checkbox',
        '#title' => t('Allow weekend reservation'),
        '#default_value' => $settings ? $settings->allow_weekends : 0,
        '#description' => t('Allow a reservation to be made over days defined as weekend.') . $warning,
      );
      $form['merci']['merci_rate_per_hour'] = array(
        '#type' => 'textfield',
        '#title' => t('Rate per hour'),
        '#size' => 10,
        '#default_value' => $settings ? $settings->rate_per_hour : 0,
        '#element_validate' => array(
          'merci_is_numeric_validate',
        ),
        '#description' => t('The per hour rental fee for the item.'),
      );
      $form['merci']['merci_late_fee_per_hour'] = array(
        '#type' => 'textfield',
        '#title' => t('Late fee per hour'),
        '#size' => 10,
        '#default_value' => $settings ? $settings->late_fee_per_hour : 0,
        '#element_validate' => array(
          'merci_is_numeric_validate',
        ),
        '#description' => t('The per hour fee for returning the item late.'),
      );
      $form['merci']['merci_fee_free_hours'] = array(
        '#type' => 'textfield',
        '#title' => t('Fee free hours'),
        '#size' => 10,
        '#default_value' => $settings ? $settings->fee_free_hours : 0,
        '#element_validate' => array(
          'merci_is_numeric_validate',
        ),
        '#description' => t('The number of hours the item can be used before fees are charged.'),
      );
      $form['merci']['merci_min_cancel_hours'] = array(
        '#type' => 'textfield',
        '#title' => t('Minimum hours for cancelation without No Show'),
        '#size' => 10,
        '#default_value' => $settings ? $settings->min_cancel_hours : 0,
        '#element_validate' => array(
          'merci_is_numeric_validate',
        ),
        '#description' => t('Minimum number of hours a user can cancel a reservation for the item.'),
      );
      $form['merci']['merci_autocheckout'] = array(
        '#type' => 'checkbox',
        '#title' => t('Automatic Checkout'),
        '#default_value' => $settings ? $settings->autocheckout : 0,
        '#description' => t('Automatically check item out when reservation starts.  Use for resources like meetings rooms that a user does not actually take.') . $warning,
      );
      $form['merci']['merci_autocheckin'] = array(
        '#type' => 'checkbox',
        '#title' => t('Automatic Checkin'),
        '#default_value' => $settings ? $settings->autocheckin : 0,
        '#description' => t('Automatically check item in when reservation ends.  Use for resources like meetings rooms that a user does not actually return.') . $warning,
      );
      $form['merci']['merci_selfcheckout'] = array(
        '#type' => 'checkbox',
        '#title' => t('Self Checkout'),
        '#default_value' => $settings ? $settings->selfcheckout : 0,
        '#description' => t('The checkout and checkin process for this resource is managed with additional software (ie. lab computer where login process can by linked to checkout status).') . $warning,
      );
      break;
    case 'node_delete_confirm':
      $node = node_load((int) arg(1));
      merci_delete_item_validate($node);
      break;
    case 'node_type_delete_confirm':
      $type = str_replace('-', '_', arg(3));
      merci_delete_node_type_validate($type);
      break;
    case 'node_admin_content':
      if (!isset($form['#validate'])) {
        $form['#validate'] = array();
      }
      $form['#validate'][] = 'merci_node_admin_delete_validate';
      break;
  }
}

/**
 * Implementation of hook_menu().
 */
function merci_menu() {
  $admin = array(
    'administer MERCI',
  );

  // Callback for AJAX adding of item selectors.
  $items['merci/js'] = array(
    'title' => 'Javascript Choice Form',
    'page callback' => 'merci_choice_js',
    'access arguments' => array(
      'access content',
    ),
    'type' => MENU_CALLBACK,
  );

  // Administration settings.
  $items['admin/settings/merci'] = array(
    'title' => 'MERCI',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'merci_admin_settings',
    ),
    'access callback' => 'user_access',
    'access arguments' => $admin,
    'description' => t('Configure system settings for MERCI.'),
  );
  $items['merci/taxonomy'] = array(
    'title' => 'JSON interface for node taxonomy',
    'description' => 'Takes a node ID and returns taxonomy data as JSON',
    'page arguments' => array(
      2,
    ),
    'page callback' => 'merci_taxonomy_json',
    'access arguments' => array(
      'access content',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['merci/contract'] = array(
    'title' => 'Printable contract',
    'description' => 'Takes a node ID and returns a printable contract',
    'page arguments' => array(
      2,
    ),
    'page callback' => 'merci_printable_contract',
    'access arguments' => array(
      'access content',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['merci/confirm'] = array(
    'title' => 'Confirm MERCI Reservation',
    'description' => 'Takes a node ID and returns a page confirming Reservation',
    'page arguments' => array(
      2,
    ),
    'page callback' => 'merci_confirm_reservation',
    'access arguments' => array(
      'access content',
    ),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Builds the MERCI admininstration settings form.
 */
function merci_admin_settings() {
  $form = array();
  $statuses = array();
  $statuses[MERCI_STATUS_UNCONFIRMED] = t('Unconfirmed');
  $statuses[MERCI_STATUS_PENDING] = t('Pending');
  $statuses[MERCI_STATUS_CHECKED_OUT] = t('Checked Out');
  $form['merci_content_types'] = array(
    '#type' => 'fieldset',
    '#title' => t('Content types'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $header = array(
    t('Type'),
    t('Usage'),
    t('Operations'),
  );
  $rows = array();
  $merci_types = array();
  $merci_type_result = db_query("SELECT type, type_setting FROM {merci_node_type} WHERE type_setting = 'resource' OR  type_setting = 'bucket'");
  while ($merci_type = db_fetch_object($merci_type_result)) {
    $merci_types[$merci_type->type] = $merci_type->type_setting;
  }

  // while
  foreach (node_get_types() as $type) {
    $type_url_str = str_replace('_', '-', $type->type);
    $usage = 'Not used by MERCI';
    if (isset($merci_types[$type->type])) {
      $usage = 'MERCI ' . $merci_types[$type->type];
    }

    // if
    $operations = l(t('Edit'), 'admin/content/node-type/' . $type_url_str, array(
      'query' => drupal_get_destination(),
    ));
    if (module_exists('merci_inventory') && merci_inventory_node_type_menu_access($type->type)) {
      $operations .= ' | ' . l(t('Inventory Sync'), 'admin/content/node-type/' . $type_url_str . '/merci_inventory', array(
        'query' => drupal_get_destination(),
      ));
    }

    // if
    $rows[] = array(
      $type->name,
      $usage,
      $operations,
    );
  }

  // foreach
  $form['merci_content_types']['content_types_table'] = array(
    '#value' => theme('table', $header, $rows),
  );
  $form['merci_general'] = array(
    '#type' => 'fieldset',
    '#title' => t('General settings'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['merci_general']['merci_default_reservation_status'] = array(
    '#type' => 'select',
    '#title' => t('Default Reservation Status'),
    '#options' => $statuses,
    '#default_value' => variable_get('merci_default_reservation_status', ''),
    '#description' => t('New Reservations will be set to this by default.  Use Checked Out if you are using MERCI to manage the checkout, but do not allow actual Reservations.'),
  );
  $form['merci_general']['merci_max_days_advance_reservation'] = array(
    '#type' => 'textfield',
    '#title' => t('Max Advance'),
    '#size' => 11,
    '#maxlength' => 11,
    '#default_value' => variable_get('merci_max_days_advance_reservation', '0'),
    '#description' => t('Maximum number of days a Reservation can be made in advance.  Set to 0 if to allow Reservations to be made an unlimitted time into the future.'),
  );
  $form['merci_general']['merci_email_receipt'] = array(
    '#type' => 'checkbox',
    '#title' => t('Send Email Receipt'),
    '#default_value' => variable_get('merci_email_receipt', 0),
    '#description' => t('Use when default Reservation Status is Unconfirmed'),
  );
  $form['merci_general']['merci_email_confirmation'] = array(
    '#type' => 'checkbox',
    '#title' => t('Send Email Confirmation'),
    '#default_value' => variable_get('merci_email_confirmation', 0),
    '#description' => t('Add link to change reservations from Unconfirmed to Confirmed'),
  );

  // Weekend
  $form['merci_general']['merci_saturday_is_weekend'] = array(
    '#type' => 'checkbox',
    '#title' => t('Saturday is a considered weekend'),
    '#default_value' => variable_get('merci_saturday_is_weekend', 1),
  );
  $form['merci_general']['merci_sunday_is_weekend'] = array(
    '#type' => 'checkbox',
    '#title' => t('Sunday is a considered weekend'),
    '#default_value' => variable_get('merci_sunday_is_weekend', 1),
  );
  $form['merci_general']['merci_monday_is_weekend'] = array(
    '#type' => 'checkbox',
    '#title' => t('Monday is considered a weekend'),
    '#default_value' => variable_get('merci_monday_is_weekend', 0),
  );
  $form['merci_general']['merci_tuesday_is_weekend'] = array(
    '#type' => 'checkbox',
    '#title' => t('Tuesday is considered a weekend'),
    '#default_value' => variable_get('merci_tuesday_is_weekend', 0),
  );
  $form['merci_general']['merci_wednesday_is_weekend'] = array(
    '#type' => 'checkbox',
    '#title' => t('Wednesday is considered a weekend'),
    '#default_value' => variable_get('merci_wednesday_is_weekend', 0),
  );
  $form['merci_general']['merci_thursday_is_weekend'] = array(
    '#type' => 'checkbox',
    '#title' => t('Thursday is considered a weekend'),
    '#default_value' => variable_get('merci_thrusday_is_weekend', 0),
  );
  $form['merci_general']['merci_friday_is_weekend'] = array(
    '#type' => 'checkbox',
    '#title' => t('Firday is a considered weekend'),
    '#default_value' => variable_get('merci_firday_is_weekend', 0),
  );

  // Hours of operation
  $hours_description = t('<div>Enter military time for both opening and closing time, separated by a dash, in the format <em>hh:mm-hh:mm</em></div>ex. <em>09:00-17:00</em> would be open at 9AM, close at 5PM. Leave blank to indicate not being open.');
  $form['merci_general']['merci_hours_mon'] = array(
    '#type' => 'textfield',
    '#title' => t('Monday hours'),
    '#size' => 11,
    '#maxlength' => 11,
    '#default_value' => variable_get('merci_hours_mon', ''),
    '#description' => $hours_description,
  );
  $form['merci_general']['merci_hours_tue'] = array(
    '#type' => 'textfield',
    '#title' => t('Tuesday hours'),
    '#size' => 11,
    '#maxlength' => 11,
    '#default_value' => variable_get('merci_hours_tue', ''),
    '#description' => $hours_description,
  );
  $form['merci_general']['merci_hours_wed'] = array(
    '#type' => 'textfield',
    '#title' => t('Wednesday hours'),
    '#size' => 11,
    '#maxlength' => 11,
    '#default_value' => variable_get('merci_hours_wed', ''),
    '#description' => $hours_description,
  );
  $form['merci_general']['merci_hours_thu'] = array(
    '#type' => 'textfield',
    '#title' => t('Thursday hours'),
    '#size' => 11,
    '#maxlength' => 11,
    '#default_value' => variable_get('merci_hours_thu', ''),
    '#description' => $hours_description,
  );
  $form['merci_general']['merci_hours_fri'] = array(
    '#type' => 'textfield',
    '#title' => t('Friday hours'),
    '#size' => 11,
    '#maxlength' => 11,
    '#default_value' => variable_get('merci_hours_fri', ''),
    '#description' => $hours_description,
  );
  $form['merci_general']['merci_hours_sat'] = array(
    '#type' => 'textfield',
    '#title' => t('Saturday hours'),
    '#size' => 11,
    '#maxlength' => 11,
    '#default_value' => variable_get('merci_hours_sat', ''),
    '#description' => $hours_description,
  );
  $form['merci_general']['merci_hours_sun'] = array(
    '#type' => 'textfield',
    '#title' => t('Sunday hours'),
    '#size' => 11,
    '#maxlength' => 11,
    '#default_value' => variable_get('merci_hours_sun', ''),
    '#description' => $hours_description,
  );
  $form['merci_general']['merci_hours_admin'] = array(
    '#type' => 'textfield',
    '#title' => t('Admin hours'),
    '#size' => 11,
    '#maxlength' => 11,
    '#default_value' => variable_get('merci_hours_admin', ''),
    '#description' => t('This setting controls the hours shown on the conflict grid for users with Adminster MERCI permessions.  The conflict grid is normally limited to the days and hours the set above.  Because users with Admister MERCI rights can create Resevervations outside the normal hours of operation, they need to see conflicts during those times as well.'),
  );
  $form['merci_general']['merci_closed_dates'] = array(
    '#type' => 'textarea',
    '#title' => t('Closed dates'),
    '#rows' => 10,
    '#cols' => 5,
    // TODO: this doesn't seem to work...
    '#default_value' => variable_get('merci_closed_dates', ''),
    '#description' => t('<div>Enter dates which are closed regardless of the day of the week, one date per line, in the format <em>mm-dd</em></div>ex. <em>07-04</em> would mean July 4th is always closed, regardless of what day of the week it falls on.'),
  );
  $form['merci_general']['merci_membership_discount'] = array(
    '#type' => 'textfield',
    '#title' => t('Membership discount'),
    '#size' => 11,
    '#maxlength' => 11,
    '#default_value' => variable_get('merci_membership_discount', ''),
    '#description' => t('Percent comercial rate is reduced for users in a role with member discount permission. Enter as decimal.  60% would be .60.  An item with a commercial rate of $100 would cost a member $40 for any hours beyond the fee free hours for that reservation.'),
  );
  $form['merci_general']['merci_contract_header'] = array(
    '#type' => 'textarea',
    '#title' => t('Contract header'),
    '#rows' => 10,
    '#cols' => 5,
    // TODO: this doesn't seem to work...
    '#default_value' => variable_get('merci_contract_header', ''),
    '#description' => t('Header portion of printable contract.  Allows HTML.'),
  );
  $form['merci_general']['merci_contract_boilerplate'] = array(
    '#type' => 'textarea',
    '#title' => t('Contract boilerplate'),
    '#rows' => 10,
    '#cols' => 5,
    // TODO: this doesn't seem to work...
    '#default_value' => variable_get('merci_contract_boilerplate', ''),
    '#description' => t('Legalese that makes the contract legally binding.'),
  );
  $form['merci_general']['merci_contract_footer'] = array(
    '#type' => 'textarea',
    '#title' => t('Contract footer'),
    '#rows' => 10,
    '#cols' => 5,
    // TODO: this doesn't seem to work...
    '#default_value' => variable_get('merci_contract_footer', ''),
    '#description' => t('Footer portion of printable contract. Normally includes signature lines. HTML allowed.'),
  );
  if (module_exists('token')) {
    $form['merci_general']['token_help'] = array(
      '#title' => t('Replacement patterns'),
      '#type' => 'fieldset',
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
    );
    $form['merci_general']['token_help']['help'] = array(
      '#value' => theme('token_help', 'node'),
    );
  }
  return system_settings_form($form);
}

/**
 * Implementation of hook_node_info().
 */
function merci_node_info() {
  return array(
    // Reservation nodes.
    'merci_reservation' => array(
      'name' => t('Reservation'),
      'module' => 'merci',
      'has_body' => FALSE,
      'description' => t("A reservation reserves a resource or group of resources for some period of time."),
    ),
  );
}

/**
 * Implementation of hook_node_info().
 */
function merci_node_type($op, $info) {
  switch ($op) {
    case 'update':

      // If type was edited, update it.
      if (isset($info->old_type) && $info->type != $info->old_type) {
        db_query("UPDATE {merci_node_type} SET type = '%s' WHERE type = '%s'", $info->type, $info->old_type);
      }
      break;
    case 'delete':
      db_query("DELETE FROM {merci_node_type} WHERE type = '%s'", $info->type);
      break;
  }
}

/**
 * Implementation of hook_form().
 */
function merci_form(&$node, $form_state) {
  $form = node_content_form($node, $form_state);
  $template = array();

  // Build existing reserved items table on existing reservations.
  if (isset($node->nid)) {
    $form['existing_items'] = merci_build_reservation_table_form($form_state, $node, TRUE);
    $merci = $node->merci;
  }
  else {
    $merci['status'] = variable_get('merci_default_reservation_status', MERCI_STATUS_UNCONFIRMED);
  }

  // Choice adding code mostly stolen from poll module.
  if (isset($form_state['choice_count'])) {
    $choice_count = $form_state['choice_count'];
  }
  else {
    $choice_count = max(3, empty($node->choice) ? 3 : count($node->choice));
  }

  // Add a wrapper for the choices and more button.
  $form['choice_wrapper'] = array(
    '#tree' => FALSE,
    '#prefix' => '<div class="clear-block" id="merci-choice-wrapper">',
    '#suffix' => '</div>',
  );

  // Container for just the item selector.
  $form['choice_wrapper']['choice'] = array(
    '#prefix' => '<div id="merci-choices">',
    '#suffix' => '</div>',
    '#theme' => 'merci_choices',
  );
  if (isset($_GET['template'])) {
    $templates = db_query("SELECT nr.body FROM {node_revisions} nr WHERE nid = %d ORDER BY vid DESC LIMIT 1", intval($_GET['template']));
    while ($template_object = db_fetch_object($templates)) {
      $template = explode(',', $template_object->body);
    }

    // while
  }

  // if
  // Add the current choices to the form.
  for ($delta = 0; $delta < $choice_count || $delta < count($template); $delta++) {
    $default = isset($node->choice[$delta]['item']) ? $node->choice[$delta]['item'] : '';
    if ($default == '' && isset($template[$delta])) {
      $default = $template[$delta];
    }

    // if
    $form['choice_wrapper']['choice'][$delta] = _merci_choice_form($node, $form_state, $delta, $default);
  }

  // We name our button 'merci_more' to avoid conflicts with other modules using
  // AHAH-enabled buttons with the id 'more'.
  $form['choice_wrapper']['merci_more'] = array(
    '#type' => 'submit',
    '#value' => t('Add more items'),
    '#description' => t("If the number of items above isn't enough, click here to add more items."),
    '#weight' => 1,
    '#submit' => array(
      'merci_more_choices_submit',
    ),
  );
  if (user_access('administer MERCI')) {
    $form['merci'] = array(
      '#type' => 'fieldset',
      '#title' => t('MERCI settings'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
    );
    $form['merci']['merci_status'] = array(
      '#title' => t('Status'),
      '#type' => 'radios',
      '#options' => merci_record_status(),
      '#default_value' => $merci['status'],
      '#description' => t('Finalized bookings can not have time conflicts with each other.'),
    );
  }
  else {
    $form['merci_status'] = array(
      '#type' => 'value',
      '#value' => $merci['status'],
    );
  }
  return $form;
}

/**
 * Submit handler to add more choices to a reservation form. This handler is used when
 * javascript is not available. It makes changes to the form state and the
 * entire form is rebuilt during the page reload.
 */
function merci_more_choices_submit($form, &$form_state) {

  // Set the form to rebuild and run submit handlers.
  node_form_submit_build_node($form, $form_state);

  // Make the changes we want to the form state.
  if ($form_state['values']['merci_more']) {
    $form_state['choice_count'] = count($form_state['values']['choice']) + 5;
  }
}

/**
 * Submit handler to date filter items on a reservation form.
 * It makes changes to the form state and the entire form is
 * rebuilt during the page reload.
 */
function merci_date_filter($form, &$form_state) {

  // Set the form to rebuild and run submit handlers.
  node_form_submit_build_node($form, $form_state);
}

/**
 * Builds an individual item selector.
 *
 * @param $node
 *   The reservation node object.
 * @param $form_state
 *   Current form state array.
 * @param $delta
 *   Which selector number to build.
 * @param $default
 *   Default value for the select.
 * @return
 *   The form array for the selector.
 */
function _merci_choice_form($node, $form_state, $delta, $default = '') {
  $form = array(
    '#tree' => TRUE,
  );

  // We'll manually set the #parents property of these fields so that
  // their values appear in the $form_state['values']['choice'] array.
  $options = merci_build_reservable_items($node, $form_state);
  $form['item'] = array(
    '#type' => 'select',
    '#title' => t('Reserve item @n', array(
      '@n' => $delta + 1,
    )),
    '#options' => $options['options'],
    '#default_value' => $default,
    '#parents' => array(
      'choice',
      $delta,
      'item',
    ),
  );
  return $form;
}

/**
 * Builds the list of all currently reservable items, filtered by date.
 *
 * @param $node
 *   The reservation node object.
 * @param $form_state
 *   Current form state array.
 * @param $reservation_nid
 *   (Optional) The nid of a reservation to ignore in the options exclusions.
 * @return
 *   An associative array with the following key/value pairs:
 *     'options'      => An array of available items, in the format used
 *                       for the item selector.
 *     'flat_options' => An array of available items, whose values are the
 *                       nids of the items.
 */
function merci_build_reservable_items($node, $form_state, $reservation_nid = NULL) {

  // Newly set dates take precedence.
  if (isset($form_state['values']['field_merci_date'])) {
    $start = $form_state['values']['field_merci_date'][0]['value'];
    $end = $form_state['values']['field_merci_date'][0]['value2'];
  }
  elseif (isset($node->nid)) {
    $date_info = $node->field_merci_date[0];
    $start = $date_info['value'];
    $end = $date_info['value2'];
  }
  else {
    $is_new = TRUE;
  }
  $options = array();
  $options['options'] = array(
    '' => t('<Select>'),
  );

  // Buckets.
  $buckets = merci_load_merci_type_settings('bucket');
  $bucket_options = array();

  // Loop through each bucket type.
  while ($bucket = db_fetch_object($buckets)) {

    // Only include active buckets, and content types the user
    // can reserve.
    if ($bucket->status == MERCI_STATUS_ACTIVE && merci_check_content_type_user_permissions($bucket->type)) {

      // No filtering for new reservations.
      if (isset($is_new)) {

        // Only add the bucket if there is at least one published item.
        if (merci_check_existing_bucket_items($bucket->type, TRUE)) {
          $bucket_options[$bucket->type] = $bucket->name;
        }

        // if
      }
      else {

        // Check bucket restrictions.
        // DIFF??
        $restrictions = merci_check_content_type_restrictions($bucket->type, $start, $end);
        if (empty($restrictions)) {

          // Check for available items in the bucket.
          $available_bucket_items = merci_get_available_bucket_count($bucket->type, $start, $end, $reservation_nid) > $bucket->spare_items;
          if ($available_bucket_items) {
            $bucket_options[$bucket->type] = $bucket->name;
          }

          // if
        }

        // if
      }

      // else
    }

    // if
  }

  // while
  // FIX - NEED TO CHANGE FORMATTING HERE
  $options['options'][t('Buckets')] = $bucket_options;
  $options['flat_options'] = array_keys($bucket_options);

  // Resources.
  $resources = merci_load_merci_type_settings('resource');

  // Loop through each resource type.
  while ($resource = db_fetch_object($resources)) {
    $item_options = array();

    // Only include active resources, and content types the user can reserve.
    if ($resource->status == MERCI_STATUS_ACTIVE && merci_check_content_type_user_permissions($resource->type)) {

      // No filtering for new reservations.
      if (isset($is_new)) {

        // Filter out any placeholder nodes for this resource.
        $items = db_query("SELECT n.nid, n.title FROM {node} n INNER JOIN {merci_resource_node} m ON n.vid = m.vid WHERE n.type = '%s' AND n.status = %d AND m.sub_type = %d ORDER BY n.title", $resource->type, 1, MERCI_SUB_TYPE_ITEM);
        while ($item = db_fetch_object($items)) {
          $item_options[$item->nid] = $item->title;
        }

        // while
      }
      else {

        // Check resource restrictions.

        //DIFF?
        $restrictions = merci_check_content_type_restrictions($resource->type, $start, $end);
        if (empty($restrictions)) {
          $item_options = merci_get_reservable_items('resource', $resource->type, $start, $end, $reservation_nid);
        }

        // if
      }

      // else
      $options['options'][$resource->name] = $item_options;
      $options['flat_options'] = array_merge($options['flat_options'], array_keys($item_options));
    }

    // if
  }

  // while
  return $options;
}

/**
 * Checks for reservation restrictions for a content type.
 *
 * These include maximum hours per reservation, and if the bucket/resource
 * is reservable overnight and/or on weekends.
 *
 * @param $content_type
 *   The content type to be checked.
 * @param $start
 *   The start date of the reservation in DATETIME format and UTC timezone.
 * @param $end
 *   The end date of the reservation in DATETIME format and UTC timezone.
 * @return
 *   An array of warning messages for any restrictions found.
 */

//DIFF?
function merci_check_content_type_restrictions($content_type, $start, $end) {

  // Users in role with administer MERCI or create reservations outside hours of operation permssion
  // are exempt from content type restriction for max hours and the checkout's hours of operation
  if (user_access('administer MERCI') || user_access('create reservations outside hours of operation')) {

    //drupal_set_message(t('You are making a Reservation outside the normal hours of operation.  This may impact access to the items you are reserving.'));
  }
  else {

    //DIFF?
    $type_settings = merci_content_type_rules($content_type);
    $hours_of_operation = merci_load_hours_of_operation($content_type);
    $return = array();

    // Convert start/end dates to local time.
    $start_object = merci_create_local_date_object($start);
    $end_object = merci_create_local_date_object($end);

    // Convert start/end dates to local time.
    $start_object = merci_create_local_date_object($start);
    $end_object = merci_create_local_date_object($end);

    // We want these timestamps generated in UTC.
    $old_timezone = date_default_timezone_get();
    date_default_timezone_set('UTC');
    $start_timestamp = strtotime($start);
    $end_timestamp = strtotime($end);
    date_default_timezone_set($old_timezone);
    $reserved_hours = ($end_timestamp - $start_timestamp) / (60 * 60);
    $start_day_of_week = date_format($start_object, 'w');
    $end_day_of_week = date_format($end_object, 'w');

    // Make sure max hours aren't exceeded.
    if ($type_settings->max_hours_per_reservation && $reserved_hours > $type_settings->max_hours_per_reservation) {
      $return[] = t('%name cannot be reserved for more than %hours hours.', array(
        '%hours' => $type_settings->max_hours_per_reservation,
      ));
    }

    // Validate allow_overnight.
    if (!$type_settings->allow_overnight) {

      // Need the 48 hour check in case somebody starts and ends their
      // reservation on the same day.
      if ($start_day_of_week != $end_day_of_week || $reserved_hours > 48) {
        $return[] = t('%name cannot be reserved overnight.');
      }
    }

    // Validate allow_weekend.
    if (!$type_settings->allow_weekends) {
      $on_weekend = FALSE;

      // Check the start and end dates for the reservation first.
      if (in_array($start_day_of_week, array(
        '6',
        '0',
      )) || in_array($end_day_of_week, array(
        '6',
        '0',
      ))) {
        $on_weekend = TRUE;
      }

      // Check all dates between the start and end dates for the reservation next.
      if (!$on_weekend) {
        $day = 60 * 60 * 24;
        $counter = $start_timestamp + $day;
        while ($counter <= $end_timestamp) {
          $utc_datetime = gmdate('Y-m-d H:i:s', $counter);
          $local_date = merci_create_local_date_object($utc_datetime);
          $day_of_week = date_format($local_date, 'w');
          if (in_array($day_of_week, array(
            '6',
            '0',
          ))) {
            $on_weekend = TRUE;
            break;
          }
          $counter += $day;
        }
      }
      if ($on_weekend) {
        $return[] = t('%name cannot be reserved on weekends.');
      }
    }
  }
  return $return;
}

// merci_check_content_type_restrictions

/**
 * Ensures the user has 'edit own [type] content' and 'delete own [type] content'
 * permissions, otherwise they are not allowed to reserve the content type.
 *
 * @return TRUE if the user has access to reserve the content type, FALSE
 *   otherwise.
 */
function merci_check_content_type_user_permissions($type) {
  return user_access("edit own {$type} content") && user_access("delete own {$type} content");
}

/**
 * Loads MERCI rules for a content type.
 *
 * @param $content_type
 *   The type to load.
 * @return
 *   An object of rules.
 */
function merci_content_type_rules($content_type) {

  // Load default rules
  $rules = merci_load_content_type_settings($content_type);
  $rules->hours_mon = variable_get('merci_hours_mon', '');
  $rules->hours_tue = variable_get('merci_hours_tue', '');
  $rules->hours_wed = variable_get('merci_hours_wed', '');
  $rules->hours_thu = variable_get('merci_hours_thu', '');
  $rules->hours_fri = variable_get('merci_hours_fri', '');
  $rules->hours_sat = variable_get('merci_hours_sat', '');
  $rules->hours_sun = variable_get('merci_hours_sun', '');

  // Allow any other modules (e.g. merci_rro) to change the rules
  module_invoke_all('merci_rules_alter', $rules);
  return $rules;
}

// merci_content_type_rules

/**
 * Loads the settings for a single MERCI content type.
 *
 * @param $content_type
 *   The type to load.
 * @return
 *   An object of type settings.
 */
function merci_load_content_type_settings($content_type) {
  return db_fetch_object(db_query("SELECT nt.type, nt.name, m.type_setting, m.max_hours_per_reservation, m.allow_overnight, m.allow_weekends, m.late_fee_per_hour, m.rate_per_hour, m.fee_free_hours, m.status, m.spare_items, m.min_cancel_hours, m.autocheckout, m.autocheckin, m.selfcheckout FROM {node_type} nt INNER JOIN {merci_node_type} m ON nt.type = m.type WHERE nt.type = '%s'", $content_type));

  //return db_fetch_object(db_query("SELECT nt.type, nt.name, m.type_setting, m.max_hours_per_reservation, m.allow_overnight, m.allow_weekends, m.late_fee_per_hour, m.rate_per_hour, m.fee_free_hours, m.status, m.spare_items, m.min_cancel_hours, m.autocheckout, m.autocheckin, m.selfcheckout, m.grouping FROM {node_type} nt INNER JOIN {merci_node_type} m ON nt.type = m.type WHERE nt.type = '%s'", $content_type));
}

/**
 * Loads the settings for an entire MERCI type (bucket/resource).
 *
 * @param $merci_type
 *   The MERCI type: bucket|resource.
 * @return
 *   A database object containing all content types for the
 *   specified MERCI type.
 */
function merci_load_merci_type_settings($merci_type) {
  return db_query("SELECT nt.type, nt.name, m.type_setting, m.max_hours_per_reservation, m.allow_overnight, m.allow_weekends, m.late_fee_per_hour, m.rate_per_hour, m.fee_free_hours, m.status, m.spare_items, m.min_cancel_hours, m.autocheckout, m.autocheckin, m.selfcheckout FROM {node_type} nt INNER JOIN {merci_node_type} m ON nt.type = m.type WHERE m.type_setting = '%s' ORDER BY nt.name", $merci_type);
}

/**
 * Menu callback for AHAH additions.
 */
function merci_choice_js() {
  $delta = count($_POST['choice']);
  $nid = isset($_POST['nid']) ? $_POST['nid'] : 0;
  if ((int) $nid) {
    $node = node_load($nid);
  }
  else {
    $node = new stdClass();
  }
  $dates = $_POST['field_merci_date'][0];

  // If a start and end date exist, we have to massage them
  // into the proper format from user input.
  // TODO: is there a more elegant way to do this?
  if ($dates['value']['date'] && $dates['value']['time'] && $dates['value2']['date'] && $dates['value2']['time']) {
    module_load_include('inc', 'date_api', 'date_api_elements');
    $date_timezone = date_default_timezone_name();
    $date_format = 'm/d/Y g:ia';
    $start = array(
      '#value' => array(
        'date' => $dates['value']['date'],
        'time' => $dates['value']['time'],
      ),
      '#date_timezone' => $date_timezone,
      '#date_format' => $date_format,
    );
    $end = array(
      '#value' => array(
        'date' => $dates['value2']['date'],
        'time' => $dates['value2']['time'],
      ),
      '#date_timezone' => $date_timezone,
      '#date_format' => $date_format,
    );
    $form_state['values']['field_merci_date'][0]['value'] = date_popup_input_value($start);
    $form_state['values']['field_merci_date'][0]['value2'] = date_popup_input_value($end);
  }
  else {
    $form_state = array();
  }

  // Build our new form element.
  $form_element = _merci_choice_form($node, $form_state, $delta);
  drupal_alter('form', $form_element, array(), 'merci_choice_js');

  // Build the new form.
  $form_state = array(
    'submitted' => FALSE,
  );
  $form_build_id = $_POST['form_build_id'];

  // Add the new element to the stored form. Without adding the element to the
  // form, Drupal is not aware of this new elements existence and will not
  // process it. We retreive the cached form, add the element, and resave.
  if (!($form = form_get_cache($form_build_id, $form_state))) {
    exit;
  }
  $form['choice_wrapper']['choice'][$delta] = $form_element;
  form_set_cache($form_build_id, $form, $form_state);
  $form += array(
    '#post' => $_POST,
    '#programmed' => FALSE,
  );

  // Rebuild the form.
  $form = form_builder('merci_reservation_node_form', $form, $form_state);

  // Render the new output.
  $choice_form = $form['choice_wrapper']['choice'];
  unset($choice_form['#prefix'], $choice_form['#suffix']);

  // Prevent duplicate wrappers.
  $choice_form[$delta]['#attributes']['class'] = empty($choice_form[$delta]['#attributes']['class']) ? 'ahah-new-content' : $choice_form[$delta]['#attributes']['class'] . ' ahah-new-content';
  $output = theme('status_messages') . drupal_render($choice_form);
  drupal_json(array(
    'status' => TRUE,
    'data' => $output,
  ));
}

/**
 * Theme the reservation form for choices.
 */
function theme_merci_choices($form) {

  // Change the button title to reflect the behavior when using JavaScript.

  //drupal_add_js('if (Drupal.jsEnabled) { $(document).ready(function() { $("#edit-merci-more").val("'. t('Add another item') .'"); }); }', 'inline');
  $output = '';
  $output .= drupal_render($form);
  return $output;
}

/**
 * Implementation of hook_nodeapi().
 */
function merci_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  if (($cache = cache_get('merci_' . $node->type . '_data')) && !empty($cache->data)) {
    $merci_node_type = $cache->data;
  }
  else {

    // Load MERCI node type settings.
    $merci_node_type = db_fetch_array(db_query("SELECT * FROM {merci_node_type} WHERE type = '%s' AND type_setting <> 'disabled'", $node->type));

    // Do your expensive calculations here, and populate $my_data
    // with the correct stuff..
    if ($merci_node_type) {
      cache_set('merci_' . $node->type . '_data', $merci_node_type);
    }
  }

  // Process active MERCI node types and reservation nodes.
  if ($merci_node_type || $node->type == 'merci_reservation') {
    $type = $node->type == 'merci_reservation' ? 'reservation' : $merci_node_type['type_setting'];
    switch ($op) {
      case 'prepare':
      case 'load':
        if (isset($node->nid)) {
          switch ($type) {
            case 'bucket':
              $merci_node = db_fetch_array(db_query("SELECT default_availability, late_fee_per_hour, rate_per_hour, fee_free_hours, min_cancel_hours, autocheckout, autocheckin, selfcheckout, sub_type FROM {merci_bucket_node} WHERE vid = %d", $node->vid));
              break;
            case 'resource':
              $merci_node = db_fetch_array(db_query("SELECT default_availability, late_fee_per_hour, rate_per_hour, fee_free_hours, min_cancel_hours, autocheckout, autocheckin, selfcheckout, sub_type FROM {merci_resource_node} WHERE vid = %d", $node->vid));
              break;
          }
        }
        if ($merci_node_type) {
          if (isset($merci_node)) {
            $node->merci = array_merge($merci_node_type, $merci_node);
          }
          else {
            $node->merci = $merci_node_type;
          }
        }
        break;
      case 'validate':
        if ($type != 'reservation') {
          merci_validate_default_availability($node);
        }
        break;
      case 'insert':
      case 'update':
        if ($op == 'insert' || $node->revision) {
          switch ($type) {
            case 'bucket':
              db_query("INSERT INTO {merci_bucket_node} (nid, vid, default_availability, late_fee_per_hour, rate_per_hour, fee_free_hours, min_cancel_hours, autocheckout, autocheckin, selfcheckout, sub_type) VALUES (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d)", $node->nid, $node->vid, $node->merci_default_availability, $node->merci_late_fee_per_hour, $node->merci_rate_per_hour, $node->merci_fee_free_hours, $node->merci_min_cancel_hours, $node->merci_autocheckout, $node->merci_autocheckin, $node->merci_selfcheckout, $node->merci_sub_type);
              break;
            case 'resource':
              db_query("INSERT INTO {merci_resource_node} (nid, vid, default_availability, late_fee_per_hour, rate_per_hour, fee_free_hours, min_cancel_hours, autocheckout, autocheckin, selfcheckout, sub_type) VALUES (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d)", $node->nid, $node->vid, $node->merci_default_availability, $node->merci_late_fee_per_hour, $node->merci_rate_per_hour, $node->merci_fee_free_hours, $node->merci_min_cancel_hours, $node->merci_autocheckout, $node->merci_autocheckin, $node->merci_selfcheckout, $node->merci_sub_type);
              break;
          }
        }
        else {
          switch ($type) {
            case 'bucket':
              db_query("UPDATE {merci_bucket_node} SET default_availability = %d, late_fee_per_hour = %d, rate_per_hour = %d, fee_free_hours = %d, autocheckout = %d, autocheckin = %d, selfcheckout = %d WHERE vid = %d", $node->merci_default_availability, $node->merci_late_fee_per_hour, $node->merci_rate_per_hour, $node->merci_fee_free_hours, $node->merci_min_cancel_hours, $node->merci_autocheckout, $node->merci_autocheckin, $node->merci_selfcheckout, $node->vid);
              break;
            case 'resource':
              db_query("UPDATE {merci_resource_node} SET default_availability = %d, late_fee_per_hour = %d, rate_per_hour = %d, fee_free_hours = %d, autocheckout = %d, autocheckin = %d, selfcheckout = %d WHERE vid = %d", $node->merci_default_availability, $node->merci_late_fee_per_hour, $node->merci_rate_per_hour, $node->merci_fee_free_hours, $node->merci_min_cancel_hours, $node->merci_autocheckout, $node->merci_autocheckin, $node->merci_selfcheckout, $node->vid);
              break;
          }
        }
        break;
      case 'delete':

        // In the case were a reservation placeholder node is being
        // deleted, remove it from the detail table here.
        if ($type != 'reservation') {
          db_query("DELETE FROM {merci_reservation_detail} WHERE placeholder_nid = %d", $node->nid);
        }
        switch ($type) {
          case 'bucket':
            db_query("DELETE FROM {merci_bucket_node} WHERE nid = %d", $node->nid);
            break;
          case 'resource':
            db_query("DELETE FROM {merci_resource_node} WHERE nid = %d", $node->nid);
            break;
        }
        break;
      case 'delete revision':
        switch ($type) {
          case 'bucket':
            db_query("DELETE FROM {merci_bucket_node} WHERE vid = %d", $node->vid);
            break;
          case 'resource':
            db_query("DELETE FROM {merci_resource_node} WHERE vid = %d", $node->vid);
            break;
          case 'reservation':
            db_query("DELETE FROM {merci_reservation} WHERE vid = %d", $node->vid);
            db_query("DELETE FROM {merci_reservation_detail} WHERE vid = %d", $node->vid);
            break;
        }
        break;
    }
  }
}

/**
 * Validates the state change of a reservable item.
 *
 * @param $node
 *   The item node.
 */
function merci_validate_default_availability($node) {

  // Only perform the check if the item is set to an unavailable state.
  if (in_array((int) $node->merci_default_availability, array(
    MERCI_UNA_F,
    MERCI_UNA_S,
  ))) {

    // Determine CCK table and columns the date data is stored in.
    $field = content_fields('field_merci_date');
    $db_info = content_database_info($field);
    $table = $db_info['table'];
    $column_end_date = $db_info['columns']['value2']['column'];
    $time = gmdate('Y-m-d H:i:s');

    // Pull any incomplete reservations that use the item in question
    $reservations = db_query("SELECT ctn.nid, ctn.title FROM {" . $table . "} ct INNER JOIN {merci_reservation_detail} md ON ct.vid = md.vid INNER JOIN {node} ctn ON ct.vid = ctn.vid WHERE md.item_nid = %d AND {$column_end_date} >= '%s' AND NOT (md.item_status <= %d)", $node->nid, $time, MERCI_ITEM_STATUS_AVAILABLE);
    $bad_reservations = array();
    while ($reservation = db_fetch_object($reservations)) {
      $bad_reservations[] = l($reservation->title, "node/{$reservation->nid}/edit", array(
        'query' => drupal_get_destination(),
      ));
    }
    if (!empty($bad_reservations)) {
      form_set_error('merci_default_availability', t('%title can not be set to an unavailable status until it is removed from the following reservations:', array(
        '%title' => $node->title,
      )) . theme('item_list', $bad_reservations));
    }
  }
}

/**
 * Validates if an item node can be deleted.
 *
 * @param $node
 *   The item node.
 * @param $single
 *   TRUE if a single item node deletion is being processed, FALSE otherwise.
 * @return
 *   TRUE if the item can be deleted, FALSE otherwise.
 */
function merci_delete_item_validate($node, $single = TRUE) {

  // Only validate bucket/resource items.
  if ($node->type != 'merci_reservation' && $node->merci['type_setting'] != 'disabled' && $node->merci['sub_type'] == MERCI_SUB_TYPE_ITEM) {

    // Determine CCK table and columns the date data is stored in.
    $field = content_fields('field_merci_date');
    $db_info = content_database_info($field);
    $table = $db_info['table'];

    // Join on nid here so that any version of the reservation that contain
    // the item is caught.
    $reservations = db_query("SELECT ctn.nid, ctn.title FROM {" . $table . "} ct INNER JOIN {merci_reservation_detail} md ON ct.vid = md.vid INNER JOIN {node} ctn ON ct.nid = ctn.nid WHERE md.item_nid = %d ORDER BY ct.nid, ct.vid", $node->nid);
    $bad_reservations = array();
    while ($reservation = db_fetch_object($reservations)) {

      // Key by nid to prevent duplicate revisions from appearing.
      $bad_reservations[$reservation->nid] = l($reservation->title, "node/{$reservation->nid}/edit", array(
        'query' => drupal_get_destination(),
      ));
    }
    if (!empty($bad_reservations)) {
      drupal_set_message(t('%title can not be deleted, because it is associated with the following reservations:', array(
        '%title' => $node->title,
      )) . theme('item_list', $bad_reservations), 'error');

      // Lock out single deletion attempts here.
      if ($single) {
        module_invoke_all('exit');
        drupal_access_denied();
      }
      else {
        return FALSE;
      }
    }
  }
  return TRUE;
}

/**
 * Validates saving of MERCI node types.
 */
function merci_node_type_save_validate($form, &$form_state) {
  $values = $form_state['values'];

  // Only validate node types set to an inactive status.
  if ($values['merci_type_setting'] != 'disabled' && (int) $values['merci_status'] == MERCI_STATUS_INACTIVE) {

    // Determine CCK table and columns the date data is stored in.
    $field = content_fields('field_merci_date');
    $db_info = content_database_info($field);
    $table = $db_info['table'];
    $column_end_date = $db_info['columns']['value2']['column'];
    $time = gmdate('Y-m-d H:i:s');
    $type_setting = $values['merci_type_setting'];

    // Pull all active reservations that use the node type.
    $reservations = db_query("SELECT ctn.nid, ctn.title FROM {" . $table . "} ct INNER JOIN {merci_reservation_detail} md ON ct.vid = md.vid INNER JOIN {node} ctn ON ct.vid = ctn.vid INNER JOIN {merci_{$type_setting}_node} m ON md.placeholder_nid = m.nid INNER JOIN {node} mn ON m.vid = mn.vid  WHERE mn.type = '%s' AND m.sub_type = %d AND {$column_end_date} >= '%s' AND NOT (md.item_status <= %d)", $values['old_type'], MERCI_SUB_TYPE_RESERVATION, $time, MERCI_ITEM_STATUS_CHECKED_IN);
    $bad_reservations = array();
    while ($reservation = db_fetch_object($reservations)) {
      $bad_reservations[] = l($reservation->title, "node/{$reservation->nid}/edit", array(
        'query' => drupal_get_destination(),
      ));
    }
    if (!empty($bad_reservations)) {
      form_set_error('merci_status', t('@type_setting can not be set to an inactive status until all @type_setting items are removed from the following reservations:', array(
        '@type_setting' => $type_setting,
      )) . theme('item_list', $bad_reservations));
    }
  }
}

/**
 * Validates deletion of node types.
 *
 * @param $type
 *   The type being deleted.
 */
function merci_delete_node_type_validate($type) {
  $settings = merci_load_content_type_settings($type);

  // Only validate active MERCI node types.
  if ($settings->type_setting != 'disabled') {

    // Determine CCK table and columns the date data is stored in.
    $field = content_fields('field_merci_date');
    $db_info = content_database_info($field);
    $table = $db_info['table'];

    // Join on nid here so that any version of the reservation that contain
    // the bucket/resource is caught.
    $reservations = db_query("SELECT ctn.nid, ctn.title FROM {" . $table . "} ct INNER JOIN {merci_reservation_detail} md ON ct.vid = md.vid INNER JOIN {node} ctn ON ct.nid = ctn.nid INNER JOIN {merci_{$settings->type_setting}_node} m ON md.placeholder_nid = m.nid INNER JOIN {node} mn ON m.vid = mn.vid  WHERE mn.type = '%s' AND m.sub_type = %d ORDER BY ct.nid, ct.vid", $settings->type, MERCI_SUB_TYPE_RESERVATION);
    $bad_reservations = array();
    while ($reservation = db_fetch_object($reservations)) {
      $bad_reservations[$reservation->nid] = l($reservation->title, "node/{$reservation->nid}/edit", array(
        'query' => drupal_get_destination(),
      ));
    }
    if (!empty($bad_reservations)) {
      drupal_set_message(t('@type can not be deleted because it is associated with the following reservations:', array(
        '@type' => $settings->name,
      )) . theme('item_list', $bad_reservations), 'error');
      module_invoke_all('exit');
      drupal_access_denied();
    }
  }
}

/**
 * Implementation of hook_theme().
 */
function merci_theme() {
  return array(
    'merci_choices' => array(
      'arguments' => array(
        'form' => NULL,
      ),
    ),
    'merci_build_reservation_table_form' => array(
      'arguments' => array(
        'form' => NULL,
      ),
    ),
    'merci_reservation_table' => array(
      'template' => 'merci_reservation_table',
      'path' => drupal_get_path('module', 'merci') . '/templates',
      'arguments' => array(
        'reservations' => NULL,
        'count' => NULL,
        'hours' => NULL,
        'title' => NULL,
      ),
    ),
  );
}

/**
 * Return the name of a type code.
 *
 * @param string|int $code
 *  if int, will return translated name of the code.
 *  if NULL, returns array of codes as keys, and translated strings as value
 * @return string|int
 */
function merci_item_status($code = NULL) {
  $statuses = array(
    MERCI_AVA_F => t('Available'),
    MERCI_UNA_F => t('Unavailable'),
    MERCI_AVA_S => t('Strictly Available'),
    MERCI_UNA_S => t('No Longer in Inventory'),
  );
  if (isset($code)) {
    return $statuses[$code];
  }
  else {
    return $statuses;
  }
}

/**
 * Return the name of a status code.
 *
 * @param string|int $code
 *  if int, will return translated name of the code.
 *  if NULL, returns array of codes as keys, and translated strings as value
 * @return string|int
 */
function merci_record_status($code = NULL) {
  $types = array(
    MERCI_STATUS_UNCONFIRMED => t('Unconfirmed'),
    MERCI_STATUS_PENDING => t('Pending'),
    MERCI_STATUS_CHECKED_OUT => t('Checked out'),
    MERCI_STATUS_CHECKED_IN => t('Checked in'),
    MERCI_STATUS_CANCELLED => t('Cancelled'),
    MERCI_STATUS_DENIED => t('Denied'),
    MERCI_STATUS_DENIED => t('No Show'),
  );
  if (isset($code)) {
    return $types[$code];
  }
  else {
    return $types;
  }
}

/**
 * Submit handler for saving MERCI node type data.
 */
function merci_node_type_save_submit($form, &$form_state) {
  $merci = $form_state['values'];

  // This hack is necessary because the node type form submit
  // automatically saves all remaining form items to {variable}
  // We're doing custom storage, so remove these.
  $variables = array(
    'merci_type_setting_',
    'merci_max_hours_per_reservation_',
    'merci_allow_overnight_',
    'merci_allow_weekends_',
    'merci_late_fee_per_hour_',
    'merci_rate_per_hour_',
    'merci_fee_free_hours_',
    'merci_status_',
    'merci_spare_items_',
    'merci_min_cancel_hours_',
    'merci_autocheckout_',
    'merci_autocheckin_',
    'merci_selfcheckout_',
    'merci_grouping_',
  );
  foreach ($variables as $variable) {
    variable_del($variable . $merci['type']);
  }
  if ($form_state['clicked_button']['#value'] == t('Save content type')) {
    db_query("UPDATE {merci_node_type} SET type_setting = '%s', max_hours_per_reservation = %d, allow_overnight = %d, allow_weekends = %d, late_fee_per_hour = %f, rate_per_hour = %f, fee_free_hours = %d, status = %d, spare_items = %d, min_cancel_hours = %d, autocheckout = %d, autocheckin = %d, selfcheckout = %d, grouping = %d WHERE type = '%s'", $merci['merci_type_setting'], $merci['merci_max_hours_per_reservation'], $merci['merci_allow_overnight'], $merci['merci_allow_weekends'], $merci['merci_late_fee_per_hour'], $merci['merci_rate_per_hour'], $merci['merci_fee_free_hours'], $merci['merci_status'], $merci['merci_spare_items'], $merci['merci_min_cancel_hours'], $merci['merci_autocheckout'], $merci['merci_autocheckin'], $merci['merci_selfcheckout'], $merci['merci_grouping'], $merci['type']);
    if (!db_affected_rows()) {
      db_query("INSERT INTO {merci_node_type} (type, type_setting, max_hours_per_reservation, allow_overnight, allow_weekends, late_fee_per_hour, rate_per_hour, fee_free_hours, status, spare_items, min_cancel_hours, autocheckout, autocheckin, selfcheckout, grouping) VALUES ('%s', '%s', %d, %d, %d, %d, %f, %f, %d, %d, %d, %d, %d, %d, %d)", $merci['type'], $merci['merci_type_setting'], $merci['merci_max_hours_per_reservation'], $merci['merci_allow_overnight'], $merci['merci_allow_weekends'], $merci['merci_late_fee_per_hour'], $merci['merci_rate_per_hour'], $merci['merci_fee_free_hours'], $merci['merci_status'], $merci['merci_spare_items'], $merci['merci_min_cancel_hours'], $merci['merci_autocheckout'], $merci['merci_autocheckin'], $merci['merci_selfcheckout'], $merci['merci_grouping']);
    }
  }
  cache_clear_all('merci', 'cache', TRUE);
}

/**
 * Validation for numeric textfields.
 */
function merci_is_numeric_validate($form) {
  if ($form['#value'] && !is_numeric($form['#value'])) {
    form_set_error($form['#name'], t('%title must be a number.', array(
      '%title' => $form['#title'],
    )));
  }
}

/**
 * Implementation of hook_insert().
 */
function merci_insert($node) {
  if ($node->type == 'merci_reservation') {
    db_query("INSERT INTO {merci_reservation} (nid, vid, status) VALUES (%d, %d, %d)", $node->nid, $node->vid, $node->merci_status);
    merci_add_reservation_items($node);
  }
}

/**
 * Implementation of hook_update().
 */
function merci_update($node) {
  if ($node->type == 'merci_reservation') {
    if ($node->revision) {
      db_query("INSERT INTO {merci_reservation} (nid, vid, status) VALUES (%d, %d, %d)", $node->nid, $node->vid, $node->merci_status);
    }
    else {
      db_query("UPDATE {merci_reservation} SET status = %d WHERE vid = %d", $node->merci_status, $node->vid);
    }
    merci_add_reservation_items($node);
  }
}

/**
 * Adds items to reservation on creation/update.
 *
 * @param $node
 *   The reservation node.
 */
function merci_add_reservation_items($node) {

  // Update existing items.
  if (isset($node->existing_items['items'])) {
    foreach ($node->existing_items['items'] as $did => $item_nid) {

      // Only selected items get their status bumped to reserved.
      if ($item_nid) {
        $item_status = MERCI_ITEM_STATUS_RESERVED;
      }
      else {
        $item_status = MERCI_ITEM_STATUS_AVAILABLE;
      }
      if ($node->revision) {
        db_query("INSERT INTO {merci_reservation_detail} (nid, vid, placeholder_nid, item_nid, item_status) VALUES (%d, %d, %d, %d, %d)", $node->nid, $node->vid, $node->existing_items['placeholders'][$did], $item_nid, $item_status);
      }
      else {
        db_query("UPDATE {merci_reservation_detail} SET item_nid = %d, item_status = %d WHERE did = %d", $item_nid, $item_status, $did);
      }
    }
  }

  // New items.
  foreach ($node->choice as $num => $choice) {

    // Resource.
    if (is_numeric($choice['item'])) {
      $item = db_fetch_object(db_query("SELECT type, title FROM {node} WHERE nid = %d", $choice['item']));
      $type = $item->type;
      $title = $item->title;
      $item_nid = $choice['item'];
      $item_node = node_load($item_nid);
      $item_status = MERCI_ITEM_STATUS_RESERVED;
    }
    elseif ($choice['item']) {
      $type = $choice['item'];
      $title = db_result(db_query("SELECT name FROM {node_type} WHERE type = '%s'", $type));
      $item_nid = 0;
      $item_node = FALSE;
      $item_status = MERCI_ITEM_STATUS_AVAILABLE;
    }
    else {

      // Nothing selected -- move along...
      continue;
    }

    // Build the item's placeholder node.
    $reservation = new stdClass();
    $reservation->type = $type;
    $reservation->name = $node->name;
    $reservation->uid = $node->uid;
    $reservation->title = "{$title} " . t('(Reservation)');
    $reservation->body = '';
    $reservation->status = 0;
    $reservation->promote = 0;
    $reservation->sticky = 0;

    // MERCI specific data.
    $merci_settings = merci_load_content_type_settings($type);
    $reservation->merci_default_availability = MERCI_AVA_F;
    $reservation->merci_sub_type = MERCI_SUB_TYPE_RESERVATION;

    // Use the item specific accounting data if an item is assigned,
    // otherwise fall back to the content type defaults.
    $reservation->merci_late_fee_per_hour = $item_node ? $item_node->merci['late_fee_per_hour'] : $merci_settings->late_fee_per_hour;
    $reservation->merci_rate_per_hour = $item_node ? $item_node->merci['rate_per_hour'] : $merci_settings->rate_per_hour;
    $reservation->merci_fee_free_hours = $item_node ? $item_node->merci['fee_free_hours'] : $merci_settings->fee_free_hours;
    $reservation->merci_min_cancel_hours = $item_node ? $item_node->merci['min_cancel_hours'] : $merci_settings->min_cancel_hours;
    $reservation->merci_autocheckout = $item_node ? $item_node->merci['autocheckout'] : $merci_settings->autocheckout;
    $reservation->merci_autocheckin = $item_node ? $item_node->merci['autocheckin'] : $merci_settings->autocheckin;
    $reservation->merci_selfcheckout = $item_node ? $item_node->merci['selfcheckout'] : $merci_settings->selfcheckout;
    $reservation = node_submit($reservation);
    node_save($reservation);
    db_query("INSERT INTO {merci_reservation_detail} (nid, vid, placeholder_nid, item_nid, item_status) VALUES (%d, %d, %d, %d, %d)", $node->nid, $node->vid, $reservation->nid, $item_nid, $item_status);
  }

  // Update the state of all items with associations.
  switch ((int) $node->merci_status) {
    case MERCI_STATUS_UNCONFIRMED:
    case MERCI_STATUS_PENDING:
      $item_status = MERCI_ITEM_STATUS_RESERVED;
      break;
    case MERCI_STATUS_CHECKED_OUT:
      $item_status = MERCI_ITEM_STATUS_CHECKED_OUT;
      break;
    case MERCI_STATUS_CHECKED_IN:
      $item_status = MERCI_ITEM_STATUS_CHECKED_IN;
      break;
    case MERCI_STATUS_CANCELLED:
    case MERCI_STATUS_DENIED:
      $item_status = MERCI_ITEM_STATUS_AVAILABLE;
      break;
  }
  db_query("UPDATE {merci_reservation_detail} SET item_status = %d WHERE vid = %d AND item_nid <> 0", $item_status, $node->vid);
}

/**
 * Implementation of hook_delete().
 */
function merci_delete($node) {
  if ($node->type == 'merci_reservation') {

    // Delete all reservation placeholder nodes for the reservation.
    $placeholders = db_query("SELECT DISTINCT(placeholder_nid) AS nid FROM {merci_reservation_detail} WHERE nid = %d", $node->nid);
    while ($placeholder = db_fetch_object($placeholders)) {
      node_delete($placeholder->nid);
    }
    db_query("DELETE FROM {merci_reservation} WHERE nid = %d", $node->nid);
    db_query("DELETE FROM {merci_reservation_detail} WHERE nid = %d", $node->nid);
  }
}

/**
 * Implementation of hook_view().
 */
function merci_view($node, $teaser = FALSE, $page = FALSE) {

  // TODO: should we fix node previews?
  if ($node->type == 'merci_reservation' && !isset($node->preview)) {
    $node->content['merci_status'] = array(
      '#value' => drupal_get_form('merci_display_reservation_status', merci_record_status($node->merci['status'])),
      '#weight' => 0,
    );
    if ($page) {
      $reservation_table = drupal_get_form('merci_build_reservation_table_form', $node);
      $node = node_prepare($node, $teaser);
      $node->content['reservation_items'] = array(
        '#value' => $reservation_table,
        '#weight' => 1,
      );
    }
  }
  return $node;
}

/**
 * Builds the form item for the status display.
 *
 * @param $form_state
 *   Current form state.
 * @param $status
 *   Current status
 * @return
 *   The form array.
 */
function merci_display_reservation_status(&$form_state, $status) {
  $form['merci_status'] = array(
    '#type' => 'item',
    '#title' => t('Status'),
    '#value' => $status,
  );
  return $form;
}

/**
 * Builds the table of existing reserved items.
 *
 * @param $form_state
 *   Current form state.
 * @param $node
 *   The reservation node.
 * @param $edit_page
 *   TRUE if the table is on the edit page for the reservation, FALSE otherwise.
 * @return
 *   The form array.
 */
function merci_build_reservation_table_form(&$form_state, $node, $edit_page = FALSE) {
  $form = array();
  $form['#theme'] = 'merci_build_reservation_table_form';
  $form['#node'] = $node;
  $form['#tree'] = TRUE;
  $form['#table'] = array();
  $form['#header'] = array(
    t('Item'),
    t('Type'),
    t('Operations'),
  );
  $reservation_items = array();
  $items = $node->merci['reservation_items'];
  foreach ($items as $did => $item) {

    // Use item title, fall back to bucket/resource content type name.
    $title = isset($item->ttitle) ? $item->ttitle : $item->name;
    $nid = isset($item->tnid) ? $item->tnid : $item->pnid;
    $operations = '';
    $placeholder_node = node_load($item->pnid);
    if (node_access('update', $placeholder_node)) {
      if (merci_has_accessories($item->type)) {
        $operations .= '<span class="edit-details" id="merci-id-' . $item->pnid . '">' . l(t('Add accessories'), "node/{$item->pnid}/edit", array(
          'query' => drupal_get_destination(),
        )) . '</span>';
      }
    }
    if ($edit_page && node_access('delete', $placeholder_node)) {
      $operations .= ' &nbsp;&nbsp;' . l(t('delete'), "node/{$item->pnid}/delete", array(
        'query' => drupal_get_destination(),
      ));
    }
    $merci_settings = merci_load_content_type_settings($item->type);

    // Only MERCI admins can change the bucket item assignment.
    if ($edit_page && user_access('administer MERCI') && $merci_settings->type_setting == 'bucket') {
      $options = array(
        0 => t('<Select>'),
      );
      $default = isset($item->tnid) ? $item->tnid : 0;
      $options += merci_get_available_bucket_items($node, $item->type);
      $form['items'][$did] = array(
        '#type' => 'select',
        '#options' => $options,
        '#default_value' => $default,
      );
    }
    else {
      $form['items'][$did] = array(
        '#type' => 'value',
        '#value' => $item->tnid,
      );
      $form['#table'][$did]['display_item'] = l($title, "node/{$item->pnid}");
    }
    $form['placeholders'][$did] = array(
      '#type' => 'value',
      '#value' => $item->pnid,
    );
    $bucket_resource = $merci_settings->type_setting == 'bucket' ? $item->type : $item->tnid;
    $form['bucket_resource'][$did] = array(
      '#type' => 'value',
      '#value' => $bucket_resource,
    );
    $form['#table'][$did]['type'] = check_plain($item->name);
    $form['#table'][$did]['ops'] = $operations;
  }
  return $form;
}

/**
 * Pulls items available to assign to a bucket for a reservation.
 *
 * @param $node
 *   The reservation node.
 * @param $bucket_type
 *   The bucket type.
 * @return
 *   An array of available items, in select options format.
 */
function merci_get_available_bucket_items($node, $bucket_type) {
  $date_info = $node->field_merci_date[0];
  $start = $date_info['value'];
  $end = $date_info['value2'];
  $options = merci_get_reservable_items('bucket', $bucket_type, $start, $end, $node->nid);
  return $options;
}

/**
 * Pulls an array of items that are reservable for the content type and date range.
 *
 * @param $merci_type
 *   The MERCI type.  bucket|resource
 * @param $content_type
 *   The content type name of the bucket/resource.
 * @param $start
 *   Start time in DATETIME format UTC timezone.
 * @param $end
 *   End time in DATETIME format UTC timezone.
 * @param $reservation_nid
 *   (Optional) A reservation nid to exclude from the reserved items.
 * @return
 *   An array of reservable items, in select option format.
 */
function merci_get_reservable_items($merci_type, $content_type, $start, $end, $reservation_nid = NULL) {

  // Determine CCK table and columns the date data is stored in.
  $field = content_fields('field_merci_date');
  $db_info = content_database_info($field);
  $table = $db_info['table'];
  $column_start_date = $db_info['columns']['value']['column'];
  $column_end_date = $db_info['columns']['value2']['column'];
  $args = array(
    MERCI_AVA_F,
    MERCI_AVA_S,
    $content_type,
    MERCI_SUB_TYPE_ITEM,
    $start,
    $end,
    $start,
    $end,
    $start,
    $end,
    MERCI_ITEM_STATUS_AVAILABLE,
  );

  // If there's an already selected bucket item, then we need to make sure we
  // include it in the list of available items.
  $where = '';
  $inner_where = '';
  if ($reservation_nid) {
    $inner_where = ' AND md2.nid <> %d';
    $args[] = $reservation_nid;
  }

  // Pull reservable items.  This query takes the following into consideration:
  //   1. Pulls all all item nodes of the content type that are in an available state,
  //   2. Excludes all item nodes that have associated reservations in the date range
  //      of the this reservation where the item is in an already reserved state.
  //   3. Allows a reservation to be excluded from the exclusions if necessary (this
  //      is usually used to allow an already assigned item to not conflict with itself.
  $items = db_query("SELECT n.nid, n.title FROM {node} n INNER JOIN {merci_{$merci_type}_node} m ON n.vid = m.vid WHERE (m.default_availability IN (%d, %d) AND n.type = '%s' AND m.sub_type = %d AND n.nid NOT IN (SELECT md2.item_nid FROM {" . $table . "} ct INNER JOIN {merci_reservation_detail} md2 ON ct.vid = md2.vid INNER JOIN {merci_{$merci_type}_node} m2 ON md2.item_nid = m2.nid INNER JOIN {node} ctn ON ctn.vid = ct.vid INNER JOIN {node} m2n ON m2.vid = m2n.vid WHERE (({$column_start_date} >= '%s' AND {$column_start_date} <= '%s') OR ({$column_end_date} >= '%s' AND {$column_end_date} <= '%s') OR ({$column_start_date} <= '%s' AND {$column_end_date} >= '%s')) AND NOT (md2.item_status <= %d){$inner_where})) ORDER BY n.title", $args);
  $options = array();
  while ($item = db_fetch_object($items)) {
    $options[$item->nid] = $item->title;
  }
  return $options;
}

// merci_get_reservable_items

/**
 * Calculates the total number of available bucket items for a reservation.
 *
 * @param $content_type
 *   The bucket content type.
 * @param $start
 *   Start time in DATETIME format UTC timezone.
 * @param $end
 *   End time in DATETIME format UTC timezone.
 * @param $reservation_nid
 *   (Optional) A reservation nid to exclude from the reserved items.
 * @return
 *   The number of available bucket items.
 */
function merci_get_available_bucket_count($content_type, $start, $end, $reservation = NULL) {

  // Determine CCK table and columns the date data is stored in.
  $field = content_fields('field_merci_date');
  $db_info = content_database_info($field);
  $table = $db_info['table'];
  $column_start_date = $db_info['columns']['value']['column'];
  $column_end_date = $db_info['columns']['value2']['column'];

  // Pull all active items for this bucket.
  $total_items = (int) db_result(db_query("SELECT COUNT(n.nid) FROM {node} n INNER JOIN {merci_bucket_node} m ON n.vid = m.vid WHERE n.type = '%s' AND m.sub_type = %d AND m.default_availability IN (%d, %d)", $content_type, MERCI_SUB_TYPE_ITEM, MERCI_AVA_F, MERCI_AVA_S));
  $args = array(
    $start,
    $end,
    $start,
    $end,
    $start,
    $end,
    $content_type,
    MERCI_ITEM_STATUS_CHECKED_IN,
  );

  // If we're checking an existing reservation, exclude it from the
  // reserved items.
  if (isset($reservation)) {
    $where = ' AND ct.nid <> %d';
    $args[] = $reservation;
  }
  else {
    $where = '';
  }

  // Pull reserved bucket items for the period of the reservation.
  $reserved_items = (int) db_result(db_query("SELECT COUNT(md.nid) FROM {" . $table . "} ct INNER JOIN {merci_reservation_detail} md ON ct.vid = md.vid INNER JOIN {merci_bucket_node} m ON md.placeholder_nid = m.nid INNER JOIN {node} ctn ON ct.vid = ctn.vid INNER JOIN {node} mn ON m.vid = mn.vid WHERE (({$column_start_date} >= '%s' AND {$column_start_date} <= '%s') OR ({$column_end_date} >= '%s' AND {$column_end_date} <= '%s') OR ({$column_start_date} <= '%s' AND {$column_end_date} >= '%s')) AND mn.type = '%s' AND NOT (md.item_status <= %d){$where}", $args));
  return $total_items - $reserved_items;
}

/**
 * Builds the reserved items table.
 */
function theme_merci_build_reservation_table_form($form) {
  $output = '';
  $header = $form['#header'];
  $node = $form['#node'];
  $rows = array();
  foreach ($form['#table'] as $did => $columns) {
    $item = drupal_render($form['items'][$did]);

    // The content type name.
    if (isset($columns['display_item'])) {
      $item .= $columns['display_item'];
    }
    $rows[] = array(
      $item,
      $form['#table'][$did]['type'],
      $form['#table'][$did]['ops'],
    );
  }
  if (!empty($rows)) {
    $table_caption = in_array((int) $node->merci['status'], array(
      MERCI_STATUS_CHECKED_OUT,
      MERCI_STATUS_CHECKED_IN,
    )) ? t('Checked out items') : t('Currently reserved items');
    $output .= '<div class="existing-items-table-header">' . $table_caption . '</div>';
    $output .= theme('table', $header, $rows);
    if (user_access('administer MERCI')) {
      $output .= '<div><a href="/merci/contract/' . $node->nid . '">Printable Contract</a></div>';
    }
  }
  return $output;
}

/**
 * Implementation of hook_load().
 */
function merci_load($node) {
  if ($node->type == 'merci_reservation') {
    $return = new stdClass();
    $return->merci = db_fetch_array(db_query("SELECT status FROM {merci_reservation} WHERE vid = %d", $node->vid));
    $reservation_items = array();

    // Pull both the general placeholder node and the item nid so we
    // can use whichever we need.
    $items = db_query("SELECT m.did, m.item_status, pn.nid AS pnid, pn.title AS ptitle, tn.nid AS tnid, tn.title AS ttitle, nt.type, nt.name FROM {merci_reservation_detail} m INNER JOIN {node} pn ON m.placeholder_nid = pn.nid INNER JOIN {node_type} nt ON pn.type = nt.type LEFT JOIN {node} tn ON m.item_nid = tn.nid WHERE m.vid = %d", $node->vid);
    while ($item = db_fetch_object($items)) {
      $reservation_items[$item->did] = $item;
    }
    $return->merci['reservation_items'] = $reservation_items;
    return $return;
  }
}

/**
 * Implementation of hook_access().
 */
function merci_access($op, $node, $account) {
  global $user;
  $type = isset($node->type) ? $node->type : $node;
  $uid = isset($node->uid) ? $node->uid : FALSE;
  if ($type == 'merci_reservation') {

    // MERCI admins and users working with their own reservations have all access.
    if (user_access('administer MERCI')) {
      return TRUE;
    }
    elseif (user_access('create reservations')) {
      if ($uid === FALSE || $uid == $account->uid) {
        return TRUE;
      }
    }
    return FALSE;
  }
}

/**
 * Implementation of hook_validate().
 */
function merci_node_validate($form, &$form_state) {
  if (user_access('suspend MERCI access') && !user_access('administer MERCI')) {
    form_set_error('merci_status', t('Your access to make new Reservations or edit existing Reservations has been suspended.'));
  }
  else {
    $node = (object) $form_state['values'];

    // No validation necessary on deletion.
    if ($form_state['clicked_button']['#id'] != 'edit-delete') {

      // Reservations with a checked out status.
      if ($node->merci_status == MERCI_STATUS_CHECKED_OUT) {

        // Make sure all existing bucket reservations have an item assigned.
        if (isset($node->existing_items['items'])) {
          foreach ($node->existing_items['items'] as $did => $item_nid) {
            if (!$item_nid) {
              form_set_error("existing_items][items][{$did}", t("The bucket reservation must have an item associated with it for finalized reservations."));
            }
          }
        }
        else {
          form_set_error('merci_status', t('You can not finalize a reservation that has no reserved items.'));
        }

        // Can't add a bucket item and finalize at the same time.
        foreach ($node->choice as $num => $choice) {
          $item = $choice['item'];
          if ($item && !is_numeric($item)) {
            form_set_error("choice][{$num}][item", t("You cannot finalize a reservation while adding a bucket item."));
          }
        }
      }

      // Build date objects we'll need for our different validations.
      $start = $node->field_merci_date[0]['value'];
      $end = $node->field_merci_date[0]['value2'];
      $start_object = merci_create_local_date_object($start);
      $end_object = merci_create_local_date_object($end);
      $hours_of_operation = merci_load_hours_of_operation();
      $start_day_of_week = (int) date_format($start_object, 'w');
      $end_day_of_week = (int) date_format($end_object, 'w');
      $start_month_day = date_format($start_object, 'm-d');
      $end_month_day = date_format($end_object, 'm-d');
      $start_hours = $hours_of_operation[$start_day_of_week];
      $end_hours = $hours_of_operation[$end_day_of_week];
      $start_date = date_format($start_object, 'm-d-Y');
      $max_days = variable_get("merci_max_days_advance_reservation", '0');

      //Users in role with Administer MERCI permssion are exempt from content type and hours of operation restrictions
      if (user_access('administer MERCI') || user_access('create reservations outside hours of operation')) {
        drupal_set_message(t('You may be making a Reservation outside the normal hours of operation.  This may impact access to the items you are reserving.'));
      }
      else {

        // Reservation start date cannot exceed the max advance
        if ($max_days) {
          $max_date = new DateTime("+{$max_days} day");
          if ($start_object > $max_date) {
            form_set_error('merci_status', t('You cannot make a Reservation more than %days days in advance. Start the Reservation before %date.', array(
              '%days' => $max_days,
              '%date' => date_format($max_date, 'm-d-Y'),
            )));
          }
        }

        // Can't start or end a reservation on days that are
        // closed dates.
        if (in_array($start_month_day, $hours_of_operation['closed_days'])) {
          $name = date_format($start_object, 'F jS');
          form_set_error('field_merci_date][0][value][date', t('Sorry, but we are closed on %day for a holiday or special event.', array(
            '%day' => $name,
          )));
        }
        if (in_array($end_month_day, $hours_of_operation['closed_days'])) {
          $name = date_format($end_object, 'F jS');
          form_set_error('field_merci_date][0][value2][date', t('Sorry, but we are closed on %day for a holiday or special event.', array(
            '%day' => $name,
          )));
        }

        // Can't start or end a reservation on a day the facility
        // has no hours of operation, or outside hours of operation.
        $start_name = date_format($start_object, 'l');
        if (!$hours_of_operation[$start_day_of_week]) {
          form_set_error('field_merci_date][0][value][date', t('Reservations cannot start on a %day.', array(
            '%day' => $start_name,
          )));
        }
        else {
          $start_time = date_format($start_object, 'H:i');
          if ($start_time < $start_hours['open']) {
            form_set_error('field_merci_date][0][value][time', t('Reservations cannot start on a %day before %start.', array(
              '%day' => $start_name,
              '%start' => merci_format_time($start_hours['open']),
            )));
          }
          elseif ($start_time > $start_hours['close']) {
            form_set_error('field_merci_date][0][value][time', t('Reservations cannot start on a %day after %end.', array(
              '%day' => $start_name,
              '%end' => merci_format_time($start_hours['close']),
            )));
          }
        }
        $end_name = date_format($end_object, 'l');
        if (!$hours_of_operation[$end_day_of_week]) {
          form_set_error('field_merci_date][0][value2][date', t('Reservations cannot end on a %day.', array(
            '%day' => $end_name,
          )));
        }
        else {
          $end_time = date_format($end_object, 'H:i');
          if ($end_time < $end_hours['open']) {
            form_set_error('field_merci_date][0][value2][time', t('Reservations cannot end on a %day before %start.', array(
              '%day' => $end_name,
              '%start' => merci_format_time($end_hours['open']),
            )));
          }
          elseif ($end_time > $end_hours['close']) {
            form_set_error('field_merci_date][0][value2][time', t('Reservations cannot end on a %day after %end.', array(
              '%day' => $end_name,
              '%end' => merci_format_time($end_hours['close']),
            )));
          }
        }
      }

      // Tests for existing items.
      if (isset($node->nid)) {

        // For saved reservations, include the items already reserved
        // in the available list.
        $options = merci_build_reservable_items($node, $form_state, $node->nid);
      }
      else {
        $options = merci_build_reservable_items($node, $form_state);
      }
      $flat_options = $options['flat_options'];
      if (isset($node->existing_items)) {

        // Check each reserved item.
        foreach ($node->existing_items['bucket_resource'] as $did => $value) {

          // The item is no longer reservable, so figure out why.
          if (!in_array($value, $flat_options)) {

            // Resource.
            if (is_numeric($value)) {
              $new_item = db_fetch_object(db_query("SELECT title, type FROM {node} WHERE nid = %d", $value));
              $title = $new_item->title;
              $type = $new_item->type;
            }
            elseif ($value) {
              $title = db_result(db_query("SELECT name FROM {node_type} WHERE type = '%s'", $value));
              $type = $value;
            }

            // Make sure the item still passes content type restrictions.
            $restrictions = merci_check_content_type_restrictions($type, $start, $end);
            if (!empty($restrictions)) {
              $message = '';
              foreach ($restrictions as $restriction) {
                $message .= '<div>' . strtr($restriction, array(
                  '%name' => theme('placeholder', $title),
                )) . '</div>';
              }
            }
            else {
              $message = t("The existing reservation for %name is no longer reservable with your current date settings.", array(
                '%name' => $title,
              ));
            }
            form_set_error("existing_items][placeholders][{$did}", $message);
          }
          elseif (!is_numeric($value)) {
            $bucket_items = array_keys(merci_get_available_bucket_items($node, $value));
            $assigned_item = (int) $node->existing_items['items'][$did];
            if ($assigned_item && !in_array($assigned_item, $bucket_items)) {
              $title_name = db_fetch_object(db_query("SELECT n.title, nt.name FROM {node} n INNER JOIN {node_type} nt ON n.type = nt.type WHERE n.nid = %d", $assigned_item));
              form_set_error("existing_items][placeholders][{$did}", t("The assignment of %item for the %bucket reservation is no longer reservable with your current date settings.", array(
                '%item' => $title_name->title,
                '%bucket' => $title_name->name,
              )));
            }
          }
        }
      }

      // Tests for new items.
      if (isset($node->nid)) {

        // Only need to rebuild this again for existing nodes.
        $options = merci_build_reservable_items($node, $form_state);
        $flat_options = $options['flat_options'];
      }

      // Check each new item.
      foreach ($node->choice as $num => $choice) {

        // The item is no longer reservable, so figure out why.
        if ($choice['item'] && !in_array($choice['item'], $flat_options)) {

          // Resource.
          if (is_numeric($choice['item'])) {
            $new_item = db_fetch_object(db_query("SELECT title, type FROM {node} WHERE nid = %d", $choice['item']));
            $title = $new_item->title;
            $type = $new_item->type;
          }
          elseif ($choice['item']) {
            $title = db_result(db_query("SELECT name FROM {node_type} WHERE type = '%s'", $choice['item']));
            $type = $choice['item'];
          }

          // Make sure the item still passes content type restrictions.
          $restrictions = merci_check_content_type_restrictions($type, $start, $end);
          if (!empty($restrictions)) {
            $message = '';
            foreach ($restrictions as $restriction) {
              $message .= '<div>' . strtr($restriction, array(
                '%name' => theme('placeholder', $title),
              )) . '</div>';
            }
          }
          else {
            $count_sql = "SELECT COUNT(n.nid) \n             FROM {node} n \n             JOIN {merci_node_type} t\n             ON t.type = n.type\n             WHERE n.status = 1 AND\n             (\n               (\n                 t.type_setting = 'bucket'\n                 AND n.type = '%s'\n               ) OR\n               (\n                 t.type_setting = 'resource'\n                 AND n.title = '%s'\n               )\n             )";
            $count = db_result(db_query($count_sql, $type, $title));
            $start_mysql = date('Y-m-d', strtotime($start));
            $end_mysql = date('Y-m-d', strtotime($end . ' +1 day'));
            $reservations = merci_load_reservations_for_type_in_timespan($type, $start_mysql, $end_mysql);
            $reservations_by_date = array();
            $hours = merci_load_hours_of_operation();
            $message = '<div> ' . t("The dates and times for %name conflict with one or more existing reservations", array(
              '%name' => $title,
            )) . '</div>';
            $message .= '<div class="merci-availability-key"><span class="available"></span> = available <span class="unavailable"></span> = unavailable</div>';
            foreach ($reservations as $date => $times) {
              $date_timestamp = strtotime($date);
              $hours_date = $hours[date('w', $date_timestamp)];
              if (user_access('administer MERCI') || user_access('create reservations outside hours of operation')) {
                $adminhours = explode('-', variable_get('merci_hours_admin', '07:00-23:00'));
                $hours_date['open'] = $adminhours[0];
                $hours_date['close'] = $adminhours[1];
              }
              if (isset($hours_date['open'])) {
                $message .= '<table class="merci-availability-schedule"><thead><tr>';
                $message .= '<th>' . date('m/d/Y', $date_timestamp) . '</th>';
                $time = $hours_date['open'];
                while ($time < $hours_date['close']) {
                  $message .= '<th colspan="4">' . date('g:i a', strtotime($time)) . '</th>';
                  $time = date('H:i', strtotime($time . ' +1 hour'));
                }
                $message .= '</tr></thead><tbody>';
                for ($i = 1; $i <= $count; $i++) {
                  $message .= '<tr><th>' . htmlspecialchars($title);
                  if ($count > 1) {
                    $message .= ' ' . $i . '/' . $count;
                  }
                  $message .= '</th>';
                  $time = $hours_date['open'];
                  while ($time < $hours_date['close']) {
                    if ($times[$time . ':00'] >= $i) {
                      $message .= '<td class="unavailable"></td>';
                    }
                    else {
                      $message .= '<td class="available"></td>';
                    }
                    $time = date('H:i', strtotime($time . ' +15 minutes'));
                  }

                  // while
                  $message .= '</tr>';
                }

                // for
                $message .= '</tbody></table>';
              }

              // if
            }

            // foreach
          }
          form_set_error("choice][{$num}][item", $message);
        }
      }

      //if message wasn't set by a validation function
      if (!$message) {
        drupal_set_message(t('There are no conflicts with this Reservation.'));
      }

      // Prevent status changes on reservations that have past.
      $current_status = db_result(db_query("SELECT m.status FROM {node} n INNER JOIN {merci_reservation} m ON n.vid = m.vid WHERE n.nid = %d", $node->nid));
      if ($current_status && $current_status != $node->merci_status && time() > strtotime($node->field_merci_date[0]['value2']) && !in_array((int) $node->merci_status, array(
        MERCI_STATUS_CANCELLED,
        MERCI_STATUS_CHECKED_IN,
        MERCI_STATUS_DENIED,
      ))) {
        $statuses = merci_record_status();
        form_set_error('merci_status', t('You cannot change the status to %status for a reservation that has past.', array(
          '%status' => $statuses[$node->merci_status],
        )));
      }
    }
  }
}

/**
 * Builds an array representing reservations for a given bucket within a given timespan
 *
 * @return
 *   An associative array with keys as times (in MySQL datetime format) and values as number of reservations.
 */
function merci_load_reservations_for_type_in_timespan($type, $start_date, $end_date) {
  $timezone_offset = variable_get('date_default_timezone', 0);
  $reservation_counts = array();
  $datetime = strtotime($start_date . ' 00:00:00');
  while (date('Y-m-d', $datetime) < $end_date) {
    $date = date('Y-m-d', $datetime);
    $time = date('H:i:s', $datetime);
    if (!isset($reservation_counts[$date])) {
      $reservation_counts[$date] = array();
    }
    $reservation_counts[$date][$time] = 0;
    $datetime = strtotime($date . ' ' . $time . ' +15 minutes');
  }

  // while
  // ! Get reservation times from database
  $reservation_times = array();
  $sql = "SELECT r.field_merci_date_value AS start, r.field_merci_date_value2 AS end\n  FROM {merci_reservation_detail} d\n  JOIN {node} n\n    ON n.nid = d.placeholder_nid\n  JOIN {content_type_merci_reservation} r\n    ON r.vid = d.vid\n  WHERE n.type = '%s'\n  AND r.field_merci_date_value < '%s'\n  AND r.field_merci_date_value2 >= '%s'";
  $times = db_query($sql, $type, $end_date, $start_date);
  while ($reservation_time = db_fetch_object($times)) {
    $reservation_times[] = $reservation_time;
  }

  // ! Update reservation count
  foreach ($reservation_times as $reservation_time) {
    $datetime = $reservation_time->start;
    while ($datetime < $reservation_time->end) {
      list($date, $time) = explode(' ', $datetime);
      $time = date('H:i:s', strtotime($time) + $timezone_offset);
      if (isset($reservation_counts[$date][$time])) {
        $reservation_counts[$date][$time]++;
      }
      $datetime = date('Y-m-d H:i:s', strtotime($datetime . ' +15 minutes'));
    }

    // while
  }

  // foreach
  return $reservation_counts;
}

// merci_load_reservations_for_type_in_timespan

/**
 * Builds an array representing the hours of operation for the facility.
 *
 * @return
 *   An associative array with the following key/value pairs:
 *     [php_day_of_week_number_as_in_date_function] => An associative
 *       array with the following key/values pairs:
 *         'open'  => Opening time (military).
 *         'close' => Closing time (military).
 *     'closed_days' => An array of closed dates in mm-dd format.
 */
function merci_load_hours_of_operation($content_type = '') {
  $days_of_the_week = array(
    'sun',
    'mon',
    'tue',
    'wed',
    'thu',
    'fri',
    'sat',
  );
  if (!empty($content_type)) {
    $rules = merci_content_type_rules($content_type);
  }
  $hours_of_operation = array();
  foreach ($days_of_the_week as $num => $day) {
    $hours = variable_get("merci_hours_{$day}", '');
    if (drupal_strlen($hours) == 11) {
      $parts = explode('-', $hours);
      if (count($parts == 2)) {
        $hours_of_operation[$num] = array(
          'open' => $parts[0],
          'close' => $parts[1],
        );
      }
      else {
        $hours_of_operation[$num] = FALSE;
      }
    }
    else {
      $hours_of_operation[$num] = FALSE;
    }
  }
  $closed_days_raw = variable_get('merci_closed_dates', '');
  $hours_of_operation['closed_days'] = array();
  $parts = explode("\n", $closed_days_raw);
  foreach ($parts as $date) {
    $date = trim($date);
    if (drupal_strlen($date) == 5) {
      $hours_of_operation['closed_days'][] = $date;
    }
  }
  return $hours_of_operation;
}
function merci_hours_str_to_array($str) {
  if (drupal_strlen($str) == 11) {
    $parts = explode('-', $str);
    if (count($parts) == 2) {
      return array(
        'open' => $parts[0],
        'close' => $parts[1],
      );
    }
  }
  return FALSE;
}

// merci_hours_str_to_array

/**
 * Creates a date object based on the site's local timezone.
 *
 * @param $datetime
 *   A date in DATETIME format, UTC timezone.
 * @return
 *   A php date object in the site's timezone.
 */
function merci_create_local_date_object($datetime) {
  $date_object = date_create($datetime, timezone_open('UTC'));
  date_timezone_set($date_object, timezone_open(date_default_timezone_name()));
  return $date_object;
}

/**
 * Custom validation function to protect merci nodes from mass deletion.
 */
function merci_node_admin_delete_validate($form, &$form_state) {

  // Look only for delete op.
  $operation = $form_state['values']['operation'];
  if ($operation != 'delete') {
    return;
  }

  // Get the checked nodes.
  $nids = array_filter($form_state['values']['nodes']);

  // Perform the check for each submitted node.
  foreach ($nids as $nid) {
    $node = node_load($nid);

    // Check to see if any of the nodes should not be deleted.
    if (!merci_delete_item_validate($node, FALSE)) {

      // If so, then unset the checked node so it will not be processed, and display a warning.
      // Note that the array element has to be completely removed here in order to prevent the
      // node from being deleted, due to the nature of the mass deletion callback.
      unset($form_state['values']['nodes'][$nid]);
      unset($nids[$nid]);
    }
  }

  // If we've unset all of the nodes that were checked, then don't continue with the form processing.
  if (!count($nids)) {
    drupal_set_message('No nodes selected.', 'error');
    drupal_goto('admin/content/node');
  }
}

/**
 * Implementation of hook_simpletest().
 */
function merci_simpletest() {
  $dir = drupal_get_path('module', 'merci') . '/tests';
  $tests = file_scan_directory($dir, '\\.test$');
  return array_keys($tests);
}

/**
 * Implementation of hook_views_api().
 */
function merci_views_api() {
  return array(
    'api' => 2,
    'path' => drupal_get_path('module', 'merci'),
  );
}

/**
 * Implementation of hook_views_handlers().
 */
function merci_views_handlers() {
  return array(
    'info' => array(
      'path' => drupal_get_path('module', 'merci') . '/handlers',
    ),
    'handlers' => array(
      'merci_handler_field_merci_node_type_type_setting' => array(
        'parent' => 'views_handler_field',
        'file' => 'merci_handler_field.inc',
      ),
      'merci_handler_filter_merci_node_type_type_setting' => array(
        'parent' => 'views_handler_filter_in_operator',
        'file' => 'merci_handler_filter_in_operator.inc',
      ),
      'merci_handler_field_merci_node_type_status' => array(
        'parent' => 'views_handler_field',
        'file' => 'merci_handler_field.inc',
      ),
      'merci_handler_filter_merci_node_type_status' => array(
        'parent' => 'views_handler_filter_in_operator',
        'file' => 'merci_handler_filter_in_operator.inc',
      ),
      'merci_handler_field_merci_reservation_status' => array(
        'parent' => 'views_handler_field',
        'file' => 'merci_handler_field.inc',
      ),
      'merci_handler_filter_merci_reservation_status' => array(
        'parent' => 'views_handler_filter_in_operator',
        'file' => 'merci_handler_filter_in_operator.inc',
      ),
      'merci_handler_field_merci_bucket_resource_node_default_availability' => array(
        'parent' => 'views_handler_field',
        'file' => 'merci_handler_field.inc',
      ),
      'merci_handler_filter_merci_bucket_resource_node_default_availability' => array(
        'parent' => 'views_handler_filter_in_operator',
        'file' => 'merci_handler_filter_in_operator.inc',
      ),
      'merci_handler_field_merci_bucket_resource_node_sub_type' => array(
        'parent' => 'views_handler_field',
        'file' => 'merci_handler_field.inc',
      ),
      'merci_handler_filter_merci_bucket_resource_node_sub_type' => array(
        'parent' => 'views_handler_filter_in_operator',
        'file' => 'merci_handler_filter_in_operator.inc',
      ),
    ),
  );
}

/**
 * Check for existing items in a bucket.
 *
 * @param $type
 *   The bucket node type.
 * @param $status
 *   TRUE to restrict to published items, FALSE otherwise.
 * @return
 *   TRUE if any items exist, FALSE otherwise.
 */
function merci_check_existing_bucket_items($type, $status = FALSE) {
  $where = $status ? ' AND status = 1' : '';
  $existing_items = db_result(db_query("SELECT nid FROM {node} WHERE type = '%s'{$where}", $type));
  return $existing_items;
}

/**
 * Returns totals for reporting.
 *
 * @param $type
 *   The bucket or resrouce node type.
 * @param $startdate
 *   TRUE to restrict to published items, FALSE otherwise.
 * @return
 *   Total reservation number for that type betweent the start and end dates
 */
function merci_reservation_totals($type, $startdate, $enddate) {
  $result = db_query("SELECT COUNT(nid) as total  FROM {node} WHERE type LIKE '%s' and status = 0 AND created > %d AND created < %d", $type, $startdate, $enddate);
  $reservationnode = db_fetch_object($result);
  return $reservationnode->total;
}

/**
 * Sort by vid
 *
 * @param $a
 *   The first object.
 * @param $b
 *   The second object
 * @return
 *   0,1, or -1 indicating which object has a higher VID
 */
function merci_by_vid() {
  if ($a->vid == $b->vid) {
    return 0;
  }
  return $a->vid > $b->vid ? -1 : 1;
}

// merci_by_vid

/**
 * Get taxonomy data as JSON for a node
 *
 * @param $node_id
 *   The node ID.
 * @return
 *   JSON string of taxonomy data
 */
function merci_taxonomy_json($node_id) {
  $node = node_load($node_id);
  $output = array();
  if (node_access('update', $node)) {

    // Current user has access to update this node
    $vocabularies = taxonomy_get_vocabularies($node->type);
    if (isset($_POST['taxonomy'])) {

      // Changes to taxonomy sent
      $required_sent = TRUE;

      // Make sure all required vocabularies were sent
      foreach ($vocabularies as $vocabulary) {
        if ($vocabulary->required && !isset($_POST['taxonomy'][$vocabulary->vid])) {
          $required_sent = FALSE;
        }

        // if
      }

      // foreach
      if ($required_sent) {

        // Save new node revision, clear taxonomy, and re-add sent term
        node_save($node);
        taxonomy_node_delete_revision($node);
        foreach ($_POST['taxonomy'] as $vocabulary => $terms) {
          foreach ($terms as $id => $term_id) {
            db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $term_id);
          }

          // foreach
        }

        // foreach
        $output['status'] = 'success';
        $output['node'] = $node_id;
      }
      else {

        // Missing required vocabulary
        $output['status'] = 'failed';
        $output['error'] = 'Required fields not selected.';

        //DIFF??
      }

      // else
    }
    else {

      // Get last author admin status and node ID
      $user = user_load($node->revision_uid);
      $output['node'] = $node_id;
      $output['vocabularies'] = array();
      $output['admin'] = user_access('administer MERCI', $user);
      usort($vocabularies, 'merci_by_vid');

      // Get the select HTML and previously selected options for each vocabulary
      foreach ($vocabularies as $vocabulary) {
        $select = taxonomy_form($vocabulary->vid);
        $select['#name'] = 'taxonomy[' . $vocabulary->vid . ']';
        $select['#parents'] = array();
        $select_html = theme_select($select);
        $vocabulary_output = array(
          'select' => $select_html,
          'selected' => array(),
        );
        foreach ($node->taxonomy as $tid => $term) {
          $vocabulary_output['selected'][] = $tid;
        }
        $output['vocabularies'][] = $vocabulary_output;
      }

      // foreach
    }

    // else
  }
  else {

    // No permission
    $output['error'] = 'Permission denied.';
  }

  // else
  drupal_json($output);
}

// merci_taxonomy_json
function merci_printable_contract($node_id) {
  global $base_path;
  $node = node_load($node_id);
  $user = user_load($node->uid);
  $username = $user->name;
  $email = $user->mail;
  if (module_exists('civicrm')) {
    civicrm_initialize(TRUE);
    global $civicrm_root;
    include_once $civicrm_root . '/api/UFGroup.php';
    $userID = crm_uf_get_match_id($user->uid);
    $cg = array(
      'contact_id' => $userID,
    );
    include_once $civicrm_root . '/api/v2/Contact.php';
    $ob = civicrm_contact_get($cg);

    //print '<pre>';

    //print_r($ob);

    //print '</pre>';
    $username = $ob[$userID]['display_name'];

    //print $username;
    $phone = $ob[$userID]['phone'];
  }
  $items = $node->merci['reservation_items'];

  // We want these timestamps generated in UTC.
  $old_timezone = date_default_timezone_get();
  date_default_timezone_set('UTC');
  $starthour = strtotime($node->field_merci_date[0]['value']);
  $endhour = strtotime($node->field_merci_date[0]['value2']);
  date_default_timezone_set($old_timezone);
  $hours = round(($endhour - $starthour) / 3600, 2);
  $logo = theme_get_setting('logo_path', '');
  ?>
  <html>
    <head>
      <title>Contract</title>
      <link type="text/css" rel="stylesheet" href="/<?php

  echo drupal_get_path('module', 'merci');
  ?>/contract.css" />
    </head>
    <body>
      <div id="page">
        <div id="header">
        <?php

  if ($logo) {
    ?>
           <img src="<?php

    print $base_path;
    print $logo;
    ?>">
        <?php

  }
  ?>
        <h2><?php

  print variable_get('site_name', '');
  ?> Equipment Rental Contract</h2>
        <?php

  if (module_exists('token')) {
    print token_replace(variable_get('merci_contract_header', ''), 'node', $node);
  }
  else {
    print variable_get('merci_contract_header');
  }
  ?>
        Start: <?php

  print date("F j, Y, g:i a", $starthour) . '<br />';
  ?>
        Returned by: <?php

  print date("F j, Y, g:i a", $endhour) . '<br />';
  ?>
        Name: <?php

  print $username;
  ?><br />
        Email: <?php

  print $email;
  ?><br />
        Phone: <?php

  print $phone;
  ?><br />

        </div>
        <table id="cost">
          <thead>
            <tr>
              <th>Item</th>
              <th>Commercial Cost</th>
              <th>Member Cost</th>
            </tr>
          </thead>
          <tbody>
          <?php

  $discount = variable_get('merci_membership_discount', 1);
  $commercial_cost_total = 0;
  $member_cost_total = 0;
  $even_odd = 'even';
  foreach ($items as $item) {
    $item_node = node_load($item->pnid);
    $type = merci_load_content_type_settings($item->type);
    $fee_hours = $hours - $type->fee_free_hours;
    $commercial_cost = $type->rate_per_hour * $hours;
    $member_cost = $fee_hours > 0 ? $type->rate_per_hour * $discount * $fee_hours : 0;
    $commercial_cost_total += $commercial_cost;
    $member_cost_total += $member_cost;
    if ($item->ttitle) {
      $ttitle = htmlspecialchars($item->ttitle);
    }
    else {
      $ttitle = '<b>SPECIFIC ITEM NOT SELECTED FROM BUCKET</b>';
    }
    ?>
            <tr class="<?php

    print $even_odd;
    ?>">
              <td>
                <div><?php

    print $ttitle;
    ?></div>
                <?php

    if (count($item_node->taxonomy) > 0) {
      ?>
                  <ul class="accessories">
                  <?php

      foreach ($item_node->taxonomy as $accessory) {
        ?>
                    <li><?php

        print $accessory->name;
        ?></li>
                    <?php

      }

      // foreach
      ?>
                  </ul>
                  <?php

    }

    // if
    ?>
              </td>
              <td>$<?php

    echo $commercial_cost;
    ?></td>
              <td>$<?php

    echo $member_cost;
    ?></td>
            </tr>
            <?php

    $even_odd = $even_odd == 'even' ? 'odd' : 'even';
  }

  // foreach
  ?>
          </tbody>
          <tfoot>
            <tr class="<?php

  echo $even_odd;
  ?>">
              <th>Total</th>
              <td>$<?php

  echo $commercial_cost_total;
  ?></td>
              <td>$<?php

  echo $member_cost_total;
  ?></td>
            </tr>
          <tfoot>
        </table>
        <div id="boilerplate"><?php

  if (module_exists('token')) {
    echo token_replace(variable_get('merci_contract_boilerplate', ''), 'node', $node);
  }
  else {
    echo variable_get('merci_contract_boilerplate');
  }
  ?></div>
        <div id="footer"><?php

  if (module_exists('token')) {
    echo token_replace(variable_get('merci_contract_footer', ''), 'node', $node);
  }
  else {
    echo variable_get('merci_contract_footer');
  }
  ?></div>
      </div>
    </body>
  <?php

}

// merci_printable_contract
// Changes the reservation from UNCONFIRMED to CONFIRMED.
// Normally accessed from email sent to user
function merci_confirm_reservation($node_id) {
  print 'nothing here yet';
}

// merci_confirm_reservation

/**
 * Implementation of hook_cron().
 */
function merci_cron() {

  //print('merci cron running<br />');

  //find all autocheckouts that have started and set their stauts to checked out

  //only change status for unconfirmed and pending reservations
  $old_timezone = date_default_timezone_get();
  date_default_timezone_set('UTC');
  $time = date('Y-m-j H:m:s');

  //2009-05-22 20:45:00
  date_default_timezone_set($old_timezone);

  //print('localized time: ' . $time . '<br />');

  //print('set to checked out <br />');

  // Reservation status options.

  //define('MERCI_STATUS_UNCONFIRMED', 1);

  //define('MERCI_STATUS_PENDING', 2);

  //define('MERCI_STATUS_CHECKED_OUT', 3);

  //define('MERCI_STATUS_CHECKED_IN', 4);

  //define('MERCI_STATUS_CANCELLED', 5);

  //define('MERCI_STATUS_DENIED', 6);

  //define('MERCI_STATUS_NO_SHOW', 7);
  $reservations = db_query("SELECT ctmr.nid FROM {content_type_merci_reservation} ctmr JOIN {merci_reservation_detail} mrd ON ctmr.nid = mrd.nid JOIN {merci_reservation} mr ON ctmr.nid = mr.nid WHERE mr.status < 3 AND field_merci_date_value < '%s'", $time);
  while ($reservation = db_fetch_object($reservations)) {

    //print($reservation->nid . '<br />');
  }

  //find all autocheckins that have ended and set their stauts to checkedin

  //only change status for checked reservations

  //print('set to checked in <br />');
  $reservations = db_query("SELECT * FROM {content_type_merci_reservation} WHERE field_merci_date_value2 > '%s'", $time);
  while ($reservation = db_fetch_object($reservations)) {

    //print($reservation->nid . '<br />');
  }

  //find all unconfirmed and pending reservations that have started and set their stauts to no show
}
function merci_has_accessories($content_type) {
  return db_fetch_object(db_query("SELECT * FROM {vocabulary_node_types} WHERE type = '%s'", $content_type));
}

/**
* Implementation of hook_token_list().
*
*/
function merci_token_list($type = 'all') {
  if ($type == 'node' || $type == 'all') {

    //$tokens['node']['merci_resources'] = t('Reserved resource');
    $tokens['node']['merci_commercial_cost'] = t('Commercial cost');
    $tokens['node']['merci_member_cost'] = t('Member cost');
    return $tokens;
  }
}

/**
* Implementation of hook_token_values().
* @see {merci_token_list}
*/
function merci_token_values($type, $object = NULL, $options = array()) {
  switch ($type) {
    case 'node':
      $node = merci_load($object);
      if ($node) {
        $values['merci_resources'] = '';
        $values['merci_commercial_cost'] = 0;
        $values['merci_member_cost'] = 0;
        $discount = variable_get('merci_membership_discount', 1);

        // We want these timestamps generated in UTC.
        $old_timezone = date_default_timezone_get();
        date_default_timezone_set('UTC');
        $starthour = strtotime($node->field_merci_date[0]['value']);
        $endhour = strtotime($node->field_merci_date[0]['value2']);
        date_default_timezone_set($old_timezone);
        $hours = round(($endhour - $starthour) / 3600, 2);
        $titles = array();
        foreach ($node->merci['reservation_items'] as $item) {
          $item_node = node_load($item->pnid);
          $type = merci_load_content_type_settings($item->type);
          $fee_hours = $hours - $type->fee_free_hours;
          $values['merci_commercial_cost'] += $type->rate_per_hour * $hours;
          $values['merci_member_cost'] += $fee_hours > 0 ? $type->rate_per_hour * $discount * $fee_hours : 0;
          if ($item->ttitle != '') {
            $titles[] = $item->ttitle;
          }
          else {
            $titles[] = $item->ptitle;
          }
        }
        $values['merci_resources'] = check_plain(implode(", ", $titles));
        return $values;
        break;
      }
  }
}

/**
* Calculates the short hour/minute time format based on the site settings.
*/
function merci_time_format() {
  static $time_only_format = NULL;
  if (empty($time_only_format)) {
    $short_date_format = variable_get('date_format_short', 'm/d/Y - H:i');
    $time_only_format = date_limit_format($short_date_format, array(
      'hour',
      'minute',
    ));
  }
  return $time_only_format;
}

/**
 * Formats a time value into the site's preferred format.
 * 
 * @param object $hours_minutes
 *   A string of the form 'H:MM' or 'HH:MM'
 * @return
 *   A string in 12- or 24-hour format with no leading zero.
 */
function merci_format_time($hours_minutes) {
  $return = date(merci_time_format(), strtotime($hours_minutes));
  if ($return[0] == '0') {
    return substr($return, 1);
  }
  return $return;
}

Functions

Namesort descending Description
merci_access Implementation of hook_access().
merci_add_reservation_items Adds items to reservation on creation/update.
merci_admin_settings Builds the MERCI admininstration settings form.
merci_build_reservable_items Builds the list of all currently reservable items, filtered by date.
merci_build_reservation_table_form Builds the table of existing reserved items.
merci_by_vid Sort by vid
merci_check_content_type_restrictions
merci_check_content_type_user_permissions Ensures the user has 'edit own [type] content' and 'delete own [type] content' permissions, otherwise they are not allowed to reserve the content type.
merci_check_existing_bucket_items Check for existing items in a bucket.
merci_choice_js Menu callback for AHAH additions.
merci_confirm_reservation
merci_content_type_rules Loads MERCI rules for a content type.
merci_create_local_date_object Creates a date object based on the site's local timezone.
merci_cron Implementation of hook_cron().
merci_date_filter Submit handler to date filter items on a reservation form. It makes changes to the form state and the entire form is rebuilt during the page reload.
merci_delete Implementation of hook_delete().
merci_delete_item_validate Validates if an item node can be deleted.
merci_delete_node_type_validate Validates deletion of node types.
merci_display_reservation_status Builds the form item for the status display.
merci_form Implementation of hook_form().
merci_format_time Formats a time value into the site's preferred format.
merci_form_alter Implementation of hook_form_alter().
merci_get_available_bucket_count Calculates the total number of available bucket items for a reservation.
merci_get_available_bucket_items Pulls items available to assign to a bucket for a reservation.
merci_get_reservable_items Pulls an array of items that are reservable for the content type and date range.
merci_has_accessories
merci_hours_str_to_array
merci_init Implementation of hook_init().
merci_insert Implementation of hook_insert().
merci_is_numeric_validate Validation for numeric textfields.
merci_item_status Return the name of a type code.
merci_load Implementation of hook_load().
merci_load_content_type_settings Loads the settings for a single MERCI content type.
merci_load_hours_of_operation Builds an array representing the hours of operation for the facility.
merci_load_merci_type_settings Loads the settings for an entire MERCI type (bucket/resource).
merci_load_reservations_for_type_in_timespan Builds an array representing reservations for a given bucket within a given timespan
merci_menu Implementation of hook_menu().
merci_more_choices_submit Submit handler to add more choices to a reservation form. This handler is used when javascript is not available. It makes changes to the form state and the entire form is rebuilt during the page reload.
merci_nodeapi Implementation of hook_nodeapi().
merci_node_admin_delete_validate Custom validation function to protect merci nodes from mass deletion.
merci_node_info Implementation of hook_node_info().
merci_node_type Implementation of hook_node_info().
merci_node_type_save_submit Submit handler for saving MERCI node type data.
merci_node_type_save_validate Validates saving of MERCI node types.
merci_node_validate Implementation of hook_validate().
merci_perm Implementation of hook_perm().
merci_printable_contract
merci_record_status Return the name of a status code.
merci_reservation_totals Returns totals for reporting.
merci_simpletest Implementation of hook_simpletest().
merci_taxonomy_json Get taxonomy data as JSON for a node
merci_theme Implementation of hook_theme().
merci_time_format Calculates the short hour/minute time format based on the site settings.
merci_token_list Implementation of hook_token_list().
merci_token_values Implementation of hook_token_values().
merci_update Implementation of hook_update().
merci_validate_default_availability Validates the state change of a reservable item.
merci_view Implementation of hook_view().
merci_views_api Implementation of hook_views_api().
merci_views_handlers Implementation of hook_views_handlers().
theme_merci_build_reservation_table_form Builds the reserved items table.
theme_merci_choices Theme the reservation form for choices.
_merci_choice_form Builds an individual item selector.

Constants