| [ Index ] |
PHP Cross Reference of Drupal 6 (gatewave) |
[Summary view] [Print] [Text view]
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
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Thu Mar 24 11:18:33 2011 | Cross-referenced by PHPXref 0.7 |