diff --git a/project_issue.module b/project_issue.module
index bb1f7b4..28ec2d3 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,138 @@ 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 display to certain file types
+        'filetypes' => array('value' => 'all'),
+        // Comma-separated list of extensions to allow
+        'extensions' => '',
+        // Show Hidden: checkbox determining whether to include 'hidden' files
+        // 'showhidden' => FALSE, // TODO: not yet implemented
+        // Sort By: default column to use for sorting
+        'sortby' => 'cid',
+        // Sort Order:  ASC or DESC
+        'sortorder' => 'desc',
+        // 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,
+      ),
+    ),
   );
 }
 
 /**
+ * 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="filetypes"]' => array('value' => 'bytype'),
+         ),
+       ),
+    );
+    // 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:'),
+      '#options' => array('cid' => 'Comment ID', 'fid' => 'File ID', 'uri' => 'File Name', 'type' => 'File Extension'),
+      '#default_value' => $settings['sortby'],
+    );
+    $form['sortorder'] = array(
+      '#type' => 'radios',
+      '#title' => t('Sort order:'),
+      '#options' => array('asc' => t('ASC'), 'desc' => t('DESC')),
+      '#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 seperate 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) {
+  $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 extensions: @extensions", array('@extensions' => check_plain($settings['extensions'])));
+    }
+  }
+  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 +1909,142 @@ 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'];
+
+  // Retrieve the file -> cid mappings for the node
+  $file_cids = project_issue_get_file_cids($entity->nid);
+
+  $filter = FALSE;
+
+  // If filtering by extension, retrieve the allowed extension types
+  if ($settings['filetypes'] == 'bytype') {
+    $filter = array_filter(array_map('trim', explode(',', $settings['extensions'])));
+  }
+
+  foreach($items as $key => $value) {
+    // If filtering by extension, exclude items which do not meet the criteria.
+    if ($filter && !in_array(pathinfo($value['filename'], PATHINFO_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;
+
+    // TODO:  Implement sortby
+    // TODO:  Implement sort order
+  }
+
+  // Assemble the default (i.e. pre drupal_alter()) table header
+  $header = array('cid' => t('Comment'), 'filename' => t('Filename'), 'filesize' => t('Size'), 'author' => t('Author'));
+
+  // Assemble the default (i.e. pre drupal_alter()) table rows
+  $rows = array();
+  foreach ($items as $item) {
+    if ($item['exclude']) {
+      continue;
+    }
+    // Assemble the individual cells
+    $row = array();
+    if ($item['cid'] === 0) {
+      $row['cid'] = l('none', 'node/' . $entity->nid, array('fragment' => 'content'));
+    }
+    else {
+      $row['cid'] = l('#' . comment_get_display_ordinal($item['cid'], $entity->type), 'node/' . $entity->nid, array('fragment' => 'comment-' . $item['cid']));
+    }
+    $row['filename'] = theme('file_link', array('file' => (object) $item));
+    $row['filesize'] = format_size($item['filesize']);
+    $row['author'] = theme('username', array('account' => user_load($item['uid'])));
+    // Add the row
+    $rows[] = $row;
+  }
+
+  // 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;
+}
+
+/**
+ * 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,
