diff -u -r1.3 trigger_example/trigger_example.info
--- trigger_example/trigger_example.info	4 Oct 2009 01:33:55 -0000	1.3
+++ trigger_example/trigger_example.info	15 Dec 2009 00:01:00 -0000
@@ -2,7 +2,7 @@
 name = Trigger example
 description = An example showing how a module can provide triggers that can have actions associated with them.
 package = Example modules
-version = VERSION
 core = 7.x
 dependencies[] = trigger
 files[] = trigger_example.module
+files[] = trigger_example.test
diff -u -r1.2 trigger_example/trigger_example.module
--- trigger_example/trigger_example.module	3 Oct 2009 14:56:46 -0000	1.2
+++ trigger_example/trigger_example.module	15 Dec 2009 00:14:18 -0000
@@ -2,153 +2,305 @@
 // $Id: trigger_example.module,v 1.2 2009/10/03 14:56:46 rfay Exp $
 
 /**
- * Implementation of hook_help().
- */
-function trigger_example_help($path, $arg) {
-  switch ($path) {
-    case 'trigger_example':
-      return '<p>'. t('Use this page to call hook_trigger_example() with the ping and pong operations. You can create actions to display a message or email the user on the <a href="@actions-url">Actions settings page</a> and assign these actions with the ping and pong events on the <a href="@triggers-url">Triggers settings page</a>.', array('@actions-url' => url('admin/settings/actions'), '@triggers-url' => url('admin/build/trigger/trigger_example'))) .'</p>';
-    case 'admin/build/trigger/trigger_example':
-      $explanation = '<p>'. t('Triggers are system events, such as when new content is added or when a user logs in. Trigger module combines these triggers with actions (functional tasks), such as unpublishing content or e-mailing an administrator. The <a href="@url">Actions settings page</a> contains a list of existing actions and provides the ability to create and configure additional actions.', array('@url' => url('admin/settings/actions'))) .'</p>';
-      return $explanation .'<p>'. t('Below you can assign actions to run when certain comment-related triggers happen. For example, you could promote a post to the front page when a comment is added.') .'</p>';
-  }
-}
-
-/**
- * Implementation of hook_menu().
+ * @file
+ * Trigger definition example module.
+ *
+ * Triggers and actions are a pair of special purpose functions allowing some
+ * kind of flexibility for non-programming Drupal configuration. Using the
+ * aproppriate action in a specific event, a site administrator will be able to
+ * add new functionalities. Examples of these are:
+ *  - Send an email after a node is published or edited.
+ *  - Display a message after a user has logged in.
+ *  - Display a message and send an email after a node has been deleted.
+ *
+ * A Trigger is a special function able to enqueue actions. The trigger module
+ * provide the interface allowing to associate certain actions with certain
+ * triggers.
+ *
+ * An action is a special function with an specific purpose. As every other
+ * function, expects some parameters in the correct format and order. Actions
+ * are functions intended to be run by triggers.
+ *
+ * A trigger should build the aproppiate context for the action to be fired, it
+ * is, should prepare the arguments to call the action. Actions are very often
+ * grouped by functionality: examples are 'user', 'node', 'taxonomy'. When some
+ * actions are grouped it is because they expect the same arguments. This way,
+ * you can enqueue as many actions understanding the 'user' object as you want,
+ * but this action grouping is not a requirement, but a consequence.
+ *
+ * Not all actions can be used in all triggers. This is because actions created
+ * for the 'node' object, can not be used with 'user' objects. But some actions
+ * are generic enough to not require this 'user' or 'node' object in their
+ * contexts, and they can be used on every available trigger. This 'group' type
+ * is used by actions to be available for this trigger.
+ *
+ * What are good candidates to be triggers? Any function can be a trigger, as
+ * long as it has the code to call the enqueued actions, but to make Drupal
+ * more extensible, you will find hooks (from Drupal and contributed modules)
+ * very good candidates. A trigger should build the arguments, ask for enqueued
+ * actions and run them. You may define a function being a trigger, and run it
+ * through a button in the front page, or you may prepare a trigger for a hook,
+ * and everytime that hook is fired, your trigger will be.
+ *
+ * What are good candidates to be actions? any function is a possible action,
+ * the problem is to find a trigger able to run it.
+ *
+ * The purpose of the trigger and action API is to make functions available as
+ * actions and triggers (appearing in lists), and meet them in the triggers and
+ * actions user interface.
+ *
+ * This module describes how to create triggers and actions for Drupal. In this
+ * example we are providing different type of triggers:
+ *
+ * - A custom trigger, in its simplest form. We provide a page with a button.
+ *   This button does nothing at all, but when you click (and submit the empty
+ *   form), all the actions associated to this button will be fired.
+ *
+ * - A system trigger, using an existing Drupal hook. In this case we have
+ *   enabled the "mail" event (hook_mail) to be used as a trigger, allowing
+ *   actions to be enqueued and executed: "After an email is sent by Drupal".
+ *
+ * - An extended user trigger, on first time ever login. This event does not
+ *   exist. In the module we will create it, and then provide a trigger for
+ *   the administrator to be able to enqueue actions. They will be executed
+ *   only the first time the user logs in the system.
  *
- * Provide a form that can be used to fire the module's triggers.
  */
