[ Index ]

PHP Cross Reference of Drupal 6 (gatewave)

title

Body

[close]

/sites/all/modules/project_issue/ -> project_issue.module (source)

   1  <?php
   2  // $Id: project_issue.module,v 1.177 2010/01/30 19:01:33 dww Exp $
   3  
   4  // issue nodes      -> project_issues
   5  // issue comments   -> project_issue_comments
   6  
   7  /// Default age in days of issues to auto close.
   8  define('PROJECT_ISSUE_AUTO_CLOSE_DAYS', 14);
   9  /// Project issue state = fixed.
  10  define('PROJECT_ISSUE_STATE_FIXED', 2);
  11  /// Project issue state = closed.
  12  define('PROJECT_ISSUE_STATE_CLOSED', 7);
  13  
  14  /**
  15   * Implementation of hook_init().
  16   */
  17  function project_issue_init() {
  18    /// @TODO: we need a real page split instead of this.
  19    module_load_include('inc', 'project_issue', 'issue');
  20    foreach (array('comment', 'mail') as $file) {
  21      module_load_include('inc', 'project_issue', "includes/$file");
  22    }
  23    /// @TODO: this should only be done on pages that need it.
  24    $path = drupal_get_path('module', 'project_issue');
  25    drupal_add_css($path .'/project_issue.css');
  26  }
  27  
  28  function project_issue_menu() {
  29    $items = array();
  30  
  31    $includes = drupal_get_path('module', 'project_issue') .'/includes';
  32  
  33    // Issues
  34    $items['project/issues/update_project'] = array(
  35      'page callback' => 'project_issue_update_project',
  36      'access callback' => 'project_issue_menu_access',
  37      'access arguments' => array('any'),
  38      'type' => MENU_CALLBACK,
  39    );
  40    $items['project/issues/statistics'] = array(
  41      'title' => 'Statistics',
  42      'page callback' => 'project_issue_statistics',
  43      'access callback' => 'project_issue_menu_access',
  44      'access arguments' => array('any'),
  45      'type' => MENU_NORMAL_ITEM,
  46      'file' => 'includes/statistics.inc',
  47    );
  48    $items['project/issues/statistics/%project_node'] = array(
  49      'title' => 'Statistics',
  50      'page callback' => 'project_issue_statistics',
  51      'page arguments' => array(3),
  52      'access callback' => 'project_issue_menu_access',
  53      'access arguments' => array('any'),
  54      'type' => MENU_NORMAL_ITEM,
  55      'file' => 'includes/statistics.inc',
  56    );
  57    $path = 'project/issues/subscribe-mail';
  58    if (!variable_get('project_issue_global_subscribe_page', TRUE)) {
  59      // If we don't want the global subscribe page, require an argument.
  60      $path .= '/%';
  61    }
  62    $items[$path] = array(
  63      'title' => 'Subscribe',
  64      'page callback' => 'drupal_get_form',
  65      'page arguments' => array('project_issue_subscribe', 3),
  66      'access callback' => 'project_issue_menu_access',
  67      'access arguments' => array('auth'),
  68      'type' => MENU_NORMAL_ITEM,
  69      'file' => 'includes/subscribe.inc',
  70    );
  71    if (module_exists('search')) {
  72      $items['search/issues'] = array(
  73        'title' => 'Issues',
  74        'page callback' => 'project_issue_search_page',
  75        'access callback' => 'project_issue_menu_access',
  76        'access arguments' => array('any'),
  77        'type' => MENU_LOCAL_TASK,
  78        'weight' => 4,
  79      );
  80    }
  81  
  82    // "My projects" page (which shows all issues for all your projects)
  83    $items['project/user'] = array(
  84      'title' => 'My projects',
  85      'page callback' => 'project_issue_user_page',
  86      'access callback' => 'project_issue_menu_access',
  87      'access arguments' => array('auth'),
  88      'type' => MENU_NORMAL_ITEM,
  89      'weight' => -49,
  90    );
  91  
  92    // Administrative pages
  93    $items['admin/project/project-issue-settings'] = array(
  94      'title' => 'Project issue settings',
  95      'description' => 'Specify where attachments to issues should be stored on your site, and what filename extensions should be allowed.',
  96      'page callback' => 'drupal_get_form',
  97      'page arguments' => array('project_issue_settings_form'),
  98      'access arguments' => array('administer projects'),
  99      'weight' => 1,
 100      'type' => MENU_NORMAL_ITEM,
 101      'file' => 'includes/admin.settings.inc',
 102    );
 103  
 104    // Administer issue status settings
 105    $items['admin/project/project-issue-status'] = array(
 106      'title' => 'Project issue status options',
 107      'description' => 'Configure what issue status values should be used on your site.',
 108      'page callback' => 'drupal_get_form',
 109      'page arguments' => array('project_issue_admin_states_form'),
 110      'access arguments' => array('administer projects'),
 111      'type' => MENU_NORMAL_ITEM,
 112      'weight' => 1,
 113      'file' => 'includes/admin.issue_status.inc'
 114    );
 115    $items['admin/project/project-issue-status/delete'] = array(
 116      'title' => 'Delete',
 117      'page callback' => 'drupal_get_form',
 118      'page arguments' => array('project_issue_delete_state_confirm', 4),
 119      'access arguments' => array('administer projects'),
 120      'type' => MENU_CALLBACK,
 121      'file' => 'includes/admin.issue_status.inc'
 122    );
 123  
 124    // Issues subtab on project node edit tab.
 125    $items['node/%project_node/edit/issues'] = array(
 126      'title' => 'Issues',
 127      'page callback' => 'project_issue_project_edit_issues',
 128      'page arguments' => array(1),
 129      'access callback' => 'node_access',
 130      'access arguments' => array('update', 1),
 131      'type' => MENU_LOCAL_TASK,
 132      'file' => 'includes/project_edit_issues.inc',
 133    );
 134    $items['node/%project_node/edit/component/delete/%'] = array(
 135      'title' => 'Delete component',
 136      'description' => 'Delete component',
 137      'page callback' => 'drupal_get_form',
 138      'page arguments' => array('project_issue_component_delete_form', 1, 5),
 139      'access callback' => 'node_access',
 140      'access arguments' => array('update', 1),
 141      'type' => MENU_CALLBACK,
 142      'file' => 'includes/project_edit_issues.inc',
 143    );
 144  
 145    $items['node/add/project-issue/%'] = array(
 146      'page callback' => 'node_add',
 147      'page arguments' => array('project-issue'),
 148      'title' => drupal_ucfirst(node_get_types('name', 'project_issue')),
 149      'title callback' => 'check_plain',
 150      'access callback' => 'node_access',
 151      'access arguments' => array('create', 'project_issue'),
 152      'page callback' => 'node_add',
 153      'page arguments' => array(2),
 154      'file' => 'node.pages.inc',
 155      'file path' => drupal_get_path('module', 'node'),
 156      'type' => MENU_CALLBACK,
 157    );
 158    // Redirect node/add/project_issue/* to node/add/project-issue.
 159    $items['node/add/project_issue'] = array(
 160      'page callback' => 'project_issue_add_redirect_page',
 161      'page arguments' => array(3, 4),
 162      'access callback' => 'node_access',
 163      'access arguments' => array('create', 'project_issue'),
 164      'file' => 'includes/issue_node_form.inc',
 165      'type' => MENU_CALLBACK,
 166    );
 167  
 168    // Autocomplete paths.
 169  
 170    // Autocomplete a comma-separated list of projects that have issues enabled.
 171    $items['project/autocomplete/issue/project'] = array(
 172      'page callback' => 'project_issue_autocomplete_issue_project',
 173      'access callback' => 'project_issue_menu_access',
 174      'access arguments' => array('any'),
 175      'file' => 'autocomplete.inc',
 176      'file path' => $includes,
 177      'type' => MENU_CALLBACK,
 178    );
 179  
 180    // Autocomplete a comma-separated list of projects from all issues a user
 181    // has either submitted or commented on.
 182    $items['project/autocomplete/issue/user/%'] = array(
 183      'page callback' => 'project_issue_autocomplete_user_issue_project',
 184      'page arguments' => array(4, 5),
 185      'access callback' => 'project_issue_menu_access',
 186      'access arguments' => array('any'),
 187      'file' => 'autocomplete.inc',
 188      'file path' => $includes,
 189      'type' => MENU_CALLBACK,
 190    );
 191  
 192    return $items;
 193  }
 194  
 195  /**
 196   * Implementation of hook_menu_alter().
 197   */
 198  function project_issue_menu_alter(&$callbacks) {
 199    // Special menu item for the "first page" of submitting a new issue.
 200    // Instead of the treachery of a true multipage form, we just have
 201    // a simple form at node/add/project-issue that provides a project
 202    // selector which redirects to node/add/project-issue/[project-name].
 203    $callbacks['node/add/project-issue']['page callback'] = 'project_issue_pick_project_page';
 204    $callbacks['node/add/project-issue']['file'] = 'issue_node_form.inc';
 205    $callbacks['node/add/project-issue']['file path'] = drupal_get_path('module', 'project_issue') . '/includes';
 206  }
 207  
 208  /**
 209   * Determine access to a given type of menu item.
 210   *
 211   * @param $type
 212   *   Type of menu item to check access for, can be 'any' if the current user
 213   *   can access any issues, or 'auth' if the current user is authenticated and
 214   *   can accses any issues.
 215   */
 216  function project_issue_menu_access($type) {
 217    global $user;
 218    if ($type == 'auth' && empty($user->uid)) {
 219      return FALSE;
 220    }
 221    return user_access('access project issues') || user_access('access own project issues');
 222  }
 223  
 224  function project_issue_help($path, $arg) {
 225    switch ($path) {
 226      case 'admin/help#project_issue':
 227        return '<h3>'. t('Mailhandler support') .'</h3>'.
 228          '<p>'. t('Basic mail format:') .'</p>'.
 229          '<pre>'. t("Type: project\nProject: chatbox\nCategory: bug report\nVersion: cvs\nPriority: normal\nStatus: active\nComponent: code\n\nWhatever I type here will be the body of the node.\n") .'</pre>'.
 230          '<p>'. t('See the mailhandler help for more information on using the mailhandler module.') .'</p>';
 231      case 'node/add#project_issue':
 232        return t('Add a new issue (bug report, feature request, etc) to an existing project.');
 233      case 'admin/project/project-issue-status':
 234        return '<p>'. t('Use this page to add new status options for project issues or to change or delete existing options.') .'</p>'.
 235          '<dl>'.
 236          '<dt>'. t('Adding') .'</dt>'.
 237          '<dd>'. t('To add a new status option, put its name in one of the blank places at the bottom of the form and assign it a weight.') .'</dd>'.
 238          '<dt>'. t('Updating') .'</dt>'.
 239          '<dd>'. t('When renaming existing issues, keep in mind that issues with the existing name will receive the new one.') .'</dd>'.
 240          '<dt>'. t('Deleting') .'</dt>'.
 241          '<dd>'. t('If you delete an existing issue status, you will be prompted for a new status to assign to existing issues with the deleted status.') .'</dd>'.
 242          '<dt>'. t('Weight') .'</dt>'.
 243          '<dd>'. t('The weight of an issue determines the order it appears in lists, like in the select box where users designate a status for their issue.') .'</dd>'.
 244          '<dt>'. t('Author may set') .'</dt>'.
 245          '<dd>'. t("Check this option to give the original poster of an issue the right to set a status option, even if she or he isn't part of a role with this permission. You may wish, for example, to allow issue authors to close their own issues.") .'</dd>'.
 246          '<dt>'. t('In default queries') .'</dt>'.
 247          '<dd>'. t('There are a number of pages that display a list of issues based on a certain query. For all of these views of the issue queues, if no status options are explicitly selected, a certain set of defaults will be used to construct the query.') .'</dd>'.
 248          '<dt>'. t('Default status') .'</dt>'.
 249          '<dd>'. t('The default status option will be used for new issues, and all users with the permission to create issues will automatically have permission to set this status. The default issue status cannot be deleted. If you wish to delete this status, first set a different status to default.') .'</dd>'.
 250          '</dl>';
 251  
 252    }
 253  
 254    // NOTE: This totally sucks, and is a dirty, ugly hack.  Since we don't
 255    // want to rely on PHP filtered headers for our views, and defining our
 256    // own display plugin breaks other nice things like RSS, we just cheat
 257    // and render these links in here.  We'd like to remove this once a
 258    // better solution is available that doesn't hard-code the paths.
 259    if ($arg[0] == 'project' && $arg[1] == 'user') {
 260      return project_issue_my_projects_table();
 261    }
 262  
 263    if ($arg[0] == 'project' && $arg[1] == 'issues') {
 264      // If there's no other arg, we're done.
 265      if (empty($arg[2])) {
 266        return project_issue_query_result_links();
 267      }
 268      // project/issues/user is a special case, since if there's an argument,
 269      // it's a username, not a project.  Furthermore, we don't want any links
 270      // for anonymous.
 271      if ($arg[2] == 'user') {
 272        global $user;
 273        if (empty($user->uid) && empty($arg[3])) {
 274          return;
 275        }
 276        return project_issue_query_result_links();
 277      }
 278      switch ($arg[2]) {
 279        case 'search':
 280        case 'statistics':
 281        case 'subscribe-mail':
 282          return project_issue_query_result_links($arg[3]);
 283  
 284        default:
 285          return project_issue_query_result_links($arg[2]);
 286      }
 287    }
 288  
 289  }
 290  
 291  /**
 292   * Implementation of hook_theme().
 293   */
 294  function project_issue_theme() {
 295    return array(
 296      'project_issue_comment_table' => array(
 297        'file' => 'includes/comment.inc',
 298        'arguments' => array(
 299          'comment_changes' => NULL,
 300        ),
 301      ),
 302      'project_issue_comment_table_row' => array(
 303        'file' => 'includes/comment.inc',
 304        'arguments' => array(
 305          'field' => NULL,
 306          'change' => NULL,
 307        ),
 308      ),
 309      'project_issue_subscribe' => array(
 310        'file' => 'issue.inc',
 311        'arguments' => array(
 312          'form' => NULL,
 313        ),
 314      ),
 315      'project_issue_summary' => array(
 316        'file' => 'includes/issue_node_view.inc',
 317        'arguments' => array(
 318          'current_data' => NULL,
 319          'summary_links' => NULL,
 320        ),
 321      ),
 322      'project_issue_admin_states_form' => array(
 323        'file' => 'includes/admin.issue_status.inc',
 324        'arguments' => array(
 325          'form' => NULL,
 326        ),
 327      ),
 328      'project_issue_project_edit_form' => array(
 329        'file' => 'includes/project_edit_issues.inc',
 330        'arguments' => array(
 331          'form' => NULL,
 332        ),
 333      ),
 334      'project_issue_query_result_links' => array(
 335        'file' => 'issue.inc',
 336        'arguments' => array(
 337          'links' => NULL,
 338        ),
 339      ),
 340      'project_issue_create_forbidden' => array(
 341        'file' => 'issue.inc',
 342        'arguments' => array(
 343          'uri' => NULL,
 344        ),
 345      ),
 346      'project_issue_mail_summary' => array(
 347        'file' => 'includes/mail.inc',
 348        'arguments' => array(
 349          'entry' => NULL,
 350          'node' => NULL,
 351          'changes' => NULL,
 352          'display_files' => NULL,
 353        ),
 354      ),
 355      'project_issue_mail_summary_field' => array(
 356        'file' => 'includes/mail.inc',
 357        'arguments' => array(
 358          'node' => NULL,
 359          'field_name' => NULL,
 360          'change' => NULL,
 361        ),
 362      ),
 363      'project_issue_auto_close_message' => array(
 364        'file' => 'project_issue.module',
 365        'arguments' => array(
 366          'auto_close_days' => NULL,
 367        ),
 368      ),
 369      'project_issue_issue_link' => array(
 370        'file' => 'project_issue.module',
 371        'arguments' => array(
 372          'node' => NULL,
 373          'comment_id' => NULL,
 374          'comment_number' => NULL,
 375          'include_assigned' => FALSE,
 376        ),
 377      ),
 378      'project_issue_issue_cockpit' => array(
 379        'arguments' => array(
 380          'node' => NULL,
 381        ),
 382        'file' => 'includes/issue_cockpit.inc',
 383        'template' => 'theme/project-issue-issue-cockpit',
 384      ),
 385    );
 386  }
 387  
 388  /**
 389   * Implementation of hook_views_api().
 390   */
 391  function project_issue_views_api() {
 392    return array(
 393      'api' => 2.0,
 394      'path' => drupal_get_path('module', 'project_issue') .'/views',
 395    );
 396  }
 397  
 398  /**
 399   * Implementation of hook_form_alter.
 400   */
 401  function project_issue_form_alter(&$form, &$form_state, $form_id) {
 402    switch ($form_id) {
 403      // Issues must be updated if any project issue comments are edited/deleted.
 404      case 'comment_admin_overview':
 405        $form['#submit'][] = 'project_issue_comment_mass_update';
 406        break;
 407  
 408      case 'project_issue_node_form':
 409        // For our theming, wrap everything in a 'project-issue' class.
 410        $form['#prefix'] = isset($form['#prefix'])? $form['#prefix'] : '';
 411        $form['#prefix'] .= '<div class="project-issue">';
 412        $form['#suffix'] = isset($form['#suffix'])? $form['#suffix'] : '';
 413        $form['#suffix'] = '</div>'. $form['#suffix'];
 414  
 415        if (isset($form['attachments'])) {
 416          if (isset($form['project_info'])) {
 417            // We already know what project it is, so make sure the 'File
 418            // attachments' fieldset is expanded.
 419            $form['attachments']['#collapsed'] = FALSE;
 420          }
 421          else {
 422            // On the first page of the multi-page form, don't have a project
 423            // selected yet, so unset the file attachments fieldset entirely.
 424            unset($form['attachments']);
 425          }
 426        }
 427        break;
 428  
 429      // see also: project_issue_form_comment_form_alter
 430      case 'comment_form':
 431        $nid = $form['nid']['#value'];
 432        $node = node_load($nid);
 433  
 434        // Allows only project_issue
 435        if ($node->type != 'project_issue') {
 436          return;
 437        }
 438        // Make sure the 'File attachments' fieldset is expanded and before the
 439        // original issue fieldset.
 440        if (isset($form['attachments'])) {
 441          $form['attachments']['#collapsed'] = FALSE;
 442          $form['attachments']['#weight'] = 2;
 443          // TODO: temporary hack until we decide how to deal with
 444          // editing attachments on issues overall.
 445          if (!empty($form['cid']['#value'])) {
 446            unset($form['attachments']);
 447          }
 448        }
 449        // Add our own custom validation to the comment form for issue nodes.
 450        $form['#validate'][] = 'project_issue_form_comment_validate';
 451        break;
 452  
 453      case 'comment_confirm_delete':
 454        $type = db_result(db_query("SELECT type FROM {node} WHERE nid = %d", $form['#comment']->nid));
 455        if (!empty($type) && $type == 'project_issue') {
 456          $form['description']['#value'] = t('This action cannot be undone.');
 457        }
 458        break;
 459  
 460      case 'views_exposed_form':
 461        project_issue_alter_views_exposed_form($form, $form_state);
 462        break;
 463  
 464      case 'project_issue_issue_cockpit_searchbox':
 465        // Since we're using a GET #action for this searchbox, unset the FAPI
 466        // cruft we don't want to see in the URL.
 467        unset($form['form_build_id']);
 468        unset($form['form_id']);
 469        unset($form['form_token']);
 470        break;
 471    }
 472  }
 473  
 474  function project_issue_node_info() {
 475    return array(
 476      'project_issue' => array(
 477        'name' => t('Issue'),
 478        'module' => 'project_issue',
 479        'description' => t('An issue that can be tracked, such as a bug report, feature request, or task.'),
 480      ),
 481    );
 482  }
 483  
 484  function project_issue_perm() {
 485    $perms = array(
 486      'create project issues',
 487      'access project issues',
 488      'edit own project issues',
 489      'access own project issues',
 490      'assign and be assigned project issues',
 491    );
 492    $states = project_issue_state();
 493    foreach($states as $key => $value) {
 494      $perms[] = "set issue status ". str_replace("'", "", $value);
 495    }
 496    return $perms;
 497  }
 498  
 499  function project_issue_access($op, $node, $account) {
 500  
 501    if (user_access('administer projects', $account)) {
 502      return TRUE;
 503    }
 504    switch ($op) {
 505      case 'view':
 506        if (user_access('access own project issues', $account) && $node->uid == $account->uid) {
 507          return TRUE;
 508        }
 509        if (!user_access('access project issues', $account)) {
 510          return FALSE;
 511        }
 512        break;
 513      case 'create':
 514        return user_access('create project issues', $account);
 515      case 'update':
 516        if (user_access('edit own project issues', $account) && $node->uid == $account->uid) {
 517          return TRUE;
 518        }
 519        break;
 520      case 'delete':
 521        // Admin case already handled, no one else should be able to delete.
 522        break;
 523    }
 524  }
 525  
 526  /**
 527   * Helper to trim all elements in an array.
 528   */
 529  function project_issue_trim(&$item, $key) {
 530    $item = trim($item);  
 531  }
 532  
 533  /**
 534   * Implementation of hook_cron().
 535   *
 536   * There is a variable (no admin UI, just via settings.php) that controls if
 537   * the admin has setup a separate cron job on their system to invoke this code
 538   * instead of relying on cron.php and hook_cron(). If this variable, called
 539   * 'project_issue_hook_cron', is set to FALSE, then there's nothing to do
 540   * in here. Otherwise, we include the cron.inc file and invoke that code
 541   * ourselves.
 542   */
 543  function project_issue_cron() {
 544    if (variable_get('project_issue_hook_cron', TRUE)) {
 545      module_load_include('inc', 'project_issue', 'includes/cron');
 546      _project_issue_cron();
 547    }
 548  }
 549  
 550  /**
 551   * Comment left when cron auto-closes an issue.
 552   *
 553   * @param $auto_close_days
 554   *   The (minimum) number of days without activity before automatically closing
 555   *   a fixed issue.
 556   * @return
 557   *   Message to be added as a comment to an issue when auto-closing that issue.
 558   */
 559  function theme_project_issue_auto_close_message($auto_close_days) {
 560    $auto_close_interval = format_interval($auto_close_days * 24 * 60 * 60, 2);
 561    return t('Automatically closed -- issue fixed for !interval with no activity.', array('!interval' => $auto_close_interval));
 562  }
 563  
 564  /**
 565   * Add a followup to a project issue using the auto-followup user.
 566   *
 567   * @param $changes
 568   *   An associative array specifying what should change in the issue. Every key
 569   *   corresponds to a database field and the value is what it should be changed
 570   *   to. Required keys are:
 571   *     - nid: Specifies the issue being changed.
 572   *     - comment: Contains the text of the followup changing the issue.
 573   *   See project_issue_add_followup() for a full list of possible keys.
 574   *
 575   * @return
 576   *   TRUE if the comment was successfully added, FALSE if either there's no
 577   *   auto-followup user configured or if the requested issue wasn't found.
 578   *
 579   * @see project_issue_add_followup().
 580   */
 581  function project_issue_add_auto_followup($changes) {
 582    // If a user for automatic followups exists, use that uid and proceed.
 583    if ($auto_user = _project_issue_followup_get_user()) {
 584      $changes['uid'] = !empty($changes['uid']) ? $changes['uid'] : $auto_user->uid;
 585      return project_issue_add_followup($changes);
 586    }
 587    else {
 588      return FALSE;
 589    }
 590  }
 591  
 592  /**
 593   * Saves a comment to the database.
 594   *
 595   * TODO: Ideally this should die as soon as core's comment_save() becomes more
 596   *       abstracted.
 597   *
 598   * @param $changes
 599   *   An associative array specifying what should change in the issue. Every key
 600   *   corresponds to a database field and the value is what it should be changed
 601   *   to.  Required keys are:
 602   *     - nid: Specifies the issue being changed.
 603   *     - comment: Contains the text of the followup changing the issue.
 604   *
 605   *   'uid' and 'name' are optional keys -- if not specified then the values
 606   *   from the currently logged in user will be used, though it's generally
 607   *   safer to specify the uid explicitly.
 608   *
 609   *   You can specify the following fields of the comment table: subject,
 610   *   hostname, timestamp, score, status, format, thread, mail, homepage.
 611   *    You can also specify the following fields from project_issues
 612   *   table: category, priority, assigned, sid, title. There is a special,
 613   *   optional key called 'project_info', its value is another associative
 614   *   array with the following fields from project_issues: pid, rid, component.
 615   *   Example: To change the issue status and set the comment text for the
 616   *   issue with nid = 100, this array might look like:
 617   *     array(
 618   *      'nid' => 100,
 619   *      'sid' => 4,
 620   *      'comment' => t('This issue was automatically closed after 2 weeks of no activity.'),
 621   *     );
 622   *
 623   * @return
 624   *   TRUE if the comment was successfully added to the requested issue, 
 625   *   otherwise FALSE.
 626   */
 627  function project_issue_add_followup($changes) {
 628    if (isset($changes['uid'])) {
 629      $account = user_load(array('uid' => $changes['uid']));
 630    }
 631    else {
 632      global $user;
 633      $account = $user;
 634    }
 635  
 636    $result = db_query('SELECT pi.nid, pi.rid, pi.component, pi.category, pi.priority, pi.assigned, pi.sid, pi.pid, n.title FROM {project_issues} pi INNER JOIN {node} n ON n.nid = pi.nid WHERE n.nid = %d', $changes['nid']);
 637  
 638    if ($issue = db_fetch_object($result)) {
 639      // Build vancode
 640      $max = db_result(db_query('SELECT MAX(thread) FROM {comments} WHERE nid = %d', $changes['nid']));
 641      // Strip the "/" from the end of the thread.
 642      $max = rtrim($max, '/');
 643      // Finally, build the thread field for this new comment.
 644      $thread = int2vancode(vancode2int($max) + 1) .'/';
 645  
 646      // These two are not allowed to be set in changes.
 647      unset($changes['cid'], $changes['pid']);
 648      $comment = $changes + array(
 649        'pid' => 0,
 650        'uid' => $account->uid,
 651        // The correct subject (#number) is supplied during the save cycle.
 652        'subject' => '--project followup subject--',
 653        'hostname' => ip_address(),
 654        'timestamp' => time(),
 655        'status' => COMMENT_PUBLISHED,
 656        'format' => FILTER_FORMAT_DEFAULT,
 657        'thread' => $thread,
 658        'name' => $account->name,
 659        'mail' => '',
 660        'homepage' => '',
 661        'category' => $issue->category,
 662        'priority' => $issue->priority,
 663        'assigned' => $issue->assigned,
 664        'sid' => $issue->sid,
 665        'title' => $issue->title,
 666      );
 667  
 668      if (!isset($comment['project_info'])) {
 669        $comment['project_info'] = array();
 670      }
 671      $comment['project_info'] += array(
 672        'pid' => $issue->pid,
 673        'rid' => $issue->rid,
 674        'component' => $issue->component,
 675        'assigned' => $issue->assigned,
 676      );
 677  
 678      db_query("INSERT INTO {comments} (pid, nid, uid, subject, comment, hostname, timestamp, status, format, thread, name, mail, homepage) VALUES (%d, %d, %d, '%s', '%s', '%s', %d, %d, %d, '%s', '%s', '%s', '%s')", $comment['pid'], $comment['nid'], $comment['uid'], $comment['subject'], $comment['comment'], $comment['hostname'], $comment['timestamp'], $comment['status'], $comment['format'], $comment['thread'], $comment['name'], $comment['mail'], $comment['homepage']);
 679  
 680      $comment['cid'] = db_last_insert_id('comments', 'cid');
 681  
 682      _comment_update_node_statistics($comment['nid']);
 683  
 684      // Tell the other modules a new comment has been submitted.
 685      comment_invoke_comment($comment, 'insert');
 686      cache_clear_all();
 687      return TRUE;
 688    }
 689    return FALSE;
 690  }
 691  
 692  /**
 693   * Load and verify the followup user.
 694   *
 695   * @return $account
 696   *   The account of the followup user (or FALSE if not found).
 697   */
 698  function _project_issue_followup_get_user() {
 699    $uid = variable_get('project_issue_followup_user', '');
 700    if ($uid === '') {
 701      return FALSE;
 702    }
 703    $account = user_load(array('uid' => $uid));
 704    // Safety check -- we have to have a valid user here.
 705    if (!$account) {
 706      watchdog('project_issue', 'Auto-change user failed to load.', WATCHDOG_ERROR);
 707      return FALSE;
 708    }
 709    $anon = variable_get('anonymous', t('Anonymous'));
 710    $account->name = $uid ? $account->name : $anon;
 711    // Safety check -- selected user must still have the correct permissions to follow up on issues.
 712    if (!user_access('access project issues', $account)) {
 713      watchdog('project_issue', '%name does not have sufficient permissions to follow up on issues.', array('%name' => $account->name), WATCHDOG_ERROR);
 714      return FALSE;
 715    }
 716    return $account;
 717  }
 718  
 719  /**
 720   * hook_nodeapi() implementation. This just decides what type of node
 721   * is being passed, and calls the appropriate type-specific hook.
 722   *
 723   * @see project_issue_issue_nodeapi().
 724   * @see project_issue_project_nodeapi().
 725   */
 726  function project_issue_nodeapi(&$node, $op, $arg) {
 727    switch ($node->type) {
 728      case 'project_project':
 729        module_load_include('inc', 'project_issue', 'includes/project_node');
 730        project_issue_project_nodeapi($node, $op, $arg);
 731        break;
 732      case 'project_issue':
 733        project_issue_issue_nodeapi($node, $op, $arg);
 734        break;
 735    }
 736  }
 737  
 738  /**
 739   * hook_nodeapi implementation specific to "project_issue" nodes.
 740   * @see project_issue_nodeapi().
 741   */
 742  function project_issue_issue_nodeapi(&$node, $op, $arg) {
 743    global $user;
 744    switch ($op) {
 745      case 'view':
 746        $_GET['mode'] = COMMENT_MODE_FLAT_EXPANDED;
 747        $_GET['sort'] = COMMENT_ORDER_OLDEST_FIRST;
 748        project_issue_comment_view($node);
 749        break;
 750      case 'presave':
 751        // Only for new nodes with files set.
 752        if (empty($node->nid) && isset($node->files)) {
 753          project_issue_rewrite_issue_filepath($node->files);
 754        }
 755        break;
 756      case 'insert':
 757        // Mark the node for email notification during hook_exit(), so all issue
 758        // and file data is in a consistent state before we generate the email.
 759        project_issue_set_mail_notify($node->nid);
 760        break;
 761    }
 762  }
 763  
 764  /**
 765   * Implement hook_load() for project issue nodes.
 766   */
 767  function project_issue_load($node) {
 768    $additions = db_fetch_array(db_query(db_rewrite_sql('SELECT pi.* FROM {project_issues} pi WHERE pi.nid = %d', 'pi'), $node->nid));
 769  
 770    // TODO: This need to be ripped out in D6.
 771    $additions['comment_form_location'] = variable_get('project_issue_comment_form_location', NULL);
 772    $issue = new stdClass;
 773    $issue->project_issue = $additions;
 774    return $issue;
 775  }
 776  
 777  /**
 778   * Implement hook_delete() for project issue nodes.
 779   */
 780  function project_issue_delete($node) {
 781    db_query('DELETE FROM {project_issues} WHERE nid = %d', $node->nid);
 782    db_query('DELETE FROM {project_issue_comments} WHERE nid = %d', $node->nid);
 783  }
 784  
 785  /**
 786   * Implement hook_form() for project issue nodes.
 787   */
 788  function project_issue_form($node, $form_state, $include_metadata_fields = FALSE) {
 789    module_load_include('inc', 'project_issue', 'includes/issue_node_form');
 790    return _project_issue_form($node, $form_state, $include_metadata_fields);
 791  }
 792  
 793  /**
 794   * Implement hook_validate() for project issue nodes.
 795   */
 796  function project_issue_validate($node) {
 797    module_load_include('inc', 'project_issue', 'includes/issue_node_form');
 798    return _project_issue_validate($node);
 799  }
 800  
 801  /**
 802   * Implement hook_insert() for project issue nodes.
 803   */
 804  function project_issue_insert($node) {
 805    module_load_include('inc', 'project_issue', 'includes/issue_node_form');
 806    return _project_issue_insert($node);
 807  }
 808  
 809  /**
 810   * Implement hook_view() for project issue nodes.
 811   */
 812  function project_issue_view($node, $teaser = FALSE, $page = FALSE) {
 813    module_load_include('inc', 'project_issue', 'includes/issue_node_view');
 814    return _project_issue_view($node, $teaser, $page);
 815  }
 816  
 817  /**
 818   * Store issue nodes that need mail notifications sent.
 819   *
 820   * It's possible that mass inserts/updates could occur, and also possible that
 821   * a given node/comment could be programatically updated more than once in a
 822   * page load -- an associative array is used in order to support these cases.
 823   *
 824   * @param $nid
 825   *   The node ID of the issue node to store, or NULL to fetch the stored nids.
 826   * @return
 827   *   If $nid is not passed, an associative array of nids that are marked for
 828   *   notification emails, with the following structure: key = nid, value = nid.
 829   */
 830  function project_issue_set_mail_notify($nid = NULL) {
 831    static $nids = array();
 832  
 833    if (!isset($nid)) {
 834      $return = $nids;
 835      $nids = array();  // Reset just in case this function gets called again.
 836      return $return;
 837    }
 838    else {
 839      $nids[$nid] = $nid;
 840    }
 841  }
 842  
 843  /**
 844   * Implementation of hook_exit().
 845   */
 846  function project_issue_exit() {
 847    // Check for issue nodes that need mail notifications sent. This is done in
 848    // hook_exit() so that all issue and file data is in a consistent state
 849    // before we generate the email.
 850    $nids = project_issue_set_mail_notify();
 851    // For cached pages, this hook is called, but there aren't any mail functions
 852    // loaded. Since the cached pages won't have any new mail notifications,
 853    // we can safely test for this case.
 854    if (!empty($nids)) {
 855      foreach ($nids as $nid) {
 856        project_mail_notify($nid);
 857      }
 858    }
 859  }
 860  
 861  /**
 862   * Rewrites the file information to move files to the issues directory.
 863   *
 864   * @param $files
 865   *   An array of file objects, keyed by file ID.
 866   */
 867  function project_issue_rewrite_issue_filepath($files) {
 868    if ($issue_dir = variable_get('project_directory_issues', 'issues') ) {
 869      foreach ($files as $key => $file) {
 870        $file = (object) $file;
 871        $old_path = $file->filepath;
 872        $final_dir = file_directory_path() .'/'. $issue_dir;
 873        $move_path = $old_path;
 874        file_move($move_path, $final_dir .'/'. basename($file->filepath));
 875        $new_basename = basename($move_path);
 876        db_query("UPDATE {files} SET filepath = '%s' WHERE fid = %d", $final_dir .'/'. $new_basename, $file->fid);
 877      }
 878    }
 879  }
 880  
 881  function project_issue_my_projects_table() {
 882    $uid = 0;
 883    $display = views_get_page_view();
 884    if (!empty($display->view->argument['uid'])) {
 885      $uid = $display->view->argument['uid']->get_value();
 886    }
 887  
 888    if (empty($uid)) {
 889      return;
 890    }
 891  
 892    $header = array(
 893      array('data' => t('Project'), 'field' => 'n.title', 'sort' => 'asc'),
 894      array(
 895        'data' => t('Last issue update'),
 896        'field' => 'max_issue_changed',
 897        'class' => 'project-issue-updated',
 898      ),
 899      array(
 900        'data' => t('Open issues'),
 901        'field' => 'count',
 902        'class' => 'project-issues',
 903      ),
 904      array('data' => t('Issue links'), 'class' => 'project-issue-links'),
 905    );
 906    $default_states = implode(',', project_issue_default_states());
 907    $result = db_query(db_rewrite_sql("SELECT n.nid, n.title, COUNT(ni.nid) AS count, MAX(ni.changed) AS max_issue_changed FROM {node} n LEFT JOIN {project_issues} pi ON n.nid = pi.pid AND pi.sid IN ($default_states) LEFT JOIN {node} ni ON ni.nid = pi.nid AND ni.status = 1 WHERE n.type = 'project_project' AND n.status = 1 AND n.uid = %d GROUP BY n.nid, n.title") . tablesort_sql($header), $uid);
 908  
 909    $any_admin = FALSE;
 910    $projects = array();
 911    while ($node = db_fetch_object($result)) {
 912      $node_obj = node_load($node->nid);
 913      $node->is_admin = node_access('update', $node_obj);
 914      $node->project['uri'] = $node_obj->project['uri'];
 915      $node->project_issue['issues'] = $node_obj->project_issue['issues'];
 916      $node->project_release['releases'] = isset($node_obj->project_release['releases']) ? $node_obj->project_release['releases'] : 0;
 917      if ($node->is_admin) {
 918        $any_admin = TRUE;
 919      }
 920      $projects[] = $node;
 921    }
 922  
 923    if (empty($projects)) {
 924      return ($uid ? t('You have no projects.') : t('This user has no projects.'));
 925    }
 926  
 927    foreach ($projects as $node) {
 928      $issue_links = array(
 929        array(
 930          'title' => t('View'),
 931          'href' => 'project/issues/'. $node->project['uri'],
 932        ),
 933        array(
 934          'title' => t('Search'),
 935          'href' => 'project/issues/search/'. $node->project['uri'],
 936        ),
 937        array(
 938          'title' => t('Create'),
 939          'href' => 'node/add/project-issue/'. $node->project['uri'],
 940        ),
 941      );
 942      if ($node->is_admin) {
 943        $project_links = array(
 944          array(
 945            'title' => t('Edit'),
 946            'href' => "node/$node->nid/edit",
 947          ),
 948        );
 949        if (module_exists('project_release') && $node->project_release['releases']) {
 950          $project_links[] = array(
 951            'title' => t('Add release'),
 952            'href' => "node/add/project-release/$node->nid",
 953          );
 954        }
 955      }
 956      if ($node->project_issue['issues']) {
 957        $row = array(
 958          array(
 959            'data' => l($node->title, "node/$node->nid"),
 960            'class' => 'project-name',
 961          ),
 962          array(
 963            'data' => $node->max_issue_changed ? format_interval(time() - $node->max_issue_changed, 2) : t('n/a'),
 964            'class' => 'project-issue-updated',
 965          ),
 966          array(
 967            'data' => $node->count,
 968            'class' => 'project-issues',
 969          ),
 970          array(
 971            'data' => theme('links', $issue_links),
 972            'class' => 'project-issue-links',
 973          ),
 974        );
 975      }
 976      else {
 977        $row = array(
 978          array(
 979            'data' => l($node->title, "node/$node->nid"),
 980            'class' => 'project-name',
 981          ),
 982          array(
 983            'data' => t('Issue tracking is disabled.'),
 984            'colspan' => $node->is_admin ? 2 : 3,
 985          ),
 986        );
 987        if ($node->is_admin) {
 988          $row[] = array(
 989            'data' => l(t('Enable'), "node/$node->nid/edit/issues", array('query' => drupal_get_destination())),
 990            'class' => 'project-issue-links',
 991          );
 992        }
 993      }
 994      if ($node->is_admin) {
 995        $row[] = array(
 996          'data' => theme('links', $project_links),
 997          'class' => 'project-project-links',
 998        );
 999      }
1000      elseif ($any_admin) {
1001        $row[] = array();
1002      }
1003  
1004      $rows[] = $row;
1005      $query->projects[] = $node->nid;
1006    }
1007  
1008    if ($any_admin) {
1009      $header[] = array('data' => t('Project links'), 'class' => 'project-project-links');
1010    }
1011    return theme('table', $header, $rows, array('class' => 'projects'));
1012  }
1013  
1014  /**
1015   * Page callback function for the "Issues" subtab at the site-wide search page.
1016   */
1017  function project_issue_search_page() {
1018    $view_info = variable_get('project_issue_search_issues_view', 'project_issue_search_all:default');
1019    $view_parts = explode(':', $view_info);
1020    $view = views_get_view($view_parts[0]);
1021    $view->override_path = 'search/issues';
1022    $output .=  $view->preview($view_parts[1]);
1023    return $output;
1024  }
1025  
1026  /**
1027   * Submit handler to adjust project issue metadata when comments are mass edited.
1028   */
1029  function project_issue_comment_mass_update($form_id, $form_values) {
1030    // This filters non-numeric values, then empty values.
1031    $cids = array_filter(array_filter($form_values['comments'], 'is_numeric'));
1032    $issue_comments = db_query("SELECT n.nid, c.cid FROM {node} n INNER JOIN {comments} c ON n.nid = c.nid WHERE c.cid IN (". implode(', ', $cids) .") AND n.type = 'project_issue'");
1033    while ($issue_comment = db_fetch_object($issue_comments)) {
1034      project_issue_update_by_comment($issue_comment, 'update');
1035    }
1036  }
1037  
1038  /**
1039   * Set the breadcrumb trail for project issues and issue followups.
1040   *
1041   * Since the comment form and a full node view of an issue can appear
1042   * on both full issue pages and comment reply pages, this function checks
1043   * to see which page is being loaded, and sets the breadcrumb appropriately.
1044   *
1045   * @param $node
1046   *   The issue node object.
1047   * @param $project
1048   *   The project node object.
1049   */
1050  function project_issue_set_breadcrumb($node, $project) {
1051    $extra = array();
1052    $extra[] = l($project->title, 'node/'. $project->nid);
1053    $extra[] = l(t('Issues'), 'project/issues/'. $project->project['uri']);
1054    // Add the issue title if we're on a comment reply page.
1055    if (project_issue_is_comment_reply() || project_issue_is_comment_edit()) {
1056      $extra[] = l($node->title, 'node/'. $node->nid);
1057    }
1058    project_project_set_breadcrumb($project, $extra);
1059  }
1060  
1061  /**
1062   * Implementation of hook_link_alter().
1063   */
1064  function project_issue_link_alter(&$links, $node) {
1065    // Only remove link for full page views.
1066    if ($node->type == 'project_issue' && arg(0) == 'node' && is_numeric(arg(1))) {
1067      unset($links['comment_add']);
1068    }
1069  }
1070  
1071  
1072  /**
1073   * @defgroup project_issue_filter Project Issue number to link filter.
1074   */
1075  
1076  /**
1077   * Theme automatic Project Issue links.
1078   * @ingroup project_issue_filter themeable
1079   *
1080   * @param $node
1081   *   The issue node object to be linked.
1082   * @param $comment_id
1083   *   The comment id to be appended to the link, optional.
1084   * @param $comment_number
1085   *   The comment's number, as visible to users, optional.
1086   * @param $include_assigned
1087   *   Optional boolean to include the user the issue is assigned to.
1088   */
1089  function theme_project_issue_issue_link($node, $comment_id = NULL, $comment_number = NULL, $include_assigned = FALSE) {
1090    $path = "node/$node->nid";
1091  
1092    // See if the issue is assigned to anyone.  If so, we'll include it either
1093    // in the title attribute on hover, or next to the issue link if there was
1094    // an '@' appended to the issue nid.
1095    if (!empty($node->project_issue['assigned'])) {
1096      $username = db_result(db_query("SELECT name FROM {users} WHERE uid = %d", $node->project_issue['assigned']));
1097    }
1098    else {
1099      $username = '';
1100    }
1101  
1102    if (!empty($username) && !$include_assigned) {
1103      // We have an assigned user, but we're not going to print it next to the
1104      // issue link, so include it in title. l() runs $attributes through
1105      // drupal_attributes() which escapes the value.
1106      $attributes = array('title' => t('Status: !status, Assigned to: !username', array('!status' => project_issue_state($node->project_issue['sid']), '!username' => $username)));
1107    }
1108    else {
1109      // Just the status.
1110      $attributes = array('title' => t('Status: !status', array('!status' => project_issue_state($node->project_issue['sid']))));
1111    }
1112  
1113    if (isset($comment_id)) {
1114      $title = "#$node->nid-$comment_number: $node->title";
1115      $link = l($title, $path, array('attributes' => $attributes, 'fragment' => "comment-$comment_id"));
1116    }
1117    else {
1118      $title = "#$node->nid: $node->title";
1119      $link = l($title, $path, array('attributes' => $attributes));
1120    }
1121    $output = '<span class="project-issue-status-'. $node->project_issue['sid'] .' project-issue-status-info">'. $link;
1122    if ($include_assigned && !empty($username)) {
1123      $output .= ' <span class="project-issue-assigned-user">'. t('Assigned to: @username', array('@username' => $username)) .'</span>';
1124    }
1125    $output .= '</span>';
1126    return $output;
1127  }
1128  
1129  /**
1130   * Implementation of hook_form_filter_tips().
1131   * @ingroup project_issue_filter
1132   */
1133  function project_issue_filter_tips($delta, $format, $long = FALSE) {
1134    if ($long) {
1135      return t("References to project issues in the form of [#1234] (or [#1234-2] for comments) turn into links automatically, with the title of the issue appended. The status of the issue is shown on hover. If '@' is appended (e.g. [#1234@]), the user the issue is assigned to will also be printed.");
1136    }
1137    else {
1138      return t('Project issue numbers (ex. [#12345]) turn into links automatically.');
1139    }
1140  }
1141  
1142  /**
1143   * Implementation of hook_filter().
1144   * @ingroup project_issue_filter
1145   */
1146  function project_issue_filter($op, $delta = 0, $format = -1, $text = '') {
1147    switch ($op) {
1148      case 'list':
1149        return array(0 => t('Project Issue to link filter'));
1150      case 'description':
1151        return t('Converts references to project issues (in the form of [#12345]) into links. Caching should be disabled if node access control modules are used.');
1152      case 'no cache':
1153        return FALSE;
1154      case 'prepare':
1155        return $text;
1156      case 'process':
1157        $regex = '(?:(?<!\w)\[#\d+(?:-\d+)?(@)?\](?!\w))|<pre>.*?<\/pre>|<code>.*?<\/code>|<a(?:[^>"\']|"[^"]*"|\'[^\']*\')*>.*?<\/a>';
1158        $text = preg_replace_callback("/$regex/", 'project_issue_link_filter_callback', $text);
1159        return $text;
1160  
1161    }
1162  }
1163  
1164  function project_issue_link_filter_callback($matches) {
1165    $parts = array();
1166    if (preg_match('/^\[#(\d+)(?:-(\d+))?(@)?\]$/', $matches[0], $parts)) {
1167      $nid = $parts[1];
1168      $node = node_load($nid);
1169      $include_assigned = isset($parts[3]);
1170      if (is_object($node) && node_access('view', $node) && $node->type == 'project_issue') {
1171        if (isset($parts[2])) {
1172          // Pull comment id based on the comment number if we have one.
1173          $comment_number = $parts[2];
1174          if ($comment_id = db_result(db_query("SELECT pic.cid FROM {project_issue_comments} pic INNER JOIN {comments} c ON pic.cid = c.cid WHERE pic.nid = %d AND pic.comment_number = %d AND c.status = %d", $nid, $comment_number, COMMENT_PUBLISHED))) {
1175            return theme('project_issue_issue_link', $node, $comment_id, $comment_number, $include_assigned);
1176          }
1177        }
1178        // If we got this far there wasn't a valid comment number, so just link
1179        // to the node instead.
1180        return theme('project_issue_issue_link', $node, NULL, NULL, $include_assigned);
1181      }
1182    }
1183    // If we haven't already returned a replacement, return the original text.
1184    return $matches[0];
1185  }
1186  
1187  /**
1188   * Implementation of hook_requirements().
1189   * @ingroup project_issue_filter
1190   *
1191   * Check for conflicts with:
1192   *   installed node access control modules,
1193   *   'access project issues' restrictions,
1194   *   filters escaping code with higher weight.
1195   */
1196  function project_issue_requirements($phase) {
1197    $requirements = array();
1198    $input_formats = array();
1199    if ($phase == 'runtime') {
1200  
1201      $grants = module_implements('node_grants');
1202      $allowed_roles = user_roles(FALSE, 'access project issues');
1203      $conflict_anonymous = empty($allowed_roles[DRUPAL_ANONYMOUS_RID]);
1204  
1205      foreach (filter_formats() as $format => $input_format) {
1206        $filters = filter_list_format($format);
1207        if (isset($filters['project_issue/0'])) {
1208          if (!empty($grants) && filter_format_allowcache($format)) {
1209            $requirements[] = array(
1210              'title' => t('Project Issue to link filter'),
1211              'value' => t('Some module conflicts were detected.'),
1212              'description' => t('%issuefilter should not be enabled when a node access control is also in use. Users may be able to see cached titles of project issues they would otherwise not have access to. You should disable this filter in !inputformat input format.', array('%issuefilter' => t('Project Issue to link filter'), '!inputformat' => l($input_format->name, "admin/settings/filters/$format"))),
1213              'severity' => REQUIREMENT_ERROR,
1214            );
1215          }
1216  
1217          if ($conflict_anonymous && filter_format_allowcache($format)) {
1218            $requirements[] = array(
1219              'title' => t('Project Issue to link filter'),
1220              'value' => t('Some security conflicts were detected.'),
1221              'description' => t('%issuefilter conflicts with project issue access settings. Users who do not have access to all project issues may be able to see titles of project issues. You should disable this filter in !inputformat input format.', array('%issuefilter' => t('Project Issue to link filter'), '!inputformat' => l($input_format->name, "admin/settings/filters/$format"))),
1222              'severity' => REQUIREMENT_ERROR,
1223            );
1224          }
1225  
1226          // Put up an error when some code escaping filter's weight is higher.
1227          $low_filters = array('filter/0', 'filter/1', 'bbcode/0', 'codefilter/0', 'geshifilter/0');
1228          foreach ($low_filters as $lfilter) {
1229            if (isset($filters[$lfilter]) && $filters['project_issue/0']->weight <= $filters[$lfilter]->weight) {
1230              $description_names['%issuefilter'] = $filters['project_issue/0']->name;
1231              $description_names['%lowfilter']  = $filters[$lfilter]->name;
1232              $requirements[] = array(
1233                'title' => t('Project Issue to link filter'),
1234                'value' => t('Some filter conflicts were detected.'),
1235                'description' => t('%issuefilter should come after %lowfilter to prevent loss of layout and highlighting.', $description_names) .' '. l(t('Please rearrange the filters.'), "admin/settings/filters/$format/order"),
1236                'severity' => REQUIREMENT_ERROR,
1237              );
1238            }
1239          }
1240  
1241        }
1242      }
1243  
1244    }
1245    return $requirements;
1246  }
1247  
1248  /**
1249   * Implement hook_token_list() (from token.module).
1250   */
1251  function project_issue_token_list($type) {
1252    if ($type == 'node') {
1253      $tokens['node'] = array(
1254        'project_issue_pid' => t("The issue's project nid"),
1255        'project_issue_project_title' => t("The issue's project title"),
1256        'project_issue_project_title-raw' => t("The issue's project title raw"),
1257        'project_issue_project_shortname' => t("The issue's project short name"),
1258        'project_issue_category' => t("The issue's category (bug, feature)"),
1259        'project_issue_component' => t("The issue's component"),
1260        'project_issue_priority' => t("The issue's priority"),
1261        'project_issue_version' => t("The issue's version (if any)"),
1262        'project_issue_assigned' => t("The name of the user an issue is assigned to"),
1263        'project_issue_status' => t("The issue's status"),
1264      );
1265      return $tokens;
1266    }
1267  }
1268  
1269  /**
1270   * Implement hook_token_values() (from token.module).
1271   */
1272  function project_issue_token_values($type = 'all', $object = NULL) {
1273    if ($type == 'node') {
1274      // Defaults in case it's not an issue or we can't load its parent project.
1275      $values = array(
1276        'project_issue_pid' => '',
1277        'project_issue_project_title' => '',
1278        'project_issue_project_title-raw' => '',
1279        'project_issue_project_shortname' => '',
1280        'project_issue_category' => '',
1281        'project_issue_component' => '',
1282        'project_issue_priority' => '',
1283        'project_issue_version' => '',
1284        'project_issue_assigned' => '',
1285        'project_issue_status' => '',
1286      );
1287      if ($object->type == 'project_issue') {
1288        if (!empty($object->project_issue)) {
1289          // If $node->project_issue exists, use it.
1290          $issue = (object)$object->project_issue;
1291        }
1292        else {
1293          $issue = $object;
1294        }
1295        if ($project = node_load($issue->pid)) {
1296          $values['project_issue_pid'] = intval($issue->pid);
1297          $values['project_issue_project_title'] = check_plain($project->title);
1298          $values['project_issue_project_title-raw'] = $project->title;
1299          $values['project_issue_project_shortname'] = check_plain($project->project['uri']);
1300        }
1301        if (module_exists('project_release') && !empty($issue->rid) && $release = node_load($issue->rid)) {
1302          $values['project_issue_version'] = check_plain($release->project_release['version']);
1303        }
1304        if (!empty($issue->assigned)) {
1305          $account = user_load($issue->assigned);
1306          $values['project_issue_assigned'] = check_plain($account->name);
1307        }
1308        $values['project_issue_category'] = project_issue_category($issue->category, FALSE);
1309        $values['project_issue_component'] = check_plain($issue->component);
1310        $values['project_issue_priority'] = project_issue_priority($issue->priority);
1311        $values['project_issue_status'] = check_plain(project_issue_state($issue->sid));
1312      }
1313      return $values;
1314    }
1315  }
1316  
1317  /**
1318   * Calculate the differences in project_issue comment metadata
1319   * between the original issue and a comment or between two
1320   * comments.
1321   *
1322   * @param $view
1323   *   A string representing the metadata view being generated.  For the comment
1324   *   metadata table, this will be 'diff'.
1325   * @param $node
1326   *   The issue node.
1327   * @param $old_data
1328   *   Object containing old metadata.
1329   * @param $new_data
1330   *   Object containing new metadata.
1331   * @param $field_labels
1332   *   An associative array of field_name=>display_name pairs.
1333   *   In most cases, this will be the array returned by project_issue_change_summary().
1334   *
1335   * @return
1336   *  An associative array containing information about changes between
1337   *  the two objects.
1338   *  For example:
1339   *  array(
1340   *    'component' => array(
1341   *      'label' => t('Component'),
1342   *      'old' => 'Code',
1343   *      'new' => 'User interface',
1344   *    ),
1345   *    'sid' => array(
1346   *      'label' => t('Status'),
1347   *      'old' => 8,
1348   *      'new' => 13,
1349   *    ),
1350   *  )
1351   */
1352  function project_issue_metadata_changes($node, $old_data, $new_data, $field_labels = array()) {
1353    $changes = array();
1354    foreach ($field_labels as $property => $name) {
1355      if ($property == 'rid' && empty($old_data->rid) && empty($new_data->rid)) {
1356        // Special case for version -- if both are empty, leave it out entirely,
1357        // since maybe this project doesn't have (and/or disabled) releases.
1358        continue;
1359      }
1360      if (isset($old_data->$property) || isset($new_data->$property)) {
1361        $changes[$property] = array('label' => $name);
1362      }
1363      if (isset($old_data->$property) && isset($new_data->$property)) {
1364        if ($old_data->$property != $new_data->$property) {
1365          $changes[$property]['old'] = $old_data->$property;
1366          $changes[$property]['new'] = $new_data->$property;
1367        }
1368      }
1369      elseif (isset($old_data->$property)) {
1370        $changes[$property]['old'] = $old_data->$property;
1371      }
1372      elseif (isset($new_data->$property)) {
1373        $changes[$property]['new'] = $new_data->$property;
1374      }
1375    }
1376  
1377    // Allow other modules to implement hook_project_issue_metadata() so that they
1378    // can find changes in additional metadata.  In most cases other modules will
1379    // be responsible for storing this metadata in their own tables.  Developers
1380    // of modules that implement this hook should keep in mind the following:
1381    // 1.  Implementations of hook_project_issue_metadata() must take the
1382    //     $changes array by reference.
1383    // 2.  Differences in properties will only be processed later on for
1384    //     elements of the array which have the 'label', 'old', and 'new' properties
1385    //     defined.
1386    // In other words, for each line in the differences table (or field in the email)
1387    // that is displayed, your hook should add something like the following as a
1388    // new element of the $changes array:
1389    //    'taxonomy_vid_10' => array(
1390    //       'label' => 'Vocabulary 10',
1391    //       'old' => 'MySQL, pgSQL, javascript',
1392    //       'new' => 'pgSQL, newbie',
1393    //     ),
1394    //
1395    // There are two methods you can use to indicate multiple changes of a field.
1396    // The first is that for 'old' and 'new' you pass strings separated by some
1397    // character, customarily a comma.  This method is used in
1398    // the example above.  When using this method, the default display of the changes
1399    // will be to show all old values followed by all new values.  In the example
1400    // above, this would be displayed like:
1401    // Vocabulary 10:  MySQL, pgSQL, javascript >> pgSQL, newbie
1402    //
1403    // The other method you can use when constructing 'old' and 'new' is to make
1404    // both of these arrays, with each element of the array one change.  If you
1405    // use this method, all elements in the 'old' array are typically interpreted
1406    // as being removed, and all elements in the 'new' array are typically interpreted
1407    // as being added.  An example of this type of structure is as follows:
1408    //    'taxonomy_vid_10' => array(
1409    //       'label' => 'Vocabulary 10',
1410    //       'old' => array('MySQL', 'javascript'),
1411    //       'new' => array('newbie'),
1412    //     ),
1413    // In this situation, the default display of these changes in a project issue
1414    // metadata table would be as follows:
1415    // Vocabulary 10:  -MySQL, -javascript    +newbie
1416    foreach (module_implements('project_issue_metadata') as $module) {
1417      $function = $module .'_project_issue_metadata';
1418      $function('diff', $node, $changes, $old_data, $new_data);
1419    }
1420    return $changes;
1421  }
1422  
1423  function project_issue_form_project_quick_navigate_form_alter(&$form, $form_state) {
1424    $form['issues'] = array(
1425      '#type' => 'submit',
1426      '#value' => t('View issues'),
1427      '#submit' => array('project_issue_quick_navigate_issues_submit'),
1428      '#validate' => array('project_issue_quick_navigate_issues_validate'),
1429    );
1430  }
1431  
1432  function project_issue_quick_navigate_issues_validate($form, &$form_state) {
1433    $project = db_fetch_object(db_query("SELECT pp.uri, pip.issues FROM {project_projects} pp INNER JOIN {project_issue_projects} pip ON pp.nid = pip.nid WHERE pp.nid = %d", $form_state['values']['project_goto']));
1434    if (empty($project)) {
1435      form_set_error('project_goto', t('You must select a project to view issues for.'));
1436    }
1437    if (empty($project->issues)) {
1438      form_set_error('project_goto', t('The selected project does not have an issue queue.'));
1439    }
1440    else {
1441      $form_state['project_uri'] = $project->uri;
1442    }
1443  }
1444  
1445  function project_issue_quick_navigate_issues_submit($form, &$form_state) {
1446    $form_state['redirect'] = 'project/issues/'. $form_state['project_uri'];
1447  }
1448  
1449  function project_issue_form_project_quick_navigate_title_form_alter(&$form, $form_state) {
1450    $form['issues'] = array(
1451      '#type' => 'submit',
1452      '#value' => t('View issues'),
1453      '#validate' => array('project_issue_quick_navigate_title_issues_validate'),
1454      '#submit' => array('project_issue_quick_navigate_title_issues_submit'),
1455    );
1456  }
1457  
1458  function project_issue_quick_navigate_title_issues_validate($form, &$form_state) {
1459    if (empty($form_state['values']['project_title'])) {
1460      form_set_error('project_title', t('You must enter a project to view issues for.'));
1461    }
1462    else {
1463      $project = db_fetch_object(db_query("SELECT pp.uri, pip.issues FROM {node} n INNER JOIN {project_projects} pp ON n.nid = pp.nid INNER JOIN {project_issue_projects} pip ON n.nid = pip.nid WHERE n.title = '%s'", $form_state['values']['project_title']));
1464      if (empty($project)) {
1465        form_set_error('project_title', t('The name you entered (%title) is not a valid project.', array('%title' => $form_state['values']['project_title'])));
1466      }
1467      if (empty($project->issues)) {
1468        form_set_error('project_title', t('The selected project does not have an issue queue.'));
1469      }
1470      else {
1471        $form_state['project_uri'] = $project->uri;
1472      }
1473    }
1474  }
1475  
1476  function project_issue_quick_navigate_title_issues_submit($form, &$form_state) {
1477    $form_state['redirect'] = 'project/issues/'. $form_state['project_uri'];
1478  }
1479  
1480  function project_issue_alter_views_exposed_form(&$form, &$form_state) {
1481    switch ($form_state['view']->name) {
1482      case 'project_issue_search_all':
1483      case 'project_issue_search_project':
1484      case 'project_issue_user_projects':
1485        $user_filters = array('assigned', 'submitted', 'participant');
1486        foreach (element_children($form) as $element) {
1487          if ($form[$element]['#type'] == 'textfield') {
1488            if ($form_state['view']->name == 'project_issue_user_projects') {
1489              $form[$element]['#size'] = 16;
1490            }
1491            else {
1492              $form[$element]['#size'] = 32;
1493            }
1494          }
1495          elseif ($form[$element]['#type'] == 'select' && $form[$element]['#multiple']) {
1496            $form[$element]['#size'] = 5;
1497          }
1498          if (in_array($element, $user_filters)) {
1499            $form[$element]['#description'] = t('Enter a comma separated list of users.');
1500          }
1501        }
1502        break;
1503    }
1504    // Rename the "Apply" button to "Search" on all project_issue_* views.
1505    if (substr($form_state['view']->name, 0, 14) == 'project_issue_') {
1506      $form['submit']['#value'] = t('Search');
1507    }
1508  }
1509  
1510  function project_issue_preprocess_views_view_table($variables) {
1511    $view = $variables['view'];
1512    if ($view->plugin_name == 'project_issue_table') {
1513      foreach ($view->result as $num => $result) {
1514        $variables['row_classes'][$num][] = "state-$result->project_issues_sid";
1515        $variables['row_classes'][$num][] = "priority-$result->project_issues_priority";
1516      }
1517      $variables['class'] .= " project-issue";
1518    }
1519  }
1520  
1521  /**
1522   * Generate the links used at the top of query result pages.
1523   *
1524   * @param $project_arg
1525   *   The node ID or project short name (uri) of the project to generate links
1526   *   for, or NULL if it's a page of site-wide issues.
1527   *
1528   * @return
1529   *   Themed HTML output for the list of links.
1530   *
1531   * @see theme_project_issue_query_result_links()
1532   */
1533  function project_issue_query_result_links($project_arg = NULL) {
1534    global $user;
1535    $links = array();
1536  
1537    if (empty($project_arg)) {
1538      // These are site-wide links, not per-project
1539      if (node_access('create', 'project_issue')) {
1540        $links['create'] = array(
1541          'title' => t('Create a new issue'),
1542          'href' => "node/add/project-issue",
1543          'attributes' => array('title' => t('Create a new issue.')),
1544        );
1545      }
1546      else {
1547        $links['create'] = array(
1548          'title' => theme('project_issue_create_forbidden'),
1549          'html'  => TRUE,
1550        );
1551      }
1552      $links['search'] = array(
1553        'title' => t('Advanced search'),
1554        'href' => "project/issues/search",
1555        'attributes' => array('title' => t('Use the advanced search page for finding issues.')),
1556      );
1557      $links['statistics'] = array(
1558        'title' => t('Statistics'),
1559        'href' => "project/issues/statistics",
1560        'attributes' => array('title' => t('See statistics about issues.')),
1561      );
1562      if (!empty($user->uid) && variable_get('project_issue_global_subscribe_page', TRUE)) {
1563        $links['subscribe'] = array(
1564          'title' => t('Subscribe'),
1565          'href' => "project/issues/subscribe-mail",
1566          'attributes' => array('title' => t('Receive e-mail updates about issues.')),
1567        );
1568      }
1569    }
1570    else {
1571      // We know the project, make project-specific links.
1572      if (is_numeric($project_arg)) {
1573        $uri = project_get_uri_from_nid($project_arg);
1574      }
1575      else {
1576        $uri = $project_arg;
1577      }
1578  
1579      if (node_access('create', 'project_issue')) {
1580        $links['create'] = array(
1581          'title' => t('Create a new issue'),
1582          'href' => "node/add/project-issue/$uri",
1583          'attributes' => array('title' => t('Create a new issue for @project.', array('@project' => $uri))),
1584        );
1585      }
1586      else {
1587        $links['create'] = array(
1588          'title' => theme('project_issue_create_forbidden', $uri),
1589          'html'  => TRUE,
1590        );
1591      }
1592      $links['search'] = array(
1593        'title' => t('Advanced search'),
1594        'href' => "project/issues/search/$uri",
1595        'attributes' => array('title' => t('Use the advanced search page to find @project issues.', array('@project' => $uri))),
1596      );
1597      $links['statistics'] = array(
1598        'title' => t('Statistics'),
1599        'href' => "project/issues/statistics/$uri",
1600        'attributes' => array('title' => t('See statistics about @project issues.', array('@project' => $uri))),
1601      );
1602      if ($user->uid) {
1603        $links['subscribe'] = array(
1604          'title' => t('Subscribe'),
1605          'href' => "project/issues/subscribe-mail/$uri",
1606          'attributes' => array('title' => t('Receive e-mail updates about @project issues.', array('@project' => $uri))),
1607        );
1608      }
1609    }
1610    return theme('project_issue_query_result_links', $links);
1611  }
1612  
1613  /**
1614   * Helper function to return an array of projects that meet a given constraint.
1615   *
1616   * @param $constraint
1617   *   Restrict the list of projects.  Valid options are 'all' (all projects
1618   *   with issue tracking enabled), 'owner' (all projects owned by a given
1619   *   user) and 'participant' (all projects from issues that a given user
1620   *   submitted or commented on.
1621   * @param $uid
1622   *   User ID to use for $constraint == 'owner' or 'participant'.
1623   *
1624   * @return
1625   *   Array of project titles, keyed by node ID (nid) that match the given
1626   *   constraint.
1627   */
1628  function project_issue_get_projects($constraint = 'all', $uid = NULL) {
1629    $options = array();
1630    $join = '';
1631  
1632    // Only published projects.
1633    $where[] = 'n.status = %d';
1634    $args[] = 1;
1635    // That have issue tracking enabled.
1636    $where[] = 'pip.issues = %d';
1637    $args[] = 1;
1638  
1639    // Add extra JOIN and WHERE depending on the requested project source.
1640    switch ($constraint) {
1641      case 'owner':
1642        // The given uid must own each project.
1643        $where[] = 'n.uid = %d';
1644        $args[] = $uid;
1645        break;
1646  
1647      case 'participant':
1648        $join = 'INNER JOIN {project_issues} pi ON n.nid = pi.pid INNER JOIN {node} pin ON pi.nid = pin.nid LEFT JOIN {comments} c ON pi.nid = c.nid';
1649        // Restrict to published issues...
1650        $where[] = 'pin.status = %d';
1651        $args[] = 1;
1652        // ...that this user submitted or commented on.
1653        $where[] = '(pin.uid = %d OR c.uid = %d)';
1654        $args[] = $uid;
1655        $args[] = $uid;
1656        break;
1657    }
1658  
1659    // Build the actual query.
1660    $sql = "SELECT n.nid, n.title FROM {node} n INNER JOIN {project_issue_projects} pip ON n.nid = pip.nid $join WHERE " . implode(' AND ', $where) . " ORDER BY n.title ASC";
1661    $query = db_query(db_rewrite_sql($sql), $args);
1662    while ($project = db_fetch_object($query)) {
1663      $options[$project->nid] = $project->title;
1664    }
1665    return $options;
1666  }
1667  
1668  /**
1669   * Menu access callback for the project_issue_plugin_access_user_list plugin.
1670   */
1671  function project_issue_views_user_access($view_name, $display_id, $argument_name) {
1672    $view = views_get_view($view_name);
1673    $view->set_display($display_id);
1674    $view->init_handlers();
1675    // Find the values for any arguments embedded in the path via '%'.
1676    $i = 0;
1677    foreach (explode('/', $view->display_handler->get_option('path')) as $element) {
1678      if ($element == '%') {
1679        $view->args[] = arg($i);
1680      }
1681      $i++;
1682    }
1683    // Now handle any implicit arguments from the end of the path.
1684    $num_arguments = count($view->argument);
1685    while (count($view->args) < $num_arguments) {
1686      $view->args[] = arg($i);
1687      $i++;
1688    }
1689  
1690    $arg_uid = $view->argument[$argument_name]->get_value();
1691    return !empty($arg_uid);
1692  }
1693  
1694  /**
1695   * Return the views filter identifier for a given project issue vocabulary.
1696   */
1697  function project_issue_views_filter_identifier($name) {
1698    return drupal_strtolower(preg_replace('/[^a-zA-Z0-9]/', '_', check_plain($name)));
1699  }
1700  
1701  /**
1702   * Implementation of hook_block().
1703   */
1704  function project_issue_block($op = 'list', $delta = 0, $edit = array()) {
1705    if ($op == 'list') {
1706      $blocks['issue_cockpit'] = array(
1707        'info' => t('Issue cockpit'),
1708        'cache' => BLOCK_CACHE_PER_ROLE | BLOCK_CACHE_PER_PAGE,
1709      );
1710      return $blocks;
1711    }
1712    elseif ($op == 'configure' && $delta == 'issue_cockpit') {
1713      $options = array('All' => t('All issues')) + project_issue_category();
1714      $form['project_issue_cockpit_categories'] = array(
1715        '#type' => 'checkboxes',
1716        '#title' => t('Issue categories to display'),
1717        '#description' => t('Select which categories the block should display a summary of total vs. open issues.'),
1718        '#default_value' => variable_get('project_issue_cockpit_categories', array('All' => 'All', 'bug' => 'bug')),
1719        '#options' => $options,
1720      );
1721      return $form;
1722    }
1723    elseif ($op == 'save' && $delta == 'issue_cockpit') {
1724      variable_set('project_issue_cockpit_categories', $edit['project_issue_cockpit_categories']);
1725      // Invalidate the cache for this block since the categories might change.
1726      cache_clear_all('project_issue_cockpit_block:', 'cache', TRUE);
1727    }
1728    elseif ($op == 'view' && ($node = project_get_project_from_menu()) && !empty($node->project_issue['issues']) && node_access('view', $node)) {
1729      $cid = 'project_issue_cockpit_block:'. $node->nid;
1730      if (($cache = cache_get($cid))) {
1731        $block = $cache->data;
1732      }
1733      else {
1734        module_load_include('inc', 'project_issue', 'includes/issue_cockpit');
1735        $block = array(
1736          'subject' => t('Issues for @project', array('@project' => $node->title)),
1737          'content' => theme('project_issue_issue_cockpit', $node),
1738        );
1739        cache_set($cid, $block);
1740      }
1741      return $block;
1742    }
1743  }
1744  
1745  /**
1746   * Implemenation of hook_project_page_link_alter().
1747   *
1748   * Add project_issue-specific links to the array of project links.
1749   */
1750  function project_issue_project_page_link_alter(&$links, $node) {
1751    // TODO: Assumes "patch" issue status values (http://drupal.org/node/27865).
1752    // Prepend a link to the development section to view pending patches.
1753    if (!empty($node->project['uri'])) {
1754      $patches['pending_patches'] = l(t('View pending patches'), 'project/issues/search/'. $node->project['uri'], array('query' => 'status[]=8&status[]=13&status[]=14'));
1755      $links['development']['links'] = $patches + $links['development']['links'];
1756    }
1757  }
1758  


Generated: Thu Mar 24 11:18:33 2011 Cross-referenced by PHPXref 0.7