Index: feedback-entry.tpl.php
===================================================================
RCS file: feedback-entry.tpl.php
diff -N feedback-entry.tpl.php
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ feedback-entry.tpl.php	14 Feb 2011 16:51:52 -0000
@@ -0,0 +1,13 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Default theme implementation to present a feedback entry.
+ *
+ * @see template_preprocess_feedback_entry()
+ */
+?>
+<div class="feedback-entry"<?php print $attributes; ?>>
+  <?php print render($content); ?>
+</div>
Index: feedback.admin.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feedback/feedback.admin.inc,v
retrieving revision 1.4
diff -u -p -r1.4 feedback.admin.inc
--- feedback.admin.inc	14 Jan 2011 01:53:55 -0000	1.4
+++ feedback.admin.inc	14 Feb 2011 16:51:53 -0000
@@ -54,10 +54,12 @@ function feedback_admin_view_form($form,
           '#return_value' => 1,
           '#default_value' => FALSE,
         );
-        $form['feedback-messages'][$status][$fid]['location'] = array('#markup' => l(truncate_utf8($entry->location, 32, FALSE, TRUE), $entry->url));
-        $form['feedback-messages'][$status][$fid]['date'] = array('#markup' => format_date($entry->timestamp, 'small'));
-        $form['feedback-messages'][$status][$fid]['user'] = array('#markup' => format_username($entry));
-        $form['feedback-messages'][$status][$fid]['message'] = array('#markup' => feedback_format_message($entry));
+        feedback_build_content($entry, 'overview');
+        $content = $entry->content;
+        $form['feedback-messages'][$status][$fid]['location'] = $content['location'];
+        $form['feedback-messages'][$status][$fid]['date'] = $content['date'];
+        $form['feedback-messages'][$status][$fid]['user'] = $content['user'];
+        $form['feedback-messages'][$status][$fid]['message'] = $content['message'];
       }
     }
   }
@@ -126,3 +128,132 @@ function feedback_admin_view_form_submit
   }
 }
 
