Index: comment.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/comment.inc,v
retrieving revision 1.85
diff -u -F^f -r1.85 comment.inc
--- comment.inc	19 Aug 2007 23:23:37 -0000	1.85
+++ comment.inc	28 Sep 2007 21:13:57 -0000
@@ -1,151 +1,119 @@
 <?php
-// $Id: comment.inc,v 1.85 2007/08/19 23:23:37 dww Exp $
-// $Name:  $
+// $Id: comment.inc,v 1.83 2007/07/30 14:31:36 dww Exp $
+// $Name: HEAD $
 
-function project_comment_page($op, $nid) {
-
-  // Load the parent node.
-  $node = node_load($nid);
-
-  if (node_access('create', 'project_issue')) {
-    $edit = (object)$_POST;
-
-    if ($_POST['op'] == t('Preview') || $_POST['op'] == t('Submit')) {
-      project_comment_validate($edit);
-    } else {
-      foreach (array('nid', 'type', 'pid', 'rid', 'category', 'component', 'priority', 'assigned', 'sid', 'title') as $var) {
-        $edit->$var = $node->$var;
-      }
-      project_comment_validate($edit);
-    }
-    $output .= drupal_get_form('project_comment_form', $edit);
-
-    // set breadcrumb
-    $project = node_load(array('nid' => $node->pid));
-    $breadcrumb[] = l($project->title, 'node/'. $project->nid);
-    $breadcrumb[] = l(t('Issues'), 'project/issues/'. $project->uri);
-    $breadcrumb[] = l($node->title, 'node/'. $node->nid);
-    project_project_set_breadcrumb($project, $breadcrumb);
-
-    drupal_set_title(t('New comment'));
-    switch ($_POST['op'] ? $_POST['op'] : arg(2)) {
-      case 'add':
-        $output .= node_view($node, NULL, TRUE);
-        return $output;
-        break;
-      case t('Preview'):
-          return $output;
-        break;
-      case t('Submit'):
-          if (!form_get_errors()) {
-            $edit->nid = $node->nid;
-
-            project_comment_save($edit);
-            drupal_goto("node/$node->nid");
-          } else {
-            return $output;
-          }
-        break;
-    }
-  }
-  else {
-    drupal_set_message(t('You are not authorized to follow up on issues.'), 'error');
-    drupal_goto("node/$nid");
-  }
-}
-
-function project_comment_form($edit, $param = NULL) {
-  $op = $_POST['op'];
-  if (isset($param)) {
-    $form = array(
-      '#method' => $param['method'],
-      '#action' => $param['action'],
-      '#attributes' => $param['options'],
-    );
-  } else {
-    $form['#attributes'] = array('enctype' => 'multipart/form-data');
-  }
-  $form['#prefix'] = '<div class="project-issue"><div class="node-form"><div class="standard">';
-  $form['#suffix'] = '</div></div></div>';
-  $form['project_issue_form'] = project_issue_form($edit, $param);
-  unset($form['project_issue_form']['#prefix']);
-  unset($form['project_issue_form']['#suffix']);
-
-  // Add any CCK fields to the follow-up form.
-  if (function_exists('_content_widget_invoke')) {
-    $db_node = node_load(array('nid' => arg(3), 'type' => 'project_issue'));
-    $type = content_types($db_node->type);
-    _content_widget_invoke('prepare form values', $db_node);
-    $form = array_merge($form, _content_widget_invoke('form', $db_node));
-  }
-
-  _project_issue_form_add_required_fields($form['project_issue_form'], FALSE);
-  if ($edit->cid) {
-    $form['cid']= array('#type' => 'hidden', '#value' => $edit->cid);
-  }
-  $form['preview'] = array('#type' => 'button', '#value' => t('Preview'));
-  if (!form_get_errors()) {
-    $form['submit'] = array('#type' => 'button', '#value' => t('Submit'));
+function project_issue_comment(&$arg, $op) {
+  static $edit;
+  // $arg can be a comment object, or a form or form_values.
+  if (is_object($arg)) {
+    $nid = $arg->nid;
   }
-  if ($op == t('Preview')) {
-    $form['#after_build'] = array('project_comment_form_add_preview');
+  elseif (is_array($arg)) {
+    $nid = is_array($arg['nid']) ? $arg['nid']['#value'] : $arg['nid'];
   }
-  return $form;
-}
-
-function project_comment_validate(&$edit) {
-  global $user;
-
-  $edit->uid = $user->uid;
-  $edit->name = $user->name;
-
-  if ($edit->cid) {
-    $comment = project_comment_load($edit->cid);
-    $edit->nid = $comment->nid;
+  $node = node_load($nid);
+  if ($node->type != 'project_issue') {
+    return;
   }
 
-  $edit->comment = true;
-
-  project_issue_comment_validate($edit);
-  $edit->validated = true;
-}
+  switch ($op) {
+    case 'form':
+      $project = node_load($node->pid);
+      $extra = array();
+      $extra[] = l($project->title, 'node/'. $project->nid);
+      // Only add issue title to the breadcrumb if this comment is a reply
+      // to another comment.
+      if ($arg['pid']['#value']) {
+        $extra[] = l($node->title, 'node/'. $node->nid);
+      }
+      project_project_set_breadcrumb($project, $extra);
 
-function project_comment_view($node, $main = 0) {
-  global $user;
-  $rows = array();
-  $result = db_query('SELECT p.*, u.name FROM {project_comments} p INNER JOIN {users} u USING (uid) WHERE p.nid = %d ORDER BY p.created ASC', $node->nid);
-  if (db_num_rows($result)) {
-    $output = '<div class="project-issue">';
-    $i = 0;
-    while ($comment = db_fetch_object($result)) {
-      $comment->body = db_decode_blob($comment->body);
-      $comment->data = db_decode_blob($comment->data);
-      $i++;
-      $output .= _project_comment_view_single($comment, $i);
-    }
-    $output .= '</div>';
-    return theme('box', t('Updates'), $output);
+      // $arg is a form
+      if (!$node->comment_count) {
+        foreach (array('nid', 'pid', 'rid', 'category', 'component', 'priority', 'assigned', 'sid', 'title') as $var) {
+          $edit->$var = $node->$var;
+        }
+      }
+      else {
+        $edit = db_fetch_object(db_query_range('SELECT nid, title, pid, rid, component, category, priority, assigned, sid, timestamp FROM {project_issue_comments} WHERE nid = %d ORDER BY timestamp DESC ', $node->nid, 0, 1));
+      }
+      // We need to ask for almost the same metadata as project issue itself
+      // so let's reuse the form.
+      $form = drupal_retrieve_form('project_issue_form', $edit, NULL);
+      // We need this otherwise pid collides with comment.
+      $form['project_info']['#tree'] = TRUE;
+      $form['project_info']['#weight'] = -2;
+      $form['issue_info']['#weight'] = -1;
+      $form['#prefix'] = '<div class="project-issue"><div class="node-form"><div class="standard">';
+      $form['#suffix'] = '</div></div></div>';
+      $form['title'] = array(
+        '#type' => 'textfield',
+        '#title' => t('Issue title'),
+        '#maxlength' => 64,
+        '#default_value' => $edit->title,
+        '#weight' => -30,
+        '#required' => TRUE,
+        '#description' => '<em>'. t('Note: modifying this value will change the title of the entire issue, not your follow-up comment.') .'</em>',
+      );
+      // Mark necessary required fields now, as these aren't added in the initial
+      // building of the project issue form.
+      _project_issue_form_add_required_fields($form, FALSE);
+      unset($form['page'], $form['issue_details']);
+      return $form;
+    case 'insert':
+      db_query("INSERT INTO {project_issue_comments} (nid, cid, pid, rid, component, category, priority, assigned, sid, title, timestamp) VALUES (%d, %d, %d, %d, '%s', '%s', %d, %d, %d, '%s', %d)", $arg['nid'], $arg['cid'], $arg['project_info']['pid'], $arg['rid'], $arg['project_info']['component'], $arg['category'], $arg['priority'], $arg['assigned'], $arg['sid'], $arg['title'], $arg['timestamp']);
+      project_issue_update_by_comment($arg, 'insert');
+      break;
+    case 'update':
+      // Updated comments always get moved to the most recently posted comment -- otherwise
+      // the state workflow tables can get screwy.
+      $time = time();
+      db_query("UPDATE {comments} SET timestamp = %d WHERE cid = %d", $time, $arg['cid']);
+      db_query("UPDATE {project_issue_comments} SET pid = %d, rid = %d, component = '%s', category = '%s', priority = %d, assigned = %d, sid = %d, title = '%s', timestamp = %d WHERE cid = %d", $arg['project_info']['pid'], $arg['rid'], $arg['project_info']['component'], $arg['category'], $arg['priority'], $arg['assigned'], $arg['sid'], $arg['title'], $time, $arg['cid']);
+      project_issue_update_by_comment($arg, 'update');
+      break;
+    case 'delete':
+      // Call the update by comment function first, so it can correctly determine if this
+      // is the most recent comment.
+      project_issue_update_by_comment($arg, 'delete');
+      db_query("DELETE FROM {project_issue_comments} WHERE cid = %d", $arg->cid);
+
+      break;
+    case 'view':
+      if (isset($arg->cid)) {
+        $project_issue_table = project_issue_comment_view($node, $arg);
+      }
+      else {
+        $test = drupal_clone($arg);
+        $test->pid = $arg->project_info['pid'];
+        $test->component = $arg->project_info['component'];
+        // Add a dummy rid if necessary -- prevents incorrect change data
+        if (!isset($arg->rid)) {
+          $test->rid = 0;
+        }
+        $project_issue_table = _project_issue_comment_table(_project_issue_comment_labels(), $edit, $test);
+      }
+      if ($project_issue_table) {
+        $arg->comment = '<div class="project-issue"><div class="summary">'. $project_issue_table .'</div></div>' . $arg->comment;
+      }
+      break;
+    case 'validate':
+      $project = node_load($node->pid);
+      if (module_exists('project_release') && ($releases = project_release_get_releases($project, 0))) {
+        empty($arg['project_info']['rid']) and form_set_error('project_info][rid', t('You have to specify a valid version.'));
+      }
+      $component = $arg['project_info']['component'];
+      if ($component && !in_array($component, $project->components)) {
+        $component = 0;
+      }
+      empty($component) && form_set_error('project_info][component', t('You have to specify a valid component.'));
+      empty($arg['category']) && form_set_error('category', t('You have to specify a valid category.'));
+      break;
   }
 }
 
-/**
- * Private method to view a single project comment (issue followup).
- *
- * @param $comment
- *   An array or object of the comment to view.
- * @param $count
- *   The integer that shows what number of comment this is.
- *
- * @return
- *   A string of validated output to theme/display.
- *
- */
-function _project_comment_view_single($comment, $count) {
-  $comment = (object)$comment;
-  $summary = array();
-  $output = '';
-
-  $fields = array(
+function _project_issue_comment_labels() {
+  return array(
     'title' => t('Title'),
     'pid' => t('Project'),
     'rid' => t('Version'),
@@ -155,154 +123,102 @@ function _project_comment_view_single($c
     'assigned' => t('Assigned to'),
     'sid' => t('Status'),
   );
+}
 
-  // If we got this from the DB, we'll have a $data field to unserialize.
-  $comment = drupal_unpack($comment);
-
-  // Print out what changed about the issue with this comment. If the
-  // comment is in the DB, we'll have 'old' and 'new' fields from the
-  // 'data' field, which record exactly what changed. If not, we'll
-  // load the origial node and compare against that.
-  if (!isset($comment->data)) {
-    $node = node_load(array('nid' => arg(3), 'type' => 'project_issue'));
-  }
-  foreach ($fields as $field => $text) {
-    if (isset($comment->old->$field) && isset($comment->new->$field)) {
-      $summary[] = array(
-        $text .':',
-        project_mail_summary($field, $comment->old->$field),
-        '&raquo; '. project_mail_summary($field, $comment->new->$field)
-      );
-    }
-    elseif (isset($node->$field) && isset($comment->$field) && $node->$field != $comment->$field ) {
-      $summary[] = array(
-        $text .':',
-        project_mail_summary($field, $node->$field),
-        '&raquo; '. project_mail_summary($field, $comment->$field)
+/**
+ * Create a project issue metadata table.
+ *
+ * @param $labels
+ *  An array, keys are field names, values are the displayed labels.
+ * @param $old
+ *  The previous comment (or the node).
+ * @param $followup
+ *  The current comment.
+ */
+function _project_issue_comment_table($labels, $old, $followup) {
+  $rows = array();
+  foreach ($labels as $field => $text) {
+    if ($old->$field != $followup->$field) {
+      $rows[] = array(
+        $labels[$field] .':',
+        project_mail_summary($field, $old->$field),
+        '&raquo; '. project_mail_summary($field, $followup->$field),
       );
     }
   }
-
-  if ($comment->file_path && file_exists($comment->file_path)) {
-    $summary[] = array(t('Attachment:'), '<a href="'. file_create_url($comment->file_path). '">'. basename($comment->file_path) .'</a> ('. format_size($comment->file_size) .')');
-  }
-
-  if ($summary || $comment->body) {
-    if ($count) {
-      $output .= '<div class="header">';
-      $output .= t('!count submitted by !user on !date', array('!count' => l("#$count", "node/$comment->nid", array ('id' => "comment-$comment->cid", 'name' => "comment-$comment->cid"), NULL, "comment-$comment->cid"), '!user' => theme('username', $comment), '!date' => format_date($comment->created))) . theme('mark', node_mark($comment->nid, $comment->changed));
-      $output .= '</div>';
-    }
-    if ($summary) {
-      $output .= '<div class="summary">';
-      $output .= theme('table', array(), $summary);
-      $output .= '</div>';
-    }
-    if ($comment->body) {
-      $output .= '<div class="content">';
-      $output .= check_markup($comment->body);
-      $output .= '</div>';
-    }
-  }
-  return $output;
+  return theme('table', array(), $rows);
 }
 
-function project_comment_load($cid) {
-  $object = db_fetch_object(db_query('SELECT p.*, u.name FROM {project_comments} p INNER JOIN {users} u USING (uid) WHERE p.cid = %d ORDER BY p.created DESC', $cid));
-  $object->body = db_decode_blob($object->body);
-  $object->data = db_decode_blob($object->data);
-  return $object;
-}
-
-function project_comment_save($edit) {
-  global $user;
-
-  if (empty($edit->cid)) {
-    $edit->cid = db_next_id('{project}_cid');
-    if ($edit->file) {
-      $directory = file_create_path(variable_get('project_directory_issues', 'issues'));
-      $edit->file->filename = project_issue_munge_filename($edit->file->filename);
-      $file = file_save_upload($edit->file, $directory);
-      unset($edit->file);
-    }
-
-    if (empty($edit->uid)) {
-      $edit->uid = $user->uid;
-    }
-    $node = node_load(array('nid' => $edit->nid, 'type' => 'project_issue'));
-
-    // Check if comment changed any of the state values and update the node if necessary
-    foreach (array('pid', 'rid', 'category', 'component', 'priority', 'assigned', 'sid', 'title') as $var) {
-      if ($node->$var != $edit->$var) {
-        $data['old']->$var = $node->$var;
-        $data['new']->$var = $edit->$var;
-        $node->$var = $edit->$var;
-      }
-    }
+/**
+ * Returns the issue metadata table for a comment.
+ *
+ * @param $node
+ *  The corresponding node.
+ * @param $comment
+ *  The comment, if it's set then metadata will be returned. If it's not
+ *  set then metadata will be precalculated.
+ * @return
+ *  A themed table of issue metadata.
+ */
+function project_issue_comment_view(&$node, $comment = NULL) {
+  static $project_issue_tables;
 
-    // Add processed cck info into the node object for node_save
-    if (function_exists('_content_widget_invoke')) {
-      $type = content_types($node->type);
-      while (list($field, $val) = each($type['fields'])) {
-        $node->$field = $edit->$field;
-      }
-      _content_widget_invoke('process form values', $node);
+  if (isset($comment)) {
+    return $project_issue_tables[$comment->cid];
+  }
+  if ($node->comment_count) {
+    $old = unserialize(db_result(db_query('SELECT original_issue_data FROM {project_issues} WHERE nid = %d', $node->nid)));
+    $labels = _project_issue_comment_labels();
+    $result = db_query('SELECT cid, title, pid, rid, component, category, priority, assigned, sid FROM {project_issue_comments} WHERE nid = %d ORDER BY timestamp ASC', $node->nid);
+    while ($followup = db_fetch_object($result)) {
+      $project_issue_tables[$followup->cid] = _project_issue_comment_table($labels, $old, $followup);
+      $old = $followup;
     }
-
-    watchdog('content', t('project_issue: added comment %title', array('%title' => $edit->title)), WATCHDOG_NOTICE, l('view', "node/$node->nid"));
-    $node->changed = time();
-    db_query("INSERT INTO {project_comments} (cid, nid, uid, created, changed, body, data, file_path, file_mime, file_size) VALUES (%d, %d, %d, %d, %d, %b, %b, '%s', '%s', %d)", $edit->cid, $edit->nid, $edit->uid, $node->changed, $node->changed, $edit->body, serialize($data), $file->filepath, $file->filemime, $file->filesize);
-
-    // Update node_comment_statistics so the tracker page lists the number of comments
-    $count = db_result(db_query('SELECT COUNT(cid) FROM {project_comments} WHERE nid = %d', $edit->nid));
-    db_query("UPDATE {node_comment_statistics} SET comment_count = %d, last_comment_timestamp = %d, last_comment_name = '%s', last_comment_uid = %d WHERE nid = %d", $count, time(), $user->name, $user->uid, $edit->nid);
-
-    node_save($node);
   }
-
-  return $edit->cid;
 }
 
 /**
- * Form API callback for previewing a project comment.
- *
- * @param $form
- *   The form to add the preview information to.
- * @param $edit
- *   The form values for the comment to preview.
- *
- * @return
- *   The modified form to render.
+ * Updates the project issue based on the comment inserted/updated/deleted.
  *
+ * @param $comment_data
+ *  The comment data that's been submitted.
+ * @param $op
+ *  The comment operation performed, 'insert', 'update', 'delete'.
  */
-function project_comment_form_add_preview($form, $edit) {
-  drupal_set_title(t('Preview comment'));
-  if (is_array($edit)) {
-    $comment = (object)$edit;
-  }
-  else {
-    $comment = $edit;
-  }
-  project_comment_validate($comment);
-
-  // Preview the comment with security check.
-  if (!form_get_errors()) {
-    $output = _project_comment_view_single($comment, 0);
+function project_issue_update_by_comment($comment_data, $op) {
+  if (is_array($comment_data)) {
+    // Massage the incoming data so the structure will work with the project_issue_update() function.
+    $comment_data['component'] = $comment_data['project_info']['component'];
+    $comment_data['pid'] = $comment_data['project_info']['pid'];
+    unset ($comment_data['project_info']);
+    $comment_data = (object) $comment_data;
+  }
+
+  switch ($op) {
+    case 'insert':
+    case 'update':
+      project_issue_update($comment_data);
+      $update_title = TRUE;
+      break;
+    case 'delete':
+      // Get the cid of the most recent comment, so we know if that's the one we're deleting.
+      $latest_cid = db_result(db_query_range('SELECT cid FROM {project_issue_comments} WHERE nid = %d ORDER BY timestamp DESC', $comment_data->nid, 0, 1));
+      if ($comment_data->cid == $latest_cid) {
+        // Get the cid of the next most recent comment, which will now be the last.
+        $latest_cid = db_result(db_query_range('SELECT cid FROM {project_issue_comments} WHERE nid = %d AND cid <> %d ORDER BY timestamp DESC', $comment_data->nid, $comment_data->cid, 0, 1));
+        $comment_data = db_fetch_object(db_query('SELECT * FROM {project_issue_comments} WHERE cid = %d', $latest_cid));
+        // Update the issue data to reflect the new final states.
+        project_issue_update($comment_data);
+        $update_title = TRUE;
+      }
+      break;
   }
 
-  $form['comment_preview'] = array(
-    '#value' => $output,
-    '#weight' => -100,
-    '#prefix' => '<div class="preview"><div class="comment">',
-    '#suffix' => '</div></div>',
-  );
-
-  $output = '';
-  if (is_numeric(arg(3))) {
-    $node = node_load(array('nid' => arg(3), 'type' => 'project_issue'));
-    $output .= node_view($node, NULL, TRUE);
-    $form['comment_preview_below'] = array('#value' => $output, '#weight' => 100);
+  // Issue title must be updated manually, when a comment is inserted, updated, or the last
+  // comment is deleted.
+  if (isset($update_title)) {
+    $node = node_load($comment_data->nid);
+    $node->title = $comment_data->title;
+    node_save($node);
   }
-  unset($form['#sorted']);
-  return $form;
 }
Index: issue.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/issue.inc,v
retrieving revision 1.255
diff -u -F^f -r1.255 issue.inc
--- issue.inc	27 Sep 2007 21:28:39 -0000	1.255
+++ issue.inc	28 Sep 2007 21:13:58 -0000
@@ -680,10 +680,6 @@ function project_issue_form($node, $para
       '#size' => 60,
       '#maxlength' => 128,
     );
-    if ($node->title) {
-      $form['issue_details']['title']['#default_value'] = $node->title;
-      $form['issue_details']['title']['#description'] = t('Note: modifying this value will change the title of the entire issue, not your follow-up comment.');
-    }
     $form['issue_details']['body'] = array(
       '#type' => 'textarea',
       '#title' => t('Description'),
@@ -826,83 +822,6 @@ function project_issue_node_form_validat
   }
 }
 
-function project_issue_comment_validate(&$node) {
-
-  // Set default values.
-  empty($node->priority) and $node->priority = 2;
-  empty($node->category) and $node->category = arg(4);
-  empty($node->sid) and $node->sid = variable_get('project_issue_default_state', 1);
-
-  // Try to find the active project
-  if (empty($node->pid)) {
-    if (isset($_POST['pid'])) {
-      $pid = $_POST['pid'];
-    }
-    else {
-      $pid = arg(3);
-    }
-    if (isset($pid)) {
-      if (is_numeric($pid)) {
-        $node->pid = db_result(db_query(db_rewrite_sql('SELECT p.nid FROM {project_projects} p WHERE p.nid = %d', 'p'), $pid), 0);
-      }
-      else {
-        $node->pid = db_result(db_query(db_rewrite_sql("SELECT p.nid FROM {project_projects} p WHERE p.uri = '%s'", 'p'), $pid), 0);
-      }
-    }
-  }
-
-  // Validate the rest of the form.
-  if (isset($node->title) && !$node->validated) {
-    if ($node->pid && $project = node_load($node->pid)) {
-      if (module_exists('project_release') &&
-          $releases = project_release_get_releases($project, 0)) {
-        empty($node->rid) and form_set_error('rid', t('You have to specify a valid version.'));
-      }
-      else {
-        // If there's no version defined, set this to 0 to match the
-        // value stored in the issue. Otherwise, project_comment_save()
-        // will think we've changed the version with this reply and
-        // will store extra data in the DB we don't want or need.
-        $node->rid = 0;
-      }
-      if ($node->component && !in_array($node->component, $project->components)) {
-        $node->component = 0;
-      }
-
-      empty($node->component) and form_set_error('component', t('You have to specify a valid component.'));
-      empty($node->category) and form_set_error('category', t('You have to specify a valid category.'));
-
-      // make sure this followup changes something or has a body
-      if (!empty($_POST) && empty($node->body)) {
-        if (is_numeric(arg(3))) {
-          $issue = node_load(arg(3));
-        }
-        if (isset($issue)
-            && $node->pid == $issue->pid
-            && $node->rid == $issue->rid
-            && $node->component == $issue->component
-            && $node->category == $issue->category
-            && $node->priority == $issue->priority
-            && $node->assigned == $issue->assigned
-            && $node->sid == $issue->sid
-            && $node->title == $issue->title) {
-          form_set_error('body', t('You must either specify a description or change something about this issue.'));
-        }
-      }
-
-      $file = file_check_upload('file_issue');
-      if ($file && project_issue_validate_file($file)) {
-        $node->file = file_save_upload($file);
-      }
-    }
-    elseif (isset($node->pid)) {
-      form_set_error('pid', t('You have to specify a valid project.'));
-    }
-  }
-
-  return $node;
-}
-
 /**
  * Ensure only files with allowed extension are uploaded.
  */
@@ -954,10 +873,6 @@ function project_issue_view($node, $teas
       '#value' => '<div class="header">'. t('Description') .'</div>',
       '#weight' => -3,
     );
-    $node->content['project_issue_comments'] = array(
-      '#value' => project_comment_view($node),
-      '#weight' => 2,
-    );
 
     // Breadcrumb navigation
     $breadcrumb[] = array('path' => 'project', 'title' => t('Projects'));
@@ -985,7 +900,28 @@ function project_issue_insert($node) {
     $node->file->filename = project_issue_munge_filename($node->file->filename);
     $file = file_save_upload($node->file, $directory);
   }
-  db_query("INSERT INTO {project_issues} (nid, pid, category, component, priority, rid, assigned, sid, file_path, file_mime, file_size) VALUES (%d, %d, '%s', '%s', %d, %d, %d, %d, '%s', '%s', %d)", $node->nid, $node->pid, $node->category, $node->component, $node->priority, $node->rid, $node->assigned, $node->sid, $file->filepath, $file->filemime, $file->filesize);
+
+  // Permanently store the original issue states in a serialized array. This is a bit
+  // yucky, but we need them for proper handling of states workflow.  The current states
+  // need to be stored in {project_issues} as well for query efficiency in issue queue
+  // searches, and it seems too messy to add a bunch of new columns to the {project_issues}
+  // table for the original states.
+  $original_issue_data = new stdClass();
+  $fields = array(
+    'pid',
+    'rid',
+    'component',
+    'category',
+    'priority',
+    'assigned',
+    'sid',
+    'title',
+  );
+  foreach ($fields as $field) {
+    $original_issue_data->$field = $node->$field;
+  }
+
+  db_query("INSERT INTO {project_issues} (nid, pid, category, component, priority, rid, assigned, sid, file_path, file_mime, file_size, original_issue_data) VALUES (%d, %d, '%s', '%s', %d, %d, %d, %d, '%s', '%s', %d, '%s')", $node->nid, $node->pid, $node->category, $node->component, $node->priority, $node->rid, $node->assigned, $node->sid, $file->filepath, $file->filemime, $file->filesize, serialize($original_issue_data));
   project_mail_notify($node);
 }
 
Index: mail.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/mail.inc,v
retrieving revision 1.76
diff -u -F^f -r1.76 mail.inc
--- mail.inc	28 Sep 2007 13:26:28 -0000	1.76
+++ mail.inc	28 Sep 2007 21:13:58 -0000
@@ -196,6 +196,7 @@ function project_mail_summary($field, $v
 }
 
 function project_mail_notify($node) {
+  return; //TODO: rewrite for ifac.
   if (defined('PROJECT_NOMAIL')) {
     return;
   }
Index: project_issue.info
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/project_issue.info,v
retrieving revision 1.3
diff -u -F^f -r1.3 project_issue.info
--- project_issue.info	13 Jun 2007 19:33:51 -0000	1.3
+++ project_issue.info	28 Sep 2007 21:13:58 -0000
@@ -1,5 +1,5 @@
 ; $Id: project_issue.info,v 1.3 2007/06/13 19:33:51 dww Exp $
 name = Project issue tracking
 description = Provides issue tracking for the project.module.
-dependencies = project
+dependencies = project comment comment_upload
 package = Project
Index: project_issue.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/project_issue.install,v
retrieving revision 1.17
diff -u -F^f -r1.17 project_issue.install
--- project_issue.install	22 Aug 2007 16:19:51 -0000	1.17
+++ project_issue.install	28 Sep 2007 21:13:58 -0000
@@ -1,6 +1,6 @@
 <?php
 // $Id: project_issue.install,v 1.17 2007/08/22 16:19:51 thehunmonkgroup Exp $
-// $Name:  $
+// $Name: HEAD $
 
 function project_issue_install() {
   // We need to check this before we try to create the table, so that
@@ -33,26 +33,28 @@ function project_issue_install() {
           file_path varchar(255) NOT NULL default '',
           file_mime varchar(255) NOT NULL default '',
           file_size int NOT NULL default 0,
+          original_issue_data text NOT NULL default '',
           PRIMARY KEY (nid),
           KEY project_issues_pid (pid),
           KEY project_issues_sid (sid),
           KEY project_issues_nid_assigned (nid, assigned)
         ) /*!40100 DEFAULT CHARACTER SET utf8 */;");
-      db_query("CREATE TABLE IF NOT EXISTS {project_comments} (
-          cid int(10) unsigned NOT NULL default '0',
-          nid int(10) unsigned NOT NULL default '0',
-          uid int(10) unsigned NOT NULL default '0',
-          name varchar(255) NOT NULL default '',
-          created int(10) unsigned NOT NULL default '0',
-          changed int(10) unsigned NOT NULL default '0',
-          body blob,
-          data blob,
-          file_path varchar(255) NOT NULL default '',
-          file_mime varchar(255) NOT NULL default '',
-          file_size int NOT NULL default 0,
-          PRIMARY KEY (cid),
-          KEY project_comments_nid (nid)
-        ) /*!40100 DEFAULT CHARACTER SET utf8 */;");
+      db_query("CREATE TABLE IF NOT EXISTS {project_issue_comments} (
+          nid int(11) default NULL,
+          cid int(11) default NULL,
+          rid int(11) default NULL,
+          component varchar(255) default NULL,
+          category varchar(255) default NULL,
+          priority int(11) default NULL,
+          assigned int(11) default NULL,
+          sid int(11) default NULL,
+          pid int(10) unsigned NOT NULL,
+          title varchar(255) NOT NULL,
+          timestamp int(10) unsigned NOT NULL,
+          original_issue_data text NOT NULL default '',
+          PRIMARY KEY(cid),
+          INDEX nid_timestamp (nid, timestamp)
+        ) /*!40100 DEFAULT CHARACTER SET utf8 */");
       db_query("CREATE TABLE IF NOT EXISTS {project_subscriptions} (
           nid int(10) unsigned NOT NULL default '0',
           uid int(10) unsigned NOT NULL default '0',
@@ -97,29 +99,29 @@ function project_issue_install() {
             file_path varchar(255) NOT NULL default '',
             file_mime varchar(255) default '' NOT NULL,
             file_size int default 0 NOT NULL,
+            original_issue_data text NOT NULL default '',
             PRIMARY KEY (nid)
           );");
         db_query("CREATE INDEX {project_issues}_pid_idx ON {project_issues}(pid)");
         db_query("CREATE INDEX {project_issues}_sid_idx ON {project_issues}(sid)");
         db_query('CREATE INDEX {project_issues}_nid_assigned_idx ON {project_issues}(nid, assigned)');
       }
-      if (!db_table_exists('project_comments')) {
-        db_query("CREATE TABLE {project_comments} (
-            cid int NOT NULL default '0',
-            nid int NOT NULL default '0',
-            uid int NOT NULL default '0',
-            name varchar(255) NOT NULL default '',
-            created int NOT NULL default '0',
-            changed int NOT NULL default '0',
-            body bytea,
-            data bytea,
-            file_path varchar(255) default '' NOT NULL,
-            file_mime varchar(255) default '' NOT NULL,
-            file_size int default 0 NOT NULL,
-            PRIMARY KEY (cid)
-          );");
-         db_query("CREATE SEQUENCE {project}_cid_seq INCREMENT 1 START 1");
-         db_query("CREATE INDEX {project_comments}_nid_idx ON {project_comments}(nid)");
+      if (!db_table_exists('project_issue_comments')) {
+        db_query("CREATE TABLE {project_issue_comments} (
+          nid int default NULL,
+          cid int default NULL,
+          rid int default NULL,
+          component varchar(255) default NULL,
+          category varchar(255) default NULL,
+          priority int default NULL,
+          assigned int default NULL,
+          sid int default NULL,
+          pid int NOT NULL,
+          title varchar(255) NOT NULL,
+          timestamp int NOT NULL,
+          PRIMARY KEY(cid)
+        )");
+        db_query('CREATE INDEX {project_issue_comments}_nid_timestamp_idx ON {project_issue_comments} (nid, timestamp)');
       }
       if (!db_table_exists('project_subscriptions')) {
         db_query("CREATE TABLE {project_subscriptions} (
@@ -374,3 +376,211 @@ function project_issue_update_5003() {
   }
   return $ret;
 }
+
+/**
+ * Add {project_issue_comments} table in preparation for issue followups as comments.
+ */
+function project_issue_update_6000() {
+  $ret = array();
+
+  switch ($GLOBALS['db_type']) {
+    case 'mysql':
+    case 'mysqli':
+      $ret[] = update_sql("CREATE TABLE IF NOT EXISTS {project_issue_comments} (
+          nid int(11) default NULL,
+          cid int(11) default NULL,
+          rid int(11) default NULL,
+          component varchar(255) default NULL,
+          category varchar(255) default NULL,
+          priority int(11) default NULL,
+          assigned int(11) default NULL,
+          sid int(11) default NULL,
+          pid int(10) unsigned NOT NULL,
+          title varchar(255) NOT NULL,
+          timestamp int(10) unsigned NOT NULL,
+          PRIMARY KEY(cid),
+          INDEX nid_timestamp (nid, timestamp)
+        ) /*!40100 DEFAULT CHARACTER SET utf8 */");
+      $ret[] = update_sql("ALTER TABLE {project_issues} ADD COLUMN original_issue_data text NOT NULL DEFAULT ''");
+      break;
+
+    case 'pgsql':
+      if (!db_table_exists('project_issue_comments')) {
+        $ret[] = update_sql("CREATE TABLE {project_issue_comments} (
+          nid int default NULL,
+          cid int default NULL,
+          rid int default NULL,
+          component varchar(255) default NULL,
+          category varchar(255) default NULL,
+          priority int default NULL,
+          assigned int default NULL,
+          sid int default NULL,
+          pid int NOT NULL,
+          title varchar(255) NOT NULL,
+          timestamp int NOT NULL,
+          PRIMARY KEY(cid)
+        )");
+        $ret[] = update_sql('CREATE INDEX {project_issue_comments}_nid_timestamp_idx ON {project_issue_comments} (nid, timestamp)');
+      }
+      db_add_column(&$ret, 'project_issues', 'original_issue_data', 'text', array('not null' => TRUE, 'default' => "''"));
+    break;
+  }
+
+  return $ret;
+}
+
+/**
+ * Convert issue followups to real comments.
+ */
+function project_issue_update_6001() {
+  // This determines how many issue nodes will be processed in each batch run. A reasonable
+  // default has been chosen, but you may want to tweak depending on your average number of
+  // issue followups per issue.
+  $limit = 20;
+
+  $fields = array(
+    'pid',
+    'rid',
+    'component',
+    'category',
+    'priority',
+    'assigned',
+    'sid',
+    'title',
+  );
+
+  // Multi-part update
+  if (!isset($_SESSION['project_issue_update_6000'])) {
+    $_SESSION['project_issue_update_6000'] = 0;
+    $_SESSION['project_issue_update_6000_max'] = db_result(db_query('SELECT COUNT(DISTINCT(n.nid)) FROM {node} n INNER JOIN {project_comments} pc ON n.nid = pc.nid'));
+  }
+
+  // Pull all issues that have followups.
+  $followup_nodes = db_query_range("SELECT DISTINCT(n.nid) FROM {node} n INNER JOIN {project_comments} pc ON n.nid = pc.nid ORDER BY n.nid", $_SESSION['project_issue_update_6000'], $limit);
+
+  // Loop through each issue.
+  while ($nid = db_fetch_object($followup_nodes)) {
+    $pid = 0;  // This sets the comment's parent as the issue node.
+    $nid = $nid->nid;
+
+    // Pull the issue.
+    $issue = db_fetch_array(db_query('SELECT title, n.nid, pid, category, component, priority, rid, assigned, sid FROM {node} n INNER JOIN {project_issues} i ON n.nid = i.nid WHERE n.nid = %d', $nid));
+
+    // Find the original metadata.
+    $issue_data = db_query("SELECT data, cid FROM {project_comments} WHERE nid = %d AND data != '' ORDER BY changed", $nid);
+    $data_array = array();
+    $original_found = array();
+    while ($comment = db_fetch_object($issue_data)) {
+      $data = unserialize(db_decode_blob($comment->data));
+      // Since we've ordered by followup creation date, the first 'old'
+      // value that's found in a sequential search is the original state
+      // for that field.
+      foreach ($fields as $field) {
+        if (isset($data['old']->$field) && !isset($original_found[$field])) {
+          $issue[$field] = $data['old']->$field;
+          $original_found[$field] = TRUE; // Mark as found.
+        }
+      }
+      // While we are here, let's store the unserialized object.
+      if (!empty($data['new'])) {
+        $data_array[$comment->cid] = $data['new'];
+      }
+    }
+
+    // Record the original issue states.
+    db_query("UPDATE {project_issues} SET original_issue_data = '%s' WHERE nid = %d", serialize((object) $issue), $nid);
+
+    // Pull the followups again.
+    $followup = db_query('SELECT cid, uid, changed, body, file_path, file_mime, file_size FROM {project_comments} WHERE nid = %d ORDER BY changed', $nid);
+    // Loop through each followup.
+    while ($comment = db_fetch_object($followup)) {
+      $comment->comment = db_decode_blob($comment->body);
+      // Check for metadata changes.
+      if (!empty($data_array[$comment->cid])) {
+        $data = $data_array[$comment->cid];
+        // Update the values in the issue copy.
+        foreach ($fields as $field) {
+          if (isset($data->$field)) {
+            $issue[$field] = $data->$field;
+          }
+        }
+      }
+      $thread = _project_update_6001_get_vancode($nid, $pid);
+      $cid = db_next_id('{comments}_cid');
+      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, '', %d, %d, 0, '', '%s', '', '', '')", $cid, $nid, $pid, $comment->uid, $issue['title'], $comment->comment, FILTER_FORMAT_DEFAULT, $comment->changed, COMMENT_PUBLISHED, $thread);
+      // To make each issue followup a child of the previous followup,
+      // uncomment the next line. Default is all followups are children of the issue
+      #$pid = $cid;
+      db_query("INSERT INTO {project_issue_comments} (nid, cid, rid, component, category, priority, assigned, sid, title, pid, timestamp) VALUES (%d, %d, %d, '%s', '%s', %d, %d, %d, '%s', %d, %d)", $nid, $cid, $issue['rid'], $issue['component'], $issue['category'], $issue['priority'], $issue['assigned'], $issue['sid'], $issue['title'], $issue['pid'], $comment->changed);
+      if ($comment->file_path) {
+        $fid = db_next_id('{files}_fid');
+        db_query("INSERT INTO {comment_upload_files} (fid, nid, cid, filename, filepath, filemime, filesize, description, list) VALUES (%d, %d, %d, '%s', '%s', '%s', %d, '%s', %d)", $fid, $nid, $cid, basename($comment->file_path), $comment->file_path, $comment->file_mime, $comment->file_size, '', 1);
+      }
+    }
+    _comment_update_node_statistics($nid);
+    $_SESSION['project_issue_update_6000']++;
+  }
+
+  if ($_SESSION['project_issue_update_6000'] >= $_SESSION['project_issue_update_6000_max']) {
+    $count = $_SESSION['project_issue_update_6000_max'];
+    unset($_SESSION['project_issue_update_6000']);
+    unset($_SESSION['project_issue_update_6000_max']);
+    return array(array('success' => TRUE, 'query' => t('Converted issue followups to comments for @count issues', array('@count' => $count))));
+  }
+  return array('#finished' => $_SESSION['project_issue_update_6000'] / $_SESSION['project_issue_update_6000_max']);
+
+}
+
+/**
+ * Drop {project_comments} table -- no longer needed.
+ */
+function project_issue_update_6002() {
+  $ret = array();
+  $ret[] = update_sql('DROP TABLE {project_comments}');
+  return $ret;
+}
+
+function _project_update_6001_get_vancode($nid, $pid) {
+  if ($pid == 0) {
+    // This is a comment with no parent comment (depth 0): we start
+    // by retrieving the maximum thread level.
+    $max = db_result(db_query('SELECT MAX(thread) FROM {comments} WHERE nid = %d', $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) .'/';
+  }
+  else {
+    // This is comment with a parent comment: we increase
+    // the part of the thread value at the proper depth.
+
+    // Get the parent comment:
+    $parent = _comment_load($pid);
+
+    // Strip the "/" from the end of the parent thread.
+    $parent->thread = (string) rtrim((string) $parent->thread, '/');
+
+    // Get the max value in _this_ thread.
+    $max = db_result(db_query("SELECT MAX(thread) FROM {comments} WHERE thread LIKE '%s.%%' AND nid = %d", $parent->thread, $nid));
+
+    if ($max == '') {
+      // First child of this parent.
+      $thread = $parent->thread .'.'. int2vancode(0) .'/';
+    }
+    else {
+      // Strip the "/" at the end of the thread.
+      $max = rtrim($max, '/');
+
+      // We need to get the value at the correct depth.
+      $parts = explode('.', $max);
+      $parent_depth = count(explode('.', $parent->thread));
+      $last = $parts[$parent_depth];
+
+      // Finally, build the thread field for this new comment.
+      $thread = $parent->thread .'.'. int2vancode(vancode2int($last) + 1) .'/';
+    }
+  }
+  return $thread;
+}
Index: project_issue.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/project_issue.module,v
retrieving revision 1.38
diff -u -F^f -r1.38 project_issue.module
--- project_issue.module	28 Sep 2007 21:00:06 -0000	1.38
+++ project_issue.module	28 Sep 2007 21:13:59 -0000
@@ -44,6 +44,21 @@ function project_issue_help($section) {
   }
 }
 
+/**
+ * Implementation of hook_form_alter.
+ */
+function project_issue_form_alter($form_id, &$form) {
+  if ($form_id == 'comment_form') {
+    $node = node_load($form['nid']['#value']);
+    if ($node->type == 'project_issue') {
+      // Comment is not required for project issue followups, we have our own
+      // validate handler.
+      // The 'your name' item just wastes screen estate.
+      unset($form['comment_filter']['comment']['#required'], $form['_author']);
+    }
+  }
+}
+
 function project_issue_node_info() {
   return array(
     'project_issue' => array(
@@ -156,31 +171,6 @@ function project_issue_cron() {
   }
 }
 
-function project_issue_link($type, $node = 0, $main = 0) {
-  $links = array();
-  switch ($type) {
-    case 'node':
-      if ($node->type == 'project_issue' &&
-          !(arg(0) == 'project' && arg(1) == 'comments')) {
-          // Only add the link if we're not already on an issue follow-up.
-            if (node_access('create', 'project_issue')) {
-              $links['project_issue_follow_up'] = array(
-                'title' => t('Follow up'),
-                'href' => "project/comments/add/$node->nid",
-              );
-            }
-            else {
-              $links['project_issue_follow_up_forbidden'] = array(
-                'title' => theme('project_issue_follow_up_forbidden', $node->nid),
-                'html' => TRUE,
-              );
-            }
-      }
-      break;
-  }
-  return $links;
-}
-
 function project_issue_menu($may_cache) {
   $items = array();
   global $user;
@@ -510,6 +500,11 @@ function project_issue_issue_nodeapi(&$n
         form_set_error('sid', t('Invalid issue status %status: you do not have permission to set this status', array('%status' => $state)));
       }
       break;
+    case 'view':
+      $_GET['mode'] = COMMENT_MODE_FLAT_EXPANDED;
+      $_GET['sort'] = COMMENT_ORDER_OLDEST_FIRST;
+      project_issue_comment_view($node);
+      break;
   }
 }
 
