diff --git a/project_issue.module b/project_issue.module
index bb1f7b4..da6bef4 100644
--- a/project_issue.module
+++ b/project_issue.module
@@ -1703,8 +1703,11 @@ function project_issue_nodereference_autocomplete_validate($element, &$form_stat
 /**
  * Implements hook_field_formatter_info().
  *
- * Provides a formatter for nodereference fields that uses
- * theme_project_issue_issue_link().
+ * Provides:
+ * i)  two formatters for nodereference fields, which use
+ *     theme_project_issue_issue_link().
+ * ii) a formatter for file fields which renders a table of files with
+ *     associated metadata.
  */
 function project_issue_field_formatter_info() {
   return array(
@@ -1718,16 +1721,159 @@ function project_issue_field_formatter_info() {
       'field types' => array('entityreference'),
       'multiple values' => FIELD_BEHAVIOR_DEFAULT,
     ),
+    'issue_files_summary_table' => array(
+      'label' => t('List of files and metadata styled as a table'),
+      'field types' => array('file'),
+      'settings' => array(
+        // Restrict initial display to certain file types
+        'filetypes' => 'all',
+        // Comma-separated list of extensions to display
+        'extensions' => 'patch,diff',
+        // Metadata columns to display
+        'columns' => array('cid', 'filename', 'filesize', 'uid'),
+        // Show Hidden: checkbox determining whether to include 'hidden' files
+        // 'showhidden' => FALSE, // TODO: not yet implemented
+        // Sort By: default property to use for sorting
+        'sortby' => 'cid',
+        // Sort Order:  ASC or DESC
+        'sortorder' => 'asc',
+        // Group by: none, extension - render individual table by group
+        // 'groupby' => 'extension', // TODO: not yet implemented
+        // Allow Hooks: allow other modules to alter data before rendering
+        'allowhooks' => TRUE,
+        // Display Filter: Output the dynamic display filter, allowing users to
+        // filter based on file types
+        'displayfilter' => FALSE,
+      ),
+    ),
   );
 }
 
 /**
+ * Implements hook_field_formatter_settings_form().
+ *
+ * Contains the settings form for the issue_files_summary_table formatter.
+ */
+function project_issue_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
+  if ($instance['display'][$view_mode]['type'] == 'issue_files_summary_table') {
+    $display = $instance['display'][$view_mode];
+    $settings = $display['settings'];
+    $widget_settings = $field['settings'];
+
+    // File types checkbox
+    $options = array('all' => t('Display all files'), 'bytype' => t('Restrict display to certain file extensions'));
+    $form['filetypes'] = array(
+      '#type' => 'radios',
+      '#title' => t('Files to include:'),
+      '#options' => $options,
+      '#default_value' => $settings['filetypes'],
+    );
+    // Custom file types textfield
+    $form['extensions'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Extension list:'),
+      '#description' => t('Comma separated list of file extensions to include'),
+      '#default_value' => $settings['extensions'],
+      '#states' => array(
+        'visible' => array(
+          ':input[name="fields[field_issue_files][settings_edit_form][settings][filetypes]"]' => array('value' => 'bytype'),
+         ),
+       ),
+    );
+    $options = project_issue_field_formatter_view_files_metadata();
+    // Display columns checkboxes
+    $form['columns'] = array(
+      '#type' => 'select',
+      '#title' => t('Display columns:'),
+      '#options' => $options,
+      '#default_value' => $settings['columns'],
+      '#size' => min(6, count($options)),
+      '#multiple' => TRUE,
+    );
+    // TODO: Determine how files are 'hidden'
+    // $form['showhidden'] = array(
+    //   '#type' => 'checkbox',
+    //   '#title' => t('Show files which have been marked as "hidden"'),
+    //   '#default_value' => $settings['showhidden'],
+    // );
+    $form['sortby'] = array(
+      '#type' => 'select',
+      '#title' => t('Sort by:'),
+      // Remove 'display' and 'status' from the sort options
+      '#options' => array_diff_key($options, array_flip(array('display', 'status'))),
+      '#default_value' => $settings['sortby'],
+    );
+    $form['sortorder'] = array(
+      '#type' => 'radios',
+      '#title' => t('Sort order:'),
+      '#options' => array('asc' => t('Ascending'), 'desc' => t('Descending')),
+      '#default_value' => $settings['sortorder'],
+    );
+    // TODO: Do we want to be able to display patches and screenshots
+    // separately? Or is this 'feature overkill'?  The same could be
+    // accomplished with a send instance of the field on the node.
+    // $form['groupby'] = array(
+    //   '#type' => 'checkbox',
+    //   '#title' => t('Group into separate tables by type.'),
+    //   '#default_value' => $settings['groupby'],
+    // );
+    $form['allowhooks'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Allow other modules to alter data prior to rendering the table.'),
+      '#default_value' => $settings['allowhooks'],
+    );
+  }
+  return $form;
+}
+
+/**
+ * Implements hook_field_formatter_settings_summary().
+ *
+ * Contains the settings summary for the issue_files_summary_table formatter.
+ */
+function project_issue_field_formatter_settings_summary($field, $instance, $view_mode) {
+  $metadata = project_issue_field_formatter_view_files_metadata();
+  $display = $instance['display'][$view_mode];
+  $settings = $display['settings'];
+  $summary = '';
+  if ($display['type'] == 'issue_files_summary_table') {
+    if ($settings['filetypes'] === 'all') {
+      $summary = t('Listing all file types ');
+    }
+    else {
+      $summary = t("Listing files with extension (@extensions) ", array('@extensions' => $settings['extensions']));
+    }
+    $summary .= t("sorted by @sorttype ", array('@sorttype' => strtolower($metadata[$settings['sortby']])));
+    $summary .= $settings['sortorder'] === 'desc' ? t("descending") : t("ascending");
+    $summary .= '<br />';
+    $summary .= t("Columns: @columns", array('@columns' => implode(',', array_intersect_key($metadata, array_flip($settings['columns'])))));
+  }
+  return $summary;
+}
+
+/**
  * Implements hook_field_formatter_view().
  */
 function project_issue_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
