You are here

casetracker.module in Case Tracker 6

Enables the handling of projects and their cases.

File

casetracker.module
View source
<?php

/**
 * @file
 * Enables the handling of projects and their cases.
 */

/**
 * Implementation of hook_views_api().
 */
function casetracker_views_api() {
  return array(
    'api' => 2,
  );
}

/**
 * Implementation of hook_help().
 */
function casetracker_help($path, $arg) {
  switch ($path) {
    case 'admin/settings/casetracker/states':
      return '<p>' . t('Current Case Tracker case states are listed below.') . '</p>';
    case 'admin/settings/casetracker/states/add':
      return '<p>' . t('You may add a new case state below.') . '</p>';
    case 'admin/settings/casetracker/states/edit/' . arg(4):
      return '<p>' . t('You may edit an existing case state below.') . '</p>';
    case 'admin/settings/casetracker':
      return '<p>' . t('Configure the various Case Tracker options with these settings.') . '</p>';
  }
}

/**
 * Implementation of hook_perm().
 */
function casetracker_perm() {
  return array(
    'administer case tracker',
    'assign cases',
    'change own case project',
    'change any case project',
    'change own case priority',
    'change any case priority',
    'change own case status',
    'change any case status',
    'change own case type',
    'change any case type',
  );
}

/**
 * Implementation of hook_menu().
 */
