You are here

signup.module in Signup 5.2

Same filename and directory in other branches
  1. 5 signup.module
  2. 6.2 signup.module
  3. 6 signup.module
  4. 7 signup.module

The Signup module (http://drupal.org/project/signup) manages replies to nodes. In particular, it's good for event management. Signup supports sending reminder emails and automatically closing signups for nodes with a start time, via the Event module (http://drupal.org/project/event) or with a CCK date field (http://drupal.org/project/date). Signup provides extensive Views integration (http://drupal.org/project/views). For more information, see the README.txt and INSTALL.txt files in this directory.

File

signup.module
View source
<?php

/**
 * @file
 * The Signup module (http://drupal.org/project/signup) manages replies to
 * nodes. In particular, it's good for event management.  Signup supports
 * sending reminder emails and automatically closing signups for nodes with
 * a start time, via the Event module (http://drupal.org/project/event) or
 * with a CCK date field (http://drupal.org/project/date).  Signup provides
 * extensive Views integration (http://drupal.org/project/views).  For more
 * information, see the README.txt and INSTALL.txt files in this directory.
 */

/**
 * @defgroup signup_core Core drupal hooks
 */

/**
 * Implementation of hook_cron().
 *
 * There are two cron-based tasks that can be performed by the signup module:
 * sending reminder emails to nodes that will begin soon, and auto-closing
 * signup on nodes that have already started (depending on settings). Each of
 * these tasks is rather complicated and depends on the specific date-based
 * backend module that's currently installed (if any), so each one is handled
 * in a separate helper function.
 *
 * @ingroup signup_core
 * @see _signup_cron_send_reminders()
 * @see _signup_cron_autoclose()
 */
function signup_cron() {
  _signup_initialize_scheduler_backend();
  _signup_cron_send_reminders();
  _signup_cron_autoclose();
}

/**
 * Helper function that sends cron-based reminder e-mails.
 *
 * Invokes the method for the installed event/date backend module to get the
 * right query fragments, and builds a query to find all nodes that need a
 * reminder email. For each one, it loops over the users signed up for that
 * node and send off the emails.
 *
 * @see signup_cron()
 * @see signup_reminder_sql()
 * @see _signup_build_query()
 */
function _signup_cron_send_reminders() {
  $type_reminder_sql = array();
  foreach (signup_content_types() as $type) {
    $type_sql = signup_reminder_sql($type);
    if (!empty($type_sql)) {
      $type_reminder_sql[$type] = $type_sql;
    }
  }
  if (empty($type_reminder_sql)) {

    // No node types support reminder emails, so bail out now.
    return;
  }
  $reminder_common_sql = array(
    'primary' => '{node} n',
    'fields' => array(
      'n.title',
      'n.nid',
      'n.type',
      's.reminder_email',
      's.forwarding_email',
    ),
    'where' => array(
      's.send_reminder = 1',
      "n.type = '%s'",
    ),
    'joins' => array(
      'INNER JOIN {signup} s ON s.nid = n.nid',
    ),
  );
  $from = variable_get('site_mail', ini_get('sendmail_from'));
  foreach ($type_reminder_sql as $type => $reminder_sql) {
    $sql = _signup_build_query($reminder_common_sql, $reminder_sql);
    $result = db_query($sql, $type);

    // Grab each node, construct the email header and subject, and query
    // the signup log to pull all users who are signed up for this node.
    while ($node = db_fetch_object($result)) {
      $subject = t('!node_type reminder: !title', array(
        '!node_type' => node_get_types('name', $type),
        '!title' => $node->title,
      ));
      $signups = db_query("SELECT u.name, u.mail, s_l.anon_mail, s_l.form_data FROM {signup_log} s_l INNER JOIN {users} u ON u.uid = s_l.uid WHERE s_l.nid = %d", $node->nid);

      // Loop through the users, composing their customized message
      // and sending the email.
      while ($signup = db_fetch_object($signups)) {

        // Get the hard-coded tokens provided by the signup module to use
        // for the reminder email.  This also gives us the email to send to.
        $signup_tokens = _signup_get_email_tokens($node, $signup);
        $user_mail = $signup_tokens['%user_mail'];
        $message = strtr($node->reminder_email, $signup_tokens);
        if (module_exists('token')) {
          $message = token_replace($message, 'node', node_load($node->nid));
        }
        drupal_mail('signup_reminder_mail', $user_mail, $subject, $message, $from);
        watchdog('signup', t('Reminder for %title sent to %user_mail.', array(
          '%title' => $node->title,
          '%user_mail' => $user_mail,
        )), WATCHDOG_NOTICE, l(t('view'), 'node/' . $node->nid));
      }

      // Reminders for this node are all sent, so mark it in the
      // database so they're not sent again.
      db_query("UPDATE {signup} SET send_reminder = 0 WHERE nid = %d", $node->nid);
    }
  }
}

/**
 * Helper function that handles auto-closing time-based nodes during cron.
 *
 * Loops over all the node types that are signup-enabled.  For each one, it
 * invokes the method for the installed event/date backend module to get the
 * right query fragments, and builds a query to find all nodes of that type
 * where signups should be closed (e.g. events that already started, etc).
 *
 * @see signup_cron()
 * @see signup_autoclose_sql()
 * @see _signup_build_query()
 */
function _signup_cron_autoclose() {
  $type_autoclose_sql = array();
  foreach (signup_content_types() as $type) {
    $type_sql = signup_autoclose_sql($type);
    if (!empty($type_sql)) {
      $type_autoclose_sql[$type] = $type_sql;
    }
  }
  if (empty($type_autoclose_sql)) {

    // No node types support auto-close, so bail out now.
    return;
  }
  $autoclose_common_sql = array(
    'primary' => '{node} n',
    'fields' => array(
      'n.nid',
      'n.type',
    ),
    'where' => array(
      's.status = 1',
      "n.type = '%s'",
    ),
    'joins' => array(
      'INNER JOIN {signup} s ON s.nid = n.nid',
    ),
  );
  foreach ($type_autoclose_sql as $type => $autoclose_sql) {
    $sql = _signup_build_query($autoclose_common_sql, $autoclose_sql);
    $result = db_query($sql, $type);

    // Loop through the results, calling the signup closing function.
    while ($signup = db_fetch_object($result)) {
      signup_close_signup($signup->nid, $cron = 'yes');
      $node = node_load($signup->nid);
      foreach (module_implements('signup_close') as $module) {
        $function = $module . '_signup_close';
        $function($node);
      }
      watchdog('signup', t('Signups closed for %title by cron.', array(
        '%title' => $node->title,
      )), WATCHDOG_NOTICE, l(t('view'), 'node/' . $node->nid));
    }
  }
}

/**
 * Private query builder helper function.
 *
 * @param $common_sql
 *   Nested array of shared query fragments that are common to all date-based
 *   backends. The supported keys are:
 *   'primary': the query's primary table and its alias (required).
 *   'fields': array of fields to SELECT.
 *   'joins': array of JOIN statements for other tables.
 *   'where': array of WHERE clauses.
 *   'group_by': array of GROUP BY fields.
 *
 * @param $backend_sql
 *   Similar nested array provided by the date-based backend include file,
 *   except that 'primary' is not allowed.
 *
 * @return
 *   Complete SQL statement based on the given query fragments.
 */
function _signup_build_query($common_sql, $type_sql) {

  // Combine type-specific sql with common_sql.
  $full_sql = array_merge_recursive($common_sql, $type_sql);
  $sql = 'SELECT ' . implode(', ', $full_sql['fields']);
  $sql .= ' FROM ' . $common_sql['primary'] . ' ';
  if (!empty($full_sql['joins'])) {
    $sql .= implode(' ', $full_sql['joins']);
  }
  if (!empty($full_sql['where'])) {
    $sql .= ' WHERE (' . implode(') AND (', $full_sql['where']) . ')';
  }
  if (!empty($full_sql['group_by'])) {
    $sql .= ' GROUP BY ' . implode(', ', $full_sql['group_by']);
  }
  return $sql;
}

/**
 * Implementation of hook_help().
 *
 * @ingroup signup_core
 */
function signup_help($section) {
  switch ($section) {
    case 'admin/help#signup':
      return '<p>' . t('Signup allows users to sign up (in other words, register) for content of any type. The most common use is for events, where users indicate they are planning to attend. This module includes options for sending a notification email to a selected email address upon a new user signup (good for notifying event coordinators, etc.) and a confirmation email to users who sign up. Each of these options are controlled per node. When used on event nodes (with <a href="@event_url">event.module</a> installed) or nodes that have a date field (with <a href="@date_url">date.module</a>) and regular cron runs, it can also send out reminder emails to all signups a configurable number of days before the start of the event (also controlled per node) and to automatically close signups 1 hour before their start (general setting). Settings exist for resticting signups to selected roles and content types.', array(
        '@event_url' => url('http://drupal.org/project/event'),
        '@date_url' => url('http://drupal.org/project/date'),
      )) . '</p><p>' . t('To use signup, you must enable which content types should allow signups in administer->settings->content types, and you must also grant the %sign_up_for_content permission to any user role that should be able to sign up in administer->access control. Each signup-enabled node will now have a place for users to sign up.', array(
        '%sign_up_for_content' => 'sign up for content',
      )) . '</p><p>' . t('There are two ways for a user to have administrative access to the signup information about a given node: either the user has the %administer_signups_for_own_content permission and they are viewing a node they created, or the user has the global %administer_all_signups permission. Administrative access allows a user to view all the users who have signed up for the node, along with whatever information they included when they signed up.  Signup administrators can also cancel other user\'s signups for the node, and can close signups on the node entirely (meaning no one else is allowed to sign up).', array(
        '%administer_signups_for_own_content' => 'administer signups for own content',
        '%administer all signups' => 'administer all signups',
      )) . '</p><p>' . t('Default settings for notification email address, reminder emails and confirmation emails are located in administer->settings->signup. These will be the default values used for a signup node unless otherwise specified (to configure these options per node, visit \'edit\' for that node and make the adjustments in the \'Sign up settings\' section).') . '</p><p>' . t('Signups can be manually closed for any node at the %signup_overview page, or on the \'signups\' tab on each node.', array(
        '%signup_overview' => t('Signup overview'),
      )) . '</p><p>' . t('The user signup form is fully themable -- form fields may be added or deleted. For more details see the instructions in signup.theme, where a sample user form is included.') . '</p>';
  }

  // If we're still here, consider the URL for help on various menu tabs.
  if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == 'signup-broadcast') {
    $node = node_load(arg(1));
    return '<p>' . t('This page allows you to send an email message to every user who signed up for this %node_type.', array(
      '%node_type' => node_get_types('name', $node->type),
    )) . '</p>';
  }

  // See if we need to add our extra checking and validation while configuring
  // CCK node types for signup.  We only want to do this if the $section
  // doesn't contain 'help#' since hook_help() is invoked twice on admin
  // pages.
  if (arg(0) == 'admin' && arg(1) == 'content' && arg(2) == 'types' && !preg_match('/.*help#.*/', $section) && function_exists('signup_date_check_node_types')) {
    $type = arg(3);
    if ($type != 'add') {

      // Thanks to stupid core handling of node types with underscores, we
      // need to convert this back to the actual machine-readable type name.
      signup_date_check_node_types(str_replace('-', '_', $type));
    }
  }
}

/**
 * Implementation of hook_menu().
 *
 * @ingroup signup_core
 */
function signup_menu($may_cache) {
  global $user;
  $items = array();
  $access = user_access('administer all signups');
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/settings/signup',
      'description' => t('Configure settings for signups.'),
      'access' => $access,
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'signup_settings_page',
      ),
      'title' => user_access('access administration pages') ? t('Signup') : t('Signup settings'),
    );
    $items[] = array(
      'path' => 'admin/content/signup',
      'description' => t('View all signup-enabled posts, and open or close signups on them.'),
      'access' => $access,
      'callback' => 'signup_admin_page',
      'title' => t('Signup administration'),
    );
  }
  else {

    // !$may_cache: dynamic menu items
    _signup_initialize_scheduler_backend();

    // Conditionally load either the views support, or the code that
    // only should happen if views is not enabled.
    $signup_path = './' . drupal_get_path('module', 'signup');
    if (module_exists('views')) {
      require_once $signup_path . '/views/views.inc';
    }
    else {
      require_once $signup_path . '/includes/views.none.inc';
      signup_no_views_menu($items, $may_cache);
    }

    // If it's a signup-enabled node, then put in a signup tab for admins.
    if (arg(0) == 'node' && is_numeric(arg(1))) {
      $node = node_load(array(
        'nid' => arg(1),
      ));
      if (!empty($node->signup)) {
        $access_own = user_access('administer signups for own content') && $user->uid == $node->uid;
        $email_own = user_access('email users signed up for own content') && $user->uid == $node->uid;
        $email_all = user_access('email all signed up users');
        if (variable_get('signup_form_location', 'node') == 'tab' && _signup_needs_output($node)) {
          $items[] = array(
            'path' => 'node/' . arg(1) . '/signup',
            'title' => t('Sign up'),
            'callback' => 'signup_node_tab',
            'callback arguments' => array(
              $node,
            ),
            'type' => MENU_LOCAL_TASK,
            'weight' => 19,
          );
        }
        $items[] = array(
          'path' => 'node/' . arg(1) . '/signups',
          'title' => t('Signups'),
          'callback' => 'signup_node_admin_page',
          'callback arguments' => array(
            $node,
          ),
          'access' => $access || $access_own,
          'type' => MENU_LOCAL_TASK,
          'weight' => 20,
        );
        $items[] = array(
          'path' => 'node/' . arg(1) . '/signups/confirm',
          'title' => t('Signups'),
          'callback' => 'drupal_get_form',
          'callback arguments' => array(
            'signup_cancel_multiple_confirm',
            $node,
          ),
          'access' => $access || $access_own,
          'type' => MENU_CALLBACK,
          'weight' => 20,
        );
        $items[] = array(
          'path' => 'node/' . arg(1) . '/signup-broadcast',
          'title' => t('Signup broadcast'),
          'callback' => 'drupal_get_form',
          'callback arguments' => array(
            'signup_broadcast_form',
            $node,
          ),
          'access' => $email_all || $email_own,
          'type' => MENU_LOCAL_TASK,
          'weight' => 21,
        );
      }
    }
  }
  return $items;
}

