Index: journal.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/journal/journal.install,v
retrieving revision 1.1.2.4
diff -u -p -r1.1.2.4 journal.install
--- journal.install	13 Aug 2008 17:38:59 -0000	1.1.2.4
+++ journal.install	1 Sep 2008 21:26:45 -0000
@@ -17,6 +17,16 @@ function journal_install() {
         PRIMARY KEY (jid),
         KEY location (location(32))
       ) /*!40100 DEFAULT CHARACTER SET UTF8 */ ");
+      db_query("CREATE TABLE {journal_patch} (
+        pid int unsigned NOT NULL default '0',
+        uid int unsigned NOT NULL default '0',
+        module text NOT NULL default '',
+        description longtext NOT NULL default '',
+        url varchar(255) NOT NULL default '',
+        status varchar(255) NOT NULL default '',
+        timestamp int NOT NULL,
+        PRIMARY KEY (pid)
+      ) /*!40100 DEFAULT CHARACTER SET UTF8 */ ");
       break;
 
     case 'pgsql':
@@ -30,6 +40,17 @@ function journal_install() {
       )");
       db_query("CREATE INDEX {journal}_jid_idx ON {journal} (jid)");
       db_query("CREATE INDEX {journal}_location_idx ON {journal} (location)");
+      db_query("CREATE TABLE {journal_patch} (
+        pid int_unsigned NOT NULL default '0',
+        uid int_unsigned NOT NULL default '0',
+        module text NOT NULL default '',
+        description text NOT NULL default '',
+        url varchar(255) NOT NULL default '',
+        status varchar(255) NOT NULL default '',
+        timestamp int NOT NULL,
+        PRIMARY KEY (pid)
+      )");
+      db_query("CREATE INDEX {journal_patch}_pid_idx ON {journal} (pid)");
       break;
   }
 }
@@ -75,3 +96,37 @@ function journal_update_5101() {
   return $ret;
 }
 
+function journal_update_5102() {
+  $ret = array();
+  switch ($GLOBALS['db_type']) {
+    case 'mysql':
+    case 'mysqli':
+      $ret[] = update_sql("CREATE TABLE {journal_patch} (
+        pid int unsigned NOT NULL default '0',
+        uid int unsigned NOT NULL default '0',
+        module text NOT NULL default '',
+        description longtext NOT NULL default '',
+        url varchar(255) NOT NULL default '',
+        status varchar(255) NOT NULL default '',
+        timestamp int NOT NULL,
+        PRIMARY KEY (pid)
+      ) /*!40100 DEFAULT CHARACTER SET UTF8 */ ");
+      break;
+
+    case 'pgsql':
+      $ret[] = update_sql("CREATE TABLE {journal_patch} (
+        pid int_unsigned NOT NULL default '0',
+        uid int_unsigned NOT NULL default '0',
+        module text NOT NULL default '',
+        description text NOT NULL default '',
+        url varchar(255) NOT NULL default '',
+        status varchar(255) NOT NULL default '',
+        timestamp int NOT NULL,
+        PRIMARY KEY (pid)
+      )");
+      $ret[] = update_sql("CREATE INDEX {journal_patch}_pid_idx ON {journal} (pid)");
+      break;
+  }
+  return $ret;
+}
+
Index: journal.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/journal/journal.module,v
retrieving revision 1.1.2.15
diff -u -p -r1.1.2.15 journal.module
--- journal.module	16 Aug 2008 20:19:55 -0000	1.1.2.15
+++ journal.module	2 Sep 2008 13:26:35 -0000
@@ -47,6 +47,28 @@ function journal_menu($may_cache) {
       'callback' => 'journal_view',
       'access' => user_access('access journal'),
     );
+    $items[] = array(
+      'path' => 'admin/logs/journal/list',
+      'title' => t('List'),
+      'type' => MENU_DEFAULT_LOCAL_TASK,
+      'weight' => -10,
+    );
+    $items[] = array(
+      'path' => 'admin/logs/journal/patches',
+      'title' => t('Patches'),
+      'description' => t('View list of applied patches and hacks on this Drupal site.'),
+      'callback' => 'journal_patch_view',
+      'access' => user_access('access journal'),
+      'type' => MENU_LOCAL_TASK,
+    );
+    $items[] = array(
+      'path' => 'admin/logs/journal/patches/edit',
+      'title' => t('Edit patch'),
+      'callback' => 'drupal_get_form',
+      'callback arguments' => array('journal_patch_form'),
+      'access' => user_access('access journal'),
+      'type' => MENU_LOCAL_TASK,
+    );
   }
   return $items;
 }
