workflow-ng/0000755000175000017500000000000010627352101011741 5ustar fagofagoworkflow-ng/workflow_ng.info0000644000175000017500000000025610627352065015170 0ustar fagofago; $Id: workflow_ng.info,v 1.1 2007/05/19 20:17:35 fago Exp $ name = Workflow-ng description = Next generation workflows for Drupal version = "$Name: $" package = Workflow-ngworkflow-ng/workflow_ng.module0000644000175000017500000006016110627352065015523 0ustar fagofago array(), 'all' => array()); $returned = module_invoke_all($name); foreach ($returned as $item_name => $info) { $info['#type'] = $name; //set the type to $name, so that element defaults can be applied later $info['#name'] = $item_name; $data[$name]['labels'][$item_name] = $info['#label']; $data[$name]['all'][$item_name] = $info; } asort($data[$name]['labels']); } if ($op != 'labels') { $op = 'all'; } if (!isset($key)) { return $data[$name][$op]; } else { return $data[$name][$op][$key]; } } /* * Returns all active configurations for the event $event_name * * @param $event_name The event name, for which the configurations should be returned. * @param $reset May be set to true to clear the cache. * @return Returns the configured configurations for this event */ function workflow_ng_event_get_configurations($event_name = NULL, $reset = FALSE) { //We prevent a lot of queries by storing all event names with activated configurations with variable_set static $configs; if (!isset($configs) || $reset) { //initialize $configs = variable_get('workflow_ng_configurations', array()); } if (isset($event_name) && !isset($configs[$event_name])) { if (!$reset && $cache = cache_get('cfg_'. $event_name, 'cache_workflow_ng')) { $configs[$event_name] = unserialize($cache->data); } else { //TODO: refresh the whole cache on cache miss $configs[$event_name] = workflow_ng_get_configurations_by_event($event_name); //TODO: filter for only active configurations if (count($configs[$event_name])) { cache_set('cfg_'. $event_name, 'cache_workflow_ng', serialize($configs[$event_name])); } else { //Remember that there is no configuration for this event $stored_configs = variable_get('workflow_ng_configurations', array()); $stored_configs[$event_name] = array(); variable_set('workflow_ng_configurations', $stored_configs); } } } return isset($event_name) ? $configs[$event_name] : NULL; } /* * Gathers the configurations and returns the configurations for the given event * This bypasses any database cache! * * @param $event_name The event name, for which the configurations should be returned. * @param $reset May be set to true to clear the static cache. */ function workflow_ng_get_configurations_by_event($event_name = NULL, $reset = FALSE) { static $configurations; if ($reset) { $configurations = NULL; } if (!isset($configurations) && isset($event_name)) { $configurations = array(); foreach (workflow_ng_get_configurations() as $key => $configuration) { $configurations += array($configuration['#event'] => array()); $configurations[$configuration['#event']][$key] = $configuration; } } return isset($configurations[$event_name]) ? $configurations[$event_name] : array(); } /* * Clears the workflow-ng cache * @param $immediate If left FALSE, the cache will be kept until the next page load */ function workflow_ng_clear_cache($immediate = FALSE) { cache_clear_all('cfg_', 'cache_workflow_ng', TRUE); variable_del('workflow_ng_configurations'); if ($immediate) { workflow_ng_event_get_configurations(NULL, TRUE); workflow_ng_get_configurations_by_event(NULL, TRUE); workflow_ng_gather_data('', 'all', NULL, TRUE); } } /* * Invokes configured actions/conditions for the given event * @param $event_name As first param pass the event name * @params $args Pass further arguments as defined in hook_event_info() for this event. */ function workflow_ng_invoke_event() { $args = func_get_args(); $event_name = array_shift($args); if ($event = workflow_ng_get_events('all', $event_name)) { //apply the event element defaults _workflow_ng_element_defaults($event); //log, if debugging is activated $log = WORKFLOW_NG_ENABLE_DEBUG ? array() : FALSE; workflow_ng_write_log($log, t('Event %label has been invoked.', array('%label' => $event['#label']))); //get the active configurations if ($configurations = workflow_ng_event_get_configurations($event_name)) { //get the processed arguments $arguments = _workflow_ng_process_arguments($event, $args); //process the configurations workflow_ng_process_configurations($configurations, $arguments, $log); } workflow_ng_show_log($log); } } /* * Processes the configurations by using workflow_ng_process_elements() * Afterwards it cares for saving the modified arguments. * * @param $configurations The configurations to process * @param $arguments An array of arguments in format as returned from _workflow_ng_process_arguments() * @param $log An array of log entries. Set it to FALSE to disable logging, otherwise initialize it with array() */ function workflow_ng_process_configurations($configurations, &$arguments, &$log) { //First apply the configurations defaults _workflow_ng_element_defaults($elements); //then start processing workflow_ng_process_elements($configurations, $arguments, $log); //save all the changed arguments.. foreach ($arguments['save'] as $argument_name) { workflow_ng_save_argument($arguments['event_name'], $argument_name, $arguments['data'][$argument_name], $log); } } /* * Processes the elements in a recursive way * The elements are a tree of configurations, conditions, actions and possible others. * * Each element is executed by using workflow_ng_execute_element(). * It evaluates to TRUE if * its execution evaluates to TRUE or if * all children of the element evaluate to TRUE * * Elements can use '#execute' to set their execution handler, which can be used to * to customize the processing of the children. E.g. the element 'OR' does this and * evaluates to TRUE if at least one of its children evaluate to TRUE. * * An element may force processing its children by invoking workflow_ng_process_elements() * for itself again. * * @param $elements An array of elements to process * @param $arguments An array of arguments in format as returned from _workflow_ng_process_arguments() * @param $log An array of log entries. Set it to FALSE to disable logging, otherwise initialize it with array() */ function workflow_ng_process_elements(&$elements, &$arguments, &$log) { //Execute the current element if not yet executed if (!isset($elements['#_executed'])) { $elements['#_executed'] = TRUE; $result = workflow_ng_execute_element($elements, $arguments, $log); } else { $result = FALSE; //just process the children } // if the result is FALSE we start processing the children if ($result === FALSE && !isset($elements['#_processed'])) { //first sort the children then process them _workflow_ng_sort_children($elements); //process them foreach (element_children($elements) as $key) { //propagate the event name down the tree, then recurse $elements[$key] += array('#event' => $elements['#event']); $result = workflow_ng_process_elements($elements[$key], $arguments, $log); if ($result === FALSE) { //stop processing the children break; } } } $elements['#_processed'] = TRUE; return $result; } /* * Sorts the children of the element */ function _workflow_ng_sort_children(&$element) { if (!isset($element['#_sorted'])) { $element['#_sorted'] = TRUE; //but before sorting make sure the defaults are applied //so that #weight is proper initialized foreach(element_children($element) as $key) { _workflow_ng_element_defaults($element[$key]); } uasort($element, "_element_sort"); } } /* * Makes sure the element defaults are applied */ function _workflow_ng_element_defaults(&$element) { if (!isset($element['#_defaults_applied'])) { if ((!empty($element['#type'])) && ($info = _element_info($element['#type']))) { // Overlay $info onto $element, retaining preexisting keys in $element. $element += $info; } $element['#_defaults_applied'] = TRUE; } } /* * Executes the element by invoking the element type's execution handler * * @param $elements An array of elements to process * @param $arguments An array of arguments in format as returned from _workflow_ng_process_arguments() * @param $log An array of log entries. Set it to FALSE to disable logging * @return The execution result, or FALSE if there is no valid execution handler. */ function workflow_ng_execute_element(&$element, &$arguments, &$log) { if (isset($element['#type']) && isset($element['#execute']) && function_exists($element['#execute'])) { return $element['#execute']($element, $arguments, $log); } return FALSE; } /* * Execution handler for configurations * We want each configuration to be proccessed, so let the following configurations * be processed by returning always TRUE. * So we have to force processing the children by calling workflow_ng_process_elements again * */ function workflow_ng_execute_configuration(&$element, &$arguments, &$log) { if ($element['#active'] && ($element['#recursion'] == TRUE || !in_array($element['#name'], workflow_ng_processed_configurations()))) { workflow_ng_write_log($log, t('Executing the configuration %name on event %event', array('%name' => $element['#label'], '%event' => workflow_ng_get_events('labels', $element['#event'])))); //force processing of the elements children workflow_ng_process_elements($element, $arguments, $log); //remember that we have processed this configuration to prevent recursion workflow_ng_processed_configurations($element['#name']); } return TRUE; } /* * Execution handler for the OR element * Evaluates to TRUE if at least one children evaluates to TRUE.. */ function workflow_ng_execute_or(&$elements, &$arguments, &$log) { //mark the children as processed $elements['#_processed'] = TRUE; //first sort the children then process them _workflow_ng_sort_children($elements); foreach (element_children($elements) as $key) { //propagate the event name down the tree, then recurse $elements[$key] += array('#event' => $elements['#event']); $result = workflow_ng_process_elements($elements[$key], $arguments, $log); if ($result == TRUE) { return TRUE; } } return FALSE; } /* * Execution handler for the NOR element * Evaluates to TRUE if all children evaluate to FALSE. * */ function workflow_ng_execute_nor(&$elements, &$arguments, &$log) { //just process it like an OR element $result = workflow_ng_execute_or($elements, $arguments, $log); //and then negate the result return !$result; } /* * Execution handler for actions * * @param $element The action's configuration element * @param $arguments An array of arguments in format as returned from _workflow_ng_process_arguments() * @param $log An array of log entries. Set it to FALSE to disable logging * @return TRUE to let workflow_ng proceed wit executing actions, only FALSE if $result['#halt'] is set. */ function workflow_ng_execute_action($element, &$arguments, &$log) { if (isset($element['#name']) && function_exists($element['#name'])) { $exec_args = workflow_ng_get_execution_arguments($element, $arguments, $log); $result = call_user_func_array($element['#name'], $exec_args); //An action may return altered arguments, which will be saved automatically if (isset($result) && is_array($result)) { $reverse_map = array_flip($element['#argument map']); foreach ($result as $argument_name => $argument) { $event_arg_name = $reverse_map[$argument_name]; //update the arguments for all following elements mark them for being saved later if (isset($event_arg_name) && isset($arguments['data'][$event_arg_name])) { $arguments['data'][$event_arg_name] = $argument; $arguments['save'][$event_arg_name] = $event_arg_name; } } } workflow_ng_write_log($log, t('Successfully executed the action %name', array('%name' => workflow_ng_get_actions('labels', $element['#name'])))); //this allows actions to act as conditions too if (isset($result['#halt']) && $result['#halt']) { workflow_ng_write_log($log, t('Action %name evaluated to %bool.', array('%name' => workflow_ng_get_actions('labels', $element['#name']), '%bool' => 'FALSE'))); return FALSE; } } return TRUE; } /* * Execution handler for conditions * Note: An condition may not alter arguments * * @param $element The condition's configuration element * @param $arguments An array of arguments in format as returned from _workflow_ng_process_arguments() * @param $log An array of log entries. Set it to FALSE to disable logging * @return The execution result of the condition, or if it is no valid condition FALSE. */ function workflow_ng_execute_condition($element, &$arguments, &$log) { if (isset($element['#name']) && function_exists($element['#name'])) { $exec_args = workflow_ng_get_execution_arguments($element, $arguments, $log); $result = call_user_func_array($element['#name'], $exec_args); workflow_ng_write_log($log, t('Condition %name evaluated to %bool.', array('%name' => workflow_ng_get_conditions('labels', $element['#name']), '%bool' => $result ? 'TRUE' : 'FALSE'))); return $result; } return FALSE; } /* * Writes the message into the log */ function workflow_ng_write_log(&$log, $message) { if ($log !== FALSE && is_array($log)) { list($usec, $sec) = explode(" ", microtime()); $log[] = array('time' => array('sec' => $sec, 'usec' => $usec), 'msg' => $message); } } /* * Returns the execution arguments needed by the given element * It applies the #argument map and gets all needed arguments. * * @param $element The configured element, which is to be executed * @param $arguments An array of arguments in format as returned from _workflow_ng_process_arguments() * @param $log An array of log entries. Set it to FALSE to disable logging */ function workflow_ng_get_execution_arguments(&$element, &$arguments, &$log) { $exec_args = array(); $element_info = workflow_ng_get_element_info($element); //initialize unset arguments in the argument map with pairs: name => name //this allows ommitting such obvious mappings $reverse_map = array_flip($element['#argument map']) + drupal_map_assoc(array_keys($element_info['#arguments'])); $element['#argument map'] = array_flip($reverse_map); //get the right execution arguments by applying the argument map of this element foreach ($element_info['#arguments'] as $argument_name => $info) { $event_arg_name = $reverse_map[$argument_name]; $exec_args[$argument_name] = workflow_ng_element_get_argument($element, $arguments, $event_arg_name, $log); } $function = $element['#name']. '_form'; if (function_exists($function)) { //this is a configurable condition/action so add the settings as first parameter $exec_args = array_merge(array($element['#settings']), $exec_args); } return $exec_args; } /* * Implementation of hook_elements() * Defines default values for all available properties of workflow_ng's element types */ function workflow_ng_elements() { $types = array(); $types['configuration'] = array('#name' => '', '#module' => '', '#event' => '', '#recursion' => FALSE, '#fixed' => FALSE, '#active' => TRUE, '#altered' => FALSE, '#execute' => 'workflow_ng_execute_configuration'); $types['condition'] = array('#name' => '', '#argument map' => array(), '#settings' => array(), '#weight' => 0, '#execute' => 'workflow_ng_execute_condition'); $types['action'] = array('#name' => '', '#argument map' => array(), '#settings' => array(), '#weight' => 1000, '#execute' => 'workflow_ng_execute_action'); $types['event_info'] = array('#name' => '', '#module' => '', '#arguments' => array()); $types['action_info'] = array('#name' => '', '#module' => '', '#arguments' => array()); $types['condition_info'] = array('#name' => '', '#module' => '', '#arguments' => array(), '#additional arguments' => array()); $types['OR'] = array('#execute' => 'workflow_ng_execute_or'); $types['NOR'] = array('#execute' => 'workflow_ng_execute_nor'); return $types; } /* * Processes the arguments of an event to an array in the style as it is required * for workflow_ng_process_elements(). * * The returned array format is like: * array( * 'event_name' => 'example', //containts the event name, in which context the arguments are processed * 'save' => array('arg1_name', ..), //an array of arguments that have to be saved * 'data' => array('arg1_name' => $arg1, 'arg2_name' => $arg2, ..), //an array of loaded arguments * ); * * Note that the order of the arguments will be kept, as it might be important for passing * the arguments to further argument loading handlers. * * @param $event The event info from the currently processed event * @param $args The argument data passed with the event invocation */ function _workflow_ng_process_arguments($event, $args) { $argument_names = array_keys($event['#arguments']); $arguments = array('save' => array(), 'data' => array(), 'event_name' => $event['#name']); foreach ($args as $index => $argument) { if (isset($argument_names[$index])) { $arguments['data'][$argument_names[$index]] = $argument; } } return $arguments; } /* * Returns the needed argument for the given element. This function will take care of loading * further needed arguments. * * @param $element The element * @param $arguments The existing arguments in the format as returned from _workflow_ng_process_arguments() * @param $argument_name The arguments's machine readable name * @param $log An array of log entries. Set it to FALSE to disable logging */ function workflow_ng_element_get_argument($element, &$arguments, $argument_name, &$log) { if (!isset($arguments['data'][$argument_name]) && $event = workflow_ng_get_events('all', $element['#event'])) { if ($event['#arguments'][$argument_name]['#handler'] && function_exists($event['#arguments'][$argument_name]['#handler'])) { //call the handler to get the runtime data $arguments['data'][$argument_name] = call_user_func_array($event['#arguments'][$argument_name]['#handler'], $arguments['data']); workflow_ng_write_log($log, t('Successfully loaded argument %arg', array('%arg' => $event['#arguments'][$argument_name]['#label']))); } } return $arguments['data'][$argument_name]; } /* * Gets the element info of an element (actions, conditions,..) */ function workflow_ng_get_element_info($element) { return workflow_ng_gather_data($element['#type']. '_info', 'all', $element['#name']); } /* * A simple helping function, which eases the creation of configurations * * @param $op One of 'AND', 'OR', 'NOR'. If ommitted AND will be assumed, as this fits for adding actions. * @param $elements The elements to configure. You must pass at least two elements. */ function workflow_ng_configure() { $args = func_get_args(); $op = array_shift($args); if (!is_string($op)) { //assume 'AND' return call_user_func_array('workflow_ng_configure', array_merge(array('AND', $op), $args)); } $op = strtoupper($op); switch($op) { case 'NOR': case 'OR': if (count($args) > 1 || $op == 'NOR') { $element = workflow_ng_use_element($op); $element[0] += call_user_func_array('array_merge', $args); return $element; } return $args; case 'AND': //build an array like array('element1' => .., 'element2' => .., 'element3' => .., ..) return call_user_func_array('array_merge', $args); } } /* * Configures a condition for using in a configuration * * @param $name The name of condition to create, as specified at hook_condition_info() * @param $params An optional array of properties to add, e.g. #argument_map */ function workflow_ng_use_condition($name, $params = array()) { $params += array('#name' => $name); return workflow_ng_use_element('condition', $params); } /* * Configures an action for using in a configuration * * @param $name The name of action to create, as specified at hook_action_info() * @param $params An optional array of properties to add, e.g. #argument_map */ function workflow_ng_use_action($name, $params = array()) { $params += array('#name' => $name); return workflow_ng_use_element('action', $params); } /* * Configures an element of type $type with the further properties $params for using in a configuration */ function workflow_ng_use_element($type, $params = array()) { $element = array(0 => array('#type' => $type)); $element[0] += $params; return $element; } /* * Shows the log if it's active */ function workflow_ng_show_log($log) { if (is_array($log) && count($log)) { $start = $log[0]['time']; foreach ($log as $data) { $diff = ($data['time']['sec'] - $start['sec'])*1000000 + $data['time']['usec'] - $start['usec']; $formatted_diff = round($diff * 1000, 3). ' ms'; drupal_set_message($formatted_diff . ' '. $data['msg']); } } } /* * Saves a modified argument */ function workflow_ng_save_argument($event_name, $argument_name, $value, &$log) { if (($event = workflow_ng_get_events('all', $event_name)) && $info = $event['#arguments'][$argument_name]) { switch ($info['#entity']) { case 'user': user_save($value, $value); workflow_ng_write_log($log, t('Updated user %name', array('%name' => $value->name))); break; case 'node': node_save($value); workflow_ng_write_log($log, t('Updated node %title.', array('%title' => $value->title))); break; default: workflow_ng_write_log($log, t('Can\'t save unknown entity %value', array('%value' => $info['#entity']))); case 'custom': break; } } } /* * Remembers the processed configurations. With this information, recursion is prevented * * @param $add_name If set to FALSE, all already processed configurations will be returned * Otherwise, the passed configuration name will be stored for further lookups. */ function workflow_ng_processed_configurations($add_name = FALSE) { static $memory = array(); if ($add_name !== FALSE) { $memory[] = $add_name; } else { return $memory; } } workflow-ng/states.info0000644000175000017500000000026710627352065014137 0ustar fagofago; $Id: states.info,v 1.1 2007/03/26 14:26:39 fago Exp $ name = States description = Provides configurable state machines for other modules. version = "$Name: $" package = Workflow-ngworkflow-ng/workflow_ng_tests.inc0000644000175000017500000001645510627352065016240 0ustar fagofago array( '#label' => t('Is equal to type'), '#arguments' => array('node' => array('#entity' => 'node', '#label' => t('The node'))), ), 'workflow_ng_condition_node_comparison' => array( '#label' => t('Node comparison by configurable property'), '#arguments' => array( 'node1' => array('#entity' => 'node', '#label' => t('The first node to compare')), 'node2' => array('#entity' => 'node', '#label' => t('The second node to compare')) ), ), 'workflow_ng_condition_user_comparison' => array( '#label' => t('Compares two users'), '#arguments' => array( 'user1' => array('#entity' => 'user', '#label' => t('The first user to compare')), 'user2' => array('#entity' => 'user', '#label' => t('The second user to compare')) ), ), ); } /* * Implementation of hook_action_info() */ function workflow_ng_action_info() { return array( 'workflow_ng_action_msg_title' => array( '#label' => t('Show a message containing the node\'s title'), '#arguments' => array('node' => array('#entity' => 'node', '#label' => t('The node'))), ), 'workflow_ng_action_mail_node' => array( '#label' => t('Mail the content of the node to the user'), '#arguments' => array( 'node' => array('#entity' => 'node', '#label' => t('The node to mail')), 'user' => array('#entity' => 'user', '#label' => t('The notified user')), ), ), 'workflow_ng_action_msg_title_and_author' => array( '#label' => t('Show a message containing the node\'s title and the author\'s name'), '#arguments' => array('node' => array('#entity' => 'node', '#label' => t('The node')), 'author' => array('#entity' => 'user', '#label' => t('The author'))), ), 'workflow_ng_action_node_set_author' => array( '#label' => t('Sets author of a node'), '#arguments' => array( 'node' => array('#entity' => 'node', '#label' => t('The node to modify')), 'author' => array('#entity' => 'user', '#label' => t('The author to set')) ), ), ); } /* * Some testing/demonstration configurations */ function workflow_ng_configuration() { $configurations = array(); $configurations['config1'] = array( '#label' => t('Print the node title of pages and stories'), '#event' => 'node_view', '#module' => 'workflow_ng', ); //configure a conditoin which evalutates if the node type is a page $condition1 = workflow_ng_use_condition('workflow_ng_condition_content_type', array('#settings' => array('type' => 'page'))); //and another one which evalutates if the node type is a story $condition2 = workflow_ng_use_condition('workflow_ng_condition_content_type', array('#settings' => array('type' => 'story'))); //OR them $conditions = workflow_ng_configure('OR', $condition1, $condition2); //configure an action $action = workflow_ng_use_action('workflow_ng_action_msg_title'); //add the elements to the configuration $configurations['config1'] = workflow_ng_configure($configurations['config1'], $conditions, $action); /* configure actions on node update, that show the new node title, but only if it has changed */ $configurations['config2'] = array( '#type' => 'configuration', '#label' => t('Print the updated title, if it has changed'), '#event' => 'node_update', '#module' => 'workflow_ng', ); $condition = workflow_ng_use_condition('workflow_ng_condition_node_comparison', array('#settings' => array('property' => 'title'), '#argument map' => array('node_unchanged' => 'node1', 'node' => 'node2'))); $conditions = workflow_ng_configure('NOR', $condition); //negate, we want to have the action fired if the titles are not equal //show the updated title $action1 = workflow_ng_use_action('workflow_ng_action_msg_title'); $configurations['config2'] = workflow_ng_configure($configurations['config2'], $conditions, $action1); /* configure actions on node update, that show the new node title, but only if it has changed */ $configurations['config3'] = array( '#type' => 'configuration', '#label' => t('Force node author to current user'), '#event' => 'node_update', '#active' => FALSE, //disabled for now !! '#module' => 'workflow_ng', ); $author_equals_user = workflow_ng_use_condition('workflow_ng_condition_user_comparison', array('#argument map' => array('author' => 'user1', 'user' => 'user2'))); $author_equals_not_user = workflow_ng_configure('NOR', $author_equals_user); //negate $set_author = workflow_ng_use_action('workflow_ng_action_node_set_author', array('#argument map' => array('user' => 'author'))); $configurations['config3'] = workflow_ng_configure($configurations['config3'], $author_equals_not_user, $set_author); /* configure actions on node update, that notify the authour about any changes from other users */ $configurations['config4'] = array( '#type' => 'configuration', '#label' => t('Notify the node author about changes from other users'), '#event' => 'node_update', '#module' => 'workflow_ng', ); $action_notify = workflow_ng_use_action('workflow_ng_action_mail_node', array('#argument map' => array('author' => 'user'))); $configurations['config4'] = workflow_ng_configure($configurations['config4'], $author_equals_not_user, $action_notify); return $configurations; } /* * A simple php-condition which compares the node type with the configured one */ function workflow_ng_condition_content_type($settings, $node) { return ($node->type == $settings['type']); } /* * Returns the configuration form function for this condition * TODO: implement */ function workflow_ng_condition_content_type_form() { return array(); } /* * A simple php-condition which compares a node property of two nodes */ function workflow_ng_condition_node_comparison($settings, $node1, $node2) { return ($node1->{$settings['property']} == $node2->{$settings['property']}); } /* * Returns the configuration form function for this condition * TODO: implement */ function workflow_ng_condition_node_comparison_form() { return array(); } /* * A simple user comparison */ function workflow_ng_condition_user_comparison($user1, $user2) { return $user1->uid == $user2->uid; } /* * A simple action without any settings */ function workflow_ng_action_msg_title($node) { return drupal_set_message("Action: The title of the node is ".check_plain($node->title)); } /* * A simple action without any settings, but two arguments */ function workflow_ng_action_msg_title_and_author($node, $author) { return drupal_set_message("Action: The title of the node authored by ".check_plain($author->name)." is ".check_plain($node->title)); } /* * Modifies a node as configured */ function workflow_ng_action_node_set_author($node, $author) { $node->uid = $author->uid; $node->name = $author->name; return array('node' => $node); } /* * Dummy action: mail a node to the user */ function workflow_ng_action_mail_node($node, $user) { //actually this action doesn't send any mail.., that's why it is called dummy.. ;) drupal_set_message(t("Informed %name about your changes.", array('%name' => $user->name))); } workflow-ng/states.module0000644000175000017500000003246010627352065014471 0ustar fagofago array( * 'name' => 'human_readable_name', * 'states' => array('available state1', 'available state2', ..), * 'entity' => 'node', //or users * 'attribute_name' => 'sample', //optional, name of the generated attribute * 'types' => array('page'), //optional, only for nodes * 'roles' => array(2), //optional, only for users * 'init_state' => 'value'. //optional, initial state (if nothing else is set) * ), * ... * ); */ /* * Returns all defined state machines * @param $op Set it to 'names' to get a list of state machine names, * to 'node' to get node machines grouped by type, * to 'user' to get a full definition list of machines for users, * otherwise you'll get the full definitions * @param $key If set, only return the value for this key. E.g. $op 'names', with key 'example' * will return the name of the machine example * @param $reset Whether the cache shall be resetted, before the machine defitions are fetched */ function states_get_machines($op = 'all', $key = NULL, $reset = FALSE) { static $machines; if (!isset($machines) || $reset) { $machines = array('node' => array(), 'user' => array(), 'names' => array(), 'all' => array()); if (($returned = variable_get('state_machines', -1)) == -1) { $returned = _states_build_machines_cache(); } foreach ($returned as $name => $info) { $machines['names'][$name] = $info['name']; $machines['all'][$name] = $info; if ($info['entity'] == 'node') { $types = $info['types'] ? $info['types'] : array_keys(node_get_types('names')); foreach ($types as $type) { $machines['node'][$type][$name] = $info; } } else if ($info['entity'] == 'user') { $machines['user'][$name] = $info; } } asort($machines['names']); } if (!in_array($op, array('all', 'node', 'user', 'names'))) { $op = 'all'; } if (!isset($key)) { return $machines[$op]; } else { return $machines[$op][$key]; } } /* * Rebuilds the list of defined state machines */ function _states_build_machines_cache() { if (($old_data = variable_get('state_machines', -1)) != -1) { states_entity_initiate_new_init_states($old_data); } $data = module_invoke_all('states'); variable_set('state_machines', $data); if ($old_data != -1) { states_entity_initiate_new_init_states($data); } return $data; } /* * Clears the machine definition cache */ function states_clear_machine_cache() { _states_build_machines_cache(); states_get_machines('all', NULL, TRUE); } /* * Implementation of hook_nodeapi() */ function states_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) { //load all states for this node if ($op == 'load') { $machines = states_get_machines('node'); if ($machines[$node->type]) { $data = array('states' => array()); $result = db_query("SELECT * FROM {node_state} WHERE vid = %d", $node->vid); while ($row = db_fetch_object($result)) { $data['states'][_states_machine_get_attribute_name($row->machine)] = $row->state; } return $data; } } else if ($op == 'insert') { $machines = states_get_machines('node'); if ($machines[$node->type]) { foreach($machines[$node->type] as $name => $info) { if (isset($info['init_state']) && states_entity_get_machine_state($node, $name) == NULL) { states_machine_set_state($node, $name, $info['init_state']); } else if (($state = states_entity_get_machine_state($node, $name)) != NULL) { states_machine_set_state($node, $name, $state); } } } } //save all defined states in the database else if ($op == 'update' && $node->states) { $machines = states_get_machines('node'); if ($machines[$node->type]) { foreach($machines[$node->type] as $name => $info) { if (($state = states_entity_get_machine_state($node, $name)) != NULL) { states_machine_set_state($node, $name, $state); } } } } else if ($op == 'delete') { db_query("DELETE FROM {node_state} WHERE nid = %d", $node->nid); } else if ($op == 'delete revision') { db_query("DELETE FROM {node_state} WHERE vid = %d", $node->vid); } } //TODO: Write a condition that fires new events for state changes through nodeapi / hook_user /** * Implementation of hook_user(). */ function states_user($op, &$edit, &$user, $category = NULL) { switch ($op) { case 'load': if (_states_user_has_machines($user) && !isset($user->states_loaded)) { $user->states = array(); $result = db_query("SELECT * FROM {users_state} WHERE uid = %d", $user->uid); while ($row = db_fetch_object($result)) { states_entity_set_machine_state($user, $row->machine, $row->state); } $user->states_loaded = TRUE; } break; case 'insert': /* * Roles aren't set correctly yet! * Fix this manually! */ if (is_array($edit['roles'])) { $user->roles = $user->roles + drupal_map_assoc($edit['roles']); } if (_states_user_has_machines($user)) { foreach (states_get_machines('user') as $name => $info) { if (isset($info['init_state']) && (!isset($info['roles']) || count(array_intersect(array_keys($user->roles), $info['roles'])))) { $attribute = _states_machine_get_attribute_name($name); if (!isset($edit['states'][$attribute])) { states_machine_set_state($user, $name, $info['init_state']); } } } } //proceed case 'update': if(_states_user_has_machines($user) && $edit['states']) { $machines = states_get_machines('user'); foreach($machines as $name => $info) { if (!isset($info['roles']) || count(array_intersect(array_keys($user->roles), $info['roles']))) { $attribute = _states_machine_get_attribute_name($name); if (isset($edit['states'][$attribute])) { states_machine_set_state($user, $name, $edit['states'][$attribute]); } } } unset($edit['states']); unset($edit['states_loaded']); } break; case 'delete': db_query("DELETE FROM {users_state} WHERE uid = %d", $user->uid); break; } } /* * Sets the state of the given machine for this entity to the given new state * This function will generate a new event for the state change and return * the modified entity object, if the operation was successfull, otherwise NULL * will be returned * * @param $entity Either a node or a user object * @param $machine_name The machine readable state machine name * @param $state The value to set for the state machine */ function states_machine_set_state(&$entity, $machine_name, $new_state) { if (entity_is_node($entity)) { $machines = states_get_machines('node'); if ($machines[$entity->type][$machine_name] && _states_is_valid_state($new_state, $machines[$entity->type][$machine_name])) { db_query("UPDATE {node_state} SET state = '%s' WHERE vid = %d AND machine = '%s'", $new_state, $entity->vid, $machine_name); // If we affected 0 rows, this is the first time saving this machine's state if (!db_affected_rows()) { db_query("INSERT INTO {node_state} (nid, vid, machine, state) VALUES(%d, %d, '%s', '%s')", $entity->nid, $entity->vid, $machine_name, $new_state); } } else { db_query("DELETE FROM {node_state} WHERE vid = %d AND machine = '%s'", $entity->vid, $machine_name); $new_state = NULL; } states_entity_set_machine_state($entity, $machine_name, $new_state); //generate event ! return $entity; } else if (entity_is_user($entity)) { $machines = states_get_machines('user'); if ($machines[$machine_name] && _states_is_valid_state($new_state, $machines[$machine_name])) { db_query("UPDATE {users_state} SET state = '%s' WHERE uid = %d AND machine = '%s'", $new_state, $entity->uid, $machine_name); // If we affected 0 rows, this is the first time saving this machine's state if (!db_affected_rows()) { db_query("INSERT INTO {users_state} (uid, machine, state) VALUES(%d, '%s', '%s')", $entity->uid, $machine_name, $new_state); } } else { db_query("DELETE FROM {users_state} WHERE uid = %d AND machine = '%s'", $entity->uid, $machine_name); $new_state = NULL; } states_entity_set_machine_state($entity, $machine_name, $new_state); //generate event ! return $entity; } } /* * Determines if the given state is valid */ function _states_is_valid_state($state, $machine_info) { return $machine_info['states'] == '*' || in_array($state, $machine_info['states']); } /* * Determines if the user has a state machine associated */ function _states_user_has_machines($user) { $machines = states_get_machines('user'); $result = FALSE; foreach($machines as $name => $info) { $result = isset($info['roles']) ? array_intersect(array_keys($user->roles), $info['roles']) : TRUE; if ($result) { break; } } return $result ? TRUE : FALSE; } /* * Determines the attribute name used for storing the machine state in the entity */ function _states_machine_get_attribute_name($machine_name) { $machines = states_get_machines(); return isset($machines[$machine_name]['attribute_name']) ? $machines[$machine_name]['attribute_name'] : $machine_name; } /* * Sets the state of the machine $machine_name to the state $new_state in the entity object * The new state isn't checked for validity, it will be just set in the entity object! */ function states_entity_set_machine_state(&$entity, $machine_name, $new_state) { $entity->states[_states_machine_get_attribute_name($machine_name)] = $new_state; } /* * Returns the state of $machine_name for the given entity */ function states_entity_get_machine_state(&$entity, $machine_name) { if (!isset($entity->states)) { //load the machine states if (entity_is_node($entity)) { if ($extra = states_nodeapi($entity, 'load')) { foreach ($extra as $key => $value) { $entity->$key = $value; } } } else if (entity_is_user($entity)) { states_user('load', $entity, $entity); } } return $entity->states[_states_machine_get_attribute_name($machine_name)]; } /* * Cares for proper initiation of init_state machine values * * Note: This function gets could two times, the first time * with the old data, the second time with the new data */ function states_entity_initiate_new_init_states($data) { static $old_data; if (!isset($old_data)) { $old_data = $data; return; } //we have all data now - search for new init_states $machines = array(); foreach ($data as $name => $info) { if (isset($info['init_state']) && !isset($old_data[$name]['init_state'])) { //a new init_state has been found $machines[$name] = $info; } } $new = $machines + variable_get('states_init_state_machines', array()); if (!empty($new)) { variable_set('states_init_state_machines', $new); states_entity_initiate_init_states(5000); } } /* * Initiates the init_states for all entities without state... */ function states_entity_initiate_init_states($limit = 50000) { if(($machines = variable_get('states_init_state_machines', -1)) == -1) { return; } foreach ($machines as $name => $info) { $result = db_query_range(_states_entity_initiate_get_sql($info), $name, 0, $limit); while ($entity = db_fetch_object($result)) { states_machine_set_state($entity, $name, $info['init_state']); $limit--; } unset($machines[$name]); if (!empty($machines)) { variable_set('states_init_state_machines', $machines); } else { variable_del('states_init_state_machines'); } } } /* * Gets the sql for a machine info, that retrieves all entities that have to be initiated */ function _states_entity_initiate_get_sql($info) { if ($info['entity'] == 'user') { $sql = "SELECT DISTINCT u.* FROM {users} u ". "LEFT JOIN {users_state} us ON us.uid = u.uid AND us.machine = '%s' "; if (!empty($info['roles']) && !in_array(DRUPAL_AUTHENTICATED_RID, $info['roles'])) { $roles = array_map('intval', array_filter($info['roles'])); $sql .= "INNER JOIN {users_roles} ur ON ur.uid = u.uid AND ur.rid IN (". implode(", ", $roles) .") "; } $sql .= "WHERE us.uid IS NULL"; } else if ($info['entity'] == 'node') { $sql = "SELECT n.* FROM {node} n ". "LEFT JOIN {node_state} ns ON ns.vid = n.vid AND ns.machine = '%s' ". "WHERE ns.vid IS NULL"; if (!empty($info['types'])) { $types = array_map('db_escape_string', $info['types']); $sql .= " AND n.type IN ('". implode("','", $types) ."')"; } } return $sql; } /* * Implementation of hook_cron() */ function states_cron() { //initate init states, if there are some uninitated entities states_entity_initiate_init_states(); }workflow-ng/workflow_ng.install0000644000175000017500000000166610627352065015711 0ustar fagofago 'node_state', 'join' => array( 'left' => array( 'table' => 'node', 'field' => 'vid' ), 'right' => array( 'field' => 'vid' ), ), 'fields' => array( 'machine' => array( 'name' => t('States: State machine name'), 'handler' => array( '' => t('machine readable name'), 'states_views_field_machine' => t('human readable name'), ), 'sortable' => true, 'help' => t('This field displays the state machine\'s name.'), ), 'state' => array( 'name' => t('States: State machine state'), 'sortable' => true, 'help' => t('This field displays the state machine\'s state.'), ), ), 'sorts' => array( 'machine' => array( 'name' => t('States: State machine name'), 'sortable' => true, 'help' => t('Sort after the state machine\'s name.'), ), 'state' => array( 'name' => t('States: State machine state'), 'sortable' => true, 'help' => t('Sort after the state machine\'s state.'), ), ), 'filters' => array( 'machine' => array( 'name' => t('States: State machine'), 'operator' => 'views_handler_operator_or', 'value' => array( '#title' => t('Machine'), '#type' => 'select', '#options' => 'states_views_machines_list', '#multiple' => TRUE, ), 'value-type' => 'array', 'help' => t('This allows you to filter by a machine name.'), ), ), ); foreach (states_get_machines('all') as $name => $info) { $tables['node_state_'. $name] = array( 'name' => 'node_state', 'join' => array( 'left' => array( 'table' => 'node', 'field' => 'vid' ), 'right' => array( 'field' => 'vid' ), 'extra' => array('machine' => $name), ), 'filters' => array( 'state' => array( 'field' => 'state', 'name' => t('States: @name', array('@name' => $info['name'])), 'operator' => 'views_handler_operator_or', 'help' => t('This allows you to filter by a machine state.'), ), ), ); if (is_array($info['states'])) { $tables['node_state_'. $name]['filters']['state'] += array( 'value' => array( '#title' => t('State'), '#type' => 'select', '#options' => drupal_map_assoc($info['states']), '#multiple' => TRUE, ), 'value-type' => 'array', ); } else { $tables['node_state_'. $name]['filters']['state']['value'] = 'string'; } } return $tables; } function states_views_machines_list() { return states_get_machines('names'); } function states_views_field_machine($fieldinfo, $fielddata, $value, $data) { $machines = states_get_machines('names'); return $machines[$value]; } workflow-ng/workflow_ng_entity.inc0000644000175000017500000000051310627352065016376 0ustar fagofagotype) && is_numeric($entity->vid) && is_numeric($entity->nid); } function entity_is_user($entity) { return !entity_is_node($entity) && is_numeric($entity->uid) && is_string($entity->name); }workflow-ng/states.install0000644000175000017500000000301410627352065014643 0ustar fagofago= 0), machine varchar(63) NOT NULL, state varchar(127), PRIMARY KEY(vid, machine) )"); db_query("CREATE INDEX {node_state}_nid_idx ON {node_state} (nid)"); db_query("CREATE TABLE {users_state} ( uid int_unsigned NOT NULL default '0', machine varchar(63) NOT NULL, state varchar(127), PRIMARY KEY (uid, machine) )"); break; default: break; } } /* * Implementation of hook_uninstall() */ function states_uninstall() { db_query("DROP TABLE {node_state}"); db_query("DROP TABLE {users_state}"); variable_del('state_machines'); } workflow-ng/workflow_ng_events.inc0000644000175000017500000001215110627352065016367 0ustar fagofago array( '#label' => t('A new node has been created.'), '#arguments' => workflow_ng_events_node_arguments(t('The created node'), t('The node\'s author')), ), 'node_update' => array( '#label' => t('A node has been edited.'), '#arguments' => workflow_ng_events_node_arguments(t('The updated node'), t('The node\'s author')) + array( 'node_unchanged' => array('#entity' => 'node', '#label' => t('The unchanged node'), '#handler' => 'workflow_ng_events_argument_node_unchanged'), 'author_unchanged' => array('#entity' => 'user', '#label' => t('The unchanged node\'s author'), '#handler' => 'workflow_ng_events_argument_unchanged_node_author'), ), ), 'node_view' => array( '#label' => t('A node has been viewed by a logged in user.'), '#arguments' => workflow_ng_events_node_arguments(t('The viewed node'), t('The node\'s author')), ), 'user_insert' => array( '#label' => t('A new user has registered.'), '#arguments' => workflow_ng_events_hook_user_arguments(t('The registered user')), ), 'user_update' => array( '#label' => t('A user has updated his account details.'), '#arguments' => workflow_ng_events_hook_user_arguments(t('The updated user')) + array('account_unchanged' => array('entity' => 'user', 'name' => t('The unchanged user'))), ), 'user_view' => array( '#label' => t('A user page has been viewed by a logged in user.'), '#arguments' => workflow_ng_events_hook_user_arguments(t('The viewed user')), ), 'user_login' => array( '#label' => t('A user has logged in.'), '#arguments' => array('account' => array('#entity' => 'user', '#label' => t('The logged in user'))), ), 'user_logout' => array( '#label' => t('A user has logged out.'), '#arguments' => array('account' => array('#entity' => 'user', '#label' => t('The logged out user'))), ), 'comment_add' => array( '#label' => t('A new comment has been created.'), ), 'comment_edit' => array( '#label' => t('A comment has been edited.'), ), 'init' => array( '#label' => t('A user is going to view a page.'), ), ); return $events; } /* * Returns some arguments suitable for using it with a node */ function workflow_ng_events_node_arguments($node_label, $author_label) { return array( 'node' => array('#entity' => 'node', '#label' => $node_label), 'author' => array('#entity' => 'user', '#label' => $author_label, '#handler' => 'workflow_ng_events_argument_node_author'), 'user' => array('#entity' => 'user', '#label' => t('The acting user'), '#handler' => 'workflow_ng_events_argument_global_user'), ); } /* * Returns some arguments suitable for hook_user */ function workflow_ng_events_hook_user_arguments($account_label) { return array( 'account' => array('#entity' => 'user', '#label' => $account_label), 'user' => array('#entity' => 'user', '#label' => t('The acting user'), '#handler' => 'workflow_ng_events_argument_global_user'), ); } /* * Implementation of hook_nodeapi */ function workflow_ng_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) { global $user; //we don't support anonymous views, so we avoid problems with the page cache if ($op == 'view' && !$user->uid) { return; } if (in_array($op, array('view', 'insert', 'update'))) { workflow_ng_invoke_event('node_'. $op, $node); } } /* * Implementation of hook_user */ function workflow_ng_user($op, $edit, $account, $category = NULL) { global $user; static $account_unchanged; //we don't support anonymous views, so we avoid problems with the page cache, //as well as updates for other categories than 'account' if (($op == 'view' && !$user->uid) || ($op == 'update' && $category != 'account')) { return; } else if ($op == 'update') { //save the unchanged account for the use with op after_update $account_unchanged = $account; } else if ($op == 'after_update') { workflow_ng_invoke_event('user_update', $account, $edit, $account_unchanged); } else if (in_array($op, array('insert', 'login', 'view'))) { workflow_ng_invoke_event('user_'. $op, $account, $edit); } } /* * Argument handlers */ /* * Gets the author's account of a node */ function workflow_ng_events_argument_node_author($node) { global $user; return $user->uid != $node->uid ? user_load(array('uid' => $node->uid)) : drupal_clone($user); } /* * Gets the user account of the "acting" user - which is always the global user */ function workflow_ng_events_argument_global_user() { global $user; return $user; } /* * Gets the node object, that doesn't contain the modified changes */ function workflow_ng_events_argument_node_unchanged($node) { return node_load($node->nid); } /* * Gets the author of the unchanged node object */ function workflow_ng_events_argument_unchanged_node_author($node) { return workflow_ng_events_argument_node_author(workflow_ng_events_argument_node_unchanged($node)); } ?>workflow-ng/README.txt0000644000175000017500000001142510627354025013451 0ustar fagofago$Id: README.txt,v 1.4 2007/05/30 20:01:06 fago Exp $ Workflow-ng Module ------------------------ by Wolfgang Ziegler, nuppla@zites.net This README is an short introduction for interested developers. This module is not ready for use for end users, as it has no UI yet. Stay tuned if you are interested in the UI... Description ----------- The workflow-ng module collects the events provided by modules. It also allows the creation of configurable conditions and actions, which may be configured to be processed on events. Events ------ An event definition has to contain the machine readable and the human readable event name, as well as a list of available arguments. The module has to invoke the event when it occurs and pass the available arguments to workflow-ng. But it can define further arguments, that will be generated at runtime with the help of a provided handler. Currently workflow-ng does already implement some events itself, which cover only basic events of drupal core. Actions -------- Each module can implement actions, which are working for specified arguments, e.g. for a node. These actions can be configured to be used, when an event occurs. Workflow-ng will pass the arguments to the action as configured. For some known entities, currently nodes and users, an action may return modified arguments, which will be saved automatically after execution of all actions. This makes sure, that each argument is only saved one time even if multiple actions modify it. Actions work with a specified list of arguments, e.g. a "Send a mail to a user" action might work with the single argument of one user object. Then an action can be configured everywhere, where at least one user object is available. This allows reusing actions, as often as possible. E.g. the "Send a mail to a user" action might be used to thank node authors for their contribution or also to notify users, who's profile has been viewed. Conditions ---------- A module may also provide conditions, which have to return a logical value (TRUE | FALSE). These conditions can also be configured to be used, when an event occurs. So this allows one to conditionally fire actions. Like actions, conditions work also with a specified list of arguments, and can be used everywhere, where the appropriate arguments are available. Conditions can be interconnected with some logical operations, currently available are OR, AND, or even a NOR, which can also be used as NOT. Configurations --------------- Each module may configure conditions and actions for a certain event. These configurations will be handled by workflow-ng automatically, which means that workflow-ng evaluates the configured conditions, if any, and only fires the actions if the conditions evaluate to TRUE. Modules may specifiy some properties for configurations, so it will be possible for a module to provide some default configurations, that can be customized by the UI module, but also to provide some fixed configurations that can't be changed. Unlike the existing workflow and actions module, workflow-ng obeys the ordering of configured elements (conditions or actions). So actions will be fired in the order they are configured. Furthermore, ordering is important when working with conditions. E.g. an action "on top", will be fired for sure, while an action behind some conditions, will be only fired if the conditions evaluate to TRUE. The order of elements may by changed by using the #weight property, like it is known from the forms API. If you don't specify any weight, actions will default to a weight of 1000, so that they are fired only if the conditions are met. Workflow-ng makes use of caching mechanism, so that the configurations can be loaded and processed quickly when an event occurs. If you are interested, check out workflow_ng_tests.inc, for some simple test configurations, or have a look at workflow_ng_events.inc, for some basic event definitions for drupal core (uncompleted). States ------- The states module is a simple API module, that provides flexible state machines for other modules. It works on the head of users and nodes. In conjunction with some conditions and actions, it will be possible to build enhanced workflows. The states module is fully working and can be considered as finished, however the useful workflow-ng integration (conditions, actions and an event for a state change) is still missing as well as a bunch of documentation. TODO/ROADMAP ------------- * Try achieving compatibility with drupal 6 actions * Write a lot of conditions, actions, events.. for drupal core and states. * Write flexible, token module enabled conditions and actions. * Write docs, docs and more docs. * Write a simple UI, that allows the configuration of conditions and actions. * Write a states CCK field with configurable state changes and state changes permissions.