/**
 * Initialize the necessary scheduler backend(s).
 */
function _signup_initialize_scheduler_backend() {
  define('SIGNUP_PATH', drupal_get_path('module', 'signup'));
  require_once SIGNUP_PATH . '/includes/scheduler.inc';
  _signup_load_scheduler_includes();
}

/**
 * Implementation of hook_perm().
 *
 * @ingroup signup_core
 */
function signup_perm() {
  return array(
    'sign up for content',
    'cancel own signups',
    'view all signups',
    'administer all signups',
    'administer signups for own content',
    'email users signed up for own content',
    'email all signed up users',
  );
}

/**
 * Implementation of hook_user().
 *
 * When a user is deleted, cancel all of that user's signups to remove all
 * instances of that user from the {signup_log} table, free up space in nodes
 * with signup limits, etc.
 *
 * @ingroup signup_core
 */
function signup_user($type, &$edit, &$user, $category = NULL) {
  switch ($type) {
    case 'delete':
      $uids = array();
      if (is_array($edit['accounts'])) {

        // A multi-user delete from Admin > User management > Users.
        $uids = $edit['accounts'];
      }
      else {

        // A single-user delete from the edit tab on the user's profile.
        $uids[] = $edit['uid'];
      }
      foreach ($uids as $uid) {
        $nids = db_query("SELECT nid FROM {signup_log} WHERE uid = %d", $uid);
        while ($data = db_fetch_object($nids)) {
          signup_cancel_signup($uid, $data->nid);
        }
      }
      break;
  }

  // If we're not using views, we need to support additional user
  // operations, for example, to add the user's current signup
  // schedule to their user profile page.
  if (!module_exists('views')) {
    $signup_path = './' . drupal_get_path('module', 'signup');
    require_once $signup_path . '/includes/views.none.inc';
    return _signup_user_no_views($type, $edit, $user, $category);
  }
}

/**
 * Implementation of hook_form_alter().
 *
 * @ingroup signup_core
 */
function signup_form_alter($form_id, &$form) {
  switch ($form_id) {
    case 'node_type_form':
      signup_alter_node_type_form($form_id, $form);
      break;
    case $form['type']['#value'] . '_node_form':
      signup_alter_node_form($form_id, $form);
      break;
    case '_content_admin_field':
      if (function_exists('_signup_date_field_form_alter')) {
        _signup_date_field_form_alter($form_id, $form);
      }
      break;
  }
}

/**
 * Alters the form for administrator settings per node type.
 * (admin/content/types)
 */