-  $elements = array();
+  switch ($display['type']) {
+    case 'issue_id':
+    case 'issue_id_assigned':
+      $elements = project_issue_field_formatter_view_issue($entity_type, $entity, $field, $instance, $langcode, $items, $display);
+      break;
+    case 'issue_files_summary_table':
+      $elements = project_issue_field_formatter_view_files($entity_type, $entity, $field, $instance, $langcode, $items, $display);
+      break;
+  }
+  return $elements;
+}
 
+/**
+ * Renders the issue_id formatters for project_issue_field_formatter_view().
+ */
+function project_issue_field_formatter_view_issue($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+  $elements = array();
   $settings = $display['settings'];
+
   $include_assigned = $display['type'] == 'issue_id_assigned' ? TRUE : FALSE;
 
   $target_type = $field['settings']['target_type'];
@@ -1784,11 +1930,212 @@ function project_issue_field_formatter_view($entity_type, $entity, $field, $inst
       '#markup' => $output,
     );
   }
-
   return $elements;
 }
 
 /**
+ * Renders the file table formatter for project_issue_field_formatter_view().
+ */
+function project_issue_field_formatter_view_files($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+
+  $settings = $display['settings'];
+  $metadata = project_issue_field_formatter_view_files_metadata();
+
+  // Retrieve the file -> cid mappings for the node
+  $file_cids = project_issue_get_file_cids($entity->nid);
+
+  // If filtering by extension, retrieve the allowed extension types
+  $filter = FALSE;
+  if ($settings['filetypes'] == 'bytype') {
+    $filter = array_filter(array_map('trim', explode(',', $settings['extensions'])));
+  }
+
+  foreach($items as $key => $value) {
+    // Add an 'extension' property to the array
+    $items[$key]['extension'] = pathinfo($value['filename'], PATHINFO_EXTENSION);
+    // If filtering by extension, exclude items which do not meet the criteria.
+    if ($filter && !in_array($items[$key]['extension'], $filter)) {
+      $items[$key]['exclude'] = TRUE;
+    }
+    // Add the comment id associated with each file to the $items array.
+    // If no comment id is found for a given file, we set it to '0'.
+    $items[$key]['cid'] = isset($file_cids[$value['fid']]) ? $file_cids[$value['fid']] : 0;
+  }
+
+  // Sort the $items array
+  project_issue_field_formatter_view_files_sort($items, $settings['sortby'], $settings['sortorder']);
+
+  // Assemble the default (i.e. pre drupal_alter()) table header
+  foreach ($settings['columns'] as $key => $column) {
+    $header[$column] = array(
+      'data' => $metadata[$column],
+      'name' => 'issue-file-summary-table-header-' . $column,
+      'class' => array('issue-file-summary-table-header'),
+    );
+  }
+
+  // Assemble the default (i.e. pre drupal_alter()) table rows
+  $rows = array();
+  foreach ($items as $item) {
+    // Assemble the individual cells
+    $row = array();
+    foreach ($settings['columns'] as $column) {
+      switch ($column) {
+        case 'cid':
+          if ($item['cid'] === 0) {
+            $data = l('none', 'node/' . $entity->nid, array('fragment' => 'content'));
+          }
+          else {
+            $data = l('#' . comment_get_display_ordinal($item['cid'], $entity->type), 'node/' . $entity->nid, array('fragment' => 'comment-' . $item['cid']));
+          }
+          break;
+        case 'filename':
+          $data = theme('file_link', array('file' => (object) $item));
+          break;
+        case 'filesize':
+          $data = format_size($item['filesize']);
+          break;
+        case 'uid':
+          $data = theme('username', array('account' => user_load($item['uid'])));
+          break;
+        default:
+          $data = check_plain($item[$column]);
+      }
+      $row[$column] = array(
+        'data' => $data,
+        'name' => 'issue-file-summary-table-' . $column . '-' . $item['fid'],
+        'class' => 'issue-file-summary-table-' . $column,
+      );
+    }
+
+    $class = array('issue-file-summary-table-row');
+
+    if (!empty($item['exclude'])) {
+      $class[] = 'element-hidden';
+    }
+    // Add the row
+    $rows[] = array(
+      'data' => $row,
+      'class' => $class,
+      'name' => 'issue-file-summary-table-row-' . $item['fid'],
+    );
+  }
+
+  // Allow other modules to modify the table
+  if ($settings['allowhooks']['value'] === TRUE) {
+    drupal_alter('issue_files_summary_table_items', $items, $header, $rows);
+  }
+
+  $element = array(
+    '#theme' => 'table',
+    '#header' => $header,
+    '#rows' => $rows,
+  );
+
+  return $element;
+}
+
+/**
+ * Sorts the field formatter's items array.
+ */
+function project_issue_field_formatter_view_files_sort(&$items, $on, $order = 'ASC') {
+  if (in_array($on, array('cid', 'fid', 'filesize', 'timestamp'))) {
+    $function = "(\$a['{$on}'] > \$b['{$on}']);";
+  }
+  else {
+    $function = "strcmp(\$a['{$on}'],\$b['{$on}']);";
+  }
+  $comparer = ($order === 'desc') ? "return -" . $function : "return " . $function;
+  usort($items, create_function('$a,$b', $comparer));
+}
+
+/**
+ * Returns default available issue_files_summary_table formatter metadata.
+ */
+function project_issue_field_formatter_view_files_metadata() {
+  return array(
+    'cid' => t('Comment ID'),
+    'fid' => t('File ID'),
+    'filename' => t('File Name'),
+    'description' => t('File Description'),
+    'extension' => t('File Extension'),
+    'filesize' => t('File Size'),
+    'timestamp' => t('Creation timestamp'),
+    'uid' => t('File Author'),
+    'uri' => t('File URI'),
+    'filemime' => t('File Mime Type'),
+    'display' => t('File Display'),
+    'status' => t('File Status'),
+  );
+}
+
+
+/**
+ * Assemble an array of comment ids for each $node->file_issue_files file.
+ *
+ * @param int $nid
+ *   The node id to inspect for files
+ *
+ * @return array $cids
+ *   An array of files found in the node's field_issue_files field, keyed by
+ *   the file ID (fid), where the value is the associated comment generated
+ *   when that file was added to the node.
+ */
+function project_issue_get_file_cids($nid) {
+
+  // Obtain list of cids for this node
+  $query = db_select('comment', 'c');
+  $query->addField('c', 'cid');
+  $query->condition('c.nid', $nid)
+        ->addTag('node_access')
+        ->addTag('comment_filter')
+        ->orderBy('c.cid', 'ASC');
+  $cids = $query->execute()->fetchCol();
+
+  // Load each comment which contains a change to the field_issue_files field.
+  $query = new EntityFieldQuery();
+  $query->entityCondition('entity_type', 'comment')
+    ->entityCondition('bundle', 'comment_node_project_issue')
+    ->entityCondition('entity_id', $cids, 'IN')
+    ->propertyCondition('nid', $nid)
+    ->propertyCondition('status', 1)
+    ->fieldCondition('field_issue_changes', 'field_name', 'field_issue_files', '=')
+    ->propertyOrderBy('cid', 'ASC');
+  $result = $query->execute();
+
+  $comments = array();
+  if (isset($result['comment'])) {
+    $cids = array_keys($result['comment']);
+    $comments = entity_load('comment', $cids);
+  }
+
+  // Initialize an array in which to store our return value
+  $cids = array();
+  // Iterate over the comments
+  foreach ($comments as $comment) {
+    $language = !empty($comment->language) ? $comment->language : 'und';
+    // Iterate over the changes, isolating those related to field_issue_files
+    foreach ($comment->field_issue_changes[$language] as $change) {
+      if ($change['field_name'] == 'field_issue_files') {
+        // Create array of 'fid' => 'cid' for all file values which existed
+        // after this particular change
+        $added = array();
+        foreach($change['new_value'] as $file) {
+          $added[$file['fid']] = $comment->cid;
+        }
+        // Remove any array values for file entries which existed before this
+        // particular change
+        foreach($change['old_value'] as $file) {
+          unset($added[$file['fid']]);
+        }
+        $cids = $cids + $added;
+      }
+    }
+  }
+  return $cids;
+}
+
+/**
  * Find the flag name for following issues for this issue type (if any).
  *
  * If the flag tracker module (http://drupal.org/project/flag_tracker) is enabled,