+/**
+ * Form builder; The general feedback settings form.
+ *
+ * @ingroup forms
+ * @see system_settings_form()
+ */
+function feedback_admin_settings_form($form, &$form_state) {
+  $form['message'] = array(
+    '#markup' => t('There are no feedback settings yet, but you can add new fields.')
+  );
+  return $form;
+}
+
+/**
+ * Page callback wrapper for feedback_view().
+ */
+function feedback_view_page($entry) {
+  // An administrator may try to view a non-existent entry,
+  // so we give them a 404 (versus a 403 for non-admins).
+  return is_object($entry) ? feedback_view($entry) : MENU_NOT_FOUND;
+}
+
+/**
+ * Generate an array for rendering the given entry.
+ *
+ * @param $entry
+ *   A feedback entry object.
+ * @param $view_mode
+ *   View mode, e.g. 'full'.
+ * @param $langcode
+ *   (optional) A language code to use for rendering. Defaults to the global
+ *   content language of the current request.
+ *
+ * @return
+ *   An array as expected by drupal_render().
+ */
+function feedback_view($entry, $view_mode = 'full', $langcode = NULL) {
+  if (!isset($langcode)) {
+    $langcode = $GLOBALS['language_content']->language;
+  }
+
+  // Retrieve all entry fields and attach to $entry->content.
+  feedback_build_content($entry, $view_mode, $langcode);
+
+  $build = $entry->content;
+  // We don't need duplicate rendering info in entry->content.
+  unset($entry->content);
+
+  $build += array(
+    '#theme' => 'feedback_entry',
+    '#entry' => $entry,
+    '#view_mode' => $view_mode,
+    '#language' => $langcode,
+  );
+
+  // Allow modules to modify the structured entry.
+  $type = 'feedback';
+  drupal_alter(array('feedback_view', 'entity_view'), $build, $type);
+
+  return $build;
+}
+
+/**
+ * Builds a structured array representing the feedback message content.
+ *
+ * @param $entry
+ *   A feedback entry object.
+ * @param $view_mode
+ *   View mode, e.g. 'full'.
+ * @param $langcode
+ *   (optional) A language code to use for rendering. Defaults to the global
+ *   content language of the current request.
+ */
+function feedback_build_content($entry, $view_mode = 'full', $langcode = NULL) {
+  if (!isset($langcode)) {
+    $langcode = $GLOBALS['language_content']->language;
+  }
+
+  $entry->content = array(
+    'location' => array(
+      '#markup' => l(truncate_utf8($entry->location, 32, FALSE, TRUE), $entry->url),
+      '#type' => 'item',
+      '#title' => $view_mode == 'full' ? t('Location') . ':&nbsp;' : '',
+     ),
+    'date' => array(
+      '#markup' => format_date($entry->timestamp, 'small'),
+      '#type' => 'item',
+      '#title' => $view_mode == 'full' ? t('Date') . ':&nbsp;' : '',
+    ),
+    'user' => array(
+      '#markup' => format_username($entry),
+      '#type' => 'item',
+      '#title' => $view_mode == 'full' ? t('User') . ':&nbsp;' : '',
+    ),
+    'message' => array(
+      '#markup' => feedback_format_message($entry),
+      '#type' => 'item',
+      '#title' => $view_mode == 'full' ? t('Message') . ':&nbsp;' : '',
+    ),
+  );
+
+  // Build fields content.
+  field_attach_prepare_view('feedback', array($entry->fid => $entry), $view_mode);
+  entity_prepare_view('feedback', array($entry->fid => $entry));
+  $entry->content += field_attach_view('feedback', $entry, $view_mode, $langcode);
+
+  module_invoke_all('feedback_view', $entry, $view_mode, $langcode);
+  module_invoke_all('entity_view', $entry, 'feedback', $view_mode, $langcode);
+}
+
+/**
+ * Process variables for feedback-entry.tpl.php.
+ *
+ * The $variables array contains the following arguments:
+ * - $entry
+ *
+ * @see feedback-entry.tpl.php
+ */
+function template_preprocess_feedback_entry(&$variables) {
+  foreach (element_children($variables['elements']) as $key) {
+    $variables['content'][$key] = $variables['elements'][$key];
+  }
+
+  $entry = $variables['elements']['#entry'];
+
+  // Preprocess fields.
+  field_attach_preprocess('feedback', $entry, $variables['content'], $variables);
+}
+
Index: feedback.api.php
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feedback/feedback.api.php,v
retrieving revision 1.1
diff -u -p -r1.1 feedback.api.php
--- feedback.api.php	14 Jan 2011 01:53:55 -0000	1.1
+++ feedback.api.php	14 Feb 2011 16:51:53 -0000
@@ -25,6 +25,19 @@ function hook_feedback_load($entries) {
 }
 
 /**
+ * Act on a feedback entry before it is saved.
+ *
+ * Modules implementing this hook can act on the feedback entry object before it
+ * is inserted or updated.
+ *
+ * @param $entry
+ *   The feedback entry object.
+ */
+function hook_feedback_presave($entry) {
+  $entry->foo = 'bar';
+}
+
+/**
  * Respond to creation of a new feedback entry.
  *
  * @param $entry
@@ -71,5 +84,51 @@ function hook_feedback_delete($entry) {
 }
 
 /**
+ * The feedback entry is being displayed.
+ *
+ * The module should format its custom additions for display and add them to the
+ * $entry->content array.
+ *
+ * @param $entry
+ *   The feedback entry object.
+ * @param $view_mode
+ *   View mode, e.g. 'full'.
+ * @param $langcode
+ *   The language code used for rendering.
+ *
+ * @see hook_feedback_view_alter()
+ * @see hook_entity_view()
+ */
+function hook_feedback_view($entry, $view_mode, $langcode) {
+  $entry->content['foo'] = array(
+    '#markup' => t('Bar'),
+  );
+}
+
+/**
+ * The feedback entry was built; the module may modify the structured content.
+ *
+ * This hook is called after the content has been assembled in a structured
+ * array and may be used for doing processing which requires that the complete
+ * content structure has been built.
+ *
+ * @param $build
+ *   A renderable array representing the feedback entry.
+ *
+ * @see feedback_view()
+ * @see hook_entity_view_alter()
+ */
+function hook_user_view_alter(&$build) {
+  // Check for the existence of a field added by another module.
+  if (isset($build['an_additional_field'])) {
+    // Change its weight.
+    $build['an_additional_field']['#weight'] = -10;
+  }
+
+  // Add a #post_render callback to act on the rendered HTML of the user.
+  $build['#post_render'][] = 'my_module_user_post_render';
+}
+
+/**
  * @} End of "addtogroup hooks".
  */
Index: feedback.info
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feedback/feedback.info,v
retrieving revision 1.7
diff -u -p -r1.7 feedback.info
--- feedback.info	14 Jan 2011 01:44:22 -0000	1.7
+++ feedback.info	14 Feb 2011 16:51:53 -0000
@@ -3,4 +3,6 @@ name = Feedback
 description = Allows site visitors and users to report issues about this site.
 package = Development
 core = 7.x
+configure = admin/config/user-interface/feedback
+files[] = feedback.module
 files[] = tests/feedback.test