function signup_alter_node_type_form($form_id, &$form) {
  $type = $form['old_type']['#value'];
  $form['signup'] = array(
    '#type' => 'fieldset',
    '#title' => t('Signup settings'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $form['signup']['signup_node_default_state'] = array(
    '#type' => 'radios',
    '#title' => t('Signup options'),
    '#options' => array(
      'disabled' => t('Disabled'),
      'allowed_off' => t('Allowed (off by default)'),
      'enabled_on' => t('Enabled (on by default)'),
    ),
    '#default_value' => variable_get('signup_node_default_state_' . $type, 'disabled'),
    '#description' => t('If %disabled is selected, signups will not be possible for this content type. If %allowed_off is selected, signups will be off by default, but users with the %admin_all_signups permission will be able to allow signups for specific posts of this content type. If %enabled_on is selected, users will be allowed to signup for this content type unless an administrator disbles signups on specific posts.', array(
      '%disabled' => t('Disabled'),
      '%allowed_off' => t('Allowed (off by default)'),
      '%enabled_on' => t('Enabled (on by default)'),
      '%admin_all_signups' => t('administer all signups'),
    )),
  );
  if (function_exists('_signup_date_alter_node_type_form')) {
    _signup_date_alter_node_type_form($form_id, $form);
  }
}

/**
 * Alters the node form to inject the appropriate per-node signup settings.
 */
function signup_alter_node_form($form_id, &$form) {
  global $user;

  // Load the node if it already exists.
  if (!empty($form['nid']['#value'])) {
    $node = node_load($form['nid']['#value']);
  }
  else {
    $node = NULL;
  }
  $node_type = $form['type']['#value'];
  $signup_type_default = variable_get('signup_node_default_state_' . $node_type, 'disabled');
  if (!empty($node)) {
    $node_scheduler = _signup_get_node_scheduler($node);
  }
  else {
    $node_scheduler = _signup_get_node_type_scheduler($node_type);
  }
  $node_has_date = $node_scheduler != 'none';

  // If signups are possible, and the current user either has the global
  // 'administer all signups' permission or has the 'administer signups
  // for own content' permission and is creating new content or editing
  // their own content, add a fieldset for signup-related settings.
  // Signups are possible if they're not explicitly disallowed for this
  // node type, or if this node is already signup-enabled (in case an
  // admin erroneously marks a node-type to disallow signups when there
  // are already nodes of that type with signups enabled).
  $signups_possible = $signup_type_default != 'disabled' || !empty($node) && !empty($node->signup);
  $admin_all = user_access('administer all signups');
  $admin_own = user_access('administer signups for own content') && (empty($node) || $node->uid == $user->uid);
  if ($signups_possible && ($admin_all || $admin_own)) {
    $form['signup'] = array(
      '#type' => 'fieldset',
      '#title' => t('Signup settings'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#weight' => 30,
    );

    // Figure out what the options should be.  If there are already
    // people signed-up for this node, we need a 3rd choice: disable
    // signups and remove all signup data.
    $has_signups = !empty($node) && db_result(db_query("SELECT COUNT(*) from {signup_log} WHERE nid = %d", $node->nid));
    $radio_options[1] = t('Enabled');
    if ($has_signups) {
      $radio_options[0] = t('Disabled, but save existing signup information');
      $radio_options[2] = t('Disabled, and remove all signup information') . ' <strong>(' . t('This can not be undone, use with extreme caution!') . ')</strong>';
    }
    else {
      $radio_options[0] = t('Disabled');
    }

    // Figure out what the default selection for signups should be.
    if (isset($node->signup)) {
      $default_option = $node->signup;
    }
    else {
      $default_option = $signup_type_default == 'enabled_on' ? 1 : 0;
    }
    if ($default_option == 1) {
      $hint = t('If enabled, you can control whether users may sign up by visiting the !signups tab and toggling if signups are %open or %closed for this %node_type.', array(
        '!signups' => !empty($node) ? l(t('Signups'), 'node/' . $node->nid . '/signups') : theme('placeholder', t('Signups')),
        '%open' => t('open'),
        '%closed' => t('closed'),
        '%node_type' => node_get_types('name', $node_type),
      ));
    }
    else {
      $hint = '';
    }

    // Add the form element to toggle if signups are allowed.
    $form['signup']['signup_enabled'] = array(
      '#type' => 'radios',
      '#options' => $radio_options,
      '#default_value' => $default_option,
      '#description' => $hint . '<div class="js-hide">' . t('If disabled, all of the other signup settings will be ignored.') . '</div>',
      '#prefix' => '<div class="signup-allow-radios">',
      '#suffix' => '</div>',
    );

    // If JS is enabled, signup.css will hide all the settings on page
    // load if signups aren't enabled on this node.
    $settings_class = "signup-node-settings";
    if ($default_option != 1) {
      $settings_class .= " js-hide";
    }

    // Add the actual settings.  We wrap this in a div to make it easy
    // to use jQuery to hide these settings when signups are disabled.
    drupal_add_js(drupal_get_path('module', 'signup') . '/js/node_form.js');
    drupal_add_css(drupal_get_path('module', 'signup') . '/signup.css');
    $form['signup']['node_settings'] = array(
      '#prefix' => '<div class="' . $settings_class . '">',
      '#suffix' => '</div>',
    );
    $form['signup']['node_settings']['settings'] = signup_node_settings_form($node, $node_type, $node_has_date);
  }
}

/**
 * Submits the cancel signup form
 *
 * @ingroup signup_core
 *
 * @param $form_id The ID of the form being submitted.
 * @param $form_values The constructed form values array of the submitted form.
 */
function signup_form_cancel_submit($form_id, $form_values) {
  signup_cancel_signup($form_values['uid'], $form_values['nid'], $form_values['signup_anon_mail']);
}

/**
 * Executes the user signup form
 *
 * @ingroup signup_core
 *
 * @param $form_id The ID of the form being submitted.
 * @param $form_values The constructed form values array of the submitted form.
 */
function signup_form_submit($form_id, $form_values) {
  if (isset($form_values['signup_username'])) {
    $account = user_load(array(
      'name' => $form_values['signup_username'],
    ));
    $form_values['uid'] = $account->uid;
  }
  signup_sign_up_user($form_values);
}

/**
 * Validates the email address on the anonymous user signup form.
 *
 * @param $form
 *   Form array for the anonymous user email field.
 * @param $nid
 *   Node id of the node the user is trying to signup for.
 */
function signup_form_validate_anon($form, $nid) {
  signup_validate_anon_email($nid, $form['#value'], 'signup_anon_mail');
}

/**
 * Validates the username on the admin form to signup another user.
 *
 * @param $form
 *   Form array for the username field.
 * @param $nid
 *   Node id of the node the user is being signed up for.
 */
function signup_form_validate_username($form, $nid) {
  $username = $form['#value'];
  $account = user_load(array(
    'name' => $username,
  ));
  if (empty($account)) {
    form_error($form, t('User %username does not exist.', array(
      '%username' => $username,
    )));
  }
  elseif (!user_access('sign up for content', $account)) {
    form_error($form, t('User !user does not have permission to sign up.', array(
      '!user' => theme('username', $account),
    )));
  }
  elseif (db_result(db_query("SELECT COUNT(*) FROM {signup_log} WHERE uid = %d AND nid = %d", $account->uid, $nid)) > 0) {
    $node = node_load($nid);
    form_error($form, t('User !user is already signed up for %title', array(
      '!user' => theme('username', $account),
      '%title' => $node->title,
    )));
  }
}

/**
 * @defgroup signup_nodeapi Functions for nodeapi integration
 */

/**
 * Implementation of hook_nodeapi().
 *
 * @ingroup signup_nodeapi
 */
function signup_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
  switch ($op) {
    case 'insert':
    case 'update':
      signup_save_node($node, $op);
      break;
    case 'delete':

      // Clean up the signup tables for the deleted node.
      db_query("DELETE FROM {signup} WHERE nid = %d", $node->nid);
      db_query("DELETE FROM {signup_log} WHERE nid = %d", $node->nid);
      break;
    case 'load':

      // Check for a signup for this node.
      // If it's a new node, load the defaults.
      $result = db_query("SELECT * FROM {signup} WHERE nid = %d", $node->nid ? $node->nid : 0);

      // Load signup data for both new nodes w/ enabled node types,
      // and any existing nodes that are already signup enabled.
      if (!$node->nid && variable_get('signup_node_default_state_' . $node->type, 'disabled') == 'enabled_on' || $node->nid && db_num_rows($result)) {
        $signup = db_fetch_object($result);
        $node->signup = 1;
        $node->signup_forwarding_email = $signup->forwarding_email;
        $node->signup_send_confirmation = $signup->send_confirmation;
        $node->signup_confirmation_email = $signup->confirmation_email;
        $node->signup_send_reminder = $signup->send_reminder;
        $node->signup_reminder_days_before = $signup->reminder_days_before;
        $node->signup_reminder_email = $signup->reminder_email;
        $node->signup_close_signup_limit = $signup->close_signup_limit;
        $node->signup_status = $signup->status;
        if ($node->nid) {
          $node->signup_total = db_result(db_query("SELECT COUNT(*) FROM {signup_log} WHERE nid = %d", $node->nid));
        }
      }
      else {
        $node->signup = 0;
      }
      break;
    case 'view':

      // If this is a signup node, figure out what (if anything) to print.
      // Only include any of this if we're trying to view the node as
      // a page, not during the view from comment validation, etc.
      if ($page && _signup_needs_output($node) && variable_get('signup_form_location', 'node') == 'node') {
        $output = _signup_node_output($node);
        if (!empty($output)) {

          // Save into a node property for retrieval from the theme layer.
          $node->signup_view = $output;

          // Store the data into the content array, for default display.
          $node->content['signup'] = array(
            '#value' => $output,
            '#weight' => variable_get('signup_info_node_weight_' . $node->type, 10),
          );
        }
      }
      break;
  }
}

/**
 * Save signup-related information when a node is created or edited.
 *
 * This is a helper function invoked via signup_nodeapi().  It ensures that
 * the currently selected signup values are properly saved into the database.
 * If the node is signup-enabled, the per-node configuration values are saved
 * to the {signup} table. If signups are disabled, the record from {signup} is
 * cleared.  If the signup administrator editing the node decided to remove
 * all signup data, all the records from the {signup_log} table for this node
 * are also removed.  This function is also responsible for testing if the
 * node * has a start time and if the autoclose period has already begun, in
 * which case signups are closed.  Finally, if the signup limit was changed
 * while editing the node, the function compares the limit against the current
 * total number of signups and opens or closes signups as appropriate.
 *
 * @param $node
 *   The node object given to signup_nodeapi().
 * @param $op
 *   The hook_nodeapi() operation, either 'insert' or 'update'.
 *
 * @return
 *   Nothing, this function is expected to update the database.
 *
 * @see signup_nodeapi()
 */
function signup_save_node($node, $op) {

  // See if the form indicates that signups are enabled on this node.
  if (isset($node->signup_enabled)) {
    if ($node->signup_enabled == 1) {
      $values = array(
        $node->signup_forwarding_email,
        $node->signup_send_confirmation,
        $node->signup_confirmation_email,
        $node->signup_close_signup_limit,
      );

      // If we're dealing with a node that doesn't have a start time, these
      // fields are missing from the signup settings form, so we can't assume
      // they're defined.
      $values[] = isset($node->signup_send_reminder) ? $node->signup_send_reminder : 0;
      $values[] = isset($node->signup_reminder_days_before) ? $node->signup_reminder_days_before : 0;
      $values[] = isset($node->signup_reminder_email) ? $node->signup_reminder_email : '';
    }
  }
  elseif ($op == 'insert' && variable_get('signup_node_default_state_' . $node->type, 'disabled') == 'enabled_on') {

    // The form doesn't include any information about signups, but the node
    // type is signup-enabled. This would happen if a user without any signup
    // admin permissions creates a node that has been signup-enabled based on
    // the node type. In this case, we use the site-wide default signup
    // settings.
    $defaults = db_fetch_object(db_query("SELECT * from {signup} WHERE nid = 0"));
    $values = array(
      $defaults->forwarding_email,
      $defaults->send_confirmation,
      $defaults->confirmation_email,
      $defaults->close_signup_limit,
      $defaults->send_reminder,
      $defaults->reminder_days_before,
      $defaults->reminder_email,
    );
  }
  if (isset($values)) {

    // If $values is set, we need to save them to the DB.
    // Before we update the DB, see if the limit is changing, so we can take
    // appropriate action after we update to the new settings.
    $has_signup_record = FALSE;
    $limit_changed = FALSE;
    if ($op == 'update') {
      $cur_limit = db_result(db_query("SELECT close_signup_limit FROM {signup} WHERE nid = %d", $node->nid));
      if ($cur_limit !== FALSE) {
        $has_signup_record = TRUE;
        $limit_changed = $cur_limit != $node->signup_close_signup_limit;
      }
    }

    // See if we need to update an existing record or insert a new one.
    // Either way, we always the nid as the final value. The nid will either
    // be used as the last column in the INSERT, or the argument to the WHERE
    // clause for the UPDATE.
    $values[] = $node->nid;
    if ($has_signup_record) {
      db_query("UPDATE {signup} SET forwarding_email = '%s', send_confirmation = %d, confirmation_email = '%s', close_signup_limit = %d, send_reminder = %d, reminder_days_before = %d, reminder_email = '%s' WHERE nid = %d", $values);
    }
    else {
      db_query("INSERT INTO {signup} (forwarding_email, send_confirmation, confirmation_email, close_signup_limit, send_reminder, reminder_days_before, reminder_email, nid) VALUES ('%s', %d, '%s', %d, %d, %d, '%s', %d)", $values);
    }
    if (_signup_node_completed($node) && !empty($node->signup_status)) {

      // If this is an time-based node, and it's already past the close in
      // advance time, and signups are still open, close them now (and don't
      // consider the limit for changing the status).
      signup_close_signup($node->nid);
      drupal_set_message(t('%node_type start time is already past the signup close-in-advance time, signups now closed.', array(
        '%node_type' => node_get_types('name', $node->type),
      )));
    }
    elseif ($limit_changed) {
      _signup_check_limit($node, 'limit');
    }
  }
  elseif ($op == 'update' && isset($node->signup_enabled)) {

    // $values was not set, because signups are now disabled on this node.
    switch ($node->signup_enabled) {
      case 2:

        // Disabled, and delete {signup_log}, too
        db_query("DELETE FROM {signup_log} WHERE nid = %d", $node->nid);

      // No break, fall through and remove from {signup} too.
      case 0:

        // Disabled, but leave {signup_log} alone
        db_query("DELETE FROM {signup} WHERE nid = %d", $node->nid);
        break;
    }
  }
}

/**
 * Controls the output displayed if this node is closed for signups.
 *
 * @param $node
 *   The fully loaded node object.
 * @param $current_signup
 *   If the user already signed up, an HTML representation of their current
 *   signup information, otherwise an empty string.
 *
 * @return
 *   Themed output to display for a node with closed signups.
 *
 * @see _signup_node_output()
 * @see _signup_print_current_signup()
 */
function theme_signup_signups_closed($node, $current_signup = '') {
  $output = '<h3>' . t('Signups closed for this %node_type', array(
    '%node_type' => node_get_types('name', $node->type),
  )) . '</h3>';
  $output .= $current_signup;
  return $output;
}

/**
 * Controls the output for anonymous users who can't signup.
 *
 * @param $anon_login_text
 *   The translated HTML help text telling users to login (and if allowed on
 *   this site, register) so they can signup, including login/register links.
 *
 * @return
 *   The themed HTML to display the login (and maybe register) help text.
 */
function theme_signup_anonymous_user_login_text($anon_login_text) {
  if (!empty($anon_login_text)) {
    return '<div class="signup_anonymous_login">' . $anon_login_text . '</div>';
  }
}

/**
 * Generate all the signup-related output for a given node.
 *
 * Because of the global setting to control if the signup details and form
 * appear at the bottom of the node or on a separate tab, this function is
 * shared by multiple callers.
 *
 * @param $node
 *   The fully loaded node object.
 * @param $type
 *   The kind of output would we render: can be either 'node' or 'tab'.
 *
 * @return
 *   The fully rendered HTML for all signup-related forms and info.
 *
 * @see signup_nodeapi()
 * @see signup_node_tab()
 *
 * @todo This needs to be much more theme-friendly.
 *
 */
function _signup_node_output($node, $type = 'node') {
  global $user;
  $output = theme('signup_node_output_header', $node);

  // The node has been closed for signups, and the user has
  // signup permissions.  Let them know it's closed.
  if (!$node->signup_status) {
    if (user_access('sign up for content')) {
      $current_signup = '';

      // If they're logged in and already signed up, show their current
      // signup info and give them the option to cancel.
      if ($user->uid) {
        $result = db_query("SELECT signup_time, form_data FROM {signup_log} WHERE uid = %d AND nid = %d", $user->uid, $node->nid);
        if (db_num_rows($result)) {
          $signup_info = db_fetch_object($result);
          $current_signup = _signup_print_current_signup($node, $signup_info);
        }
      }
      $output .= theme('signup_signups_closed', $node, $current_signup);
    }
  }
  else {
    $fieldset = $type == 'node' ? TRUE : FALSE;
    if ($user->uid == 0) {

      // This is an anonymous user.
      if (user_access('sign up for content')) {

        // If they can signup, render the anonymous sigup form.
        $output .= drupal_get_form('signup_form', $node, 'anon', $fieldset);
      }
      else {

        // If not, then display the appropriate login/register link if the
        // default authenticated user role can signup.
        $anon_login_text = '';
        $signup_roles = user_roles(FALSE, 'sign up for content');
        if (!empty($signup_roles[DRUPAL_AUTHENTICATED_RID])) {
          $token_array = array(
            '!login' => l(t('login'), 'user/login', array(), drupal_get_destination()),
            '!register' => l(t('register'), 'user/register', array(), drupal_get_destination()),
            '%node_type' => node_get_types('name', $node->type),
          );
          if (variable_get('user_register', 1) == 0) {
            $anon_login_text = t('Please !login to sign up for this %node_type.', $token_array);
          }
          else {
            $anon_login_text = t('Please !login or !register to sign up for this %node_type.', $token_array);
          }
        }
        $output .= theme('signup_anonymous_user_login_text', $anon_login_text);
      }
    }
    else {

      // An authenticated user.
      // See if the user is already signed up for this node.
      $result = db_query("SELECT signup_time, form_data FROM {signup_log} WHERE uid = %d AND nid = %d", $user->uid, $node->nid);
      $signup_info = NULL;
      if (db_num_rows($result) == 0) {

        // Not yet signed up
        if (user_access('sign up for content')) {

          // User has permission to do so, so give them the form.
          $output .= drupal_get_form('signup_form', $node, 'auth', $fieldset);
        }
      }
      else {

        // Already signed up, display their info.
        $signup_info = db_fetch_object($result);
        $output .= _signup_print_current_signup($node, $signup_info);
      }
    }
  }

  // How should the list of signed-up users be displayed, if at all?
  $display_list = variable_get('signup_display_signup_user_list', 'signup');

  // If the user has the view signups perm and the admin decides to display
  // the list at the bottom of the page, display the current signups.
  if (user_access('view all signups')) {
    if ($display_list == 'signup') {

      // Admin wants the hard-coded signup listing.
      $registered_query = db_query("SELECT u.uid, u.name, s.signup_time, s.form_data FROM {signup_log} s INNER JOIN {users} u ON u.uid = s.uid WHERE s.nid = %d AND u.uid <> 0", $node->nid);
      $registered_signups = array();
      while ($signed_up_user = db_fetch_object($registered_query)) {
        $registered_signups[] = $signed_up_user;
      }
      $anon_query = db_query("SELECT * FROM {signup_log} WHERE nid = %d AND uid = 0", $node->nid);
      $anon_signups = array();
      while ($signed_up_user = db_fetch_object($anon_query)) {
        $anon_signups[] = $signed_up_user;
      }
      $output .= theme('signup_user_list', $node, $registered_signups, $anon_signups);
    }
    elseif ($display_list == 'embed-view' && module_exists('views')) {
      $view_name = variable_get('signup_user_list_view_name', 'signup_user_list');
      $view_type = variable_get('signup_user_list_view_type', 'embed');
      $view = views_get_view($view_name);
      $args = array(
        $node->nid,
      );
      $output .= views_build_view($view_type, $view, $args);
    }

    // Otherwise, they're on their own, and either don't want it displayed at
    // all, or they want to handle where/how it's displayed via views.
  }
  return $output;
}

/**
 * Return HTML desired at the top of the signup output for a node.
 *
 * @param $node
 *   The fully loaded node object to generate a header for.
 *
 * @return
 *   HTML to display at the top of the signup output.
 *
 * @see _signup_node_output()
 */
function theme_signup_node_output_header($node) {
  return '<a name="signup"></a>';
}

/**
 * Formats the list of users signed up for a particular node.
 *
 * @param $node
 *   The node object that users have signed up for.
 * @param $registered_signups
 *   Array of objects with data for each registered user signed up.
 * @param $anon_signups
 *   Array of objects with data for each anonymous user signed up.
 */
function theme_signup_user_list($node, $registered_signups, $anon_signups) {
  $header = array(
    array(
      'data' => t('!users signed up', array(
        '!users' => format_plural(count($registered_signups) + count($anon_signups), '1 individual', '@count individuals'),
      )),
    ),
  );
  $rows = array();
  foreach ($registered_signups as $signup) {
    $rows[] = array(
      theme('username', $signup),
    );
  }
  if (!empty($anon_signups)) {
    $rows[] = array(
      t('!count anonymous', array(
        '!count' => count($anon_signups),
      )),
    );
  }
  return theme('table', $header, $rows);
}

/**
 * Helper function that determines if a given node should have any
 * signup-related output.
 *
 * @param $node A fully loaded node object.
 *
 * @return TRUE if this node should have signup output, FALSE if not.
 *
 * @see signup_nodeapi()
 */
function _signup_needs_output($node) {
  if (!$node->signup) {

    // Not signup enabled at all.
    return FALSE;
  }
  $suppress = module_invoke_all('signup_suppress', $node);
  if (in_array(TRUE, $suppress)) {

    // Someone doesn't want signup details printed.
    return FALSE;
  }
  return TRUE;
}
function signup_node_tab($node) {
  drupal_set_title(check_plain($node->title));
  return _signup_node_output($node, 'tab');
}

/**
 * Controls the output of the users signup data and optional cancel button.
 *
 * @param $signup_data
 *   Array containing information about the user's signup.  Contains:
 *   'signup_timestamp' - Integer timestamp when the user signed up.
 *   'custom_data' - Array containing the user's custom signup data.
 * @param $cancel_signup_form
 *   Optional HTML for a "Cancel signup" button if the user is allowed.
 *
 * @return
 *   Themed output containing the user's current signup information.
 */
function theme_signup_current_signup($signup_data, $cancel_signup_form = '') {
  $output = theme('signup_custom_data_table', $signup_data['custom_data']);
  $output .= $cancel_signup_form;
  return $output;
}

/**
 * Helper function to display the current user's signup information.
 *
 * Contains the logic to determine what should be displayed, then invokes the
 * appropriate form builder and theme functions.
 *
 * @param $node
 *   The fully-loaded node object to display signup data about.
 * @param $signup_info
 *   Database object with information about the signup to display.
 *
 * @return
 *   Themed HTML to output for the given node and signup.
 *
 * @see theme_signup_current_signup()
 */
function _signup_print_current_signup($node, $signup_info) {

  // Generate an array of data about the current signup for the theme function.
  $signup_data = array();
  $signup_data['custom_data'] = unserialize($signup_info->form_data);
  $signup_data['signup_timestamp'] = $signup_info->signup_time;

  // See if the current user is allowed to cancel their signup, and if so,
  // render the HTML for the cancel form (which is just a single button).
  $cancel_signup_form = '';
  if (user_access('cancel own signups')) {
    $cancel_signup_form = drupal_get_form('signup_form_cancel', $node);
  }

  // Hand off everything to the theme function for actual HTML generation.
  return theme('signup_current_signup', $signup_data, $cancel_signup_form);
}

/**
 * @defgroup signup_callback
 *   Functions which are the menu callbacks for this module.
 */

/**
 * Builder function for the signup form
 * @ingroup signup_callback
 *
 * @param $node
 *   The fully loaded node object.
 * @param $signup_type
 *   Determines what kind of signup to generate a form for. Possible values:
 *    'auth' -- regular authenticated user signup form
 *    'anon' -- anonymous user signup form (includes required email field).
 *    'admin' -- admin form to signup another user (includes user selector).
 * @param $fieldset
 *   Boolean that indicates if the signup form should be in a fieldset.
 */
function signup_form($node, $signup_type = 'auth', $fieldset = TRUE) {
  global $user;
  require_once SIGNUP_PATH . '/theme/signup.theme';
  $form = array();
  $form['nid'] = array(
    '#type' => 'value',
    '#value' => $node->nid,
  );
  $form['uid'] = array(
    '#type' => 'value',
    '#value' => $user->uid,
  );
  if ($fieldset) {
    $form['collapse'] = array(
      '#type' => 'fieldset',
      '#collapsible' => TRUE,
      '#collapsed' => variable_get('signup_fieldset_collapsed', 1),
    );
    if ($signup_type == 'admin') {
      $form['collapse']['#title'] = t('Sign up another user');

      // We always want this fieldset expanded on the node/N/signups tab.
      $form['collapse']['#collapsed'] = FALSE;
    }
    else {
      $form['collapse']['#title'] = t('Sign up for @title', array(
        '@title' => $node->title,
      ));
    }
  }
  else {
    $form['collapse'] = array();
  }
  $signup_form = array();
  if ($signup_type == 'anon') {
    $anon_form = array();
    $anon_form['signup_anon_mail'] = array(
      '#type' => 'textfield',
      '#title' => t('Email'),
      '#description' => t('An e-mail address is required for users who are not registered at this site. If you are a registered user at this site, please !login to sign up for this %node_type.', array(
        '!login' => l(t('login'), 'user/login', array(), drupal_get_destination()),
        '%node_type' => node_get_types('name', $node->type),
      )),
      '#size' => 40,
      '#validate' => array(
        'signup_form_validate_anon' => array(
          $node->nid,
        ),
      ),
      '#maxlength' => 255,
      '#required' => TRUE,
    );
    $signup_form += $anon_form;
  }
  elseif ($signup_type == 'admin') {
    $admin_form = array();
    $admin_form['signup_username'] = array(
      '#title' => t('Username'),
      '#type' => 'textfield',
      '#autocomplete_path' => 'user/autocomplete',
      '#maxlength' => USERNAME_MAX_LENGTH,
      '#validate' => array(
        'signup_form_validate_username' => array(
          $node->nid,
        ),
      ),
      '#size' => 40,
      '#weight' => -1,
      '#required' => TRUE,
    );
    $signup_form += $admin_form;
  }

  // Build the themed signup form for this site and include that.
  $signup_themed_form = theme('signup_user_form', $node);
  if ($signup_type == 'admin') {

    // Special case hack for the default signup form, where the current
    // username is being filled in as the default for the 'Name' field.
    if (!empty($signup_themed_form['signup_form_data']['Name']['#default_value'])) {
      unset($signup_themed_form['signup_form_data']['Name']['#default_value']);
    }
  }
  $signup_form += $signup_themed_form;
  $form['collapse']['signup_user_form'] = $signup_form;
  $form['collapse']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Sign up'),
  );
  return $form;
}

/**
 * Builder function for the cancel signup form
 * @ingroup signup_callback
 */
function signup_form_cancel($node) {
  global $user;
  $form['nid'] = array(
    '#type' => 'value',
    '#value' => $node->nid,
  );
  $form['uid'] = array(
    '#type' => 'value',
    '#value' => $user->uid,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Cancel signup'),
  );
  return $form;
}

/**
 * Controls the output of the signup administration overview page.
 *
 * This page is located at admin/content/signup, and allows site-wide signup
 * administrators to view signup-related information, close/open signups, etc,
 * for all signup-enabled nodes on the site.  There's a form to filter the
 * results by signup status, which is rendered at the top of the page by
 * default.
 *
 * @param $filter_status_form
 *   HTML representation of the signup status filter form.
 * @param $signup_admin_form
 *   HTML representation of the main signup administration form.
 *
 * @return
 *   Themed output for the signup administration overview page.
 */
function theme_signup_admin_page($filter_status_form, $signup_admin_form) {
  $output = $filter_status_form;
  $output .= $signup_admin_form;
  return $output;
}

/**
 * Prints the admin signup overview page located at admin/content/signup
 * @ingroup signup_callback
 */
function signup_admin_page() {
  drupal_add_css(drupal_get_path('module', 'signup') . '/signup.css');
  $filter_status_form = drupal_get_form('signup_filter_status_form');
  $signup_admin_form = drupal_get_form('signup_admin_form');
  return theme('signup_admin_page', $filter_status_form, $signup_admin_form);
}
function signup_filter_status_form() {
  $options = array(
    'all' => t('All'),
    'open' => t('Open'),
    'closed' => t('Closed'),
  );
  if (empty($_SESSION['signup_status_filter'])) {
    $_SESSION['signup_status_filter'] = 'all';
  }
  $form['filter'] = array(
    '#type' => 'select',
    '#title' => t('Filter by signup status'),
    '#options' => $options,
    '#default_value' => $_SESSION['signup_status_filter'],
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Filter'),
  );
  $form['#redirect'] = FALSE;
  return $form;
}
function theme_signup_filter_status_form($form) {
  return '<div class="container-inline">' . drupal_render($form) . '</div>';
}
function signup_filter_status_form_submit($form_id, $form_values) {
  $_SESSION['signup_status_filter'] = $form_values['filter'];
}
function signup_admin_form() {

  // Figure out if the current user has permission to use signup broadcast.
  $access_broadcast = user_access('email all signed up users');
  $header = array(
    array(
      'data' => t('Title'),
      'field' => 'n.title',
      'sort' => 'asc',
    ),
    array(
      'data' => t('Signups'),
      'field' => 'signup_total',
    ),
    array(
      'data' => t('Limit'),
      'field' => 'signup_close_signup_limit',
    ),
    array(
      'data' => t('Status'),
      'field' => 'signup_status',
    ),
    array(
      'data' => t('Operations'),
    ),
  );
  $start_column = signup_admin_form_header();
  if (!empty($start_column)) {
    array_unshift($header, $start_column);
  }
  list($sql, $sql_count) = signup_admin_form_sql();
  $form['header']['#value'] = $header;
  $sql .= tablesort_sql($header);
  $result = pager_query($sql, 25, 0, $sql_count);

  // Loop through the signup nodes, and generate our form elements
  while ($signup_node = db_fetch_object($result)) {
    $row = array();
    if (!empty($start_column)) {
      $row['start'] = signup_admin_form_extra($signup_node);
    }

    // Instead of duplicating the logic from the node/N/signups admin
    // form, we just call that form builder here and lift the elements
    // we need directly from that.
    $node_admin_form = signup_node_admin_summary_form($signup_node);
    $row['title'] = array(
      '#type' => 'markup',
      '#value' => l($signup_node->title, "node/{$signup_node->nid}"),
    );
    $row['status'] = $node_admin_form['status'];
    $row['total'] = array(
      '#type' => 'markup',
      '#value' => $signup_node->signup_total,
    );
    $row['limit'] = $node_admin_form['limit'];
    $op_links = l(t('View signups'), "node/{$signup_node->nid}/signups");
    if ($access_broadcast) {
      $op_links .= '<br />';
      $op_links .= l(t('Signup broadcast'), "node/{$signup_node->nid}/signup-broadcast", array(
        'title' => t('Send an email message to all users who signed up.'),
      ));
    }
    $row['operations'] = array(
      '#type' => 'markup',
      '#value' => $op_links,
    );
    $form['nids'][$signup_node->nid] = $row;
  }
  $form['#tree'] = TRUE;
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Update'),
  );
  return $form;
}
function signup_admin_form_header() {
  if (module_exists('date')) {

    // If we're using CCK date, we can't sort since the date field used for
    // each content type can come from different tables.
    return array(
      'data' => t('Start'),
      'field' => NULL,
    );
  }
  elseif (module_exists('event')) {

    // If we've got event, but not date, we can sort by e.event_start.
    return array(
      'data' => t('Start'),
      'field' => 'e.event_start',
    );
  }

  // If we've got no scheduling backend at all, there's no start time column.
  return array();
}
function signup_admin_form_extra($signup_node) {
  return array(
    '#type' => 'markup',
    '#value' => signup_format_date($signup_node),
  );
}
function signup_admin_form_sql() {
  $admin_common_sql = array(
    'primary' => '{node} n',
    'fields' => array(
      'n.nid',
      'n.title',
      'n.type',
      's.status AS signup_status',
      'COUNT(s_l.nid) AS signup_total',
      's.close_signup_limit AS signup_close_signup_limit',
    ),
    'group_by' => array(
      'n.nid',
      'n.title',
      'signup_status',
      'signup_close_signup_limit',
    ),
    'joins' => array(
      'INNER JOIN {signup} s ON s.nid = n.nid',
      'LEFT JOIN {signup_log} s_l ON s.nid = s_l.nid',
    ),
  );
  $type = $_SESSION['signup_status_filter'];
  if ($type == 'open') {
    $filter_status = 1;
  }
  elseif ($type == 'closed') {
    $filter_status = 0;
  }
  if (isset($filter_status)) {
    $admin_common_sql['where'] = array(
      "s.status = {$filter_status}",
    );
  }

  // Get the right query elements from the currently installed backend
  $admin_sql = array();
  foreach (signup_content_types() as $type) {
    $admin_sql = array_merge_recursive($admin_sql, signup_admin_sql($type));
  }

  // Build the main query.
  $sql = _signup_build_query($admin_common_sql, $admin_sql);

  // Construct the proper pager query using just the WHERE clauses (if any).
  $all_fragments = array_merge_recursive($admin_common_sql, $admin_sql);
  $sql_count = "SELECT COUNT(s.nid) FROM {signup} s";
  if (!empty($all_fragments['where'])) {
    $sql_count .= ' WHERE ' . implode(' AND ', $all_fragments['where']);
  }
  return array(
    db_rewrite_sql($sql),
    db_rewrite_sql($sql_count, 's'),
  );
}
function theme_signup_admin_form($form) {
  if (!isset($form['nids'])) {
    $type = $_SESSION['signup_status_filter'];
    switch ($type) {
      case 'open':
        $filter = t('open');
        break;
      case 'closed':
        $filter = t('closed');
        break;
      default:
        $filter = t('enabled');
        break;
    }
    return t('No content is currently !status for signups.', array(
      '!status' => $filter,
    ));
  }
  foreach ($form['nids'] as $nid => $node_form) {
    if (!is_numeric($nid)) {
      continue;
    }
    $row = array();
    if (isset($node_form['start'])) {
      $row[] = drupal_render($form['nids'][$nid]['start']);
    }
    $row[] = drupal_render($form['nids'][$nid]['title']);
    $row[] = drupal_render($form['nids'][$nid]['total']);
    $row[] = drupal_render($form['nids'][$nid]['limit']);
    $row[] = drupal_render($form['nids'][$nid]['status']);
    $row[] = drupal_render($form['nids'][$nid]['operations']);
    $rows[] = $row;
  }
  $header = $form['header']['#value'];
  unset($form['header']);
  $output = theme('table', $header, $rows, array(
    'style' => 'width:100%',
  ));
  $output .= drupal_render($form);
  $pager = theme('pager', NULL, 25, 0);
  if (!empty($pager)) {
    $output .= $pager;
  }
  return $output;
}
function signup_admin_form_submit($form_id, $form_values) {
  foreach ($form_values['nids'] as $nid => $values) {
    $values['nid'] = $nid;
    signup_node_admin_summary_form_submit($form_id, $values);
  }
}

