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('<none>'));
@@ -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