Index: feedback.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feedback/feedback.module,v
retrieving revision 1.83
diff -u -p -r1.83 feedback.module
--- feedback.module	15 Jan 2011 20:18:17 -0000	1.83
+++ feedback.module	14 Feb 2011 16:51:53 -0000
@@ -7,6 +7,83 @@
  */
 
 /**
+ * Implements hook_theme().
+ */
+function feedback_theme() {
+  return array(
+    'feedback_admin_view_form' => array(
+      'render element' => 'form',
+    ),
+    'feedback_entry' => array(
+      'render element' => 'elements',
+      'template' => 'feedback-entry',
+      'file' => 'feedback.admin.inc',
+    ),
+  );
+}
+
+/**
+ * Implements hook_entity_info().
+ */
+function feedback_entity_info() {
+  $return = array(
+    'feedback' => array(
+      'label' => t('Feedback'),
+      'controller class' => 'FeedbackController',
+      'base table' => 'feedback',
+      'uri callback' => 'feedback_uri',
+      'fieldable' => TRUE,
+      'entity keys' => array(
+        'id' => 'fid',
+      ),
+      'bundles' => array(
+        'feedback' => array(
+          'label' => t('Feedback'),
+          'admin' => array(
+            'path' => 'admin/config/user-interface/feedback',
+            'access arguments' => array('administer feedback'),
+          ),
+        ),
+      ),
+      'view modes' => array(
+        'full' => array(
+          'label' => t('Full feedback entry'),
+          'custom settings' => FALSE,
+        ),
+      ),
+    ),
+  );
+
+  return $return;
+}
+
+/**
+ * Entity uri callback.
+ */
+function feedback_uri($entry) {
+  return array(
+    'path' => 'admin/reports/feedback/' . $entry->fid,
+  );
+}
+
+/**
+ * Controller class for feedback entries.
+ *
+ * This extends the DrupalDefaultEntityController class, adding required
+ * special handling for feedback objects.
+ */
+class FeedbackController extends DrupalDefaultEntityController {
+
+  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
+    $query = parent::buildQuery($ids, $conditions, $revision_id);
+    // Specify an additional field from the user table.
+    $query->innerJoin('users', 'u', 'base.uid = u.uid');
+    $query->fields('u', array('name'));
+    return $query;
+  }
+}
+
+/**
  * Implements hook_permission().
  */
 function feedback_permission() {
@@ -19,16 +96,8 @@ function feedback_permission() {
       'title' => t('View feedback messages'),
       'description' => t('View, process, and delete submitted feedback messages.'),
     ),
-  );
-}
-
-/**
- * Implements hook_theme().
- */
-function feedback_theme() {
-  return array(
-    'feedback_admin_view_form' => array(
-      'render element' => 'form',
+    'administer feedback' => array(
+      'title' => t('Administer feedback settings'),
     ),
   );
 }
@@ -45,6 +114,27 @@ function feedback_menu() {
     'access arguments' => array('view feedback messages'),
     'file' => 'feedback.admin.inc',
   );
+  $items['admin/reports/feedback/%feedback'] = array(
+    'title' => 'Feedback entry',
+    'page callback' => 'feedback_view_page',
+    'page arguments' => array(3),
+    'access arguments' => array('view feedback messages'),
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/config/user-interface/feedback'] = array(
+    'title' => 'Feedback settings',
+    'description' => 'Administer feedback settings.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('feedback_admin_settings_form'),
+    'access arguments' => array('administer feedback'),
+    'file' => 'feedback.admin.inc',
+  );
+  $items['admin/config/user-interface/feedback/settings'] = array(
+    'title' => 'Settings',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+
   return $items;
 }
 
@@ -124,10 +214,10 @@ function feedback_form($form, &$form_sta
   );
   if (user_access('view feedback messages')) {
     if (arg(0) != 'node') {
-      $feedbacks = feedback_load_multiple(array(), array('f.status' => 0, 'f.location_masked' => feedback_mask_path($_GET['q'])));
+      $feedbacks = feedback_load_multiple(array(), array('status' => 0, 'location_masked' => feedback_mask_path($_GET['q'])));
     }
     else {
-      $feedbacks = feedback_load_multiple(array(), array('f.status' => 0, 'f.location' => $_GET['q']));
+      $feedbacks = feedback_load_multiple(array(), array('status' => 0, 'location' => $_GET['q']));
     }
     if ($feedbacks) {
       $rows = '';
@@ -150,6 +240,10 @@ function feedback_form($form, &$form_sta
     '#required' => TRUE,
     '#wysiwyg' => FALSE,
   );
+
+  $entry = new stdClass();
+  field_attach_form('feedback', $entry, $form, $form_state);
+
   $form['actions'] = array(
     '#type' => 'actions',
     // Without clearfix, the AJAX throbber wraps in an ugly way.
@@ -178,6 +272,7 @@ function feedback_form($form, &$form_sta
  */
 function feedback_form_submit($form, &$form_state) {
   $entry = new stdClass();
+  entity_form_submit_build_entity('feedback', $entry, $form, $form_state);
   $entry->message = $form_state['values']['message'];
   $entry->location = $form_state['values']['location'];
   feedback_save($entry);
@@ -242,6 +337,10 @@ function feedback_format_message($entry)
       $message .= '<div class="browserinfo">(' . check_plain($entry->useragent) . ')</div>';
     }
   }
+  $uri = entity_uri('feedback', $entry);
+  if ($uri['path'] != $_GET['q']) {
+    $message .= l("view full", $uri['path']);
+  }
   return $message;
 }
 
@@ -274,23 +373,14 @@ function feedback_load($fid) {
  *
  * @return
  *   An array of feedback entry objects indexed by fid.
+ *
+ * @see hook_feedback_load()
+ * @see feedback_load()
+ * @see entity_load()
+ * @see EntityFieldQuery
  */
 function feedback_load_multiple($fids = array(), $conditions = array()) {
-  $query = db_select('feedback', 'f')->fields('f');
-  $query->join('users', 'u', 'f.uid = u.uid');
-  $query->fields('u', array('name'));
-
-  if (!empty($fids)) {
-    $query->condition('fid', $fids, 'IN');
-  }
-  if (!empty($conditions)) {
-    foreach ($conditions as $key => $value) {
-      $query->condition($key, $value);
-    }
-  }
-  $entries = $query->execute()->fetchAllAssoc('fid');
-  module_invoke_all('feedback_load', $entries);
-  return $entries;
+  return entity_load('feedback', $fids, $conditions);
 }
 
 /**
@@ -306,30 +396,47 @@ function feedback_load_multiple($fids = 
 function feedback_save($entry) {
   global $user;
 
+  // Load the stored entity, if any.
+  if (!empty($entry->fid) && !isset($entry->original)) {
+    $entry->original = entity_load_unchanged('feedback', $entry->fid);
+  }
+
+  field_attach_presave('feedback', $entry);
+
+  // Allow modules to alter the feedback entry before saving.
+  module_invoke_all('feedback_presave', $entry);
+  module_invoke_all('entity_presave', $entry, 'feedback');
+
   if (empty($entry->fid)) {
-    if (!isset($entry->uid)) {
-      $entry->uid = $user->uid;
-    }
     $entry->message = trim($entry->message);
-    if (!isset($entry->location_masked)) {
-      $entry->location_masked = feedback_mask_path($entry->location);
-    }
-    if (!isset($entry->url)) {
-      $entry->url = url($entry->location, array('absolute' => TRUE));
-    }
-    if (!isset($entry->timestamp)) {
-      $entry->timestamp = REQUEST_TIME;
-    }
-    if (!isset($entry->useragent)) {
-      $entry->useragent = $_SERVER['HTTP_USER_AGENT'];
+
+    $defaults = array(
+      'uid' => $user->uid,
+      'location_masked' => feedback_mask_path($entry->location),
+      'url' => url($entry->location, array('absolute' => TRUE)),
+      'timestamp' => REQUEST_TIME,
+      'useragent' => $_SERVER['HTTP_USER_AGENT'],
+    );
+    foreach ($defaults as $key => $default) {
+      if (!isset($entry->$key)) {
+        $entry->$key = $default;
+      }
     }
+
     $status = drupal_write_record('feedback', $entry);
+    field_attach_insert('feedback', $entry);
     module_invoke_all('feedback_insert', $entry);
+    module_invoke_all('entity_insert', $entry, 'feedback');
   }
   else {
     $status = drupal_write_record('feedback', $entry, 'fid');
+
+    field_attach_update('feedback', $entry);
     module_invoke_all('feedback_update', $entry);
+    module_invoke_all('entity_update', $entry, 'feedback');
   }
+  unset($entry->original);
+
   return $status;
 }
 
@@ -353,7 +460,9 @@ function feedback_delete_multiple($fids)
   if (!empty($fids)) {
     $entries = feedback_load_multiple($fids);
     foreach ($entries as $fid => $entry) {
+      field_attach_delete('feedback', $entry);
       module_invoke_all('feedback_delete', $entry);
+      module_invoke_all('entity_delete', $entry, 'feedback');
     }
     db_delete('feedback')
       ->condition('fid', $fids, 'IN')