/**
 * Callback function for canceling signups
 * @ingroup signup_callback
 */
function signup_cancel_signup($uid, $nid, $anon_mail = NULL) {
  $node = node_load($nid);
  $node->signup_total--;

  // Invoke hook_signup_cancel().
  module_invoke_all('signup_cancel', $node, $uid);

  // Delete the record from the {signup_log} table.
  if ($anon_mail) {
    db_query("DELETE FROM {signup_log} WHERE anon_mail = '%s' AND nid = %d", $anon_mail, $nid);
  }
  else {
    db_query('DELETE FROM {signup_log} WHERE uid = %d AND nid = %d', $uid, $nid);
  }
  drupal_set_message(t('Signup to !title cancelled.', array(
    '!title' => l($node->title, "node/{$node->nid}"),
  )));

  // See if signups should be re-opened if the total dropped below the limit.
  _signup_check_limit($node, 'total');
}

/**
 * Callback function for closing signups
 * @ingroup signup_callback
 */
function signup_close_signup($nid, $cron = 'no') {
  db_query("UPDATE {signup} SET status = 0 WHERE nid = %d", $nid);
  if ($cron == 'no') {
    $node = node_load($nid);
    foreach (module_implements('signup_close') as $module) {
      $function = $module . '_signup_close';
      $function($node);
    }
    watchdog('signup', t('Signups closed for %title.', array(
      '%title' => $node->title,
    )), WATCHDOG_NOTICE, l(t('view'), 'node/' . $nid));
  }
}

/**
 * Callback function for reopening signups
 * @ingroup signup_callback
 */
function signup_open_signup($nid, $cron = 'no') {
  db_query("UPDATE {signup} SET status = 1 WHERE nid = %d", $nid);
  if ($cron == 'no') {
    $node = node_load(array(
      'nid' => $nid,
    ));
    foreach (module_implements('signup_open') as $module) {
      $function = $module . '_signup_open';
      $function($node);
    }
    watchdog('signup', t('Signups reopened for %title.', array(
      '%title' => $node->title,
    )), WATCHDOG_NOTICE, l(t('view'), 'node/' . $nid));
  }
}

/**
 * Form builder for the settings page under admin/setttings/signup
 */