@@ -144,6 +166,7 @@ function journal_form_ids_default() {
     'devel_admin_settings' => 0,
     'devel_execute_form' => 0,
     'devel_switch_user_form' => 0,
+    'journal_patch_form' => 0,
     'search_block_form' => 0,
     'search_theme_form' => 0,
     'search_box' => 0,
@@ -345,3 +368,151 @@ function journal_add_entry($description,
   db_query("INSERT INTO {journal} (jid, uid, message, location, timestamp) VALUES (%d, %d, '%s', '%s', %d)", $jid, $user->uid, $description, $location, time());
 }
 
+/**
+ * Output a sortable table containing all recorded patches.
+ */
+function journal_patch_view() {
+  $header = array(
+    array('data' => t('Date'), 'field' => 'j.timestamp', 'sort' => 'desc'),
+    array('data' => t('Module'), 'field' => 'j.module'),
+    array('data' => t('User'), 'field' => 'u.name'),
+    t('Description'),
+    t('Issue'),
+    t('Status'),
+    t('Operations'),
+  );
+  $sql = "SELECT j.*, u.name FROM {journal_patch} j INNER JOIN {users} u ON j.uid = u.uid";
+  $result = pager_query($sql . tablesort_sql($header), 25);
+  $module_list = module_list(FALSE, FALSE);
+  $rows = array();
+  while ($entry = db_fetch_object($result)) {
+    $modules = array();
+    foreach (explode(',', $entry->module) as $module) {
+      $info = _module_parse_info_file(drupal_get_path('module', $module) .'/'. $module .'.info');
+      if ($info['project']) {
+        $url = 'http://drupal.org/project/issues/'. $info['project'];
+        $modules[] = l($info['name'], $url);
+      }
+      else {
+        $modules[] = $info['name'];
+      }
+    }
+    if ($entry->url != '') {
+      if (preg_match('@drupal.org/node/(\d+)@', $entry->url, $issue_title)) {
+        $issue_link = l('#'. $issue_title[1], $entry->url);
+      }
+      else {
+        $issue_link = l(t('View'), $entry->url);
+      }
+    }
+    else {
+      $issue_link = '';
+    }
+    $rows[] = array(
+      format_date($entry->timestamp, 'small'),
+      implode(', ', $modules),
+      theme('username', $entry),
+      filter_xss_admin($entry->description),
+      $issue_link,
+      t($entry->status),
+      l(t('edit'), "admin/logs/journal/patches/edit/$entry->pid"),
+    );
+  }
+
+  if (empty($rows)) {
+    $rows[] = array(array('data' => t('No patch entries available.'), 'colspan' => 7));
+  }
+
+  $output = drupal_get_form('journal_patch_form');
+  $output .= theme('table', $header, $rows);
+  $output .= theme('pager', NULL, 50, 0);
+  return $output;
+}
+
+/**
+ * Form builder function for patches.
+ */
+function journal_patch_form($pid = NULL) {
+  drupal_add_css(drupal_get_path('module', 'journal') .'/journal_patch.css', 'module', 'all', FALSE);
+  $patch = array();
+  if (isset($pid)) {
+    $patch = db_fetch_array(db_query("SELECT j.* FROM {journal_patch} j WHERE j.pid = %d", $pid));
+  }
+  $patch += array(
+    'module' => array(),
+    'description' => '',
+    'url' => '',
+    'status' => 'open',
+  );
+
+  $form = array();
+  $form['patch'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Add patch record'),
+    '#tree' => TRUE,
+  );
+  $form['patch']['module'] = array(
+    '#type' => 'select',
+    '#title' => t('Affected modules'),
+    '#options' => module_list(FALSE, FALSE, TRUE),
+    '#multiple' => TRUE,
+    '#default_value' => explode(',', $patch['module']),
+    '#size' => 8,
+    '#required' => TRUE,
+    '#prefix' => '<div class="journal-patch-module-select">',
+    '#suffix' => '</div>',
+  );
+  $form['patch']['description'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Description'),
+    '#default_value' => $patch['description'],
+    '#required' => TRUE,
+  );
+  $form['patch']['url'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Issue URL'),
+    '#default_value' => $patch['url'],
+    '#prefix' => '<div class="journal-patch-issue clear-block">',
+  );
+  $form['patch']['status'] = array(
+    '#type' => 'select',
+    '#title' => t('Status'),
+    '#options' => array('open' => t('open'), 'fixed' => t('fixed'), "won't fix" => t("won't fix")),
+    '#default_value' => $patch['status'],
+    '#suffix' => '</div>',
+  );
+  if (isset($patch['pid'])) {
+    $form['patch']['pid'] = array(
+      '#type' => 'value',
+      '#value' => $patch['pid'],
+    );
+  }
+  $form['patch']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => isset($patch['pid']) ? t('Save') : t('Add'),
+  );
+  return $form;
+}
+
+/**
+ * Submit handler for journal patch form.
+ */
+function journal_patch_form_submit($form_id, $form_values) {
+  global $user;
+
+  $patch = $form_values['patch'];
+
+  if (preg_match('@^#\d+@', $patch['url'])) {
+    $patch['url'] = 'http://drupal.org/node/'. substr($patch['url'], 1);
+  }
+
+  if ($form_values['op'] == t('Add')) {
+    $pid = db_next_id("{journal_patch}_pid");
+    db_query("INSERT INTO {journal_patch} (pid, uid, module, description, url, status, timestamp) VALUES (%d, %d, '%s', '%s', '%s', '%s', %d)", $pid, $user->uid, implode(',', $patch['module']), $patch['description'], $patch['url'], $patch['status'], time());
+  }
+  else if ($form_values['op'] == t('Save')) {
+    db_query("UPDATE {journal_patch} SET uid = %d, module = '%s', description = '%s', url = '%s', status = '%s' WHERE pid = %d", $user->uid, implode(',', $patch['module']), $patch['description'], $patch['url'], $patch['status'], $patch['pid']);
+    drupal_goto('admin/logs/journal/patches');
+  }
+}
+
Index: journal_patch.css
===================================================================
RCS file: journal_patch.css
diff -N journal_patch.css
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ journal_patch.css	2 Sep 2008 12:33:54 -0000
@@ -0,0 +1,16 @@
+/* $Id: journal.css,v 1.1.2.2 2008/04/18 02:22:09 sun Exp $ */
+
+#journal-patch-form fieldset, #journal-patch-form fieldset .form-item {
+  overflow: hidden;
+  zoom: 1;
+}
+.journal-patch-module-select, .journal-patch-issue .form-item {
+  float: left;
+}
+.patch-description {
+  padding-left: 1em;
+  overflow: hidden;
+}
+.journal-patch-issue .form-item {
+  margin-right: 1em;
+}
