Index: comment.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/comment.inc,v
retrieving revision 1.128
diff -u -F^f -u -F^f -r1.128 comment.inc
--- comment.inc	7 Mar 2008 04:41:56 -0000	1.128
+++ comment.inc	7 Mar 2008 17:06:17 -0000
@@ -128,8 +128,8 @@ function project_issue_comment(&$arg, $o
         $test->component = $arg->project_info['component'];
         // Add a dummy rid if necessary -- prevents incorrect change data.
         $test->rid = $arg->project_info['rid'] ? $arg->project_info['rid'] : 0;
-        $comment_changes = project_issue_comment_changes($node, $original_node, $test, project_issue_field_labels('web'));
-        $project_issue_table = _project_issue_comment_table($comment_changes);
+        $comment_changes = project_issue_diff_metadata($node, $original_node, $test, project_issue_field_labels('web'));
+        $project_issue_table = theme('project_issue_comment_table', $comment_changes);
       }
       if ($project_issue_table) {
         $arg->comment = '<div class="project-issue"><div class="summary">'. $project_issue_table .'</div></div>' . $arg->comment;
@@ -201,7 +201,7 @@ function project_issue_comment(&$arg, $o
         $comment->component = $arg['project_info']['component'];
         // Add a dummy rid if necessary -- prevents incorrect change data.
         $comment->rid = $arg['project_info']['rid'] ? $arg['project_info']['rid'] : 0;
-        $comment_changes = project_issue_comment_changes($node, $original_node, $comment, project_issue_field_labels('web'));
+        $comment_changes = project_issue_diff_metadata($node, $original_node, $comment, project_issue_field_labels('web'));
         $has_change = FALSE;
         foreach ($comment_changes as $field => $changes) {
           if (isset($changes['new'])) {
@@ -218,27 +218,88 @@ function project_issue_comment(&$arg, $o
 }
 
 /**
- * Create a project issue metadata table.
+ * Theme a project issue metadata table.
  *
  * @param $comment_changes
  *  Array containing metadata differences between comments
- *  as returned by project_issue_comment_changes().
+ *  as returned by project_issue_diff_metadata().
+ * @return
+ *  The themed metadata table.
  */
-function _project_issue_comment_table($comment_changes) {
+function theme_project_issue_comment_table($comment_changes) {
   $rows = array();
   foreach ($comment_changes as $field => $change) {
     if (!empty($change['label']) && isset($change['old']) && isset($change['new'])) {
-      $rows[] = array(
-        check_plain($change['label']) .':',
-        check_plain(project_issue_change_summary($field, $change['old'])),
-        '&raquo; '. check_plain(project_issue_change_summary($field, $change['new'])),
-      );
+      $rows[] = theme('project_issue_comment_table_row', $field, $change);
     }
   }
   return theme('table', array(), $rows);
 }
 
 /**
+ * Theme a single row of the project issue metadata changes table.
+ *
+ * @param $field
+ *   The name of the field to theme.
+ * @param $change
+ *   A nested array containing changes to project issue metadata
+ *   for the given issue or comment.
+ * @return
+ *  An array representing one row of the table.
+ *
+ * NOTE:  If you override this theme function, you *must* make sure
+ * that you sanitize all output from this function that is displayed
+ * to the user.  No further escaping/filtering of the data in this
+ * table will take place after this function.  In most cases
+ * this means that you need to run the $change['label'], $change['old'],
+ * and $change['new'] values through either the check_plain() or
+ * filter_xss() function to prevent XSS and other types
+ * of problems due to any malicious input in these
+ * field values.
+ */
+function theme_project_issue_comment_table_row($field, $change) {
+  // Allow anchor, emphasis, and strong tags in metadata tables.
+  $allowed_tags = array('a', 'em', 'strong');
+
+  if (is_array($change['old']) || is_array($change['new'])) {
+    $removed = array();
+    if (is_array($change['old'])){
+      foreach ($change['old'] as $item) {
+        $removed[] = '-'. $item .' ';
+      }
+    }
+    elseif (!empty($change['old'])) {
+      $removed[] = '-'. $change['old'] .' ';
+    }
+
+    $added = array();
+    if (is_array($change['new'])) {
+      foreach ($change['new'] as $item) {
+        $added[] = '+'. $item .' ';
+      }
+    }
+    elseif (!empty($change['new'])) {
+      $added[] = '+'. $change['new'] .' ';
+    }
+
+    $row = array(
+      filter_xss(($change['label']), $allowed_tags) .':',
+      filter_xss(check_plain(implode(', ', $removed)), $allowed_tags),
+      filter_xss((implode(', ', $added)), $allowed_tags),
+    );
+    return $row;
+  }
+  else {
+    $row = array(
+      filter_xss(($change['label']), $allowed_tags) .':',
+      filter_xss((project_issue_change_summary($field, $change['old'])), $allowed_tags),
+      '&raquo; '. filter_xss((project_issue_change_summary($field, $change['new'])), $allowed_tags),
+    );
+    return $row;
+  }
+}
+
+/**
  * Returns the issue metadata table for a comment.
  *
  * @param $node
@@ -260,95 +321,14 @@ function project_issue_comment_view(&$no
     $labels = project_issue_field_labels('web');
     $result = db_query('SELECT p.cid, p.title, p.pid, p.rid, p.component, p.category, p.priority, p.assigned, p.sid FROM {project_issue_comments} p INNER JOIN {comments} c ON p.cid = c.cid WHERE p.nid = %d AND c.status = %d ORDER BY p.timestamp ASC', $node->nid, COMMENT_PUBLISHED);
     while ($followup = db_fetch_object($result)) {
-      $followup_changes = project_issue_comment_changes($node, $old, $followup, project_issue_field_labels('web'));
-      $project_issue_tables[$followup->cid] = _project_issue_comment_table($followup_changes);
+      $followup_changes = project_issue_diff_metadata($node, $old, $followup, project_issue_field_labels('web'));
+      $project_issue_tables[$followup->cid] = theme('project_issue_comment_table', $followup_changes);
       $old = $followup;
     }
   }
 }
 
 /**
- * Calculate the differences in project_issue comment metadata
- * between the original issue and a comment or between two
- * comments.
- *
- * @param $node
- *  The issue node.
- * @param $old_data
- *  Object containing old metadata.
- * @param $new_data
- *  Object containing new metadata.
- * @param $field_labels
- *  An associative array of field_name=>display_name pairs.
- *  In most cases, this will be the array returned by project_issue_change_summary().
- *
- * @return
- *  An associative array containing information about changes between
- *  the two objects.
- *  For example:
- *  array(
- *    'component' => array(
- *      'label' => t('Component'),
- *      'old' => 'Code',
- *      'new' => 'User interface',
- *    ),
- *    'sid' => array(
- *      'label' => t('Status'),
- *      'old' => 8,
- *      'new' => 13,
- *    ),
- *  )
- */
-function project_issue_comment_changes($node, $old_data, $new_data, $field_labels = array()) {
-  $changes = array();
-  foreach ($field_labels as $property => $name) {
-    if ($property == 'rid' && empty($old_data->rid) && empty($new_data->rid)) {
-      // Special case for version -- if both are empty, leave it out entirely,
-      // since maybe this project doesn't have (and/or disabled) releases.
-      continue;
-    }
-    if (isset($old_data->$property) || isset($new_data->$property)) {
-      $changes[$property] = array('label' => $name);
-    }
-    if (isset($old_data->$property) && isset($new_data->$property)) {
-      if ($old_data->$property != $new_data->$property) {
-        $changes[$property]['old'] = $old_data->$property;
-        $changes[$property]['new'] = $new_data->$property;
-      }
-    }
-    elseif (isset($old_data->$property)) {
-      $changes[$property]['old'] = $old_data->$property;
-    }
-    else {
-      $changes[$property]['new'] = $new_data->$property;
-    }
-  }
-
-  // Allow other modules to implement hook_followup_metadata_changes() so that they
-  // can find changes in additional metadata.  In most cases other modules will
-  // be responsible for storing this metadata in their own tables.  Developers
-  // of modules that implement this hook should keep in mind the following:
-  // 1.  Implementations of hook_followup_metadata_changes() must take the
-  //     $project_issue_comment_changes array by reference.
-  // 2.  Differences in properties will only be processed later on for
-  //     elements of the array which have the 'label', 'old', and 'new' properties
-  //     defined.
-  // In other words, for each line in the differences table (or field in the email)
-  // that is displayed, your hook should add something like the following as a
-  // new element of the $changes:
-  //    'taxonomy_vid_10' => array(
-  //       'label' => 'Vocabulary 10',
-  //       'old' => 'MySQL,PGSQL',
-  //       'new' => 'PGSQL',
-  //     ),
-  foreach (module_implements('followup_metadata_changes') as $module) {
-    $function = $module .'_followup_metadata_changes';
-    $function($node, $changes, $old_data, $new_data);
-  }
-  return $changes;
-}
-
-/**
  * Updates the project issue based on the comment inserted/updated/deleted.
  *
  * @param $comment_data
Index: issue.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/issue.inc,v
retrieving revision 1.303
diff -u -F^f -u -F^f -r1.303 issue.inc
--- issue.inc	7 Mar 2008 04:41:56 -0000	1.303
+++ issue.inc	7 Mar 2008 17:06:17 -0000
@@ -706,7 +706,7 @@ function project_issue_form($node, $incl
     $categories = array_merge(array(t('<none>')), project_issue_category(0, 0));
     $priorities = project_issue_priority();
     $states = project_issue_state(0, true, $node->nid && ($node->uid == $user->uid), $node->sid);
-  
+
     // Setup the array of choices for who the issue is assigned to.
     $assigned = array();
     foreach (module_implements('project_issue_assignees') as $module) {
@@ -904,16 +904,76 @@ function project_issue_view($node, $teas
     }
     $assigned = ($node->assigned && ($account = user_load(array('uid' => $node->assigned))) ? $account->name : t('Unassigned'));
 
-    $rows = array();
-    $rows[] = array('Project:', check_plain($project->title));
+    $current_data = array();
+    $current_data['pid'] = array(
+      'label' => t('Project'),
+      'current' => check_plain($project->title),
+    );
     if ($release->version) {
-      $rows[] = array('Version:', check_plain($release->version));
+      $current_data['rid'] = array(
+        'label' => t('Version'),
+        'current' => check_plain($release->version),
+      );
+    }
+    $current_data['component'] = array(
+      'label' => t('Component'),
+      'current' => check_plain($node->component),
+    );
+    $current_data['category'] = array(
+      'label' => t('Category'),
+      'current' => project_issue_category($node->category, 0),
+    );
+    $current_data['priority'] = array(
+      'label' => t('Priority'),
+      'current' => project_issue_priority($node->priority),
+    );
+    $current_data['assigned'] = array(
+      'label' => t('Assigned'),
+      'current' => $assigned,
+    );
+    $current_data['sid'] = array(
+      'label' => t('Status'),
+      'current' => project_issue_state($node->sid),
+    );
+
+    // Allow modules to alter the metadata displayed in the table on the actual
+    // issue node itself (at the very top of the issue). Modules should accept
+    // the $current_data parameter by reference and add additional
+    // elements for additional lines in the table.
+    //
+    // NOTE:  Modules implementing this hook are responsible for making sure
+    // that all content printed in additional rows created by said module have
+    // properly escaped and/or filtered text.  In most cases this means passing
+    // any output through either the check_plain() or filter_xss() function.
+    //
+    // This hook is only necessary for display of metadata on the project issue
+    // node.  For display of initial metadata values in the original e-mail
+    // sent after an issue is created, your module should implement
+    // hook_project_issue_metadata() and watch for the case where
+    // $old_data is empty, which means that $new_data represents
+    // the original values of the metadata fields for the issue.
+    //
+    // Modules implementing this hook should take the following parameters:
+    // @param $view
+    //  A string representing the metadata view being generated.  For the issue
+    //  node main table, this will be 'current'.
+    // @param $node
+    //  The project_issue node object.
+    // @param $current_data
+    //  An associative array of rows in the project issue metadata table that
+    //  will be displayed, with the following key/value pairs:
+    //    'label' => The metadata label.
+    //    'current' => The current metadata value.
+    //  This parameter should be accepted by reference.
+    foreach (module_implements('project_issue_metadata') as $module) {
+      $function = $module .'_project_issue_metadata';
+      $function('current', $node, $current_data);
+    }
+
+    $rows = array();
+    foreach ($current_data as $name => $values) {
+    	$rows[] = array($values['label'] .':', $values['current']);
     }
-    $rows[] = array(t('Component:'),  check_plain($node->component));
-    $rows[] = array(t('Category:'), project_issue_category($node->category, 0));
-    $rows[] = array(t('Priority:'), project_issue_priority($node->priority));
-    $rows[] = array(t('Assigned:'), $assigned);
-    $rows[] = array(t('Status:'), project_issue_state($node->sid));
 
     $node->content['project_issue_summary'] = array(
       '#value' => theme('project_issue_summary', $rows, project_issue_internal_links($node)),
@@ -2123,7 +2183,7 @@ function project_issue_change_summary($f
       return t('<none>');
     case 'assigned':
       $user = user_load(array('uid' => $value));
-      return $value === 0 ? variable_get('anonymous', t('Anonymous')) : $user->name;
+      return (int) $value === 0 ? variable_get('anonymous', t('Anonymous')) : $user->name;
     case 'sid':
       return $value ? project_issue_state($value) : t('<none>');
     default:
Index: mail.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/mail.inc,v
retrieving revision 1.104
diff -u -F^f -u -F^f -r1.104 mail.inc
--- mail.inc	7 Mar 2008 04:41:56 -0000	1.104
+++ mail.inc	7 Mar 2008 17:06:18 -0000
@@ -346,22 +346,23 @@ function project_mail_generate_followup_
     $content = $entry->comment;
   }
 
-  $comment_changes = project_issue_comment_changes($node, $previous, $entry, $fields);
+  $comment_changes = project_issue_diff_metadata($node, $previous, $entry, $fields);
 
-  // Mail summary (status values).
-  $summary = '';
-  foreach ($comment_changes as $field => $change) {
-    $text = str_pad($change['label']. ':', 14);
-    if (!empty($change['label']) && isset($change['old']) && isset($change['new']) && $field != 'updator' && $field != 'name') {
-      $summary .= "-$text". project_issue_change_summary($field, $change['old']) ."\n";
-      $summary .= "+$text". project_issue_change_summary($field, $change['new']) ."\n";
-    }
-    elseif (!empty($change['label'])) {
-      $summary .= " $text". project_issue_change_summary($field, $node->$field) ."\n";
-    }
-  }
+  // Since $node->name will always be the original issue author, and since $node->updator
+  // isn't a property of either $previous or #entry, these two properties
+  // will never show up as being different when project_issue_diff_metadata() is called,
+  // and therefore neither of these will ever be elements of the $comment_changes array.
+  // Since we do want them to be printed in issue emails, we just need to add their labels
+  // back into the $comment_changes array here, so that theme_project_issue_mail_summary_field()
+  // will know to print the data for these two fields.
+  $comment_changes['name'] = array(
+    'label' => $fields['name'],
+  );
+  $comment_changes['updator'] = array(
+    'label' => $fields['updator'],
+  );
 
-  $summary .= project_mail_format_attachments($entry, $display_files);
+  $summary = theme('project_issue_mail_summary', $entry, $node, $comment_changes, $display_files);
 
   // Create main body content
   project_mail_output($content, 1, $entry->format);
@@ -396,6 +397,109 @@ function project_mail_generate_followup_
 }
 
 /**
+ * Themes the display of the issue metadata summary
+ * that is shown at the top of an issue emai.
+ *
+ * @param $entry
+ *  The object representing the current entry.  This will be a node object
+ *  if the current entry is the original issue node; otherwise this will be
+ *  a comment object.
+ * @param $node
+ *  The original issue node object.
+ * @param $changes
+ *  A nested array containing the metadata changes between the original
+ *  issue and the first comment, or two consecutive comments.  This array
+ *  is the output of the project_issue_diff_metadata() function.
+ * @param $display_files
+ *   Boolean indicating if file attachments should be displayed.
+ * @return
+ *   A string containing the themed text of the issue metadata table.
+ */
+function theme_project_issue_mail_summary($entry, $node, $changes, $display_files) {
+  // Mail summary (status values).
+  $summary = '';
+  foreach ($changes as $field => $change) {
+    $summary .= theme('project_issue_mail_summary_field', $node, $field, $change);
+  }
+
+  $summary .= project_mail_format_attachments($entry, $display_files);
+  return $summary;
+}
+
+/**
+ * Theme the email output of one project issue metadata field.
+ *
+ * @param $node
+ *   The project issue node object.
+ * @param $field_name
+ *   The name of the field to theme.
+ * @param $change
+ *   A nested array containing changes to project issue metadata
+ *   for the given issue or comment.
+ * @return
+ *  A themed line or lines of text ready for inclusion into the email body.
+ */
+function theme_project_issue_mail_summary_field($node, $field_name, $change) {
+  // We need to run the label name through strip_tags here so that
+  // the spacing isn't messed up if there are HTML tags in $change['label'].
+  $text = str_pad(strip_tags($change['label']). ':', 14);
+  $summary_row = '';
+  if (!empty($change['label']) && isset($change['old']) && isset($change['new']) && $field_name != 'updator' && $field_name != 'name') {
+    if (is_array($change['old']) || is_array($change['new'])) {
+      $removed = array();
+      if (is_array($change['old'])) {
+        foreach ($change['old'] as $item) {
+          $removed[] = '-'. $item .' ';
+        }
+      }
+      elseif (!empty($change['old'])) {
+        $removed[] = '-'. $change['old'];
+      }
+
+      $added = array();
+      if (is_array($change['new'])) {
+        foreach ($change['new'] as $item) {
+          $added[] = '+'. $item .' ';
+        }
+      }
+      elseif (!empty($change['new'])) {
+        $added[] = '+'. $change['new'];
+      }
+
+      $summary_row = " $text". trim(implode(', ', $removed). '  ' .implode(', ', $added)) ."\n";
+    }
+    else {
+      $summary_row .= "-$text". project_issue_change_summary($field_name, $change['old']) ."\n";
+      $summary_row .= "+$text". project_issue_change_summary($field_name, $change['new']) ."\n";
+    }
+  }
+  elseif (!empty($change['label'])) {
+    if (!empty($change['new'])) {
+      // This condition is necessary when building the first email message of an
+      // issue, since in this case $change['old'] should not exist.
+      if (is_array($change['new'])) {
+        $summary_row .= " $text". implode(', ', $change['new']) ."\n";
+      }
+      else {
+        $summary_row .= " $text". project_issue_change_summary($field_name, $change['new']) ."\n";
+      }
+    }
+    else {
+      // This condition is where fields that are stored in the $node object and
+      // which haven't changed but should be printed anyway get processed.
+      // For example, the project, category, etc. are printed in each email
+      // whether or not they have changed.
+      if (isset($node->$field_name)) {
+        $summary_row .= " $text". project_issue_change_summary($field_name, $node->$field_name) ."\n";
+      }
+    }
+  }
+  // HTML tags in the email will make it hard to read, so pass
+  // this output through strip_tags().
+  return strip_tags($summary_row);
+}
+
+/**
  * Formats attachments for issue notification e-mails.
  *
  * @param $entry
Index: project_issue.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/project_issue.module,v
retrieving revision 1.87
diff -u -F^f -u -F^f -r1.87 project_issue.module
--- project_issue.module	7 Mar 2008 04:41:56 -0000	1.87
+++ project_issue.module	7 Mar 2008 17:06:18 -0000
@@ -1227,3 +1227,106 @@ function project_issue_requirements($pha
   }
   return $requirements;
 }
+
+/**
+ * Calculate the differences in project_issue comment metadata
+ * between the original issue and a comment or between two
+ * comments.
+ *
+ * @param $node
+ *  The issue node.
+ * @param $old_data
+ *  Object containing old metadata.
+ * @param $new_data
+ *  Object containing new metadata.
+ * @param $field_labels
+ *  An associative array of field_name=>display_name pairs.
+ *  In most cases, this will be the array returned by project_issue_change_summary().
+ *
+ * @return
+ *  An associative array containing information about changes between
+ *  the two objects.
+ *  For example:
+ *  array(
+ *    'component' => array(
+ *      'label' => t('Component'),
+ *      'old' => 'Code',
+ *      'new' => 'User interface',
+ *    ),
+ *    'sid' => array(
+ *      'label' => t('Status'),
+ *      'old' => 8,
+ *      'new' => 13,
+ *    ),
+ *  )
+ */
+function project_issue_diff_metadata($node, $old_data, $new_data, $field_labels = array()) {
+  $changes = array();
+  foreach ($field_labels as $property => $name) {
+    if ($property == 'rid' && empty($old_data->rid) && empty($new_data->rid)) {
+      // Special case for version -- if both are empty, leave it out entirely,
+      // since maybe this project doesn't have (and/or disabled) releases.
+      continue;
+    }
+    if (isset($old_data->$property) || isset($new_data->$property)) {
+      $changes[$property] = array('label' => $name);
+    }
+    if (isset($old_data->$property) && isset($new_data->$property)) {
+      if ($old_data->$property != $new_data->$property) {
+        $changes[$property]['old'] = $old_data->$property;
+        $changes[$property]['new'] = $new_data->$property;
+      }
+    }
+    elseif (isset($old_data->$property)) {
+      $changes[$property]['old'] = $old_data->$property;
+    }
+    else {
+      $changes[$property]['new'] = $new_data->$property;
+    }
+  }
+
+  // Allow other modules to implement hook_project_issue_metadata() so that they
+  // can find changes in additional metadata.  In most cases other modules will
+  // be responsible for storing this metadata in their own tables.  Developers
+  // of modules that implement this hook should keep in mind the following:
+  // 1.  Implementations of hook_project_issue_metadata() must take the
+  //     $changes array by reference.
+  // 2.  Differences in properties will only be processed later on for
+  //     elements of the array which have the 'label', 'old', and 'new' properties
+  //     defined.
+  // In other words, for each line in the differences table (or field in the email)
+  // that is displayed, your hook should add something like the following as a
+  // new element of the $changes array:
+  //    'taxonomy_vid_10' => array(
+  //       'label' => 'Vocabulary 10',
+  //       'old' => 'MySQL, pgSQL, javascript',
+  //       'new' => 'pgSQL, newbie',
+  //     ),
+  //
+  // There are two methods you can use to indicate multiple changes of a field.
+  // The first is that for 'old' and 'new' you pass strings separated by some
+  // character, customarily a comma.  This method is used in
+  // the example above.  When using this method, the default display of the changes
+  // will be to show all old values followed by all new values.  In the example
+  // above, this would be displayed like:
+  // Vocabulary 10:  MySQL, pgSQL, javascript >> pgSQL, newbie
+  //
+  // The other method you can use when constructing 'old' and 'new' is to make
+  // both of these arrays, with each element of the array one change.  If you
+  // use this method, all elements in the 'old' array are typically interpreted
+  // as being removed, and all elements in the 'new' array are typically interpreted
+  // as being added.  An example of this type of structure is as follows:
+  //    'taxonomy_vid_10' => array(
+  //       'label' => 'Vocabulary 10',
+  //       'old' => array('MySQL', 'javascript'),
+  //       'new' => array('newbie'),
+  //     ),
+  // In this situation, the default display of these changes in a project issue
+  // metadata table would be as follows:
+  // Vocabulary 10:  -MySQL, -javascript    +newbie
+  foreach (module_implements('project_issue_metadata') as $module) {
+    $function = $module .'_project_issue_metadata';
+    $function('diff', $node, $changes, $old_data, $new_data);
+  }
+  return $changes;
+}