function signup_settings_page() {
  if (signup_site_has_dates()) {
    $form['signup_close_early'] = array(
      '#title' => t('Close x hours before'),
      '#type' => 'textfield',
      '#default_value' => variable_get('signup_close_early', 1),
      '#size' => 5,
      '#maxlength' => 10,
      '#description' => t('The number of hours before the event which signups will no longer be allowed. Use negative numbers to close signups after the event start (example: -12).'),
    );
  }
  $form['node_defaults'] = array(
    '#type' => 'fieldset',
    '#title' => t('Default signup information'),
    '#description' => t('New signup-enabled nodes will start with these settings.'),
    '#collapsible' => TRUE,
  );
  $form['node_defaults']['signup_node_settings_form'] = signup_node_settings_form(NULL, NULL, signup_site_has_dates());
  $form['adv_settings'] = array(
    '#type' => 'fieldset',
    '#title' => t('Advanced settings'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['adv_settings']['signup_date_format'] = array(
    '#title' => t('Format string for displaying signup-related dates'),
    '#type' => 'select',
    '#options' => array(
      'small' => t('Small'),
      'medium' => t('Medium'),
      'large' => t('Large'),
    ),
    '#default_value' => variable_get('signup_date_format', 'small'),
    '#description' => t('Whenever this module needs to print a date (both in the administrative interface, and in the various e-mail messages it can send), this setting controls which date format string to use. The format strings are defined at the <a href="!settings_url">Date and time settings page</a>.', array(
      '!settings_url' => url('admin/settings/date-time'),
    )),
  );
  if (!module_exists('views')) {
    $form['adv_settings']['signup_no_views_user_info'] = array(
      '#title' => t('Display signup information on user profile pages'),
      '#type' => 'checkbox',
      '#default_value' => variable_get('signup_no_views_user_info', TRUE),
    );
  }
  $form['adv_settings']['signup_form_location'] = array(
    '#title' => t('Location of the signup form and related information'),
    '#type' => 'radios',
    '#options' => array(
      'node' => t('At the bottom of each node'),
      'tab' => t('On a separate %sign_up tab', array(
        '%sign_up' => t('Sign up'),
      )),
      'none' => t('Do not display signup form'),
    ),
    '#default_value' => variable_get('signup_form_location', 'node'),
    '#description' => t('On every signup-enabled node, users with permission to
 sign up can be presented with a form and additional signup-related information. This setting controls where this information should be displayed: either directly on the node itself, on a separate tab, or not at all.'),
    '#prefix' => '<div class="signup-form-location-radios">',
    '#suffix' => '</div>',
  );

  // The rest of the advanced settings are conditional on if/where the signup
  // form is being displayed.  We use jQuery to hide settings when they're not
  // relevant.
  drupal_add_js(SIGNUP_PATH . '/js/admin.settings.js');
  drupal_add_css(SIGNUP_PATH . '/signup.css');

  // For each setting that should be hidden, signup.css will hide all the
  // settings on page load if.
  $class = 'signup-fieldset_collapsed-setting';
  if (variable_get('signup_form_location', 'node') != 'node') {
    $class .= ' js-hide';
  }
  $form['adv_settings']['signup_fieldset_collapsed'] = array(
    '#title' => t('Default fieldset behavior for per-node signup form'),
    '#type' => 'radios',
    '#options' => array(
      1 => t('Collapsed'),
      0 => t('Expanded'),
    ),
    '#default_value' => variable_get('signup_fieldset_collapsed', 1),
    '#description' => t('If the signup form is included at the bottom of each node, the signup form will be encapsulated in a collapsible fieldset. This setting controls if that fieldset is expanded or collapsed by default.'),
    '#prefix' => '<div class="' . $class . '">',
    '#suffix' => '</div>',
  );

  // If views.module is enabled provide the option to display the list
  // of signed-up users in a tab and/or at the bottom of the node.
  $display_options = array();
  $display_options['signup'] = t('Use the built-in listing');
  $views_help_text = '';
  if (module_exists('views')) {
    $display_options['embed-view'] = t('Embed a view');
    $views_help_text = t('If you choose to embed a view, you will be able to select which view you want to use below. The view you select will have a single argument passed in, the node id (nid) of the signup-enabled node being viewed. You can also use views to display this listing on its own tab or in a block if you disable this setting.');
  }
  else {
    $views_help_text = t('If you enable the !views_url, you will be able to embed a view directly onto the page for this listing.', array(
      '!views_url' => l(t('Views module'), 'http://drupal.org/project/views', array(), NULL, NULL, TRUE),
    ));
  }
  $display_options['none'] = t('Do not display a listing at all');
  $class = 'signup-display-signup-user-list-setting';
  if (variable_get('signup_form_location', 'node') == 'none') {
    $class .= ' js-hide';
  }
  $form['adv_settings']['signup_display_signup_user_list'] = array(
    '#title' => t('How to display the list of signed-up users'),
    '#type' => 'radios',
    '#options' => $display_options,
    '#default_value' => variable_get('signup_display_signup_user_list', 'signup'),
    '#description' => t('If the signup form is being displayed, users with the %view_signups permission can see a list of all users who have signed up. This setting controls if and how that list should be generated. !views_help_text', array(
      '%view_signups' => t('view all signups'),
      '!views_help_text' => $views_help_text,
    )),
    '#prefix' => '<div class="' . $class . '">',
    '#suffix' => '</div>',
  );
  if (module_exists('views')) {
    $class = 'signup-user-list-view-settings';
    if (variable_get('signup_form_location', 'node') == 'none' || variable_get('signup_display_signup_user_list', 'signup') != 'embed-view') {
      $class .= ' js-hide';
    }
    $form['adv_settings']['view_settings'] = array(
      '#prefix' => '<div class="' . $class . '">',
      '#suffix' => '</div>',
    );
    $views = array();
    $result = db_query("SELECT name, description, page, block FROM {view_view}");
    while ($view = db_fetch_object($result)) {
      $views[$view->name] = theme('signup_view_label', $view);
    }
    views_load_cache();

    // Necessary for _views_get_default_views().
    $default_views = _views_get_default_views();
    foreach ($default_views as $view) {
      if (!$views[$view->name]) {
        $views[$view->name] = theme('signup_view_label', $view);
      }
    }
    $form['adv_settings']['view_settings']['signup_user_list_view_name'] = array(
      '#title' => t('View to embed for the signup user list'),
      '#type' => 'select',
      '#options' => $views,
      '#default_value' => variable_get('signup_user_list_view_name', 'signup_user_list'),
      '#description' => t("If the signup user list is being generated by embedding a view, this selects which view should be used. The view's name, description, and which display type(s) it defines are listed.  NOTE: if you enable or customize the view being used for this, you should strongly consider disabling the view's menu items to prevent a duplicate tab showing the same information."),
    );
    $form['adv_settings']['view_settings']['signup_user_list_view_type'] = array(
      '#title' => t('Display type of the view to embed for the signup user list'),
      '#type' => 'radios',
      '#options' => array(
        'embed' => t('Page'),
        'block' => t('Block'),
      ),
      '#default_value' => variable_get('signup_user_list_view_type', 'embed'),
      '#description' => t('Choose whether to use the Page or the Block display from the view. NOTE: if the selected display type is not provided by the view, nothing will be shown, so be sure that the display type is enabled and configured properly.'),
    );
  }

  // Use our own submit handler, so we can do some processing before
  // we hand control to system_settings_form_submit.
  $form['#submit']['signup_settings_page_submit'] = array();
  return system_settings_form($form);
}

/**
 * Generates the appropriate selector label for a given view.
 *
 * @param $view
 *   An object containing data about the view to generate a label for.
 *   Contains at least a name (string), description (string), page
 *   (bool), and block (bool) fields.
 *
 * @return
 *   The plain text (no HTML allowed) to include as the label for this view in
 *   the drop-down selector for which view to embed on the site-wide signup
 *   settings page.
 *
 * @see signup_settings_page()
 */
function theme_signup_view_label($view) {
  $display_types = array();
  if (!empty($view->page)) {
    $display_types[] = t('Page');
  }
  if (!empty($view->block)) {
    $display_types[] = t('Block');
  }
  $label = check_plain($view->name);
  $label .= ' [' . implode(', ', $display_types) . ']: ';
  $label .= check_plain($view->description);
  if (drupal_strlen($label) > 90) {
    $label = drupal_substr($label, 0, 90) . '...';
  }
  return $label;
}

/**
 * Submits the signup settings form
 *
 * @param $form_id The ID of the form being submitted.
 * @param $form_values The constructed form values array of the submitted form.
 */
function signup_settings_page_submit($form_id, $form_values) {
  $op = isset($form_values['op']) ? $form_values['op'] : '';
  if ($op == t('Save configuration') && db_num_rows(db_query('SELECT nid FROM {signup} WHERE nid = 0'))) {
    db_query("UPDATE {signup} SET forwarding_email = '%s', send_confirmation = %d, confirmation_email = '%s', send_reminder = %d, reminder_days_before = %d, reminder_email = '%s', close_signup_limit = %d WHERE nid = 0", $form_values['signup_forwarding_email'], $form_values['signup_send_confirmation'], $form_values['signup_confirmation_email'], $form_values['signup_send_reminder'], $form_values['signup_reminder_days_before'], $form_values['signup_reminder_email'], $form_values['signup_close_signup_limit']);
  }
  else {
    require_once 'signup.install';
    db_query("DELETE FROM {signup} WHERE nid = 0");
    signup_insert_default_signup_info();
  }

  // Now, remove all the settings we just processed from our copy of
  // $form_values, so system_settings_form_submit() doesn't see them.
  $settings = array(
    'signup_forwarding_email',
    'signup_send_confirmation',
    'signup_confirmation_email',
    'signup_send_reminder',
    'signup_reminder_days_before',
    'signup_reminder_email',
    'signup_close_signup_limit',
  );
  foreach ($settings as $setting) {
    unset($form_values[$setting]);
  }

  // Remove the hidden element from signup_node_settings_form(), too.
  unset($form_values['signup']);

  // Finally, let system_settings_form_submit() do its magic with the
  // rest of the settings.
  system_settings_form_submit($form_id, $form_values);
}

/**
 * Returns a list of content types that have signups enabled
 */
function signup_content_types() {
  $signup_content_types = array();
  foreach (node_get_types('names') as $content_type => $content_name) {
    if (variable_get('signup_node_default_state_' . $content_type, 'disabled') != 'disabled') {
      $signup_content_types[] = $content_type;
    }
  }
  return $signup_content_types;
}

/**
 * Signs up a user to a node.
 *
 * NOTE: other modules can call this function. To do so, $signup_form
 * must be as follows:
 *
 * $signup_form['nid'] : nid of the node to which the user will be signed up
 * $signup_form['uid'] : uid of the user to sign up
 * $signup_form['signup_anon_mail'] : Optional. An email address of an
 *   anonymous user to sign up. Only include this if the user is not
 *   already registered with the site.  $signup_form['uid'] will
 *   automatically be set to 0 if this element is passed in. NOTE: It's
 *   highly recommended to call the signup_validate_anon_email
 *   function in the external module's validation cycle or perform
 *   that function's validation logic prior to passing in this element!
 * $signup_form['signup_form_data'] : an array of key/value pairs --
 *   key is the data category, value is the user input
 */
function signup_sign_up_user($signup_form) {
  $node = node_load($signup_form['nid']);

  // Since this is an API call, we need to validate that there are no
  // duplicate signups being generated, even though through the usual
  // UI, there's no way to reach this function if it's a duplicate.
  // How to find duplicates is different for anonymous and
  // authenticated signups.
  if (!empty($signup_form['signup_anon_mail'])) {

    // Ensure the uid is 0 for anonymous signups, even if it's not duplicate.
    $signup_form['uid'] = 0;

    // Now, see if this email is already signed-up.
    if (db_num_rows(db_query("SELECT anon_mail FROM {signup_log} WHERE anon_mail = '%s' AND nid = %d", $signup_form['signup_anon_mail'], $node->nid))) {
      drupal_set_message(t('Anonymous user %email is already signed up for %title', array(
        '%email' => $signup_form['signup_anon_mail'],
        '%title' => $node->title,
      ), 'error'));
      return FALSE;
    }
  }
  else {

    // This query does the JOIN on {users} so we can avoid a full
    // user_load() just so theme('username') can have the data it
    // needs for the error message we might print out.
    $query = db_query("SELECT sl.uid, u.name FROM {signup_log} sl INNER JOIN {users} u ON sl.uid = u.uid WHERE sl.uid = %d AND sl.nid = %d", $signup_form['uid'], $signup_form['nid']);
    if (db_num_rows($query)) {
      $user = db_fetch_object($query);
      drupal_set_message(t('User !user is already signed up for %title', array(
        '!user' => theme('username', $user),
        '%title' => $node->title,
      )), 'error');
      return FALSE;
    }
  }

  // If we made it this far, we're going to need the full $user object.
  $user = user_load(array(
    'uid' => $signup_form['uid'],
  ));
  if (user_access('sign up for content') && $node->signup_status) {

    // Grab the current time once, since we need it in a few places.
    $curtime = time();

    // Allow other modules to inject data into the user's signup data.
    $extra = module_invoke_all('signup_sign_up', $node, $user);
    $signup_info = array();
    if (!empty($signup_form['signup_form_data'])) {
      $signup_info = $signup_form['signup_form_data'];
    }
    if (!empty($extra)) {
      $signup_info = array_merge($signup_info, $extra);
    }
    $signup_form_data = serialize($signup_info);

    // Figure out if confirmation or reminder emails will be sent and
    // inform the user.
    $confirmation_email = $node->signup_send_confirmation ? '  ' . t('You will receive a confirmation email shortly which contains further information about this %node_type.', array(
      '%node_type' => node_get_types('name', $node->type),
    )) : '';
    $reminder_email = $node->signup_send_reminder ? '  ' . t('You will receive a reminder email !number !days before the %node_type.', array(
      '!number' => $node->signup_reminder_days_before,
      '!days' => format_plural($node->signup_reminder_days_before, 'day', 'days'),
      '%node_type' => node_get_types('name', $node->type),
    )) : '';

    // Insert the user into the signup_log.
    db_query("INSERT INTO {signup_log} (uid, nid, anon_mail, signup_time, form_data) VALUES (%d, %d, '%s', %d, '%s')", $signup_form['uid'], $signup_form['nid'], $signup_form['signup_anon_mail'], $curtime, $signup_form_data);
    $from = variable_get('site_mail', ini_get('sendmail_from'));

    // Get the hard-coded tokens provided by the signup module to use
    // for the confirmation and/or forwarding emails.  We need to create
    // an object representing the user's signup to get the right values.
    $signup = $user;
    $signup->form_data = $signup_info;
    if (!empty($signup_form['signup_anon_mail'])) {
      $signup->anon_mail = $signup_form['signup_anon_mail'];
    }
    $signup_tokens = _signup_get_email_tokens($node, $signup);

    // The right email to use (anon vs. auth) is one of the tokens.
    $user_mail = $signup_tokens['%user_mail'];
    $node_type_name = node_get_types('name', $node->type);

    // If a confirmation is to be sent, compose the mail message,
    // replace the tokens with the right values, and send it.
    if ($node->signup_send_confirmation && $user_mail) {
      $subject = t('Signup confirmation for !node_type: !title', array(
        '!node_type' => $node_type_name,
        '!title' => $node->title,
      ));
      $message = strtr($node->signup_confirmation_email, $signup_tokens);
      if (module_exists('token')) {

        // If the token.module is enabled, also handle any tokens it provides.
        $message = token_replace($message, 'node', $node);
      }
      drupal_mail('signup_confirmation_mail', $user_mail, $subject, $message, $from);
    }

    // If a forwarding email is to be sent, compose the mail message,
    // replace the tokens with the right values, and send it.
    if ($node->signup_forwarding_email) {
      $header = array(
        'From' => t('New !node_type Signup', array(
          '!node_type' => $node_type_name,
        )) . "<{$from}>",
      );
      $subject = t('Signup confirmation for !node_type: !title', array(
        '!node_type' => $node_type_name,
        '!title' => $node->title,
      ));
      $message = t('The following information was submitted as a signup for !title', array(
        '!title' => $node->title,
      ));
      if (_signup_get_node_scheduler($node) != 'none') {
        $message .= "\n\r" . t('Date/Time: !time', array(
          '!time' => $signup_tokens['%node_start_time'],
        ));
      }
      $message .= "\n\r\n\r" . t('Username: !name', array(
        '!name' => empty($user->uid) ? variable_get('anonymous', t('Anonymous')) : $user->name,
      ));
      if (!empty($user->uid)) {

        // For authenticated users, just include a link to their profile page.
        $message .= "\n\r" . t('Profile page: !url', array(
          '!url' => url('user/' . $user->uid, NULL, NULL, TRUE),
        ));
      }
      else {

        // For anonymous users, their email is all we've got, so disclose it.
        $message .= "\n\r" . t('E-mail: !email', array(
          '!email' => $user_mail,
        ));
      }
      if (!empty($signup_tokens['%user_signup_info'])) {
        $message .= "\n\r\n\r" . $signup_tokens['%user_signup_info'];
      }
      drupal_mail('signup_forwarding_mail', $node->signup_forwarding_email, $subject, $message, $from, $header);
    }
    drupal_set_message(t('Signup to !title confirmed.', array(
      '!title' => l($node->title, "node/{$node->nid}"),
    )) . $confirmation_email . $reminder_email);
    $node->signup_total++;
    if ($node->signup_close_signup_limit) {
      _signup_check_limit($node, 'total');
    }
  }
  else {
    drupal_access_denied();
  }
}

/**
 * Prints the signup details for a single node when the signups tab is clicked
 * @ingroup signup_callback
 */
function signup_node_admin_page($node) {
  drupal_set_title(check_plain($node->title));

  // Administrative table to control signups for this node.
  $signup_node_admin_summary_form = drupal_get_form('signup_node_admin_summary_form', $node);

  // Signup details table, including cancel checkboxes.
  $signup_node_admin_details_form = drupal_get_form('signup_node_admin_details_form', $node);
  if ($node->signup_status) {

    // Add a form to allow the administrator to signup other users.
    $signup_form = drupal_get_form('signup_form', $node, 'admin');
  }
  else {
    $signup_form = '';
  }
  return theme('signup_node_admin_page', $node, $signup_node_admin_summary_form, $signup_node_admin_details_form, $signup_form);
}
function signup_node_admin_details_form($node) {
  unset($_SESSION['signup_cancel_multiple_users']);
  $form = array();

  // Prepare a table header that allows sorting on name and signup time.
  $header = array(
    theme('table_select_header_cell'),
    array(
      'data' => t('Name'),
      'field' => 'u.name',
      'sort' => 'asc',
    ),
    array(
      'data' => t('Signup time'),
      'field' => 's.signup_time',
    ),
    array(
      'data' => t('Extra information'),
    ),
  );
  $sql = "SELECT u.uid, u.name, s.anon_mail, s.signup_time, s.form_data FROM {signup_log} s INNER JOIN {users} u ON u.uid = s.uid WHERE s.nid = %d";
  $sql .= tablesort_sql($header);
  $result = db_query($sql, $node->nid);

  // Loop through the users, unserializing their user data.
  while ($signed_up_user = db_fetch_object($result)) {

    // The username and the unique form identifier are different for
    // anon signups and registered user signups.  For registered users,
    // provide a link to the user profile, and use the uid as the
    // identifier.  For anon, use the user's email address as the
    // identifier and name.
    if ($signed_up_user->uid == 0) {
      $key = '__anon:' . $signed_up_user->anon_mail;
      $username = check_plain($signed_up_user->anon_mail);
    }
    else {
      $key = $signed_up_user->uid;
      $username = theme('username', $signed_up_user);
    }
    $users[$key] = '';
    $form['username'][$key] = array(
      '#value' => $username,
    );
    $form['signup_date'][$key] = array(
      '#value' => format_date($signed_up_user->signup_time, variable_get('signup_date_format', 'small')),
    );
    $form['signup_form_data'][$key] = array(
      '#value' => theme('signup_custom_data', unserialize($signed_up_user->form_data)),
    );
  }
  if (empty($users)) {
    $form['no_users'] = array(
      '#value' => t('No users have signed up for this %node_type.', array(
        '%node_type' => node_get_types('name', $node->type),
      )),
    );
  }
  else {
    $form['nid'] = array(
      '#type' => 'hidden',
      '#value' => $node->nid,
    );
    $form['users'] = array(
      '#type' => 'checkboxes',
      '#options' => $users,
    );
    $form['submit_cancel'] = array(
      '#type' => 'submit',
      '#value' => t('Cancel signups'),
    );
    $form['#header'] = $header;
  }
  return $form;
}
function signup_node_admin_details_form_validate($form_id, $form_values) {
  $users = array_filter($form_values['users']);
  if (empty($users)) {
    form_set_error('', t('No users selected.'));
  }
}

/**
 * Submit handler for cancelling multiple signups via node/N/signups.
 *
 * This saves the selected users into SESSION and redirects to a confirm form
 * which is registered at node/N/signups/confirm.
 */
function signup_node_admin_details_form_submit($form_id, $form_values) {
  $_SESSION['signup_cancel_multiple_users'] = array_filter($form_values['users']);
  return 'node/' . $form_values['nid'] . '/signups/confirm';
}

/**
 * Builds the confirm form when canceling multiple signups from node/N/signups.
 */
function signup_cancel_multiple_confirm($node) {
  $form = array();
  $form['nid'] = array(
    '#type' => 'hidden',
    '#value' => $node->nid,
  );
  $form['users'] = array(
    '#prefix' => '<ul>',
    '#suffix' => '</ul>',
    '#tree' => TRUE,
  );
  foreach ($_SESSION['signup_cancel_multiple_users'] as $key) {
    $label = '';
    $matches = array();
    if (preg_match('/__anon:(.*)/', $key, $matches)) {
      $label = t('Anonymous signup: %anon_mail', array(
        '%anon_mail' => $matches[1],
      ));
    }
    else {
      $account = db_fetch_object(db_query("SELECT uid, name FROM {users} WHERE uid = %d", $key));
      $label = theme('username', $account);
    }
    $form['users'][$key] = array(
      '#type' => 'hidden',
      '#value' => $key,
      '#prefix' => '<li>',
      '#suffix' => $label . "</li>\n",
    );
  }
  return confirm_form($form, t('Are you sure you want to cancel signups for these users?'), 'node/' . $node->nid . '/signups', t('This action cannot be undone.'), t('Cancel signups'), t('Keep signups'));
}

/**
 * Submit handler for the confirm form to cancel multiple signups.
 */
function signup_cancel_multiple_confirm_submit($form_id, $form_values) {
  $nid = $form_values['nid'];
  foreach ($form_values['users'] as $key) {
    $matches = array();
    if (preg_match('/__anon:(.*)/', $key, $matches)) {
      $uid = 0;
      $anon_mail = $matches[1];
    }
    else {
      $uid = $key;
      $anon_mail = NULL;
    }
    signup_cancel_signup($uid, $nid, $anon_mail);
  }
  unset($_SESSION['signup_cancel_multiple_users']);
  return 'node/' . $nid . '/signups';
}

/**
 * Theme function for the signup administrative tab (node/N/signups).
 *
 * This is responsible for rendering the signup summary form (allows
 * admins to open/close signups, set a signup limit, and see the total
 * number of signups), the table of signup details (generated by
 * signup_node_admin_details_form), and if the node is signup-enabled,
 * the form to signup other users.
 *
 * @param $signup_node_admin_summary_form
 *   The rendered HTML for the signup node summary form (to set the signup
 *   limit, open/close signups, see the total number of signups, etc).
 * @param $signup_node_admin_details_form
 *   The rendered HTML for the signup node details form (to view all the users
 *   who have signed up, their full signup details, and checkboxes to cancel
 *   multiple signups at once.
 * @param $signup_form
 *   If the signups are open on this node, the HTML to signup another user,
 *   otherwise, and empty string.
 */
function theme_signup_node_admin_page($node, $signup_node_admin_summary_form, $signup_node_admin_details_form, $signup_form) {
  $output = '';

  // Administrative summary table to control signups for this node.
  $output .= $signup_node_admin_summary_form;

  // Details for each user who signed up.
  $output .= $signup_node_admin_details_form;

  // Form for admins to signup other users (if signups are still open).
  $output .= $signup_form;
  return $output;
}
function theme_signup_node_admin_details_form($form) {
  $fieldset = array(
    '#title' => t('Signup details'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  if (!empty($form['users']['#options'])) {
    $header = $form['#header'];
    $rows = array();
    foreach ($form['users']['#options'] as $key => $value) {
      $rows[] = array(
        'cancel_checkbox' => drupal_render($form['users'][$key]),
        'username' => drupal_render($form['username'][$key]),
        'signup_date' => drupal_render($form['signup_date'][$key]),
        'signup_form_data' => drupal_render($form['signup_form_data'][$key]),
      );
    }
    $fieldset['#value'] = theme('table', $header, $rows) . drupal_render($form['submit_cancel']);
  }
  else {
    $fieldset['#value'] = '<span>' . drupal_render($form['no_users']) . '</span>';
  }
  return theme('fieldset', $fieldset) . drupal_render($form);
}

/**
 * Renders the HTML for the per-node signup summary administrative form.
 */
function theme_signup_node_admin_summary_form($form) {
  $row = array(
    drupal_render($form['status']),
    drupal_render($form['total']),
    drupal_render($form['limit']),
    drupal_render($form),
  );
  $header = array(
    t('Status'),
    t('Total'),
    t('Limit'),
    t('Operations'),
  );
  $fieldset = array(
    '#title' => t('Signup summary'),
    '#collapsible' => TRUE,
    '#value' => theme('table', $header, array(
      $row,
    )),
  );
  return theme('fieldset', $fieldset);
}
function signup_node_admin_summary_form($node) {
  if ($node->signup_close_signup_limit && $node->signup_total >= $node->signup_close_signup_limit) {
    $form['status'] = array(
      '#value' => t('Closed (limit reached)'),
    );
  }
  else {
    $form['status'] = array(
      '#type' => 'select',
      '#options' => array(
        0 => t('Closed'),
        1 => t('Open'),
      ),
      '#default_value' => $node->signup_status,
    );
  }
  $form['limit'] = array(
    '#type' => 'textfield',
    '#default_value' => $node->signup_close_signup_limit,
    '#size' => 4,
    '#maxlength' => 8,
  );
  $form['total'] = array(
    '#value' => $node->signup_total,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Update'),
  );
  $form['nid'] = array(
    '#type' => 'value',
    '#value' => $node->nid,
  );
  return $form;
}
function signup_node_admin_summary_form_submit($form_id, $form_values) {
  $nid = $form_values['nid'];
  $node = node_load($nid);
  $limit_status = 0;
  if (isset($form_values['limit']) && $form_values['limit'] != $node->signup_close_signup_limit) {
    db_query("UPDATE {signup} SET close_signup_limit = %d WHERE nid = %d", $form_values['limit'], $nid);
    $node->signup_close_signup_limit = $form_values['limit'];
    $limit_status = _signup_check_limit($node, 'limit');
  }

  // Only consider the form's status value if the signup limit didn't
  // touch the status already.
  if (!$limit_status && isset($form_values['status']) && $form_values['status'] != $node->signup_status) {
    if ($form_values['status']) {
      signup_open_signup($nid);
      drupal_set_message(t('Signups opened for !title.', array(
        '!title' => l($node->title, "node/{$node->nid}"),
      )));
    }
    else {
      signup_close_signup($nid);
      drupal_set_message(t('Signups closed for !title.', array(
        '!title' => l($node->title, "node/{$node->nid}"),
      )));
    }
  }
}

/**
 * Checks the signup limit for a given node, and sees if a change in
 * either the limit or total # of signups should result in a change in
 * signup status (open vs. closed) and prints a message indicating
 * what happened.
 *
 * @param $node
 *  The node to update, can be a full $node object or a numeric nid.
 * @param $type
 *  String indicating what changed -- can be either 'limit' or 'total'.
 *
 * @return
 *  A flag indicating what change (if any) to the signup status was
 *  required due to the change in limit.  0 if there was no change, -1
 *  if signups are now closed, and 1 if signups are now open.
 */
function _signup_check_limit($node, $type) {
  $status_change = 0;
  if (is_numeric($node)) {
    $node = node_load($node);
  }
  $node_link = l($node->title, "node/{$node->nid}");
  $limit = $node->signup_close_signup_limit;
  if ($limit) {
    if ($node->signup_total >= $limit) {
      if ($node->signup_status) {
        signup_close_signup($node->nid);
        $status_change = -1;
        drupal_set_message(t('Signup limit reached for !title, signups closed.', array(
          '!title' => $node_link,
        )));
      }
      elseif ($type == 'limit') {

        // This is a weird special case, where signups are already
        // closed, but the admin lowers the limit to the signup total
        // or lower. We need to print a message about this, and also
        // return -1 so that callers know signups must remain closed.
        drupal_set_message(t('Signup limit reached.'));
        $status_change = -1;
      }
    }
    elseif ($node->signup_total < $limit && !$node->signup_status && !_signup_node_completed($node)) {
      signup_open_signup($node->nid);
      $status_change = 1;
      if ($type == 'limit') {
        drupal_set_message(t('Signup limit increased for !title, signups re-opened.', array(
          '!title' => $node_link,
        )));
      }
      else {
        drupal_set_message(t('Total signups for !title now below limit, signups re-opened.', array(
          '!title' => $node_link,
        )));
      }
    }
    elseif ($type == 'limit') {
      drupal_set_message(t('Signup limit updated for !title.', array(
        '!title' => $node_link,
      )));
    }
  }
  elseif ($type == 'limit') {

    // These checks should only happen if the limit was just changed...
    if (!$node->signup_status && !_signup_node_completed($node)) {
      signup_open_signup($node->nid);
      $status_change = 1;
      drupal_set_message(t('Signup limit removed for !title, signups now open.', array(
        '!title' => $node_link,
      )));
    }
    else {
      drupal_set_message(t('Signup limit removed for !title.', array(
        '!title' => $node_link,
      )));
    }
  }
  return $status_change;
}

/**
 * Implementation of hook_forms().
 */
function signup_forms() {
  $args = func_get_args();
  $args = $args[0];
  $form_id = array_shift($args);
  if (strpos($form_id, 'signup_user_cancel_form') !== FALSE) {
    if ($form_id == 'signup_user_cancel_form_' . $args[0]) {
      array_shift($args);

      // Get rid of the extra uid arg.
      $forms[$form_id] = array(
        'callback' => 'signup_user_cancel_form',
        'callback arguments' => $args,
      );
      return $forms;
    }
  }
}
function signup_user_cancel_form($nid, $uid, $anon_mail) {
  $form['#base'] = 'signup_form_cancel';
  $form['nid'] = array(
    '#type' => 'value',
    '#value' => $nid,
  );
  $form['uid'] = array(
    '#type' => 'value',
    '#value' => $uid,
  );
  $form['signup_anon_mail'] = array(
    '#type' => 'value',
    '#value' => $anon_mail,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Cancel signup'),
  );
  return $form;
}

/**
 * Validates an anonymous signup email.
 *
 * @param $nid The node the user is signing up for.
 * @param $anon_mail The anonymous email address to validate.
 * @param $name Optional. The form element being validated.
 *
 * @return Boolean.  TRUE if the address validates, FALSE otherwise.
 */
function signup_validate_anon_email($nid, $anon_mail, $name = FALSE) {
  if (!valid_email_address($anon_mail)) {
    $message = t('Invalid email address entered for signup.');
  }
  elseif (db_num_rows(db_query("SELECT mail FROM {users} WHERE mail = '%s'", $anon_mail))) {
    $message = t('The email address entered belongs to a registered user.');
  }
  elseif (db_num_rows(db_query("SELECT anon_mail FROM {signup_log} WHERE anon_mail = '%s' AND nid = %d", $anon_mail, $nid))) {
    $node = node_load($nid);
    $message = t('The email address entered has already been used to sign up for this %node_type.', array(
      '%node_type' => node_get_types('name', $node->type),
    ));
  }

  // If there's no message, it's a valid email, so return success.
  if (!isset($message)) {
    return TRUE;
  }

  // Depending on how we were called, propagate the error accordinly.
  if ($name) {
    form_set_error($name, $message);
  }
  else {
    drupal_set_message($message, 'error');
  }
  return FALSE;
}

/**
 * @defgroup signup_internal Internal module functions
 */

/**
 * Returns the form for the per-node signup settings.
 *
 * This is shared by the settings page and the node edit page.
 *
 * @param $node
 *   The fully loaded node object if we've got it.
 * @param $node_type
 *   The type of the node.  When creating new content, the caller can know the
 *   node type, even if $node is NULL.
 * @param $has_date
 *   Boolean flag indicating if this node (or site) has signup-aware
 *   date functionality, which is required for reminder emails to be in
 *   the form.
 *
 * @return
 *   The form array for the per-node signup settings.
 *
 * @ingroup signup_internal
 */
function signup_node_settings_form($node = NULL, $node_type = NULL, $has_date = FALSE) {
  if (module_exists('token')) {
    $signup_token_description = t('Supported string substitutions: %node_title, %node_url, %node_start_time, %user_name, %user_mail, %user_signup_info (additional information from the signup form), and any tokens in the %replacement_tokens list.', array(
      '%replacement_tokens' => t('Replacement tokens'),
    ));
  }
  else {
    $signup_token_description = t('Supported string substitutions: %node_title, %node_url, %node_start_time, %user_name, %user_mail, %user_signup_info (additional information from the signup form).');
  }

  // Load the default admin form data for new nodes.
  if (!$node || !$node->signup) {
    $result = db_fetch_object(db_query("SELECT * FROM {signup} WHERE nid = 0"));
    $node->signup_forwarding_email = $result->forwarding_email;
    $node->signup_send_confirmation = $result->send_confirmation;
    $node->signup_confirmation_email = $result->confirmation_email;
    $node->signup_send_reminder = $result->send_reminder;
    $node->signup_reminder_days_before = $result->reminder_days_before;
    $node->signup_reminder_email = $result->reminder_email;
    $node->signup_close_signup_limit = $result->close_signup_limit;
  }
  $form['signup_forwarding_email'] = array(
    '#type' => 'textfield',
    '#title' => t('Send signups to'),
    '#default_value' => $node->signup_forwarding_email,
    '#size' => 40,
    '#maxlength' => 64,
    '#description' => t('Email address where notification of new signups will be sent. Leave blank for no notifications.'),
  );
  $form['signup_send_confirmation'] = array(
    '#type' => 'checkbox',
    '#title' => t('Send confirmation'),
    '#default_value' => $node->signup_send_confirmation,
  );
  $form['signup_confirmation_email'] = array(
    '#type' => 'textarea',
    '#title' => t('Confirmation email'),
    '#default_value' => $node->signup_confirmation_email,
    '#cols' => 40,
    '#rows' => 6,
    '#description' => t('Email sent to user upon signup. !token_description', array(
      '!token_description' => $signup_token_description,
    )),
  );
  if (module_exists('token')) {
    _signup_token_help($form, 'signup_confirmation_token_fieldset');
  }
  if ($has_date) {

    // Define a sub-tree to wrap the next 2 form elements together in an
    // inline div for better display.
    $form['signup_reminder'] = array(
      '#prefix' => '<div class="container-inline">',
      '#suffix' => '</div>',
    );
    $form['signup_reminder']['signup_send_reminder'] = array(
      '#type' => 'checkbox',
      '#title' => t('Send reminder'),
      '#default_value' => $node->signup_send_reminder,
    );
    $options = array();
    for ($i = 1; $i <= 60; $i++) {
      $options[$i] = $i;
    }
    $node_type_name = isset($node_type) ? node_get_types('name', $node_type) : '';
    $form['signup_reminder']['signup_reminder_days_before'] = array(
      '#type' => 'select',
      '#default_value' => $node->signup_reminder_days_before,
      '#options' => $options,
      '#suffix' => !empty($node_type_name) ? t('day(s) before this %node_type', array(
        '%node_type' => $node_type_name,
      )) : t('day(s) before start time'),
    );
    $form['signup_reminder_email'] = array(
      '#type' => 'textarea',
      '#title' => t('Reminder email'),
      '#default_value' => $node->signup_reminder_email,
      '#cols' => 40,
      '#rows' => 6,
      '#description' => !empty($node_type_name) ? t('Email sent to user as a reminder before the %node_type starts. !token_description', array(
        '%node_type' => $node_type_name,
        '!token_description' => $signup_token_description,
      )) : t('Email sent to user as a reminder before the start time. !token_description', array(
        '!token_description' => $signup_token_description,
      )),
    );
    if (module_exists('token')) {
      _signup_token_help($form, 'signup_reminder_token_fieldset');
    }
  }
  $form['signup_close_signup_limit'] = array(
    '#type' => 'textfield',
    '#title' => t('Signup limit'),
    '#default_value' => $node->signup_close_signup_limit,
    '#size' => 4,
    '#maxlength' => 8,
    '#description' => t('Maximum number of users who can sign up before signups are automatically closed. If set to 0, there is no limit.'),
  );
  $form['signup'] = array(
    '#type' => 'hidden',
    '#value' => 1,
  );
  return $form;
}

/**
 * Deprecated function to render signup data in a human-readable form.
 *
 * @see theme_signup_custom_data()
 * @see theme_signup_custom_data_email()
 * @see theme_signup_custom_data_rows()
 */
function signup_build_signup_data($data, $type = 'output') {
  switch ($type) {
    case 'email':
      return theme('signup_custom_data_email', $data);
    case 'table':

      // This function used to take $type == 'table' when it wanted rows. :(
      return theme('signup_custom_data_rows', $data);
    default:
      return theme('signup_custom_data', $data);
  }
}

/**
 * Return the value for the %user_signup_info email token for custom signup data.
 *
 * @param $signup_data
 *   Array of custom data for a particular signup.
 *
 * @see theme_signup_user_form()
 * @see theme_signup_custom_data_email()
 */
function theme_signup_email_token_custom_data($signup_data) {
  return t('SIGNUP INFORMATION') . "\n\r" . theme('signup_custom_data_email', $signup_data);
}

/**
 * Renders custom signup data into unfiltered output for use in email.
 *
 * WARNING: This theme function is recursive (it calls itself for
 * nested data), so if you override it, be sure not to change the part
 * where it does "call_user_func(__FUNCTION__)".
 *
 * @param $data
 *   Array of custom user signup data.
 *
 * @return
 *   Plain text output with newlines.
 *
 * @see theme_signup_user_form()
 */
function theme_signup_custom_data_email($data) {
  $output = '';

  // All of the possible array key values should already be translated as
  // string literals in theme_signup_user_form() via the #title attributes, so
  // passing a variable to t() is actually safe here.  However, to avoid
  // warnings when extracting strings, "hide" the call to t() by using a
  // variable to hold the function name.
  $tr = 't';

  // Loop through each first level element.
  foreach ($data as $key => $value) {
    if (is_array($value)) {

      // Element is nested, render it recursively.
      // Instead of the overhead of theme(), just call ourself directly.
      $output .= "\n\r" . call_user_func(__FUNCTION__, $value) . "\n\r";
    }
    else {
      $output .= $tr($key) . ': ' . $value . "\n\r";
    }
  }
  return $output;
}

/**
 * Renders custom signup user data into a table.
 *
 * @param $data
 *   Array of custom user signup data.
 *
 * @return
 *   The themed table with custom signup user data.
 *
 * @see theme_signup_user_form()
 */
function theme_signup_custom_data_table($data) {
  $output = '';
  if (is_array($data)) {
    $header = array(
      array(
        'data' => t('Your signup information'),
        'colspan' => 2,
      ),
    );
    $rows = theme('signup_custom_data_rows', $data);
    $output .= theme('table', $header, $rows);
  }
  return $output;
}

/**
 * Renders custom signup user data into table rows.
 *
 * WARNING: This theme function is recursive (it calls itself for
 * nested data), so if you override it, be sure not to change the part
 * where it does "call_user_func(__FUNCTION__)".
 *
 * @param $data
 *   Array of custom user signup data.
 *
 * @return
 *   An array of table rows.
 *
 * @see theme_signup_user_form()
 */
function theme_signup_custom_data_rows($data) {
  $rows = array();

  // All of the possible array key values should already be translated as
  // string literals in theme_signup_user_form() via the #title attributes, so
  // passing a variable to t() is actually safe here.  However, to avoid
  // warnings when extracting strings, "hide" the call to t() by using a
  // variable to hold the function name.
  $tr = 't';

  // Loop through each first level element.
  foreach ($data as $key => $value) {
    if (is_array($value)) {

      // Element is nested, render it recursively.
      // Instead of the overhead of theme(), just call ourself directly.
      $rows += call_user_func(__FUNCTION__, $value);
    }
    else {
      $rows[] = array(
        $tr($key) . ':',
        check_plain($value),
      );
    }
  }
  return $rows;
}

/**
 * Renders custom signup user data into a human-readable format.
 *
 * WARNING: This theme function is recursive (it calls itself for
 * nested data), so if you override it, be sure not to change the part
 * where it does "call_user_func(__FUNCTION__)".
 *
 * @param $data
 *   Array of custom user signup data.
 *
 * @return
 *   User data directly formatted in divs.
 *
 * @see theme_signup_user_form()
 */
function theme_signup_custom_data($data) {
  $output = '';

  // All of the possible array key values should already be translated as
  // string literals in theme_signup_user_form() via the #title attributes, so
  // passing a variable to t() is actually safe here.  However, to avoid
  // warnings when extracting strings, "hide" the call to t() by using a
  // variable to hold the function name.
  $tr = 't';

  // Loop through each first level element.
  foreach ($data as $key => $value) {
    $output .= '<div id="' . signup_id_safe($key) . '">';
    if (is_array($value)) {

      // Element is nested, render it recursively.
      // Instead of the overhead of theme(), just call ourself directly.
      $output .= call_user_func(__FUNCTION__, $value);
    }
    else {
      $output .= $tr($key) . ': ' . check_plain($value);
    }
    $output .= "</div>\n";
  }
  return $output;
}

/**
 * Converts an arbitrary string into something safe to use for a CSS id.
 *
 * Stolen wholesale from the Zen theme. ;)
 * @see zen_id_safe()
 */
function signup_id_safe($string) {

  // Replace with dashes anything that isn't a-zA-Z, numbers, dashes, or
  // underscores.
  $string = drupal_strtolower(preg_replace('/[^a-zA-Z0-9_-]+/', '-', $string));

  // If the first character is not a-z, add 'id' in front.
  // Don't use ctype_alpha since it's locale aware.
  if (!ctype_lower($string[0])) {
    $string = 'id' . $string;
  }
  return $string;
}

/**
 * Form builder for the signup broadcast form.
 *
 * @param $node
 *   The node that the broadcast form is being attached to.
 */
function signup_broadcast_form($node) {

  // Seems lame we need this here, but apparently, we do. :(
  drupal_set_title(check_plain($node->title));
  $addresses = signup_get_email_addresses($node->nid);
  if (empty($addresses)) {
    $form['no_users'] = array(
      '#value' => t('No users have signup up for this %node_type.', array(
        '%node_type' => node_get_types('name', $node->type),
      )),
    );
    return $form;
  }
  $tokens = array(
    '%node_title',
    '%node_url',
    '%user_name',
    '%user_mail',
  );
  $tokens = array_merge($tokens, signup_extra_tokens($node));
  sort($tokens);
  if (module_exists('token')) {
    $token_text = t('Supported string substitutions: %tokens, and any tokens in the %replacement_tokens list.', array(
      '%tokens' => implode(', ', $tokens),
      '%replacement_tokens' => t('Replacement tokens'),
    ));
  }
  else {
    $token_text = t('Supported string substitutions: %tokens.', array(
      '%tokens' => implode(', ', $tokens),
    ));
  }
  $form['subject'] = array(
    '#type' => 'textfield',
    '#title' => t('Subject'),
    '#required' => TRUE,
  );
  $form['message'] = array(
    '#type' => 'textarea',
    '#title' => t('Message body'),
    '#required' => TRUE,
    '#description' => t('Body of the email message you wish to send to all users who have signed up for this %node_type. !token_description', array(
      '%node_type' => node_get_types('name', $node->type),
      '!token_description' => $token_text,
    )),
    '#rows' => 10,
  );
  if (module_exists('token')) {
    _signup_token_help($form, 'message_tokens_fieldset');
  }
  $form['copy'] = array(
    '#type' => 'checkbox',
    '#title' => t('Send yourself a copy.'),
  );
  $form['send'] = array(
    '#type' => 'submit',
    '#value' => t('Send'),
  );
  $form['nid'] = array(
    '#type' => 'value',
    '#value' => $node->nid,
  );
  global $user;
  if (user_access('administer site configuration')) {
    $form['from'] = array(
      '#type' => 'textfield',
      '#title' => t('From'),
      '#required' => TRUE,
      '#default_value' => $user->mail,
      '#weight' => '-10',
    );
  }
  else {
    $form['from'] = array(
      '#value' => t('This message will be sent from: %from', array(
        '%from' => $user->mail,
      )),
      '#pre' => '<strong>',
      '#post' => '</strong>',
    );
  }
  return $form;
}

/**
 * Return an array of extra email tokens supported by the given node.
 */
function signup_extra_tokens($node) {
  $scheduler = _signup_get_node_scheduler($node);
  return $scheduler == 'none' ? array() : array(
    '%node_start_time',
  );
}

/**
 * Retrieve a list of all users who have signed up for a node.
 *
 * @param $nid
 *
 * @return An array of objects containing signup data
 */
function signup_get_email_addresses($nid) {
  $signup_data = array();
  $signups = db_query("SELECT u.uid, u.name, u.mail, s_l.anon_mail, s_l.form_data FROM {signup_log} s_l INNER JOIN {users} u ON u.uid = s_l.uid WHERE s_l.nid = %d", $nid);
  while ($signup_entry = db_fetch_object($signups)) {
    $signup_data[] = $signup_entry;
  }
  return $signup_data;
}

/**
 * Send an email message to all those signed up to a node.
 *
 * @param $form_id
 * @param $form_values
 */
function signup_broadcast_form_submit($form_id, $form_values) {
  global $user;
  $addresses = signup_get_email_addresses($form_values['nid']);
  if (is_array($addresses)) {
    if (user_access('administer site configuration')) {
      $from = $form_values['from'];
    }
    else {
      $from = $user->mail;
    }
    $subject = $form_values['subject'];
    $node = node_load($form_values['nid']);
    foreach ($addresses as $signup) {

      // Get the hard-coded tokens provided by the signup module to use
      // for the broadcast email.  This also gives us the email to send to.
      $signup_tokens = _signup_get_email_tokens($node, $signup);
      $user_mail = $signup_tokens['%user_mail'];

      // Replace the signup-provided tokens with the right values.
      $message = strtr($form_values['message'], $signup_tokens);
      if (module_exists('token')) {

        // If the token.module is enabled, also handle any tokens it provides.
        $message = token_replace($message, 'node', $node);
      }
      drupal_mail('signup_broadcast_mail', $user_mail, $subject, $message, $from);
      watchdog('signup', t('Broadcast email for %title sent to %email.', array(
        '%title' => $node->title,
        '%email' => $user_mail,
      )), WATCHDOG_NOTICE, l(t('view'), 'node/' . $node->nid));
    }
    if ($form_values['copy']) {
      $signup_tokens = _signup_get_email_tokens($node, $user);
      $sender_email = $signup_tokens['%user_mail'];
      $message = strtr($form_values['message'], $signup_tokens);
      if (module_exists('token')) {

        // If the token.module is enabled, also handle any tokens it provides.
        $message = token_replace($message, 'node', $node);
      }
      $final_text = theme('signup_broadcast_sender_copy', $form_values['message'], $message);
      drupal_mail('signup_broadcast_mail', $sender_email, $subject, $final_text, $from);
      watchdog('signup', t('Broadcast email copy for %title sent to %email.', array(
        '%title' => $node->title,
        '%email' => $sender_email,
      )), WATCHDOG_NOTICE, l(t('view'), 'node/' . $node->nid));
      drupal_set_message(t('Sent a copy of this message to %email', array(
        '%email' => $sender_email,
      )));
    }
  }
  drupal_set_message(t('Message sent to all users who have signed up'));
}

/**
 * Controls the body of the copy of the broadcast message sent to the sender.
 *
 * @param $raw_message
 *   The raw message typed in by the sender with no tokens replaced.
 * @param $cooked_message
 *   The message after all the tokens have been replaced.
 *
 * @return
 *   The final text to put in the email body sent to the broadcast sender.
 */
function theme_signup_broadcast_sender_copy($raw_message, $cooked_message) {
  $output = t('This is a copy of the signup broadcast you just sent.') . "\n";
  $output .= wordwrap(t('Here is the original text you entered, with none of the tokens replaced:'), 72) . "\n";
  $output .= "----------\n";
  $output .= $raw_message;
  $output .= "\n----------\n\n";
  $output .= wordwrap(t('Here is how the message that was sent to each user looked, with all of the tokens replaced (using your account for the user-related tokens):'), 72) . "\n";
  $output .= "----------\n";
  $output .= $cooked_message;
  $output .= "\n----------\n\n";
  return $output;
}

/**
 * @defgroup signup_views Views-integration hooks
 */

/**
 * Implementation of hook_views_tables().
 *
 * @ingroup signup_views
 * @see _signup_views_tables()
 */
function signup_views_tables() {
  require_once drupal_get_path('module', 'signup') . '/views/views.inc';
  return _signup_views_tables();
}

/**
 * Implementation of hook_views_arguments().
 *
 * @ingroup signup_views
 * @see _signup_views_arguments()
 */
function signup_views_arguments() {
  require_once drupal_get_path('module', 'signup') . '/views/views.inc';
  return _signup_views_arguments();
}

/**
 * Implementation of hook_views_default_views().
 *
 * @ingroup signup_views
 * @see _signup_views_default_views()
 */
function signup_views_default_views() {
  require_once drupal_get_path('module', 'signup') . '/views/views_default.inc';
  return _signup_views_default_views();
}

/**
 * Private function to generate HTML for showing the available tokens.
 *
 * @param $form
 *   Reference to the form array to include the help fieldset in.
 * @param $element_name
 *   Name of the form element to use for the help fieldset.
 */
function _signup_token_help(&$form, $element_name) {
  $form[$element_name] = array(
    '#type' => 'fieldset',
    '#title' => t('Replacement tokens'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form[$element_name]['help_text'] = array(
    '#value' => _signup_build_token_help(),
  );
}

/**
 * Private function to generate HTML for showing the available tokens
 *
 * @return The themed representation of the available tokens.
 */
function _signup_build_token_help() {
  static $help_html = '';
  if (empty($help_html)) {
    $patterns = token_get_list('node');
    foreach ($patterns as $type => $pattern_set) {
      foreach ($pattern_set as $pattern => $description) {
        $tokens[$pattern] = $description;
      }
    }
    $help_html = theme('signup_token_help', $tokens);
  }
  return $help_html;
}

/**
 * theme_signup_token_help()
 *
 * @param $tokens
 *   An array of token patterns mapping to their description
 *
 * @return The themed representation of the tokens.
 */
function theme_signup_token_help($tokens) {
  $tokens_html = "<dl>\n";
  foreach ($tokens as $name => $description) {
    $tokens_html .= '<dt>[' . $name . ']</dt>';
    $tokens_html .= '<dd>' . $description . "</dd>\n";
  }
  $tokens_html .= "</dl>\n";
  return $tokens_html;
}

/**
 * Helper function that returns an array of tokens for signup emails.
 *
 * These are the hard-coded tokens provided by signup.module which are
 * available even when token.module is not enabled.
 *
 * @param $node
 *   An object with info about the signup-enabled node. Must contain
 *   at least 'nid', 'title', and 'TODO: time?' fields.
 * @param $signup
 *   An object with info about the user who signed up.
 *
 * @return
 *   An array of tokens and their replacement values.
 */
function _signup_get_email_tokens($node, $signup) {

  // Tokens about the node itself are easy and don't require any logic.
  $tokens = array(
    '%node_title' => $node->title,
    '%node_url' => url('node/' . $node->nid, NULL, NULL, TRUE),
    '%node_start_time' => signup_format_date($node),
  );

  // Tokens about the user are harder, since we need to potentially do
  // things differently for anonymous vs. authenticated signups, and
  // might want to do other processing on the signup form data.
  // Get the array of custom signup data (if any).
  $signup_data = array();
  if (!empty($signup->form_data)) {
    if (is_array($signup->form_data)) {
      $signup_data = $signup->form_data;
    }
    else {
      $signup_data = unserialize($signup->form_data);
    }
    $tokens['%user_signup_info'] = theme('signup_email_token_custom_data', $signup_data);
  }
  else {
    $tokens['%user_signup_info'] = '';
  }

  // Determine if this is an anon signup or not, and get the right info.
  if (!empty($signup->anon_mail)) {
    $tokens['%user_mail'] = $signup->anon_mail;
    $tokens['%user_name'] = theme('signup_email_token_anonymous_username', $signup_data, $signup->anon_mail);
  }
  else {
    $tokens['%user_mail'] = $signup->mail;
    $tokens['%user_name'] = $signup->name;
  }
  return $tokens;
}

/**
 * Implementation of hook_panels_include_directory().
 *
 * @param $plugin_type
 *   The plugin type for which the Panels engine is currently requesting the
 *   location of an include directory.
 *
 * @return
 *   The location of the include directory for plugin type being requested,
 *   relative to the base directory of the module implementing this hook.
 */
function signup_panels_include_directory($plugin_type) {
  if ($plugin_type == 'content_types') {
    return 'panels/' . $plugin_type;
  }
}

Functions

Namesort descending Description
signup_admin_form
signup_admin_form_extra
signup_admin_form_header
signup_admin_form_sql
signup_admin_form_submit
signup_admin_page Prints the admin signup overview page located at admin/content/signup
signup_alter_node_form Alters the node form to inject the appropriate per-node signup settings.
signup_alter_node_type_form Alters the form for administrator settings per node type. (admin/content/types)
signup_broadcast_form Form builder for the signup broadcast form.
signup_broadcast_form_submit Send an email message to all those signed up to a node.
signup_build_signup_data Deprecated function to render signup data in a human-readable form.
signup_cancel_multiple_confirm Builds the confirm form when canceling multiple signups from node/N/signups.
signup_cancel_multiple_confirm_submit Submit handler for the confirm form to cancel multiple signups.
signup_cancel_signup Callback function for canceling signups
signup_close_signup Callback function for closing signups
signup_content_types Returns a list of content types that have signups enabled
signup_cron Implementation of hook_cron().
signup_extra_tokens Return an array of extra email tokens supported by the given node.
signup_filter_status_form
signup_filter_status_form_submit
signup_form Builder function for the signup form
signup_forms Implementation of hook_forms().
signup_form_alter Implementation of hook_form_alter().
signup_form_cancel Builder function for the cancel signup form
signup_form_cancel_submit Submits the cancel signup form
signup_form_submit Executes the user signup form
signup_form_validate_anon Validates the email address on the anonymous user signup form.
signup_form_validate_username Validates the username on the admin form to signup another user.
signup_get_email_addresses Retrieve a list of all users who have signed up for a node.
signup_help Implementation of hook_help().
signup_id_safe Converts an arbitrary string into something safe to use for a CSS id.
signup_menu Implementation of hook_menu().
signup_nodeapi Implementation of hook_nodeapi().
signup_node_admin_details_form
signup_node_admin_details_form_submit Submit handler for cancelling multiple signups via node/N/signups.
signup_node_admin_details_form_validate
signup_node_admin_page Prints the signup details for a single node when the signups tab is clicked
signup_node_admin_summary_form
signup_node_admin_summary_form_submit
signup_node_settings_form Returns the form for the per-node signup settings.
signup_node_tab
signup_open_signup Callback function for reopening signups
signup_panels_include_directory Implementation of hook_panels_include_directory().
signup_perm Implementation of hook_perm().
signup_save_node Save signup-related information when a node is created or edited.
signup_settings_page Form builder for the settings page under admin/setttings/signup
signup_settings_page_submit Submits the signup settings form
signup_sign_up_user Signs up a user to a node.
signup_user Implementation of hook_user().
signup_user_cancel_form
signup_validate_anon_email Validates an anonymous signup email.
signup_views_arguments Implementation of hook_views_arguments().
signup_views_default_views Implementation of hook_views_default_views().
signup_views_tables Implementation of hook_views_tables().
theme_signup_admin_form
theme_signup_admin_page Controls the output of the signup administration overview page.
theme_signup_anonymous_user_login_text Controls the output for anonymous users who can't signup.
theme_signup_broadcast_sender_copy Controls the body of the copy of the broadcast message sent to the sender.
theme_signup_current_signup Controls the output of the users signup data and optional cancel button.
theme_signup_custom_data Renders custom signup user data into a human-readable format.
theme_signup_custom_data_email Renders custom signup data into unfiltered output for use in email.
theme_signup_custom_data_rows Renders custom signup user data into table rows.
theme_signup_custom_data_table Renders custom signup user data into a table.
theme_signup_email_token_custom_data Return the value for the %user_signup_info email token for custom signup data.
theme_signup_filter_status_form
theme_signup_node_admin_details_form
theme_signup_node_admin_page Theme function for the signup administrative tab (node/N/signups).
theme_signup_node_admin_summary_form Renders the HTML for the per-node signup summary administrative form.
theme_signup_node_output_header Return HTML desired at the top of the signup output for a node.
theme_signup_signups_closed Controls the output displayed if this node is closed for signups.
theme_signup_token_help theme_signup_token_help()
theme_signup_user_list Formats the list of users signed up for a particular node.
theme_signup_view_label Generates the appropriate selector label for a given view.
_signup_build_query Private query builder helper function.
_signup_build_token_help Private function to generate HTML for showing the available tokens
_signup_check_limit Checks the signup limit for a given node, and sees if a change in either the limit or total # of signups should result in a change in signup status (open vs. closed) and prints a message indicating what happened.
_signup_cron_autoclose Helper function that handles auto-closing time-based nodes during cron.
_signup_cron_send_reminders Helper function that sends cron-based reminder e-mails.
_signup_get_email_tokens Helper function that returns an array of tokens for signup emails.
_signup_initialize_scheduler_backend Initialize the necessary scheduler backend(s).
_signup_needs_output Helper function that determines if a given node should have any signup-related output.
_signup_node_output Generate all the signup-related output for a given node.
_signup_print_current_signup Helper function to display the current user's signup information.
_signup_token_help Private function to generate HTML for showing the available tokens.