Index: journal.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/journal/journal.install,v
retrieving revision 1.2.2.6
diff -u -p -r1.2.2.6 journal.install
--- journal.install	21 Jun 2009 00:02:23 -0000	1.2.2.6
+++ journal.install	25 Jun 2009 08:02:04 -0000
@@ -28,6 +28,9 @@ function journal_schema() {
       'timestamp' => array('type' => 'int', 'not null' => TRUE, 'default' => 0,
         'description' => t('The UNIX timestamp when the journal entry was created.'),
       ),
+      'tag' => array('type' => 'varchar', 'length' => 255, 'default' => '',
+        'description' => t('A tag used to group/sort journal entries.'),
+      ),
     ),
     'primary key' => array('jid'),
     'indexes' => array(
@@ -135,3 +138,14 @@ function journal_update_6103() {
   return $ret;
 }
 
+/**
+ * Add a tag column to the journal table.
+ */
+function journal_update_6104() {
+  $ret = array();
+  db_add_field($ret, 'journal', 'tag', array(
+    'type' => 'varchar', 'length' => 255, 'default' => '', 
+    'description' => t('A tag used to group/sort journal entries.')));
+  return $ret;
+}
+
Index: journal.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/journal/journal.module,v
retrieving revision 1.5.2.16
diff -u -p -r1.5.2.16 journal.module
--- journal.module	21 Jun 2009 00:02:23 -0000	1.5.2.16
+++ journal.module	25 Jun 2009 08:02:04 -0000
@@ -4,14 +4,14 @@
 /**
  * @file
  * Allows to maintain a custom log of performed setup and configuration actions.
- * 
+ *
  * Journal module adds additional fields to all forms in a Drupal site.
  * Only users granted the 'access journal' permission are able to add entries
  * to the journal.
  *
  * Journal form fields may be disabled for certain forms, for example forms that
  * are displayed in blocks.
- * 
+ *
  * @todo Describe Demo module implementation.
  * @todo Are we able to track enabling/disabling of modules automatically?
  * @todo Allow to include all form field values in a journal entry.
@@ -70,12 +70,17 @@ function journal_menu() {
     'access arguments' => array('access journal'),
     'type' => MENU_CALLBACK,
   );
+  $items['admin/journal/ajax/autocomplete/tag'] = array(
+    'page callback' => 'journal_autocomplete_tag',
+    'access arguments' => array('access journal'),
+    'type' => MENU_CALLBACK,
+  );
   return $items;
 }
 
 /**
  * Add Journal fields to all forms.
- * 
+ *
  * Any form, except user-defined form_ids, will be extended by a fieldset
  * to enter a journal entry.  All journal form ids are stored in one variable
  * array; having form_ids as keys and a boolean value whether to skip a form id
@@ -114,7 +119,7 @@ function journal_form_alter(&$form, &$fo
   else {
     $journal_weight = 100;
   }
-  
+
   // Prepend our journal submit handler, so we can eliminate the form value of
   // journal_entry, which would be saved as a variable in system_settings_form()
   // otherwise.
@@ -141,6 +146,13 @@ function journal_form_alter(&$form, &$fo
     '#required' => $entry_required,
     '#wysiwyg' => FALSE,
   );
+  $form['journal']['journal_tag'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Journal tag'),
+    '#description' => t('Optionally tag this journal entry; only used to filter entries in the <a href="@journal-url">journal</a>.', array('@journal-url' => url('admin/reports/journal'))),
+    '#autocomplete_path' => 'admin/journal/ajax/autocomplete/tag',
+  );
+
   if ($entry_required && user_access('access devel information')) {
     $form['journal']['journal_entry']['#required'] = FALSE;
     $form['journal']['journal_omit'] = array(
@@ -168,14 +180,14 @@ function journal_form_validate($form, &$
  */
 function journal_form_submit($form, &$form_state) {
   if (!empty($form_state['values']['journal_entry'])) {
-    journal_add_entry($form_state['values']['journal_entry'], $form_state['values']['journal_location']);
+    journal_add_entry($form_state['values']['journal_entry'], $form_state['values']['journal_location'], $form_state['values']['journal_tag']);
   }
-  unset($form_state['values']['journal_entry'], $form_state['values']['journal_location']);
+  unset($form_state['values']['journal_entry'], $form_state['values']['journal_location'], $form_state['values']['journal_tag']);
 }
 
 /**
  * Indicate if a form must not extended.
- * 
+ *
  * @param string $form_id
  *   A form_id to check against.
  *
@@ -263,31 +275,36 @@ function journal_block($op = 'list', $de
 /**
  * Output a sortable table containing all journal entries.
  */
-function journal_view() {
+function journal_view($tag = '') {
   $sql = "SELECT j.*, u.name FROM {journal} j INNER JOIN {users} u ON j.uid = u.uid";
-  
+
   $header = array(
     array('data' => t('Date'), 'field' => 'j.timestamp', 'sort' => 'desc'),
     array('data' => t('User'), 'field' => 'u.name'),
     t('Message'),
     t('Location'),
+    t('Tag'),
   );
   $tablesort = tablesort_sql($header);
-  $result = pager_query($sql . $tablesort, 50);
+
+  if (!empty($tag)) {
+    $sql .= " WHERE tag LIKE '%s'";
+  } 
+  $result = pager_query($sql . $tablesort, 50, 0, NULL, $tag);
 
   return journal_output($result, 'table', $header);
 }
 
 /**
  * Render journal entries.
- * 
+ *
  * Use this function to render and return
  * - a journal provided as a database query result resource or
  * - a custom journal provided as an array containing journal entry objects.
  *
  * This function may look insane to some, but it ensures that implementation of
  * journal module into other modules is as easy as possible.
- * 
+ *
  * @param array $journal
  *   A database query result resource or an array containing journal entry
  *   objects to output.
@@ -310,11 +327,12 @@ function journal_output($journal, $forma
           $entry->uid,
           $entry->message,
           $entry->location,
+          $entry->tag,
         );
         $output .= implode("\t", $row) ."\n";
       }
       break;
-      
+
     case 'list':
       $output = '';
       while ($entry = (is_array($journal) ? array_shift($journal) : db_fetch_object($journal))) {
@@ -337,6 +355,7 @@ function journal_output($journal, $forma
           theme('username', $entry),
           filter_xss_admin($entry->message),
           l(truncate_utf8($entry->location, 32, FALSE, TRUE), $entry->location),
+          l(truncate_utf8($entry->tag, 15, FALSE, TRUE), 'admin/reports/journal/' . $entry->tag),
         );
       }
 
@@ -354,7 +373,7 @@ function journal_output($journal, $forma
 
 /**
  * Convert a journal into an array.
- * 
+ *
  * @param mixed $data
  *   Journal data.
  * @param string $type
@@ -365,22 +384,22 @@ function journal_output($journal, $forma
  */
 function journal_convert($data, $type = 'text') {
   $journal = array();
-  
+
   switch ($type) {
     case 'text':
     default:
       $data = explode('\n', $data);
-      
+
       // Determine delimiter string.
       $delimiter = array_shift($data);
-      
+
       while ($row = array_shift($data)) {
         $row = explode($delimiter, $row);
         $journal[] = (object)$row;
       }
       break;
   }
-  
+
   return $journal;
 }
 
@@ -392,10 +411,10 @@ function journal_convert($data, $type = 
  * @param string $location
  *   The path on which the journal entry has been entered.
  */
-function journal_add_entry($description, $location) {
+function journal_add_entry($description, $location, $tag = '') {
   global $user;
-  
-  db_query("INSERT INTO {journal} (uid, message, location, timestamp) VALUES (%d, '%s', '%s', %d)", $user->uid, $description, $location, time());
+
+  db_query("INSERT INTO {journal} (uid, message, location, tag, timestamp) VALUES (%d, '%s', '%s', '%s', %d)", $user->uid, $description, $location, $tag, time());
 }
 
 /**
@@ -583,3 +602,18 @@ function journal_patch_delete_confirm_su
   $form_state['redirect'] = 'admin/reports/journal/patches';
 }
 
+/**
+ * Page callback for journal tag autocomplete.
+ */
+function journal_autocomplete_tag($string = '') {
+  $matches = array();
+  if ($string) {
+    $result = db_query_range("SELECT DISTINCT tag FROM {journal} WHERE LOWER(tag) LIKE LOWER('%%%s%%')", $string, 0, 10);
+    while ($journal = db_fetch_object($result)) {
+      $matches[$journal->tag] = check_plain($journal->tag);
+    }
+  }
+
+  drupal_json($matches);
+}
+
Index: tests/journal.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/journal/tests/journal.test,v
retrieving revision 1.1.2.2
diff -u -p -r1.1.2.2 journal.test
--- tests/journal.test	21 Jun 2009 00:02:24 -0000	1.1.2.2
+++ tests/journal.test	25 Jun 2009 08:02:04 -0000
@@ -64,4 +64,46 @@ class JournalTestCase extends DrupalWebT
     $this->drupalGet('admin/reports/journal');
     $this->assertText($journal_entry, t('Entry submitted in form using #tree appears in journal.'));
   }
+
+  /**
+   * Test adding and filtering by tags.
+   *
+   * Testing adding tags and filtering by tags is included in the same unit
+   * test so that the added tags can be later used as filters.
+   */
+  function testJournalTag() {
+    $journals = array();
+
+    // Add a few journal entries.
+    $journals = array();
+    for ($i = 0; $i < 3; $i++) {
+      $journals[] = $edit = array(
+        'journal_entry' => $this->randomName(8),
+        'journal_tag' => $this->randomName(8),
+      );
+      $this->drupalPost('admin/build/block', $edit, 'Save blocks');
+    }
+
+    foreach ($journals as $journal) {
+      // Verify that log with our tag contains recently added entry.
+      $this->drupalGet('admin/reports/journal');
+      $this->assertText($journal['journal_entry'], t('Default log contains added entry.'));
+
+      $this->drupalGet('admin/reports/journal/list/'. $journal['journal_tag']);
+      $this->assertText($journal['journal_entry'], t("Entry appears in list log when filtered by it's tag."));
+
+      // Verify that we can filter by tag.
+      $this->drupalGet('admin/reports/journal/'. $journal['journal_tag']);
+      $this->assertText($journal['journal_entry'], t('Entry appears in default log when filtered by tag.'));
+
+      // Verify that only entries with the same tag are listed when filtered
+      // by tag.
+      foreach($journals as $sub_journal) {
+        if ($journal['journal_tag'] == $sub_journal['journal_tag']) {
+          continue;
+        }
+        $this->assertNoText($sub_journal['journal_entry'], 'Only entries with the same tag appear in the default log when filtered by tag.');
+      }
+    }
+  }
 }