-function trigger_example_menu() {
-  $items['trigger_example'] = array(
-    'title' => t('Trigger Example'),
-    'description' => t('Provides a form to test the triggers.'),
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('trigger_example_form'),
-    'access callback' => TRUE,
-  );
-  return $items;
-}
 
 /**
- * Implementation of hook_hook_info().
+ * Implements hook_trigger_info().
  *
- * This hook is used to tell Drupal which hooks we provide that can be used
- * as triggers for actions. We provide a text description of when each of the
- * operations occurs.
+ * We call hook_trigger_info when we are defining the triggers we provide.
+ * Triggers are the events that make fire any number of assigned actions. In
+ * this example, we are registering our three new triggers, providing the group
+ * (system, user..), the callback 'hook' (only informative, does not require a
+ * real hook) function and the label to be shown in the triggers interface.
+ *
+ * Example: In the group (a tab) 'system', for the 'mail' functionality, show:
+ * An email is sent by Drupal.
  */
-function trigger_example_hook_info() {
+function trigger_example_trigger_info() {
   return array(
-    'trigger_example' => array(
-      // One important thing to note here. If you want to use the trigger
-      // module's default menu and forms handlers your hook needs to have the
-      // same name as your module. If you've got the "foo" module with a hook
-      // "hook_fooapi" then you're going to have to implement hook_forms and
-      // map the form ids back to the trigger_assign_form callback.
-      'trigger_example' => array(
-        'ping' => array(
-          'runs when' => t('A ping happens'),
-        ),
-        'pong' => array(
-          'runs when' => t('A pong happens'),
-        ),
+    'system' => array(
+      'mail' => array(
+        'label' => t('An email is sent by Drupal'),
+      ),
+    ),
+    'user' => array(
+      'user_first_time_login' => array(
+        'label' => t('After a user has logged in for the first time'),
+      ),
+    ),
+    'custom' => array(
+      'ping' => array(
+        'label' => t('After the PING button is clicked'),
       ),
     ),
   );
 }
 
 /**
- * Function to generate ping events and invoke hook_trigger_example().
+ * We have defined the triggers, now we should implement them. Lets get into
+ * each trigger and their required code. Triggers are used most of the time to
+ * react on events, and events are most often known as hooks, but that is not a
+ * requirement.
+ *
+ * We have defined a trigger that should work when 'an email is sent by Drupal'.
+ * So we need a function that really gets executed on every mail sent, for this
+ * we are going to use hook_mail, a hook executed on every email submission.
  */
-function trigger_example_ping() {
-  $count = variable_get('trigger_example_pings', 0) + 1;
-
-  // Invoke hook_trigger_example() with the ping operation.
-  module_invoke_all('trigger_example', 'ping', $count);
 
-  variable_set('trigger_example_pings', $count);
+/**
+ * Ping trigger: Run actions associated to a button.
+ *
+ * This is the simplest of all the example implementations. We have defined a
+ * custom function as a trigger (trigger_example_ping), that will ask for all
+ * actions enqueued for the 'ping' event, prepare a basic 'context' for them
+ * and run all of them. There is no need for a hook to be fired, we can call
+ * our trigger at any moment.
+ *
+ * This function is executed during the submission of the example form defined
+ * in this module.
+ *
+ * @param array $options arguments used to call the ping function, if any.
+ */
+function trigger_example_ping($options = array()) {
+  // Ask the trigger module for all actions enqueued for the 'ping' trigger.
+  $aids = trigger_get_assigned_actions('ping');
+  // prepare a basic context, indicating group and "hook", and call all the
+  // actions with this context as arguments.
+  $context = array(
+    'group' => 'custom',
+    'hook' => 'ping'
+  );
+  actions_do(array_keys($aids), (object) $options, $context);
 }
 
 /**
- * Function to generate pong events and invoke hook_trigger_example().
+ * Lets make a new and more complex approach. We have also defined a trigger as
+ * 'After an email is sent by Drupal'. To make this trigger react on the event,
+ * we have implement hook_mail().
  */
-function trigger_example_pong() {
-  $count = variable_get('trigger_example_pings', 0);
-
-  // Invoke hook_trigger_example() with the pong operation.
-  module_invoke_all('trigger_example', 'pong', $count);
 
-  variable_set('trigger_example_pings', max(0, $count - 1));
+/**
+ * Implements hook_mail().
+ *
+ * Mail trigger: Run actions after an email is sent by Drupal.
+ *
+ * This hook is called everytime an email is about being prepared or is sent.
+ * As the purpose of the hook implementation is to serve as a trigger function,
+ * we should perform as a trigger.
+ *
+ * If we want to provide actions for this trigger, we should take care of the
+ * context we are creating. All actions associated to this trigger should expect
+ * email key in $context['key'], email params in $context['params'] and the
+ * built message pased as $object.
+ */
+function trigger_example_mail($key, &$message, $params) {
+  // Ask the trigger module for all actions enqueued for the 'mail' trigger.
+  $aids = trigger_get_assigned_actions('mail');
+  // Prepare a context for this kind of actions.
+  $context = array(
+    'group'   => 'system',
+    'hook'    => 'mail',
+    'key'     => $key,
+    'params'  => $params
+  );
+  // Call all the actions using this context.
+  actions_do(array_keys($aids), (object) $message, $context);
 }
 
 /**
- * Implementation of hook_trigger_example().
+ * The next trigger is even more complex, we are providing a trigger for a non
+ * existant event: "user first time login". We need to create this event first.
+ */
+
+/**
+ * Implements hook_user_login().
+ *
+ * User first login trigger: Run actions on user first login.
+ *
+ * The event "User first time login" does not exist, we should create it before
+ * it can be used. We use hook_user_login to be informed when a user logs in and
+ * try to find if the user has previously logged in before. If the user has not
+ * accessed previously, we make a call to our trigger function.
+ *
  */
-function trigger_example_trigger_example($op, $count) {
-  // Our module is dependent on the trigger module but other modules that this
-  // code get's copied into might not so it's a good idea to check first.
-  if (!module_exists('trigger')) {
-    break;
+function trigger_example_user_login(&$edit, $account, $category = NULL) {
+  // Verify user has never accessed the site: last access was creation date.
+  if ($account->created == $account->access) {
+    // Call the aproppriate trigger function
+    _trigger_example_first_time_login('user_first_time_login', $edit, $account, $category);
   }
+}
 
-  // Find any the ids of any actions associated with this hook/operation pair.
-  if ($aids = _trigger_get_hook_aids('trigger_example', $op)) {
-    // Setup the context for our trigger.
-    $context = array(
-      'hook' => 'trigger_example',
-      'op' => $op,
-      'count' => $count
-    );
-
-    // Since we're not operating on an object we need to create a dummy.
-    $dummy = new stdClass();
-    foreach ($aids as $aid => $action_info) {
-      actions_do($aid, $dummy, $context);
+/**
+ * Trigger function for "User first time login"
+ *
+ * This is our own implementation of the user_first_time_login event. You could
+ * realize that this function is not a hook, and the name does not match the
+ * trigger definition 'user_first_time_login' hook. It is because the 'hook'
+ * attribute for the definition does not imply a real hook to be used, it is
+ * the identifier for the enqueued actions.
+ *
+ * This trigger can be 'grouped' into 'user' type triggers: all actions that
+ * understand this type of trigger could be available for it, so we have to
+ * prepare the context expected by this type of actions. For this example,
+ * we are going to copy the trigger.module implementation for the 'User has
+ * logged in' event.
+ *
+ * This function to run all the enqueued actions assigned to the
+ * 'user_first_time_login' trigger.
+ *
+ * @param string $hook the trigger identification.
+ * @param array  $edit modifications for the account object (should be empty).
+ * @param object $account user object that has logged in.
+ * @param string $category of the profile.
+ *
+ */
+function _trigger_example_first_time_login($hook, &$edit, $account, $category = NULL) {
+  // Keep objects for reuse so that changes actions make to objects can persist.
+  static $objects;
+  // Get all assigned actions for the 'user_first_time_login' trigger.
+  $aids = trigger_get_assigned_actions($hook);
+  $context = array(
+    'group' => 'user',
+    'hook' => $hook,
+    'form_values' => &$edit,
+  );
+  // Instead of making a call to actions_do for all triggers, doing this loop
+  // we provide the oportunity for actions to alter the account object, and
+  // the next action should have this altered account object as argument.
+  foreach ($aids as $aid => $info) {
+    $type = $info['type'];
+    if ($type != 'user') {
+      if (!isset($objects[$type])) {
+        $objects[$type] = _trigger_normalize_user_context($type, $account);
+      }
+      $context['account'] = $account;
+      actions_do($aid, $objects[$type], $context);
+    }
+    else {
+      actions_do($aid, $account, $context, $category);
     }
   }
 }
 
 /**
-* Implementation of hook_action_info_alter().
-*
-* None of the built-in actions will be enabled for our hook by default. We
-* need to implement hook_action_info_alter() so that we can enable a couple.
-*/
-function trigger_example_action_info_alter(&$info) {
-  if (isset($info['system_message_action']['hooks'])) {
-    $info['system_message_action']['hooks']['trigger_example'] = array('ping', 'pong');
-  }
-  if (isset($info['system_send_email_action']['hooks'])) {
-    $info['system_send_email_action']['hooks']['trigger_example'] = array('ping', 'pong');
+ * We have finished creating all the trigger implementations. Now we provide 
+ * helper functions for the module interface to test the PING trigger.
+ */
+
+/**
+ * Implementation of hook_help().
+ */
+function trigger_example_help($path, $arg) {
+  switch ($path) {
+    case 'examples/trigger_example':
+      return '<p>'. t('Use this page to call trigger_example_ping() and fire the ping event. You can create actions to display a message or email the user on the <a href="@actions-url">Actions settings page</a> and assign these actions to the ping event on the <a href="@triggers-url">Triggers settings page</a>.', array('@actions-url' => url('admin/config/system/actions'), '@triggers-url' => url('admin/structure/trigger/custom'))) .'</p>';
+    case 'admin/structure/trigger/system':
+      return t('you can assign actions to run everytime an email is sent by Drupal');
+    case 'admin/structure/trigger/custom':
+      $explanation = '<p>'. t('Triggers are system events, such as when new content is added or when a user logs in. Trigger module combines these triggers with actions (functional tasks), such as unpublishing content or e-mailing an administrator. The <a href="@url">Actions settings page</a> contains a list of existing actions and provides the ability to create and configure additional actions.', array('@url' => url('admin/settings/actions'))) .'</p>';
+      return $explanation .'<p>'. t('Below you can assign actions to run when certain comment-related triggers happen. For example, you could promote a post to the front page when a comment is added.') .'</p>';
   }
 }
 
 /**
- * A form to help fire our triggers.
+ * Implementation of hook_menu().
+ *
+ * Provide a form that can be used to fire the module's triggers.
  */
-function trigger_example_form(&$form_state) {
-  $form['help'] = array(
-    '#type' => 'item',
-    '#value' => format_plural(variable_get('trigger_example_pings', 0), 'There is only @count ping out there.', 'There are @count pings out there. Come on pong them back.'),
+function trigger_example_menu() {
+  $items['examples/trigger_example'] = array(
+    'title'           => t('Trigger Example'),
+    'description'     => t('Provides a form to test the PING trigger example.'),
+    'page callback'   => 'drupal_get_form',
+    'page arguments'  => array('trigger_example_form'),
+    'access callback' => TRUE,
   );
+  return $items;
+}
+
+/**
+ * Trigger example test form
+ *
+ * Provide a button to run the ping event.
+ */
+function trigger_example_form($form_state) {
   $form['ping'] = array(
-    '#type' => 'submit',
-    '#value' => t('Ping'),
+    '#type'  => 'submit',
+    '#value' => t('Run ping event'),
   );
-  if (variable_get('trigger_example_pings', 0)) {
-    $form['pong'] = array(
-      '#type' => 'submit',
-      '#value' => t('Pong'),
-    );
-  }
   return $form;
 }
 
-function trigger_example_form_submit($form, &$form_state) {
-  if ($form_state['values']['op'] == t('Ping')) {
+/**
+ * Submit handler for the trigger_example_form.
+ */
+function trigger_example_form_submit($form, $form_state) {
+  // If the user clicked the button, then run the ping trigger.
+  if ($form_state['values']['op'] == t('Run ping event')) {
     trigger_example_ping();
   }
-  else {
-    trigger_example_pong();
-  }
 }
--- trigger_example/trigger_example.test
+++ trigger_example/trigger_example.test
@@ -0,0 +1,54 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * test file for trigger_example module.
+ */
+
+/**
+ * Default test case for the trigger_example module.
+ */
+class TriggerExampleTestCase extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Trigger example',
+      'description' => 'Perform various tests on trigger_example module.' ,
+      'group' => 'Examples',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('trigger', 'trigger_example');
+  }
+
+  /**
+   * Test assigning a configurable action to the PING event.
+   */
+  function testPingEvent() {
+    // Create an administrative user.
+    $test_user = $this->drupalCreateUser(array('administer actions'));
+    $this->drupalLogin($test_user);
+
+    // Create a configurable action for display a message to the user
+    $hash = md5('system_message_action');
+    $action_label = $this->randomName();
+    $edit = array(
+      'actions_label' => $action_label,
+      'message' => $action_label,
+    );
+    $this->drupalPost('admin/config/system/actions/configure/' . $hash, $edit, t('Save'));
+    $aid = db_query('SELECT aid FROM {actions} WHERE callback = :callback', array(':callback' => 'system_message_action'))->fetchField();
+    // $aid is likely 3 but if we add more uses for the sequences table in
+    // core it might break, so it is easier to get the value from the database.
+    $edit = array('aid' => md5($aid));
+    $this->drupalPost('admin/structure/trigger/custom', $edit, t('Assign'));
+
+    // Request ping form and submit.
+    $this->drupalPost('examples/trigger_example', array(), t('Run ping event'));
+    // Verify the message is shown to the user.
+    $this->assertText($action_label, t('The ping event executed the action.'));
+  }
+}
+


