Index: journal.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/journal/journal.install,v
retrieving revision 1.8
diff -u -p -r1.8 journal.install
--- journal.install	21 Jun 2009 00:02:13 -0000	1.8
+++ journal.install	23 Jun 2009 03:59:59 -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.20
diff -u -p -r1.20 journal.module
--- journal.module	21 Jun 2009 00:02:13 -0000	1.20
+++ journal.module	23 Jun 2009 03:59:59 -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.
@@ -51,9 +51,9 @@ function journal_menu() {
     'weight' => -10,
   );
   $items['admin/reports/journal/patches'] = array(
+    'page callback' => 'journal_patch_view',
     'title' => 'Patches',
     'description' => 'View list of applied patches and hacks on this Drupal site.',
-    'page callback' => 'journal_patch_view',
     'access arguments' => array('access journal'),
     'type' => MENU_LOCAL_TASK,
   );
@@ -70,12 +70,17 @@ function journal_menu() {
     'access arguments' => array('access journal'),
     'type' => MENU_CALLBACK,
   );
+  $items['admin/journal/ajax/autocomplete/tag'] = array(
+    'access arguments' => array('access journal'),
+    'type' => MENU_CALLBACK,
+    'page callback' => 'journal_autocomplete_tag',
+  );
   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,15 +119,19 @@ 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.
   array_unshift($form['#submit'], 'journal_form_submit');
 
   $form['journal'] = array(
+    '#title' => t('Journal'),
     '#weight' => $journal_weight,
     '#tree' => FALSE,
+    '#type' => 'fieldset',
+    '#collapsible' => TRUE,
+    '#collapsed' => FALSE,
   );
 
   // Store the path on which this form was initially displayed.
@@ -133,7 +142,6 @@ function journal_form_alter(&$form, &$fo
     '#value' => (!empty($_REQUEST['journal_location']) ? $_REQUEST['journal_location'] : $_GET['q']),
   );
 
-  // Add journal entry field.
   $form['journal']['journal_entry'] = array(
     '#type' => 'textarea',
     '#title' => t('Journal entry'),
@@ -141,6 +149,14 @@ function journal_form_alter(&$form, &$fo
     '#required' => $entry_required,
     '#wysiwyg' => FALSE,
   );
+  // Add journal entry field.
+  $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 +184,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 +279,35 @@ 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";
-  
+  if (!empty($tag)) {
+    $sql .= " WHERE LOWER(tag) LIKE LOWER('%s%%')";
+  }
+
   $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);
+  $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 +330,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 +358,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($entry->tag, 'admin/reports/journal/' . $entry->tag),
         );
       }
 
@@ -354,7 +376,7 @@ function journal_output($journal, $forma
 
 /**
  * Convert a journal into an array.
- * 
+ *
  * @param mixed $data
  *   Journal data.
  * @param string $type
@@ -365,22 +387,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 +414,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 +605,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%%')", array($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: tests/journal.test
diff -N tests/journal.test
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ tests/journal.test	23 Jun 2009 03:59:59 -0000
@@ -0,0 +1,105 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Simpletest integration for the Journal module.
+ */
+class JournalTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+     'name' => t('Journal module functionality'),
+     'description' => t('Test the Journal module functionality.'),
+     'group' => t('Journal'),
+    );
+  }
+
+  function setUp() {
+    parent::setUp('journal');
+
+    $user = $this->drupalCreateUser(array('administer blocks', 'access journal'));
+    $this->drupalLogin($user);
+  }
+
+  /**
+   * Test a basic journal entry.
+   */
+  function testJournalAddEntry() {
+    // Add a new block.
+    $title = $this->randomName(8);
+    $description = $this->randomName(8);
+    $journal_entry = $this->randomName(8);
+    $edit = array(
+      'info' => $description,
+      'title' => $title,
+      'body' => $this->randomName(8),
+      'pages' => '',
+      'journal_entry' => $journal_entry,
+    );
+    $this->drupalPost('admin/build/block/add', $edit, 'Save block');
+
+    // Make sure the journal entry we added above is in the journal report.
+    $this->drupalGet('admin/reports/journal');
+    $this->assertText($journal_entry, t('Verify that the journal entry is listed.'));
+    $this->assertText('admin/build/block/add', t('Verify that the journal entry location is listed.'));
+  }
+
+  /**
+   * Test a journal entry where #tree = TRUE.
+   *
+   * The admin/build/block page is used to test this case because the
+   * form there is nested.  We do not need to position a block to make a
+   * a journal entry.
+   */
+  function testJournalAddEntryWhereTreeTrue() {
+    $journal_entry = $this->randomName(8);
+    $edit = array(
+      'journal_entry' => $journal_entry,
+    );
+    $this->drupalPost('admin/build/block', $edit, 'Save blocks');
+
+    // Verify the entry is in the journal report.
+    $this->drupalGet('admin/reports/journal');
+    $this->assertText($journal_entry, t('Verify that the journal entry is listed.'));
+    $this->assertText('admin/build/block', t('Verify that the journal entry location is listed.'));
+  }
+
+  /**
+   * 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.
+    for ($i = 0; $i < 10; $i++) {
+      $journal_entry = $this->randomName(8);
+      $journal_tag = $this->randomName(8);
+      $edit = array (
+        'journal_entry' => $journal_entry,
+        'journal_tag' => $journal_tag,
+      );
+      $this->drupalPost('admin/build/block', $edit, 'Save blocks');
+      $journals[$journal_entry] = $journal_tag;
+    }
+
+    // Verify entries and the tags exist in the view.
+    foreach ($journals as $entry => $tag) {
+      $this->drupalGet('admin/reports/journal');
+      $this->assertText($entry, t('Verify that the journal entry is listed.'));
+
+      // Verify that the location is listed.
+      $this->assertText('admin/build/block', t('Verify that the location is listed.'));
+
+      // Verify that we can filter by tag.
+      $this->drupalGet('admin/reports/journal/'. $tag);
+      $this->assertText($tag, t('Verify that the journal entry is listed when filtered by tag.'));
+
+      $this->drupalGet('admin/reports/journal/list/'. $tag);
+      $this->assertText($tag, t('Verify that the journal entry is listed when filtered by tag.'));
+    }
+  }
+}
+
