UPDATE 9 July 2015:

If you’re looking for a comprehension workflow solution to automate business processes on WordPress check out my new plugin Gravity Flow

Note: This post is written for developers with some knowledge of Gravity Forms.

This is the second in a series of tutorials helping developers to extend Gravity Forms.

In the first tutorial, I gave an introduction to the Gravity Forms developer platform and demonstrated how each of the tools could be used to build a simple add-on. In this second tutorial, I’m going to be delving a little deeper by using the Gravity Forms Feed Add-On Framework to demonstrate how Gravity Forms and WordPress can be extended further to build a fairly sophisticated workflow application.

There’s no shortage of use cases for a form-based approval workflow. Let’s take a look at a few to get some context for our solution:

A manager in the Human Resources department wants to create a collection forms including vacation requests for employees to complete company-wide. These forms must be approved or rejected by the employee’s department director. Pending approvals must be displayed in a personalised list for each Director. The data from approved forms will be sent to a Google Spreadsheet.

The CFO has asked you to implement a system to handle the approval of all the incoming invoices arriving each day. The receptionist is currently making a note of each bill and then sends them over to the appropriate department head for approval. Invoices over $9,999 must also have the approval of the Finance Director before the data is sent to OpenERP.

WordPress account requests before they are created and before the activation emails are sent to the users. Both Mum and Dad must approve the new accounts. A new Mum has set up a membership site documenting the first few years of her new-born for her family and friends by following a great tutorial explaining how to set up WordPress as a membership site. She wants users to activate their own accounts but she wants to approve thebefore they are created and before the activation emails are sent to the users. Both Mum and Dad must approve the new accounts.

Requirements

Flexible configuration by non-technical users; no hard-coded workflows.

Each Form will have different rules for approval and different users as approvers.

The rules for approval will depend on conditional logic for field values (e.g. invoice amount, approval status).

Notifications must be sent in response to approval and rejection and must be flexible enough to send multiple notifications each with a different conditional logic.

Notifications to approvers must contain a deep link to the form with immediate access to the approve and reject buttons.

When approval is required for a form, delay actions performed by the User Registration Add-On and the Zapier Add-On until the form has been approved by all approvers.

Pending approvals for each approver must appear in a personalised list on the WordPress Dashboard.

Approved or rejected forms cannot be edited.

Approvers can’t change their approval status once committed.

We’ll need to extend Gravity Forms in the following steps:

Create an Add-On that uses the Feed Add-On Framework. One feed will be created for each approver and will support conditional logic. Add custom Entry Meta fields to store the approval status for each approver. Add a Feed Settings tab to the Form Settings UI that will list approvers and allow admins to add new approvers to a form. Add a box on the Entry detail page to show the current approval status and approval buttons to approvers. Add a WordPress Dashboard widget and display a list of pending approvals for the currently logged in user. Integrate with the User Registration Add-On and the Zapier Add-On so that form approval triggers WordPress user account creation and the sending of data to third party services e.g. Google Sheets, Freshbooks, OpenERP… Add a notification event ‘form is approved or rejected’ to the Notifications UI.

Step 1: Extending the Feed Add-On Framework

A feed is a user-defined action that gets processed conditionally after a form has been submitted. Feeds are used in a number of Gravity Forms Add-Ons including the Paypal Add-Ons, Coupons Add-On, User Registration Add-On, MailChimp Add-On, Zapier Add-On and the AWeber Add-On to allow multiple actions to be triggered after a form has been submitted. In this case, we’re going to use feeds to assign approvers to each form. Feeds support conditional logic so approvers can be assigned to entries based on the value of a field. For example, in the case of incoming invoices our admins can configure an approval feed for the Finance Director when the invoice amount exceeds $9,999.

<?php /* Plugin Name: Gravity Forms Approvals Plugin URI: http://www.stevenhenty.com Description: Aggregate entries from multiple sites into a single installation Version: 0.1 Author: Steve Henty Author URI: http://www.stevenhenty.com License: GPL-2.0+ ------------------------------------------------------------------------ Copyright 2014 Steven Henty */ // Make sure Gravity Forms is active and already loaded. if (class_exists("GFForms")) { // The Add-On Framework is not loaded by default. // Use the following function to load the appropriate files. GFForms::include_feed_addon_framework(); class GFApprovals extends GFFeedAddOn { // The following class variables are used by the Framework. // They are defined in GFAddOn and should be overridden. // The version number is used for example during add-on upgrades. protected $_version = '0.1'; // The Framework will display an appropriate message on the plugins page if necessary protected $_min_gravityforms_version = '1.8.7'; // A short, lowercase, URL-safe unique identifier for the add-on. // This will be used for storing options, filters, actions, URLs and text-domain localization. protected $_slug = 'gravityformsapprovals'; // Relative path to the plugin from the plugins folder. protected $_path = 'gravityformsapprovals/approvals.php'; // Full path the the plugin. protected $_full_path = __FILE__; // Title of the plugin to be used on the settings page, form settings and plugins page. protected $_title = 'Gravity Forms Approvals'; // Short version of the plugin title to be used on menus and other places where a less verbose string is useful. protected $_short_title = 'Approvals'; } } new GFApprovals();

