Index: project_issue.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/project_issue.module,v retrieving revision 1.93 diff -u -r1.93 project_issue.module --- project_issue.module 15 May 2008 23:45:14 -0000 1.93 +++ project_issue.module 28 Jun 2008 17:36:28 -0000 @@ -7,6 +7,12 @@ /// How many issues should be displayed per page by default. define('PROJECT_ISSUES_PER_PAGE', 20); +/// Default age in days of issues to auto close. +define('PROJECT_ISSUE_AUTO_CLOSE_DAYS', 14 * 24 * 60 * 60); +/// Project issue state = fixed. +define('PROJECT_ISSUE_STATE_FIXED', 2); +/// Project issue state = closed. +define('PROJECT_ISSUE_STATE_CLOSED', 7); if (function_exists('drupal_get_path')) { $path = drupal_get_path('module', 'project_issue'); @@ -203,29 +209,33 @@ '#description' => t('All issue e-mails sent via subscriptions will appear from this e-mail address. You can use %project as a placeholder which will be replaced with the %short_project_name setting for the issue\'s current project.', array('%project' => '%project', '%short_project_name' => t('Short project name'))), ); - // Determine the auto-close username from the auto-close setting. - $auto_close_username = ''; - $uid = variable_get('project_issue_auto_close_user', 'anon'); + // Determine the auto-change username from the auto-change setting. + $auto_change_username = ''; $anon = variable_get('anonymous', t('Anonymous')); - if ($uid) { - if ($uid == 'anon') { - $auto_close_username = $anon; - } - elseif ($account = user_load(array('uid' => $uid))) { - $auto_close_username = $account->name; - } + if ($auto_user = project_issue_get_auto_change_user()) { + $auto_change_username = $auto_user->name; } - $form['project_issue_auto_close_user'] = array( - '#title' => t('Auto-close user'), + $form['project_issue_auto_change_user'] = array( + '#title' => t('Auto-change user'), '#type' => 'textfield', - '#default_value' => $auto_close_username, + '#default_value' => $auto_change_username, '#maxlength' => 60, - '#description' => t('Enter the user which will auto-close fixed issues -- leave empty to disable auto-closing or set to %anon to use the anonymous user.', array('%anon' => $anon)), - '#validate' => array('project_issue_validate_auto_close_user' => array()), + '#description' => t('Enter the user which will auto-change issues -- leave empty to disable auto-changing or set to %anon to use the anonymous user.', array('%anon' => $anon)), + '#validate' => array('project_issue_validate_auto_change_user' => array()), '#autocomplete_path' => 'user/autocomplete', ); + $form['project_issue_auto_close_days'] = array( + '#title' => t('Auto-close days'), + '#type' => 'textfield', + '#default_value' => variable_get('project_issue_auto_close_days', PROJECT_ISSUE_AUTO_CLOSE_DAYS), + '#size' => 4, + '#maxlength' => 10, + '#description' => t('When cron is run, all issues marked fixed that were last updated more than the number of days shown will be closed by the auto-change user. A value of 14 will close issues unchanged for two weeks.'), +// '#validate' => array('project_issue_validate_auto_close_days' => array()), + ); + if (module_exists('mailhandler')) { // TODO: move this stuff to mailhandler.module ? $items = array(t('')); @@ -261,23 +271,25 @@ } /** - * Validates that the auto-close user exists, and has sufficient permissions - * to auto-close issues. + * Validates that the auto-change user exists, and has sufficient permissions + * to auto-change issues. */ -function project_issue_validate_auto_close_user($form) { +function project_issue_validate_auto_change_user($form) { $name = $form['#value']; if ($name) { + $anon = variable_get('anonymous', t('Anonymous')); // Make this check case-insensitive to allow the admin some data entry leeway. - $is_anon = drupal_strtolower($name) == drupal_strtolower(variable_get('anonymous', t('Anonymous'))); + $is_anon = drupal_strtolower($name) == drupal_strtolower($anon); // Load the user. (don't see a constant for uid 0... ) $account = $is_anon ? user_load(array('uid' => 0)) : user_load(array('name' => $name)); if ($account) { if (user_access('access project issues', $account)) { // Transform the username into the more stable user ID. - form_set_value($form, $is_anon ? 'anon' : $account->uid); +// form_set_value($form, $is_anon ? 'anon' : $account->uid); + form_set_value($form, $account->uid); // Is there a reason not to store the actual uid? } else { - form_error($form, t('%name does not have sufficient permissions to auto-close issues.', array('%name' => $is_anon ? variable_get('anonymous', t('Anonymous')) : $name))); + form_error($form, t('%name does not have sufficient permissions to auto-change issues.', array('%name' => $is_anon ? $anon : $name))); } } else { @@ -302,105 +314,312 @@ } /** - * Automatically closes issues marked as fixed after two weeks. + * Automatically close issues marked as fixed for a specified number of days + * and add a comment to each documenting the change. */ function project_issue_auto_close() { + drupal_set_message(t('Inside auto close.'), 'warning'); + // Set query parameters. + $sid_fixed = PROJECT_ISSUE_STATE_FIXED; + $seconds = 24 * 60 * 60 * variable_get('project_issue_auto_close_days', PROJECT_ISSUE_AUTO_CLOSE_DAYS); + + $changes = array(); + $comment = theme('project_issue_auto_close_message'); + $result = db_query('SELECT pi.nid FROM {project_issues} pi INNER JOIN {node} n ON n.nid = pi.nid WHERE n.status = 1 AND pi.sid = %d AND n.changed < %d', $sid_fixed, time() - $seconds); + while ($issue = db_fetch_object($result)) { + // Set auto-close status and message. + $change = array( + 'nid' => $issue->nid, + 'sid' => PROJECT_ISSUE_STATE_CLOSED, + 'comment' => $comment, // theme('project_issue_auto_close_message'), + // For testing only. + 'cid' => '', + 'pid' => '', + 'uid' => '', + 'subject' => '', + 'hostname' => '', + ); + $changes[] = $change; + drupal_set_message(t('Change values = ' . print_r($change, TRUE)), 'warning'); +// $changes[] = array( +// 'nid' => $issue->nid, +// 'sid' => PROJECT_ISSUE_STATE_CLOSED, +// 'comment' => $comment, // theme('project_issue_auto_close_message'), +// ); + } + + if (!empty($changes)) { + drupal_set_message(t('Changes not empty.'), 'warning'); + project_issue_auto_change($changes); + } +} + +/** + * Comment left when cron auto-closes an issue. + */ +function theme_project_issue_auto_close_message() { + return t('Automatically closed -- issue fixed for two weeks with no activity.'); +} + +/** + * Automatically make changes to one or more project issues and add a comment + * to each documenting the change. + * + * @param $changes + * An array of keyed arrays of project issue fields to change. + * Required keys are nid, sid and comment. + * Keys ignored include the {comment} fields of cid, pid, uid, subject, + * hostname, timestamp, score, status, format, thread, users, name, mail, + * and homepage. + * + * Example: To change the issue status and set the comment text for the + * issue with nid = 100, this array might look like: + * $changes = array( + * 0 => array( + * 'nid' => 100, + * 'sid' => 4, + * 'comment' => t('This issue was automatically closed after 2 weeks of no activity.'), + * ); + * ); + * @param $messages + * Not implemented. An array of comment messages. This array would be used + * to avoid redundant comment text in the changes array. To utilize this, + * include a key of 'message_id' in the changes array whose value is the + * index of the message text in the messages array. + */ +function project_issue_auto_change($changes, $messages = array()) { global $user; - if ($uid = variable_get('project_issue_auto_close_user', 'anon')) { - // If a user exists for auto-closing comments, load them into + drupal_set_message(t('Inside auto change.'), 'warning'); + if (!isset($changes) || empty($changes)) { + return; + } + + drupal_set_message(t('Params are not empty.'), 'warning'); + if ($auto_user = project_issue_get_auto_change_user()) { + // If a user exists for auto-changes, load them into // the global user object temporarily. We use session_save_session() // to provide safe user impersonation. - $auto_close = TRUE; $original_user = $user; session_save_session(FALSE); - $is_anon = $uid == 'anon'; - $user = $is_anon ? user_load(array('uid' => 0)) : user_load(array('uid' => $uid)); - // Safety check -- we have to have a valid user here. - if(!$user) { - watchdog('project_issue', t('Auto-close user failed to load.'), WATCHDOG_ERROR); - $auto_close = FALSE; - } - // Safety check -- selected user must still have the correct permissions to auto-close issues. - if (!user_access('access project issues')) { - watchdog('project_issue', t('%name does not have sufficient permissions to auto-close issues.', array('%name' => $is_anon ? variable_get('anonymous', t('Anonymous')) : $user->name)), WATCHDOG_ERROR); - $auto_close = FALSE; + $user = $auto_user; + +// drupal_set_message(t('Change values = ' . print_r($changes, TRUE)), 'warning'); + $changes_made = _project_issue_insert_auto_changes($changes, $messages); + drupal_set_message(t('Changes made = ' . $changes_made), 'warning'); + + if ($changes_made) { + // Clear cache so anonymous users can see the new post. + cache_clear_all(); } - if ($auto_close) { - $result = db_query('SELECT p.nid, p.pid, p.category, p.component, p.priority, p.rid, p.assigned, p.sid, n.title FROM {project_issues} p INNER JOIN {node} n ON n.nid = p.nid WHERE n.status = 1 AND p.sid = 2 AND n.changed < %d', time() - 14 * 24 * 60 * 60); + // Reload the original user. + $user = $original_user; + session_save_session(TRUE); + } +} - // Set up the persistent {comments} data. +/** + * Load and verify the auto-change user. + * + * @return $account + * The account of the auto-change user (or FALSE if not found). + */ +function project_issue_get_auto_change_user() { + $uid = variable_get('project_issue_auto_change_user', 0); + if ($uid == '') { + return FALSE; + } + $account = user_load(array('uid' => $uid)); + // Safety check -- we have to have a valid user here. + if (!$account) { + watchdog('project_issue', t('Auto-change user failed to load.'), WATCHDOG_ERROR); + return FALSE; + } + $anon = variable_get('anonymous', t('Anonymous')); + $account->name = $uid ? $account->name : $anon; + // Safety check -- selected user must still have the correct permissions to auto-change issues. + if (!user_access('access project issues', $account)) { + watchdog('project_issue', t('%name does not have sufficient permissions to auto-change issues.', array('%name' => $account->name)), WATCHDOG_ERROR); + return FALSE; + } + return $account; +} + +/** + * Automatically make changes to one or more project issues and add a comment + * to each documenting the change. + * Internal function for use with auto-change of issues. + * + * @param $changes + * An array of keyed arrays of project issue fields to change. + * Required keys are nid, sid and comment. + * Keys ignored include the {comment} fields of cid, pid, uid, subject, + * hostname, timestamp, score, status, format, thread, users, name, mail, + * and homepage. + * + * Example: To change the issue status and set the comment text for the + * issue with nid = 100, this array might look like: + * $changes = array( + * 0 => array( + * 'nid' => 100, + * 'sid' => 4, + * 'comment' => t('This issue was automatically closed after 2 weeks of no activity.'), + * ); + * ); + * @param $messages + * Not implemented. An array of comment messages. This array would be used + * to avoid redundant comment text in the changes array. To utilize this, + * include a key of 'message_id' in the changes array whose value is the + * index of the message text in the messages array. + * @return + * Number of comment records inserted. + */ +function _project_issue_insert_auto_changes($changes, $messages = array()) { + global $user; + + /* + * We would prefer to call comment_save() except that: + * It sets status based on user access value and we want it always to be published. + * $edit['status'] = user_access('post comments without approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED; + * It calls cache_clear_all() each time (which would be inefficient for the cron auto_close). + * It adds an entry to the watchdog log for each comment added. + */ + + if (!isset($changes) || empty($changes)) { + return 0; + } + + // Code from comment_save. + $roles = variable_get('comment_roles', array()); + $score = 0; + foreach (array_intersect(array_keys($roles), array_keys($user->roles)) as $rid) { + $score = max($roles[$rid], $score); + } + $users = serialize(array(0 => $score)); + + $changes_made = 0; + foreach ($changes as $change) { + // Verify required keys. + if (!_project_issue_verify_required_keys($change)) { + continue; + } + + // Remove keys ignored. + _project_issue_remove_disallowed_keys($change); + + // TODO If message_id key exists, replace comment with and message[message_id]. + + // Retrieve current project issue. + drupal_set_message(t('$nid = ' . print_r($change['nid'], TRUE)), 'warning'); + $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', $change['nid']); + + if ($issue = db_fetch_object($result)) { + // Build vancode + $max = db_result(db_query('SELECT MAX(thread) FROM {comments} WHERE nid = %d', $change['nid'])); + // Strip the "/" from the end of the thread. + $max = rtrim($max, '/'); + // Finally, build the thread field for this new comment. + $thread = int2vancode(vancode2int($max) + 1) .'/'; + + $comment = array(); + + // Set the {comments} data. + $comment['cid'] = db_next_id('{comments}_cid'); $comment['pid'] = 0; + $comment['nid'] = $issue->nid; $comment['uid'] = $user->uid; // The correct subject number is supplied during the save cycle. $comment['subject'] = 'temp'; - $comment['comment'] = theme('project_issue_auto_close_message'); - $comment['format'] = FILTER_FORMAT_DEFAULT; + $comment['comment'] = 'No comment.'; // $issue->comment; + $comment['hostname'] = $_SERVER['REMOTE_ADDR']; + $comment['timestamp'] = time(); + $comment['score'] = $score; $comment['status'] = COMMENT_PUBLISHED; + $comment['format'] = FILTER_FORMAT_DEFAULT; + $comment['thread'] = $thread; + $comment['users'] = $users; $comment['name'] = $user->name; $comment['mail'] = ''; $comment['homepage'] = ''; - $roles = variable_get('comment_roles', array()); - $score = 0; - foreach (array_intersect(array_keys($roles), array_keys($user->roles)) as $rid) { - $score = max($roles[$rid], $score); - } - $users = serialize(array(0 => $score)); + // Set the {project_issue_comments} data. + $comment['category'] = $issue->category; + $comment['priority'] = $issue->priority; + $comment['assigned'] = $issue->assigned; + $comment['sid'] = $issue->sid; + $comment['title'] = $issue->title; + $comment['project_info']['pid'] = $issue->pid; + $comment['project_info']['rid'] = $issue->rid; + $comment['project_info']['component'] = $issue->component; - // Set up the persistent {project_issue_comments} data. - // TODO: It's evil to hard-code the status here. - $comment['sid'] = 7; - - $clear_cache = FALSE; - - while ($issue = db_fetch_object($result)) { - $clear_cache = TRUE; - - $comment['cid'] = db_next_id('{comments}_cid'); - $comment['timestamp'] = time(); - $comment['nid'] = $issue->nid; - $comment['category'] = $issue->category; - $comment['priority'] = $issue->priority; - $comment['assigned'] = $issue->assigned; - $comment['title'] = $issue->title; - $comment['project_info']['pid'] = $issue->pid; - $comment['project_info']['rid'] = $issue->rid; - $comment['project_info']['component'] = $issue->component; - - // Build vancode - $max = db_result(db_query('SELECT MAX(thread) FROM {comments} WHERE nid = %d', $comment['nid'])); - // Strip the "/" from the end of the thread. - $max = rtrim($max, '/'); - // Finally, build the thread field for this new comment. - $thread = int2vancode(vancode2int($max) + 1) .'/'; + // Set the {node_comment_statistics} data +// $comment['???']['last_comment_name'] = $user->name; // What is the prefix for this table? - db_query("INSERT INTO {comments} (cid, nid, pid, uid, subject, comment, format, hostname, timestamp, status, score, users, thread, name, mail, homepage) VALUES (%d, %d, %d, %d, '%s', '%s', %d, '%s', %d, %d, %d, '%s', '%s', '%s', '%s', '%s')", $comment['cid'], $comment['nid'], $comment['pid'], $comment['uid'], $comment['subject'], $comment['comment'], $comment['format'], $_SERVER['REMOTE_ADDR'], $comment['timestamp'], $comment['status'], $score, $users, $thread, $comment['name'], $comment['mail'], $comment['homepage']); + // Merge with change array (should only affect fields that may be overridden). + $comment = array_merge($comment, $change); + drupal_set_message(t('Merged array = ' . print_r($comment, TRUE)), 'warning'); - _comment_update_node_statistics($comment['nid']); +// db_query("INSERT INTO {comments} (cid, pid, nid, uid, subject, comment, hostname, timestamp, score, status, format, thread, users, name, mail, homepage) VALUES (%d, %d, %d, %d, '%s', '%s', '%s', %d, %d, %d, %d, '%s', '%s', '%s', '%s', '%s')", $comment['cid'], $comment['pid'], $comment['nid'], $comment['uid'], $comment['subject'], $comment['comment'], $comment['hostname'], $comment['timestamp'], $comment['score'], $comment['status'], $comment['format'], $comment['thread'], $comment['users'], $comment['name'], $comment['mail'], $comment['homepage']); + db_query("INSERT INTO {comments} (cid, pid, nid, uid, subject, comment, hostname, timestamp, score, status, format, thread, users, name, mail, homepage) VALUES (%d, %d, %d, %d, '%s', '%s', '%s', %d, %d, %d, %d, '%s', '%s', '%s', '%s', '%s')", $comment); + $changes_made++; - // Tell the other modules a new comment has been submitted. - comment_invoke_comment($comment, 'insert'); - } + _comment_update_node_statistics($comment['nid']); - if ($clear_cache) { - // Clear cache so anonymous users can see the new post. - cache_clear_all(); - } + // Tell the other modules a new comment has been submitted. + comment_invoke_comment($comment, 'insert'); } + } - // Load the original user back in. - $user = $original_user; - session_save_session(TRUE); + return $changes_made; +} + +/** + * Verify the presence of required keys. + * Internal function for use with auto-change of issues. + * + */ +function _project_issue_verify_required_keys($change) +{ + $required = array('nid', 'sid', 'comment'); + foreach ($required as $key) { + if (!array_key_exists($key, $change)) { + drupal_set_message(t('Required key missing = ' . print_r($key, TRUE)), 'warning'); + drupal_set_message(t('Change values = ' . print_r($change, TRUE)), 'warning'); + return FALSE; + } } + return TRUE; } /** - * Comment left when cron auto-closes an issue. + * Remove any elements (based on key) we want to set. + * Internal function for use with auto-change of issues. + * + * @param $change + * An keyed array of project issue fields to change. + * @return + * The modified $change array with disallowed keys removed. */ -function theme_project_issue_auto_close_message() { - return t('Automatically closed -- issue fixed for two weeks with no activity.'); +function _project_issue_remove_disallowed_keys(&$change) +{ + $disallowed = array( + 'cid' => '', + 'pid' => '', + 'uid' => '', + 'subject' => '', + 'hostname' => '', + 'timestamp' => '', + 'score' => '', + 'status' => '', + 'format' => '', + 'thread' => '', + 'users' => '', + 'name' => '', + 'mail' => '', + 'homepage' => '', + ); + $change = array_diff_key($change, $disallowed); } function project_issue_menu($may_cache) { @@ -870,7 +1089,7 @@ array('data' => t('Issue links'), 'class' => 'project-issue-links'), ); $default_states = implode(',', project_issue_default_states()); - $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} p ON n.nid = p.pid AND p.sid IN ($default_states) LEFT JOIN {node} ni ON ni.nid = p.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), $user->uid); + $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), $user->uid); if (!db_num_rows($result)) { return ($current_user ? t('You have no projects.') : t('This user has no projects.')); Index: project_issue.install =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/project_issue.install,v retrieving revision 1.49 diff -u -r1.49 project_issue.install --- project_issue.install 13 Apr 2008 21:13:18 -0000 1.49 +++ project_issue.install 28 Jun 2008 17:36:27 -0000 @@ -208,6 +208,7 @@ 'project_issue_show_comment_signatures', 'project_issue_site_help', 'project_issue_invalid_releases', + 'project_issue_auto_change_user', ); foreach ($variables as $variable) { variable_del($variable); @@ -774,6 +775,20 @@ } /** + * Replace project_issue_auto_close_user in {variable}. + */ +function project_issue_update_5208() { + $ret = array(); +// $ret[] = update_sql("UPDATE {variable} SET name = 'project_issue_auto_change_user' WHERE name = 'project_issue_auto_close_user'"); + $uid = variable_get('project_issue_auto_close_user', NULL); + if (isset($uid)) { + variable_set('project_issue_auto_change_user', $uid); + variable_del('project_issue_auto_close_user'); + } + return $ret; +} + +/** * Helper function for determining new module dependencies. * * @param $modules