function casetracker_menu() {

  /* casetracker main settings */
  $items['admin/settings/casetracker'] = array(
    'file' => 'casetracker_admin.inc',
    'access arguments' => array(
      'administer case tracker',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'casetracker_settings',
    ),
    'description' => 'Configure the various Case Tracker options with these settings.',
    'title' => 'Case Tracker',
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/settings/casetracker/settings'] = array(
    'file' => 'casetracker_admin.inc',
    'access arguments' => array(
      'administer case tracker',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'casetracker_settings',
    ),
    'title' => 'Settings',
    'weight' => -10,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );

  /* casetracker state handling */
  $items['admin/settings/casetracker/states'] = array(
    'file' => 'casetracker_admin.inc',
    'access arguments' => array(
      'administer case tracker',
    ),
    'page callback' => 'casetracker_case_state_overview',
    'type' => MENU_LOCAL_TASK,
    'title' => 'Case states',
    'description' => 'Add, edit and delete Case States, Types and Priorities',
  );
  $items['admin/settings/casetracker/states/list'] = array(
    'file' => 'casetracker_admin.inc',
    'access arguments' => array(
      'administer case tracker',
    ),
    'page callback' => 'casetracker_case_state_overview',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'title' => 'Overview',
    'weight' => -10,
    'description' => 'Add, edit and delete Case States, Types and Priorities',
  );
  $items['admin/settings/casetracker/states/add'] = array(
    'file' => 'casetracker_admin.inc',
    'access arguments' => array(
      'administer case tracker',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'casetracker_case_state_edit',
    ),
    'title' => 'Add case state',
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/settings/casetracker/states/edit/%casetracker_case_state'] = array(
    'file' => 'casetracker_admin.inc',
    'access arguments' => array(
      'administer case tracker',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'casetracker_case_state_edit',
      5,
    ),
    'title' => 'Edit case state',
    'type' => MENU_CALLBACK,
  );
  $items['admin/settings/casetracker/states/delete/%casetracker_case_state'] = array(
    'file' => 'casetracker_admin.inc',
    'access arguments' => array(
      'administer case tracker',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'casetracker_case_state_confirm_delete',
      5,
    ),
    'title' => 'Delete case state',
    'type' => MENU_CALLBACK,
  );

  /* casetracker autocomplete */
  $items['casetracker_autocomplete'] = array(
    'title' => 'Case Tracker autocomplete',
    'page callback' => 'casetracker_autocomplete',
    'access callback' => 'user_access',
    'access arguments' => array(
      'assign cases',
    ),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implementation of hook_nodeapi().
 */
function casetracker_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {

  // CASES
  if (casetracker_is_case($node->type)) {
    switch ($op) {
      case 'delete':

        // delete case and its comments.
        $comment_results = db_query("SELECT cid FROM {comments} WHERE nid = %d", $node->nid);
        while ($comment_result = db_fetch_object($comment_results)) {
          db_query("DELETE FROM {casetracker_comment_status} WHERE cid = %d", $comment_result->cid);
        }
        db_query('DELETE FROM {casetracker_case} WHERE nid = %d', $node->nid);
        break;
      case 'presave':

        // $node->casetracker is an Array and we wanna it to be an object.
        $node->casetracker = (object) $node->casetracker;
        break;
      case 'insert':

        // cases: generate a case ID and send it along.
        $record = $node->casetracker;
        $record->assign_to = is_numeric($record->assign_to) ? $record->assign_to : casetracker_get_uid($record->assign_to);
        $record->nid = $node->nid;
        $record->vid = $node->vid;
        drupal_write_record('casetracker_case', $record);
        break;
      case 'load':
        $casetracker = db_fetch_object(db_query('SELECT pid, case_priority_id, case_type_id, assign_to, case_status_id FROM {casetracker_case} WHERE nid = %d AND vid = %d', $node->nid, $node->vid));
        if ($casetracker) {
          return array(
            'casetracker' => $casetracker,
          );
        }
        break;
      case 'update':
        $record = (object) $node->casetracker;
        $record->assign_to = is_numeric($record->assign_to) ? $record->assign_to : casetracker_get_uid($record->assign_to);
        $record->nid = $node->nid;
        $record->vid = $node->vid;
        $primary = $node->revision ? array(
          'nid',
        ) : array(
          'nid',
          'vid',
        );
        drupal_write_record('casetracker_case', $record, $primary);
        break;
      case 'view':

        // On preview the case will be an array, we want an object.
        if (isset($node->build_mode) && $node->build_mode == NODE_BUILD_PREVIEW) {
          $node->casetracker = (object) $node->casetracker;
        }

        // used in the breadcrumb and our theme function, mostly for nid and project number display.
        $project = node_load($node->casetracker->pid);
        if ($page) {
          $trail = array(
            l(t('Home'), NULL),
            l(t('Case Tracker'), 'casetracker/projects'),
            l($project->title, "node/{$node->casetracker->pid}"),
            l(t('All cases'), "casetracker/cases/{$node->casetracker->pid}/all"),
          );
          drupal_set_breadcrumb($trail);
        }
        $node->content['casetracker_case_summary'] = array(
          '#value' => theme('casetracker_case_summary', $node, $project),
          '#weight' => -10,
        );
        break;
    }
  }
  else {
    if (casetracker_is_project($node->type)) {
      switch ($op) {
        case 'delete':

          // projects: delete all the cases under the project and all the comments under each case.
          $case_results = db_query("SELECT nid from {casetracker_case} WHERE pid = %d", $node->nid);
          while ($case_result = db_fetch_object($case_results)) {
            db_query("DELETE FROM {casetracker_case} WHERE nid = %d", $case_result->nid);
            $comment_results = db_query("SELECT cid FROM {comments} WHERE nid = %d", $case_result->nid);
            while ($comment_result = db_fetch_object($comment_results)) {
              db_query("DELETE FROM {casetracker_comment_status} WHERE cid = %d", $comment_result->cid);
            }
            node_delete($case_result->nid);

            // this'll handle comment deletion too.
          }
          break;
        case 'view':
          if ($page) {
            $trail = array(
              l(t('Home'), NULL),
              l(t('Case Tracker'), 'casetracker/projects'),
            );
            drupal_set_breadcrumb($trail);
          }
          $node->content['casetracker_project_summary'] = array(
            '#value' => theme('casetracker_project_summary', $node),
            '#weight' => -10,
          );
          break;
      }
    }
  }
}

/**
 * Implementation of hook_comment().
 */
function casetracker_comment(&$comment, $op) {

  // Load the node here anyway -- it is almost certainly static cached already.
  $node = is_array($comment) ? node_load($comment['nid']) : node_load($comment->nid);

  // Bail if this is not a casetracker node.
  if (!casetracker_is_case($node->type)) {
    return;
  }
  if ($op == 'insert' || $op == 'update') {
    $new = (object) $comment['casetracker'];
    $new->cid = $comment['cid'];
    $new->nid = $comment['nid'];
    $new->vid = $comment['revision_id'];
    $new->state = 1;
    $new->assign_to = casetracker_get_uid($new->assign_to);

    // Populate old state values from node
    $old = $node->casetracker;
    $old->cid = $comment['cid'];
    $old->state = 0;
    drupal_write_record('casetracker_case', $new, array(
      'nid',
      'vid',
    ));
  }
  switch ($op) {
    case 'insert':
      drupal_write_record('casetracker_comment_status', $old);
      drupal_write_record('casetracker_comment_status', $new);
      break;
    case 'update':
      drupal_write_record('casetracker_comment_status', $old, array(
        'cid',
        'state',
      ));
      drupal_write_record('casetracker_comment_status', $new, array(
        'cid',
        'state',
      ));
      break;
    case 'delete':

      // @todo theoretically, if you delete a comment, we should reset all the values
      // to what they were before the comment was submitted. this doesn't happen yet.
      db_query("DELETE FROM {casetracker_comment_status} WHERE cid = %d", $comment->cid);
      break;
    case 'view':

      // If this is a preview we won't have a cid yet.
      if (empty($comment->cid)) {
        $case_data['new'] = (object) $comment->casetracker;
        $case_data['new']->assign_to = casetracker_get_uid($case_data['new']->assign_to);
        $case = node_load($comment->nid);
        $case_data['old'] = drupal_clone($case->casetracker);
      }
      else {
        $results = db_query("SELECT * FROM {casetracker_comment_status} WHERE cid = %d", $comment->cid);
        while ($result = db_fetch_object($results)) {
          $state = $result->state ? 'new' : 'old';
          $case_data[$state] = $result;
        }
      }
      $comment->comment = theme('casetracker_comment_changes', $case_data['old'], $case_data['new']) . $comment->comment;
      break;
  }
}

/**
 * Implementation of hook_form_alter().
 */
function casetracker_form_alter(&$form, &$form_state, $form_id) {
  if (!empty($form['#node'])) {
    $node = $form['#node'];

    // Add case options to our basic case type.
    if (casetracker_is_case($node->type)) {
      $count = count(casetracker_project_options());
      if ($count == 0) {
        drupal_set_message(t('You must create a project before adding cases.'), 'error');
        return;
      }
      else {
        $default_project = null;
        if (!isset($form['#node']->nid) && is_numeric(arg(3))) {
          $default_project = arg(3);
        }
        casetracker_case_form_common($form, $default_project);
      }
    }
  }
}

/**
 * Implementation of hook_form_comment_form_alter().
 */
function casetracker_form_comment_form_alter(&$form, &$form_state) {
  $node = isset($form['nid']['#value']) ? node_load($form['nid']['#value']) : NULL;
  if (casetracker_is_case($node->type)) {
    $form['#node'] = $node;

    // add case options to the comment form.
    casetracker_case_form_common($form);

    // necessary for our casetracker_comment() callback.
    $form['revision_id'] = array(
      '#type' => 'hidden',
      '#value' => $node->vid,
    );
  }
}

/**
 * Common form elements for cases, generic enough for use either in
 * a full node display, or in comment displays and updating. Default
 * values are calculated based on an existing $form['nid']['#value'].
 *
 * @param $form
 *   A Forms API $form, as received from a hook_form_alter().
 * @param $default_project
 *   The project ID that should be pre-selected.
 * @return $form
 *   A modified Forms API $form.
 */
function casetracker_case_form_common(&$form, $default_project = NULL) {
  global $user;
  $node = $form['#node'];

  // On preview the case will be an array, we want an object.
  if (isset($node->build_mode) && $node->build_mode == NODE_BUILD_PREVIEW) {
    $node->casetracker = (object) $node->casetracker;
  }

  // project to set as the default is based on how the user got here.
  if (empty($default_project) && !empty($node->casetracker->pid)) {
    $default_project = $node->casetracker->pid;
  }
  $project_options = casetracker_project_options();
  $form['casetracker'] = array(
    '#type' => 'fieldset',
    '#title' => t('Case information'),
    '#weight' => -10,
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
    '#tree' => TRUE,
    '#theme' => 'casetracker_case_form_common',
  );

  // if there's no project ID from the URL, or more than one project,
  // we'll create a select menu for the user; otherwise, we'll save
  // the passed (or only) project ID into a hidden field.
  if (count($project_options) > 1) {
    $form['casetracker']['pid'] = array(
      '#title' => t('Project'),
      '#type' => 'select',
      '#default_value' => $default_project,
      '#options' => $project_options,
      '#disabled' => !user_access('change any case project') && !(user_access('change any case project') && $node->uid == $user->uid),
    );
  }
  else {
    $form['casetracker']['pid'] = array(
      '#type' => 'value',
      // default value, or the only the project ID in the project_options array.
      '#value' => !empty($default_project) ? $default_project : key($project_options),
    );
  }

  // Retrieve the assign_to default value.
  if (isset($node->casetracker->assign_to)) {
    $default_assign_to = is_numeric($node->casetracker->assign_to) ? casetracker_get_name($node->casetracker->assign_to) : $node->casetracker->assign_to;
  }
  else {
    $default_assign_to = casetracker_default_assign_to();
  }

  // Only show this element if the user has access.
  $form['casetracker']['assign_to'] = array(
    '#title' => t('Assign to'),
    '#required' => TRUE,
    '#access' => user_access('assign cases'),
  );

  // Use different widgets based on the potential assignees.
  $options = drupal_map_assoc(casetracker_user_options());
  $assign_to_widget = variable_get('casetracker_assign_to_widget', 'flexible');
  if ($assign_to_widget == 'flexible' && count($options) < 25 || $assign_to_widget == 'radios') {
    $form['casetracker']['assign_to']['#type'] = 'radios';
    $form['casetracker']['assign_to']['#options'] = $options;
  }
  else {
    if ($assign_to_widget == 'flexible' && count($options) < 50 || $assign_to_widget == 'select') {
      $form['casetracker']['assign_to']['#type'] = 'select';
      $form['casetracker']['assign_to']['#options'] = $options;
    }
    else {
      $form['casetracker']['assign_to']['#type'] = 'textfield';
      $form['casetracker']['assign_to']['#autocomplete_path'] = 'casetracker_autocomplete';
      $form['casetracker']['assign_to']['#size'] = 12;
    }
  }

  // Set the default value if it is valid.
  $form['casetracker']['assign_to']['#default_value'] = in_array($default_assign_to, $options, TRUE) ? $default_assign_to : NULL;
  $case_status_options = casetracker_realm_load('status');
  $default_status = !empty($node->casetracker->case_status_id) ? $node->casetracker->case_status_id : variable_get('casetracker_default_case_status', key($case_status_options));
  $form['casetracker']['case_status_id'] = array(
    '#type' => 'select',
    '#title' => t('Status'),
    '#options' => $case_status_options,
    '#default_value' => $default_status,
    '#disabled' => !user_access('change any case status') && !(user_access('change own case status') && $node->uid == $user->uid),
  );
  $case_priority_options = casetracker_realm_load('priority');
  $default_priority = !empty($node->casetracker->case_priority_id) ? $node->casetracker->case_priority_id : variable_get('casetracker_default_case_priority', key($case_priority_options));
  $form['casetracker']['case_priority_id'] = array(
    '#type' => 'select',
    '#title' => t('Priority'),
    '#options' => $case_priority_options,
    '#default_value' => $default_priority,
    '#disabled' => !user_access('change any case priority') && !(user_access('change own case priority') && $node->uid == $user->uid),
  );
  $case_type_options = casetracker_realm_load('type');
  $default_type = !empty($node->casetracker->case_type_id) ? $node->casetracker->case_type_id : variable_get('casetracker_default_case_type', key($case_type_options));
  $form['casetracker']['case_type_id'] = array(
    '#type' => 'select',
    '#title' => t('Type'),
    '#options' => $case_type_options,
    '#default_value' => $default_type,
    '#disabled' => !user_access('change any case type') && !(user_access('change own case type') && $node->uid == $user->uid),
  );
  return $form;
}

/**
* Implementation of hook_block
*/
function casetracker_block($op = 'list', $delta = 0, $edit = array()) {
  switch ($op) {
    case 'list':
      $blocks[0] = array(
        'info' => t('Jump to case'),
      );
      return $blocks;
    case 'configure':
      return array();
    case 'save':
      return;
    case 'view':
      switch ($delta) {
        case 0:
          if (user_access('access content')) {
            $block['subject'] = t('Jump to case');
            $block['content'] = drupal_get_form('casetracker_block_jump_to_case_number');
          }
          break;
      }
      return $block;
  }
}

/**
 * Form for "Jump to case number" block.
 */
function casetracker_block_jump_to_case_number() {
  $form = array();
  $form['case_number'] = array(
    '#maxlength' => 60,
    '#required' => TRUE,
    '#size' => 15,
    '#title' => t('Case number'),
    '#type' => 'textfield',
    '#prefix' => '<div class="container-inline">',
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Go'),
    '#suffix' => '</div>',
  );
  return $form;
}

/**
 * Submit function for our "Jump to case number" block.
 */
function casetracker_block_jump_to_case_number_submit($form, $form_state) {
  list($pid, $nid) = explode('-', $form_state['values']['case_number']);
  $case_nid = db_result(db_query("SELECT nid FROM {casetracker_case} WHERE pid = %d AND nid = %d", $pid, $nid));
  if (!$case_nid) {
    drupal_set_message(t('Your case number was not found.'), 'error');
    return;
  }
  drupal_goto('node/' . $case_nid);
}

/**
 * CASE STATE CRUD ====================================================
 */

/**
 * Returns information about the various case states and their options.
 * The number of parameters passed will determine the return value.
 *
 * @param $csid
 *   Optional; the state ID to return from the passed $realm.
 * @param $realm
 *   Optional; the name of the realm ('status', 'priority', or 'type').
 * @param $reset
 *   Optional; set to TRUE to reset the static cache.
 *
 * @return $values
 *   If only $realm is passed, you'll receive an array with the keys
 *   being the state ID and the values being their names. If a $csid
 *   is also passed, you'll receive just a string of the state name.
 *   If ONLY a $csid is passed, we'll return a list of 'name', 'realm'.
 */
function casetracker_case_state_load($csid = NULL, $realm = NULL, $reset = FALSE) {
  static $states_lookup;
  if (!$states_lookup || $reset) {
    $results = db_query("SELECT csid, case_state_name AS name, case_state_realm AS realm, weight \n                         FROM {casetracker_case_states} ORDER BY weight");
    $states_lookup = array();
    while ($row = db_fetch_object($results)) {
      $row->display = casetracker_tt("case_states:{$row->csid}:name", $row->name);
      $states_lookup[$row->realm][$row->csid] = $states_lookup['all'][$row->csid] = $row;
    }
  }
  if ($csid && $realm) {
    return $states_lookup['all'][$csid]->display;
  }
  elseif ($csid && !$realm) {
    return $states_lookup['all'][$csid];
  }
  elseif (!$csid && $realm) {
    $options = array();

    // suitable for form api.
    if (!empty($states_lookup[$realm])) {
      foreach ($states_lookup[$realm] as $state) {
        $options[$state->csid] = $state->display;
      }
    }
    return $options;
  }
}

/**
 * Translate user defined string. Wrapper function for tt() if i18nstrings enabled.
 * 
 * The string id for case states will be: case:[realm]#[csid]:name
 * 
 * @param $name
 *   String id without 'casetracker', which will be prepended automatically
 */
function casetracker_tt($name, $string, $langcode = NULL) {
  return function_exists('i18nstrings') ? i18nstrings('casetracker:' . $name, $string, $langcode) : $string;
}

/**
 * Implementation of hook_locale().
 */
function casetracker_locale($op = 'groups', $group = NULL) {
  switch ($op) {
    case 'groups':
      return array(
        'casetracker' => t('Case Tracker'),
      );
    case 'info':
      $info['casetracker']['refresh callback'] = 'casetracker_locale_refresh';
      $info['casetracker']['format'] = FALSE;
      return $info;
  }
}

/**
 * Refresh locale strings.
 */
function casetracker_locale_refresh() {
  $results = db_query("SELECT csid, case_state_name AS name, case_state_realm AS realm FROM {casetracker_case_states}");
  while ($row = db_fetch_object($results)) {
    i18nstrings_update("casetracker:case_states:{$row->csid}:name", $row->name);
  }

  // Meaning it completed with no issues. @see i18nmenu_locale_refresh().
  return TRUE;
}

/**
 * Load states for a particular realm. Wrapper around casetracker_case_state_load()
 *
 * @param $realm
 *   Name of the realm ('status', 'priority', or 'type').
 *
 * @return
 *   array with the keys being the state ID and the values being their names.
 */
function casetracker_realm_load($realm) {
  return casetracker_case_state_load(null, $realm);
}

/**
 * Saves a case state.
 *
 * @param $case_state
 *   An array containing 'name' and 'realm' keys. If no 'csid'
 *   is passed, a new state is created, otherwise, we'll update
 *   the record that corresponds to that ID.
 */
function casetracker_case_state_save($case_state = NULL) {
  if (!$case_state['name'] || !$case_state['realm']) {
    return NULL;
  }

  // Need to collect information into another array since the db columns have different names : (
  $record = array(
    'case_state_name' => $case_state['name'],
    'case_state_realm' => $case_state['realm'],
    'weight' => $case_state['weight'],
  );
  if (isset($case_state['csid'])) {
    $record['csid'] = $case_state['csid'];
    drupal_write_record('casetracker_case_states', $record, array(
      'csid',
    ));
  }
  else {
    drupal_write_record('casetracker_case_states', $record);
  }

  // Update translations
  if (function_exists('i18nstrings_update')) {
    i18nstrings_update('casetracker:case_states:' . $record['csid'] . ':name', $case_state['name']);
  }
  return $result;
}

/**
 * Deletes a case state.
 *
 * @todo There is currently no attempt to do anything with cases which
 * have been assigned the $csid that is about to be deleted. We should
 * reset them to the default per our settings (and warn the user on our
 * confirmation page), or something else entirely.
 * 
 * @param $csid
 *   The case state ID to delete.
 */
function casetracker_case_state_delete($csid = NULL) {
  if (!empty($csid)) {
    db_query('DELETE FROM {casetracker_case_states} WHERE csid = %d', $csid);
  }
}

/**
 * COMMENT DISPLAY ====================================================
 */

/**
 * Retrieve autocomplete suggestions for assign to user options.
 *
 * @TODO: In order to get this down to 1 query and respect any custom
 * views selected for use as user option filters, we need to:
 * - Submit a patch to the Views user name filter/argument handler to support LIKE filtering.
 * - Ensure that the custom view uses this handler or add it if does not.
 * - Generate the query & result set using this modified View.
 */
function casetracker_autocomplete($string) {
  $matches = array();
  $options = casetracker_user_options();
  $result = db_query_range("SELECT name FROM {users} WHERE LOWER(name) LIKE LOWER('%s%%')", $string, 0, 10);
  while ($user = db_fetch_object($result)) {
    if (in_array($user->name, $options, TRUE)) {
      $matches[$user->name] = check_plain($user->name);
    }
  }

  // Special case for 'Unassigned'
  $unassigned = t('Unassigned');
  if (strpos(strtolower($unassigned), strtolower($string)) !== FALSE) {
    $matches[$unassigned] = $unassigned;
  }
  drupal_json($matches);
}

/**
 * Returns an query string needed in case of Organic Groups
 * providing preselected audience checkboxes for projects as groups (og)
 * 
 * @param   object  CT project
 * @return  string
 */
function _casetracker_get_og_query_string(&$project) {
  $querystring = array();

  // checking if project is group
  if ($project->type == 'group') {
    $querystring[] = 'gids[]=' . $project->nid;

    //checking if group-project is part of another group
    if (isset($project->og_groups) && is_array($project->og_groups)) {
      foreach ($project->og_groups as $group) {
        $querystring[] = 'gids[]=' . $group;
      }
    }
  }
  elseif (isset($project->og_groups) && is_array($project->og_groups) && $project->type !== 'group') {
    foreach ($project->og_groups as $group) {
      $querystring[] = 'gids[]=' . $group;
    }
  }
  return 0 < count($querystring) ? implode('&', $querystring) : NULL;
}

/**
 * THEME ==============================================================
 */

/**
 * Implementation of hook_theme
 */
function casetracker_theme() {
  return array(
    'casetracker_comment_changes' => array(),
    'casetracker_case_form_common' => array(),
    'casetracker_case_summary' => array(),
    'casetracker_project_summary' => array(),
  );
}

/**
 * Displays the changes a comment has made to the case fields.
 *
 * @param $case_data
 *   An array of both 'old' and 'new' objects that contains
 *   the before and after values this comment has changed.
 */
function theme_casetracker_comment_changes($old, $new) {
  $rows = array();
  $fields = array(
    'pid' => t('Project'),
    'title' => t('Title'),
    'case_status_id' => t('Status'),
    'assign_to' => t('Assigned'),
    'case_priority_id' => t('Priority'),
    'case_type_id' => t('Type'),
  );
  foreach ($fields as $field => $label) {
    if ($new->{$field} != $old->{$field}) {
      switch ($field) {
        case 'pid':
          $old_title = db_result(db_query("SELECT title FROM {node} WHERE nid = %d", $old->pid));
          $new_title = db_result(db_query("SELECT title FROM {node} WHERE nid = %d", $new->pid));
          $old->{$field} = l($old_title, "node/{$old->pid}");
          $new->{$field} = l($new_title, "node/{$new->pid}");
          break;
        case 'case_status_id':
          $old->{$field} = check_plain(casetracker_case_state_load($old->{$field}, 'status'));
          $new->{$field} = check_plain(casetracker_case_state_load($new->{$field}, 'status'));
          break;
        case 'assign_to':
          $old->{$field} = check_plain(casetracker_get_name($old->{$field}));
          $new->{$field} = check_plain(casetracker_get_name($new->{$field}));
          break;
        case 'case_priority_id':
          $old->{$field} = check_plain(casetracker_case_state_load($old->{$field}, 'priority'));
          $new->{$field} = check_plain(casetracker_case_state_load($new->{$field}, 'priority'));
          break;
        case 'case_type_id':
          $old->{$field} = check_plain(casetracker_case_state_load($old->{$field}, 'type'));
          $new->{$field} = check_plain(casetracker_case_state_load($new->{$field}, 'type'));
          break;
      }
      $rows[] = array(
        t('@label: !old &raquo; !new', array(
          '@label' => $label,
          '!old' => $old->{$field},
          '!new' => $new->{$field},
        )),
      );
    }
  }
  if (!empty($rows)) {
    return theme('table', NULL, $rows, array(
      'class' => 'case_changes',
    ));
  }
}

/**
 * Theme function for cleaning up the casetracker common form.
 */
function theme_casetracker_case_form_common($form) {
  drupal_add_css(drupal_get_path('module', 'casetracker') . '/casetracker.css');
  $output = '';
  $output .= drupal_render($form['pid']);
  $output .= drupal_render($form['case_title']);
  if ($form['assign_to']['#type'] == 'radios') {
    if ($form['assign_to']['#access']) {
      $header = array_fill(0, 5, array());
      $header[0] = $form['assign_to']['#title'];
      $radios = array();
      foreach (element_children($form['assign_to']) as $id) {
        $radios[] = drupal_render($form['assign_to'][$id]);
      }
      $radios = array_chunk($radios, 5);
      $output .= theme('table', $header, $radios, array(
        'class' => 'casetracker-assign-to',
      ));
    }
    drupal_render($form['assign_to']);
  }
  else {
    $output .= drupal_render($form['assign_to']);
  }
  $row = array();
  foreach (element_children($form) as $id) {
    if (!in_array($id, array(
      'pid',
      'case_title',
      'assign_to',
    ))) {
      $row[] = drupal_render($form[$id]);
    }
  }
  $rows = array(
    $row,
  );
  $output .= theme('table', array(), $rows);
  $output .= drupal_render($form);
  return $output;
}

/**
 * Theme the case summary shown at the beginning of a case's node.
 *
 * @param $case
 *   The node object of the case being viewed.
 * @param $project
 *   The node object of the project this case belongs to.
 */
function theme_casetracker_case_summary($case, $project) {
  $last_comment = db_result(db_query('SELECT last_comment_timestamp FROM {node_comment_statistics} WHERE nid = %d', $case->nid));
  $rows = array();

  // On node preview the form logic can't translate assign_to back to a uid for
  // us so we need to be able handle it either way.
  if (is_numeric($case->casetracker->assign_to)) {
    $assign_to = user_load(array(
      'uid' => $case->casetracker->assign_to,
    ));
  }
  else {
    $assign_to = user_load(array(
      'name' => $case->casetracker->assign_to,
    ));
  }
  if (empty($assign_to) || $assign_to->uid == 0) {
    $rows[] = array(
      t('Assigned to:'),
      '<em>' . t('Unassigned') . '</em>',
    );
  }
  else {
    $rows[] = array(
      t('Assigned to:'),
      theme('username', $assign_to),
    );
  }
  $rows[] = array(
    t('Created:'),
    t('!username at !date', array(
      '!username' => theme('username', $case),
      '!date' => format_date($case->created, 'medium'),
    )),
  );
  $rows[] = array(
    t('Status:'),
    t('<strong>@status</strong> (@type / Priority @priority)', array(
      '@status' => casetracker_case_state_load($case->casetracker->case_status_id, 'status'),
      '@type' => casetracker_case_state_load($case->casetracker->case_type_id, 'type'),
      '@priority' => casetracker_case_state_load($case->casetracker->case_priority_id, 'priority'),
    )),
  );

  // On node preview a case may not have a nid, so we use some placeholder text.
  $case_id = isset($case->nid) ? $case->nid : t("NEW");
  $rows[] = array(
    t('Case ID:'),
    l($project->title, 'node/' . $case->casetracker->pid) . ': ' . $project->nid . '-' . $case_id,
  );
  if ($last_comment != $case->created) {
    $rows[] = array(
      t('Last modified:'),
      format_date($last_comment, 'medium'),
    );
  }
  $output = '<div class="case">';
  $output .= theme('table', NULL, $rows, array(
    'class' => 'summary',
  ));
  $output .= '</div>';
  return $output;
}

/**
 * Theme the project summary shown at the beginning of a project's node.
 *
 * @param $project
 *   The node object of the project being viewed.
 */
function theme_casetracker_project_summary($project) {
  $rows = array();
  $rows[] = array(
    t('Project number:'),
    $project->nid,
  );
  $rows[] = array(
    t('Opened by:'),
    theme('username', $project),
  );
  $rows[] = array(
    t('Opened on:'),
    format_date($project->created, 'large'),
  );
  $rows[] = array(
    t('Last modified:'),
    format_date($project->changed, 'large'),
  );
  $querystring = _casetracker_get_og_query_string($project);
  $operations = array();
  $node_types = node_get_types('names');
  foreach (array_filter(variable_get('casetracker_case_node_types', array(
    'casetracker_basic_case',
  ))) as $type) {
    $operations[] = l(t('add !name', array(
      '!name' => $node_types[$type],
    )), 'node/add/' . str_replace('_', '-', $type) . '/' . $project->nid, array(
      'query' => $querystring,
    ));
  }
  $operations = implode(' | ', $operations);

  // ready for printing in our Operations table cell - delimited by a pipe. nonstandard.
  $rows[] = array(
    t('Operations:'),
    $operations . ' | ' . l(t('view all project cases'), 'casetracker', array(
      'query' => 'keys=&pid=' . $project->nid,
    )),
  );
  $output = '<div class="project">';
  $output .= theme('table', NULL, $rows, array(
    'class' => 'summary',
  ));
  $output .= '</div>';
  return $output;
}

/**
 * API FUNCTIONS ======================================================
 */

/**
 * API function that returns valid project options.
 */
function casetracker_project_options() {
  $projects = array();

  // Fetch the views list of projects, which is space-aware.
  if ($view = views_get_view(variable_get('casetracker_view_project_options', 'casetracker_project_options'))) {
    $view
      ->set_display();
    $view
      ->set_items_per_page(0);
    $view
      ->execute();
    foreach ($view->result as $row) {
      $projects[$row->nid] = $row->node_title;
    }
  }
  return $projects;
}

/**
 * API function that returns valid user options.
 */
function casetracker_user_options() {
  $users = array();
  $options = array();
  if ($view = views_get_view(variable_get('casetracker_view_assignee_options', 'casetracker_assignee_options'))) {
    $view
      ->set_display();
    $view
      ->set_items_per_page(0);
    $view
      ->execute();
    foreach ($view->result as $row) {
      $options[$row->uid] = $row->users_name;
    }
  }
  $anon_user = casetracker_default_assign_to();

  // fill in "Unassigned" value because view is not rendered and the redundant option in views is irrelevant
  // @TODO render the view before display so this isn't needed.
  if (isset($options[0])) {
    $options[0] = $anon_user;
  }
  elseif (in_array(variable_get('casetracker_default_assign_to', $anon_user), array(
    $anon_user,
    variable_get('anonymous', t('Anonymous')),
  ))) {
    $options = array(
      $anon_user,
    ) + $options;
  }
  return $options;
}

/**
 * API function for checking whether a node type is a casetracker case.
 */
function casetracker_is_case($node) {
  if (is_object($node) && !empty($node->type)) {
    $type = $node->type;
  }
  else {
    if (is_string($node)) {
      $type = $node;
    }
  }
  if ($type) {
    return in_array($type, variable_get('casetracker_case_node_types', array(
      'casetracker_basic_case',
    )), TRUE);
  }
  return FALSE;
}

/**
 * API function for checking whether a node type is a casetracker project.
 */
function casetracker_is_project($node) {
  if (is_object($node) && !empty($node->type)) {
    $type = $node->type;
  }
  else {
    if (is_string($node)) {
      $type = $node;
    }
  }
  if ($type) {
    return in_array($type, variable_get('casetracker_project_node_types', array(
      'casetracker_basic_project',
    )), TRUE);
  }
  return FALSE;
}

/**
 * Given a user name, returns the uid of that account.
 * If the passed name is not found, returns 0.
 * See also casetracker_get_name().
 */
function casetracker_get_uid($name = NULL, $reset = FALSE) {
  static $users = array();
  if (!isset($users[$name]) || $reset) {
    $result = db_result(db_query("SELECT uid FROM {users} WHERE name = '%s'", $name));
    $users[$name] = $result ? $result : 0;
  }
  return $users[$name];
}

/**
 * Given a uid, returns the name of that account. If the passed uid is
 * not found, returns the default "assign to" name as specified in the
 * settings. @todo This may not always be desired, but is how we use it.
 * See also casetracker_get_uid().
 */
function casetracker_get_name($uid = NULL, $reset = FALSE) {
  static $users = array();
  if (!isset($users[$uid]) || $reset) {
    if ($uid == 0) {
      $users[0] = t('Unassigned');
    }
    else {
      $result = db_result(db_query("SELECT name FROM {users} WHERE uid = %d", $uid));
      $users[$uid] = $result ? $result : '';
    }
  }
  return !empty($users[$uid]) ? $users[$uid] : casetracker_default_assign_to();
}

/**
 * Fetch the proper default assignee.
 */
function casetracker_default_assign_to() {
  $assign_to = variable_get('casetracker_default_assign_to', t('Unassigned'));
  if ($assign_to == variable_get('anonymous', t('Anonymous'))) {
    return t('Unassigned');
  }
  return $assign_to;
}

/**
 * Implementation of hook_token_values().
 */
function casetracker_token_values($type, $object = NULL) {
  module_load_include('inc', 'casetracker', 'casetracker.token');
  return _casetracker_token_values($type, $object);
}

/**
 * Implementation of hook_token_list().
 */
function casetracker_token_list($type = 'all') {
  module_load_include('inc', 'casetracker', 'casetracker.token');
  return _casetracker_token_list($type);
}

Functions

Namesort descending Description
casetracker_autocomplete Retrieve autocomplete suggestions for assign to user options.
casetracker_block Implementation of hook_block
casetracker_block_jump_to_case_number Form for "Jump to case number" block.
casetracker_block_jump_to_case_number_submit Submit function for our "Jump to case number" block.
casetracker_case_form_common Common form elements for cases, generic enough for use either in a full node display, or in comment displays and updating. Default values are calculated based on an existing $form['nid']['#value'].
casetracker_case_state_delete Deletes a case state.
casetracker_case_state_load Returns information about the various case states and their options. The number of parameters passed will determine the return value.
casetracker_case_state_save Saves a case state.
casetracker_comment Implementation of hook_comment().
casetracker_default_assign_to Fetch the proper default assignee.
casetracker_form_alter Implementation of hook_form_alter().
casetracker_form_comment_form_alter Implementation of hook_form_comment_form_alter().
casetracker_get_name Given a uid, returns the name of that account. If the passed uid is not found, returns the default "assign to" name as specified in the settings. @todo This may not always be desired, but is how we use it. See also casetracker_get_uid().
casetracker_get_uid Given a user name, returns the uid of that account. If the passed name is not found, returns 0. See also casetracker_get_name().
casetracker_help Implementation of hook_help().
casetracker_is_case API function for checking whether a node type is a casetracker case.
casetracker_is_project API function for checking whether a node type is a casetracker project.
casetracker_locale Implementation of hook_locale().
casetracker_locale_refresh Refresh locale strings.
casetracker_menu Implementation of hook_menu().
casetracker_nodeapi Implementation of hook_nodeapi().
casetracker_perm Implementation of hook_perm().
casetracker_project_options API function that returns valid project options.
casetracker_realm_load Load states for a particular realm. Wrapper around casetracker_case_state_load()
casetracker_theme Implementation of hook_theme
casetracker_token_list Implementation of hook_token_list().
casetracker_token_values Implementation of hook_token_values().
casetracker_tt Translate user defined string. Wrapper function for tt() if i18nstrings enabled.
casetracker_user_options API function that returns valid user options.
casetracker_views_api Implementation of hook_views_api().
theme_casetracker_case_form_common Theme function for cleaning up the casetracker common form.
theme_casetracker_case_summary Theme the case summary shown at the beginning of a case's node.
theme_casetracker_comment_changes Displays the changes a comment has made to the case fields.
theme_casetracker_project_summary Theme the project summary shown at the beginning of a project's node.
_casetracker_get_og_query_string Returns an query string needed in case of Organic Groups providing preselected audience checkboxes for projects as groups (og)