Step 2: Entry Meta

Entry Meta fields are like form fields except they don’t appear either on the front-end form or the entry editor and they can only be updated via code. This makes them ideal for storing meta data such as approval status. Users will be able to filter the entries by approver and by approval status on the entry list and we’ll be able to use the Gravity Forms API to display the personalised list of pending approvals on the WordPress Dashboard.

To configure the Entry Meta fields we just override GFAddOn::get_entry_meta() and return the configuration for the approval status of each approver and the overall approval status.

<?php /** * Entry meta data is custom data that's stored and retrieved along with the entry object. * For example, entry meta data may contain the results of a calculation made at the time of the entry submission. * * To add entry meta override the get_entry_meta() function and return an associative array with the following keys: * * label * - (string) The label for the entry meta * is_numeric * - (boolean) Used for sorting * is_default_column * - (boolean) Default columns appear in the entry list by default. Otherwise the user has to edit the columns and select the entry meta from the list. * update_entry_meta_callback * - (string | array) The function that should be called when updating this entry meta value * filter * - (array) An array containing the configuration for the filter used on the results pages, the entry list search and export entries page. * The array should contain one element: operators. e.g. 'operators' => array('is', 'isnot', '>', '<') * * * @param array $entry_meta An array of entry meta already registered with the gform_entry_meta filter. * @param int $form_id The Form ID * * @return array The filtered entry meta array. */ function get_entry_meta( $entry_meta, $form_id ) { $feeds = $this->get_feeds( $form_id ); $has_approver = false; foreach ( $feeds as $feed ) { if ( ! $feed['is_active'] ) { continue; } $approver = $feed['meta']['approver']; $user_info = get_user_by( 'login', $approver ); $display_name = $user_info ? $user_info->display_name : $approver; $entry_meta[ 'approval_status_' . $approver ] = array( 'label' => 'Approval Status: ' . $display_name, 'is_numeric' => false, 'is_default_column' => false, // this column will not be displayed by default on the entry list 'filter' => array( 'operators' => array( 'is', 'isnot' ), 'choices' => array( array( 'value' => 'pending', 'text' => 'Pending' ), array( 'value' => 'approved', 'text' => 'Approved' ), array( 'value' => 'rejected', 'text' => 'Rejected' ), ) ) ); $has_approver = true; } if ( $has_approver ) { $entry_meta['approval_status'] = array( 'label' => 'Approval Status', 'is_numeric' => false, 'update_entry_meta_callback' => array( $this, 'update_approval_status' ), 'is_default_column' => true, // this column will be displayed by default on the entry list 'filter' => array( 'operators' => array( 'is', 'isnot' ), 'choices' => array( array( 'value' => 'pending', 'text' => 'Pending' ), array( 'value' => 'approved', 'text' => 'Approved' ), array( 'value' => 'rejected', 'text' => 'Rejected' ), ) ) ); } return $entry_meta; } /** * The target of update_entry_meta_callback. * * @param string $key The entry meta key * @param array $entry The Entry Object * @param array $form The Form Object * * @return string|void */ function update_approval_status( $key, $entry, $form ) { return 'pending'; }

Notice that we register a callback for the overall approval status update event, but not for the individual approval status for each approver. This is because we don’t need to set the entry meta for the approvers when the entry is updated – we only need to set the overall approval status. We’ll assign the approvers conditionally using the the GFFeedAddOn::process_feed() in the next step.

Step 3: Feed Settings

GFFeedAddOn extends the Add-On Framework so we get everything that comes with GFAddOn except in place of the Form Settings UI we get the Feeds UI.

The Feed Settings UI is going to provide a way for admins to assign approvers conditionally to forms. So instead of overriding GFAddOn::form_settings_fields() as we did in the previous tutorial we’ll need to override GFFeedAddOn::feed_settings_fields() and return the array with the configuration of the fields.

We’ll need to override GFFeedAddOn::feed_list_columns() so our fields are added as columns to the feed list page, and GFFeedAddOn::process_feed() to handle the form submission event. The processing of the feed, in our case, just means assigning the approver to the entry. We don’t need to hardcode any conditional logic because this method will only run if the conditions are met – the logic is handled behind the scenes by the framework.

See the Settings API for further details on how to create settings pages.

<?php /** * Override the feed_settings_field() function and return the configuration for the Feed Settings. * Updating is handled by the Framework. * * @return array */ function feed_settings_fields() { $accounts = get_users(); $account_choices = array( array( 'label' => 'None', 'value' => '' ) ); foreach ( $accounts as $account ) { $account_choices[] = array( 'label' => $account->display_name, 'value' => $account->user_login ); } return array( array( 'title' => 'Approver', 'fields' => array( array( 'name' => 'description', 'label' => 'Description', 'type' => 'text', ), array( 'name' => 'approver', 'label' => 'Approver', 'type' => 'select', 'choices' => $account_choices, ), array( 'name' => 'condition', 'tooltip' => "Build the conditional logic that should be applied to this feed before it's allowed to be processed.", 'label' => 'Condition', 'type' => 'feed_condition', 'checkbox_label' => 'Enable Condition for this approver', 'instructions' => 'Require approval from this user if', ), ) ), ); } /** * Adds columns to the list of feeds. * * setting name => label * * @return array */ function feed_list_columns() { return array( 'description' => 'Description', 'approver' => 'Approver', ); } /** * Fires after form submission only if conditions are met. * * @param $feed * @param $entry * @param $form */ function process_feed( $feed, $entry, $form ) { $approver = $feed['meta']['approver']; gform_update_meta( $entry['id'], 'approval_status_' . $approver, 'pending' ); }

This will produce a list of feeds and an edit page that looks like this:

Step 4: WordPress Dashboard Widget

The personalised inbox of pending approvals will be displayed using the WordPress Dashboard API.

For this we’ll need GFAPI::get_entries() to search the entries with a couple of search criteria to ensure we only get the entries pending approval for the current user.

<?php public function init_admin() { parent::init_admin(); add_action( 'wp_dashboard_setup', array( $this, 'dashboard_setup' ) ); } //Registers the dashboard widget public function dashboard_approvals() { wp_add_dashboard_widget( 'gf_approvals_dashboard', 'Forms Pending My Approval', array( $this, 'dashboard' ) ); } /** * Displays the Dashboard UI */ public static function dashboard() { global $current_user; $search_criteria['field_filters'][] = array( 'key' => 'approval_status_' . $current_user->user_login, 'value' => 'pending' ); $search_criteria['field_filters'][] = array( 'key' => 'approval_status', 'value' => 'pending' ); $entries = GFAPI::get_entries( 0, $search_criteria ); if ( sizeof( $entries ) > 0 ) { ?> <table class="widefat" cellspacing="0" style="border:0px;"> <thead> <tr> <td><i>Form</i></td> <td><i>User</i></td> <td><i>Submission Date</i></td> </tr> </thead> <tbody class="list:user user-list"> <?php foreach ( $entries as $entry ) { $form = GFAPI::get_form( $entry['form_id'] ); $user = get_user_by( 'id', (int) $entry['created_by'] ); ?> <tr> <td> <?php $url_all_pending = sprintf( 'admin.php?page=gf_entries&id=%d&s=pending&field_id=approval_status_%s&operator=is', $entry['form_id'], $current_user->user_login ); $url_all_pending = admin_url( $url_all_pending ); echo "<a href='{$url_all_pending}'>{$form['title']}</a>"; ?> </td> <td> <?php $url_all_for_user = sprintf( 'admin.php?page=gf_entries&id=%d&s=%d&field_id=created_by&operator=is', $entry['form_id'], $user->ID ); $url_all_for_user = admin_url( $url_all_for_user ); echo "<a href='{$url_all_for_user}'>{$user->display_name}</a>"; ?> </td> <td> <?php $url_entry = sprintf( 'admin.php?page=gf_entries&view=entry&id=%d&lid=%d', $entry['form_id'], $entry['id'] ); $url_entry = admin_url( $url_entry ); echo "<a href='{$url_entry}'>{$entry['date_created']}</a>"; ?> </td> </tr> <?php } ?> </tbody> </table> <?php } else { ?> <div> Hurray, inbox zero!. </div> <?php } }

Notice the three different links:

All entries pending the approval of the current user

All entries submitted by the requester

Direct link to the entry

Step 5: Add a custom event to the Notifications UI

Notifications in Gravity Forms are user-defined emails that can be triggered conditionally, routed to different email addresses and contain field values. By default, notifications are processed on form submission.

The Notifications UI page contains a filter called gform_notification_events which allows us to add custom events. This provides us with a way to separate notifications sent on form submission from the notifications we’ll need to send when a form is approved or rejected. So for example, in the case of the incoming invoices, we don’t want to notify the Finance Director until the invoice has been approved by the department Director. We’ll still need to write the code to handle the event and send the notifications at the appropriate time but this is a handy way to reuse some existing admin functionality in Gravity Forms.

<?php public function init_admin() { parent::init_admin(); add_filter( 'gform_notification_events', array( $this, 'add_notification_event' ) ); } function add_notification_event( $events ) { $events['form_approval'] = 'Form is approved or rejected'; return $events; }

Step 6: Integration with the User Registration and Zapier Add-Ons

The Zapier Add-On providers users with point-and-click integration of Gravity Forms with literally hundreds of different Web Services, for example Freshbooks, QuickBooks, Trello and Salesforce. In our use cases, we can use the Zapier Add-On to send form data to Google Sheets and OpenERP.

The User Registration Add-On handles the creation and updating of WordPress user accounts. It’s very flexible and allows conditional registration and will map form field to user profile fields and custom fields.

For our solution, we’ll need to make sure that the Zapier and User Registration Add-Ons don’t trigger their actions immediately on form submission so our Approvals Add-on can trigger them once the entry has been approved. The User Registration Add-On has the gform_disable_registration filter we can use but Zapier currently doesn’t have an equivalent hook so we’ll need to get a little creative and make sure its registered action hook is removed before it can be triggered.

<?php public function init_frontend() { parent::init_frontend(); add_filter( 'gform_disable_registration', array( $this, 'disable_registration' ), 10, 4 ); add_action( 'gform_after_submission', array( $this, 'after_submission' ), 9, 2 ); } function disable_registration( $is_disabled, $form, $entry, $fulfilled ) { //check status to decide if registration should be stopped if ( isset( $entry['approval_status'] ) && $entry['approval_status'] == 'approved' ) { //disable registration return false; } else { return true; } } function after_submission( $entry, $form ) { //check submitted values to decide if data should be should be stopped before sending to Zapier if ( isset( $entry['approval_status'] ) && $entry['approval_status'] != 'approved' ) { remove_action( 'gform_after_submission', array( 'GFZapier', 'send_form_data_to_zapier' ) ); } }

Step 7: Entry Detail Approvals Meta Box

Finally, this is the code that brings everything together. It’s the UI that approvers use to approve or reject entries. It also contains the code that triggers the notifications registered with our custom notification event, user account creation, and sending data to Zapier.

<?php public function init_admin() { parent::init_admin(); add_action( 'gform_entry_detail_sidebar_before', array( $this, 'entry_detail_approval_box' ), 10, 2 ); } function entry_detail_approval_box( $form, $entry ) { global $current_user; if ( ! isset( $entry['approval_status'] ) ) { return; } if ( isset( $_POST['gf_approvals_status'] ) && check_admin_referer( 'gf_approvals' ) ) { $new_status = $_POST['gf_approvals_status']; gform_update_meta( $entry['id'], 'approval_status_' . $current_user->user_login, $new_status ); $entry[ 'approval_status_' . $current_user->user_login ] = $new_status; $entry_approved = true; $entry_rejected = false; foreach ( $this->get_feeds( $form['id'] ) as $feed ) { if ( $feed['is_active'] ) { $approver = $feed['meta']['approver']; if ( ! empty( $entry[ 'approval_status_' . $approver ] ) ) { if ( $entry[ 'approval_status_' . $approver ] != 'approved' ) { $entry_approved = false; } if ( $new_status == 'rejected' ) { $entry_rejected = true; } } } } if ( $entry_rejected ) { gform_update_meta( $entry['id'], 'approval_status', 'rejected' ); $entry['approval_status'] = 'rejected'; } elseif ( $entry_approved ) { gform_update_meta( $entry['id'], 'approval_status', 'approved' ); $entry['approval_status'] = 'approved'; // Integration with the User Registration Add-On if ( class_exists( 'GFUser' ) ) { GFUser::gf_create_user( $entry, $form ); } // Integration with the Zapier Add-On if ( class_exists( 'GFZapier' ) ) { GFZapier::send_form_data_to_zapier( $entry, $form ); } } $notifications_to_send = GFCommon::get_notifications_to_send( 'form_approval', $form, $entry ); foreach ( $notifications_to_send as $notification ) { GFCommon::send_notification( $notification, $form, $entry ); } } $status = 'Pending Approval'; $approve_icon = '<i class="fa fa-check" style="color:green"></i>'; $reject_icon = '<i class="fa fa-times" style="color:red"></i>'; if ( $entry['approval_status'] == 'approved' ) { $status = $approve_icon . ' Approved'; } elseif ( $entry['approval_status'] == 'rejected' ) { $status = $reject_icon .' Rejected'; } ?> <div class="postbox"> <h3><?php echo $status ?></h3> <div style="padding:10px;"> <ul> <?php $has_been_approved = false; foreach ( $this->get_feeds( $form['id'] ) as $feed ) { if ( $feed['is_active'] ) { $approver = $feed['meta']['approver']; if ( ! empty( $entry[ 'approval_status_' . $approver ] ) ) { $user_info = get_user_by( 'login', $approver ); $status = $entry[ 'approval_status_' . $approver ]; if ( $status != 'pending' ) { $has_been_approved = true; } echo '<li>' . $user_info->display_name . ': ' . $status . '</li>'; } } } if ( $has_been_approved ) { add_action( 'gform_entrydetail_update_button', array( $this, 'remove_entrydetail_update_button' ), 10 ); } ?> </ul> <div> <?php if ( isset( $entry[ 'approval_status_' . $current_user->user_login ] ) && $entry[ 'approval_status_' . $current_user->user_login ] == 'pending' ) { wp_nonce_field( 'gf_approvals' ); ?> <button name="gf_approvals_status" value="approved" type="submit" class="button"> <?php echo $approve_icon; ?> Approve </button> <button name="gf_approvals_status" value="rejected" type="submit" class="button"> <?php echo $reject_icon; ?> Reject </button> <?php } ?> </div> </div> </div> <?php }

Complete Code Listing

The complete add-on is available in the Github repository for Gravity Forms Approvals.

View the complete listing: approvals.php

Download the latest version of the add-on from the WordPress.org plugins repository: Gravity Forms Approvals Add-On

Configuration

Now we’ve got Gravity Forms to support approvals we need to set up the forms and configure the business logic for each workflow. If you’re already familiar with Gravity Forms then this section is perhaps unnecessary but I’ve included it here for for completeness and for readers who are not so familiar.

Vacation Request

In the use cases described above, the HR dept needs only one approval feed per form and per department. The example vacation request form contains the following fields. Some of these fields can be set automatically by specifying the appropriate merge tags in the default value settings.

So for each form we need to add one approver per department. Each approval feed will look something like this.

Notifications are configured separately on the Notifications tab. In this case we’d need one notification for the approvers – something like this:

Incoming invoices

In the case of the incoming invoices form, we’ll need a form that looks something like this:

The approval feed for each department might like this:

We also need an additional feed for the Finance Director in the case of high value invoices.

We’ll need to configure a notification with conditional routing like the vacation request form and an additional notification for the Finance Director for forms that need her approval. Notice the conditional logic ensuring that the form has not previously been rejected by a colleague.

Next Steps

Although this add-on is very simple, it’s not intended to be used in a production environment and it hasn’t been tested in a multi-site installation. It’s intended to be used primarily as a learning tool. If you do decide to use it in a production environment, please ensure that you do sufficient testing and that security issues are taken into consideration. At minimum use SSL in the WordPress admin.

There are many ways this idea could be developed further or adapted to fit different scenarios. Here are some ideas for next steps:

Integrate the forms with internal systems

LDAP integration

Create the default notifications automatically when each feed is saved

Install the Members plugin or similar and configure appropriate permissions

Add entry meta for approval comments, timestamp and previous status

Add support for approval steps

Add a visual workflow designer

A desktop widget displaying pending approvals

If you decide to implement any of these, please do let me know.

Conclusion

With relatively little code (under 100 logical SLOC) it’s possible to turn Gravity Forms + WordPress into a sophisticated custom workflow application. Granted there are excellent dedicated solutions on the market with advanced features and visual workflow designers, so this approach is not going to be the most suitable for every situation, but if you’re aware of the possibilities it’ll help you make the right choice. Were you aware you could do this with Gravity Forms? I’m sure there are many additional use cases and adaptations for this application. If you come up with any, please let me know either in in a private message or in the comments below.

I’ll be very happy to answer any questions you have about this tutorial in the comments. Please direct all specific questions about your project to the Gravity Forms technical support team.

********

Legal bits:

The views expressed here are my own and do not necessarily reflect those of Rocketgenius.

Gravity Forms is a trademark of Rocketgenius

WordPress is a trademark of The WordPress Foundation



