This tutorial discusses the following Ariadne features:

  • Defining your own workflow
  • Using workflow in your site
  • Changing default wizards

Ariadne has support for custom workflow built in, but its not visible to a normal user, unless the designer/programmer has enabled it. This tutorial will show you how to enable workflow, build a simple workflow mechanism, include the workflow information in the normal editing process and finally it will show some other useful tricks you can do with the workflow templates in Ariadne.

Workflow basics

Ariadne has a predefined property state and two templates, user.workflow.pre.html and user.workflow.post.html to help define a workflow.

Every time an object is saved, Ariadne first calls the user.workflow.pre.html template, then saves the object and finally calls the user.workflow.post.html template. I will show a simple moderation workflow here, but first some important information about working with Ariadne’s workflow:

Any change you make in the current object in user.workflow.pre.html is automatically saved. If you return a value, it must be an array of properties to be saved.

Any change made in user.workflow.post.html is ignored, unless you return an array of properties. The array may be empty, but if it doesn’t return something, the changes made will not be saved.

You must never call save() directly in the user.workflow.pre.html or user.workflow.post.html template. This will create an instant infinite loop, since save() calls both these templates.

You may call save() on other objects, but be aware of the dangers. It is easy to forget that the same user.workflow templates may apply to the other objects you want to save.

So how do you use this to create a workflow? Well first, you will need to build a simple state machine. Each time you save (change) an object, we must check and possibly change the state of the object. We’ll simply keep the state of the object in a variable in the object itself, $data->workflow. Because of later additions, we’ll actually use $data->workflow[‘state’], but the idea is the same.

Now lets define a simple publish-approve workflow. When a user creates a new object, the object is first saved as a draft. The user saves the object and returns later to change something, untill everything seems ok, and then publishes the object. If the user has insufficient grants to publish the object, someone else, a supervisor, editor, etc. may inspect the object and decide that it indeed is ready to be published, or perhaps that it is rubbish and must be removed.

This can be expressed by actions and states. The states are ‘draft’, ‘forapproval’, ‘published’ and ‘deleted’. The actions are ‘save’, ‘publish’, ‘retract’, ‘delete’ and ‘restore’.

Notice that we defined a specific state for ‘deleted’, since we may want to restore objects which have been deleted in error. In addition it is not a good idea to delete an object in the workflow templates, since Ariadne doesn’t expect objects to disappear when saving them.

Enough talk, now for some code. This is the state machine, which we cunningly put in a user.workflow.pre.html template.

ppage::user.workflow.pre.html

<pinp>
  $properties = Array();
  $action = getvar('workflow');
  switch ($data->workflow['state']) {
    case 'draft':
    default:
      switch ($action) {
        case 'publish':
          if (checkgrant('publish')) {
            $data->workflow["state"] = "published";
            $properties["state"][0]["value"]="published";
          } else {
            $data->workflow["state"] = "forapproval";
            $properties["state"][0]["value"]="forapproval";
          }
        break;
        case 'delete':
          $data->workflow["state"] = "deleted";
          $properties["state"][0]["value"]="deleted";
        break;
        default:
          // set initial state property for a new form
          $data->workflow["state"] = "draft";
          $properties["state"][0]["value"]="draft";
      }
    break;

   case 'deleted':
      switch ($action) {
        case 'restore':
          $data->workflow["state"] = "draft";
          $properties["state"][0]["value"]="draft";
        break;
        default:
      }
    break;

   case 'forapproval':
      switch ($action) {
        case 'publish':
          if (checkgrant('publish')) {
            $data->workflow["state"] = "published";
            $properties["state"][0]["value"]="published";
          }
        break;
        case 'unpublish':
            $data->workflow["state"] = "draft";
            $properties["state"][0]["value"]="draft";
        break;
        case 'delete':
            $data->workflow["state"] = "deleted";
            $properties["state"][0]["value"]="deleted";
        break;
        default:
      }
    break;

    case 'published':
      switch ($action) {
        case 'unpublish':
            $data->workflow["state"] = "draft";
            $properties["state"][0]["value"]="draft";
        break;
        case 'delete':
            $data->workflow["state"] = "deleted";
            $properties["state"][0]["value"]="deleted";
        break;
        default:
      }
    break;
  }

putvar("arResult", $properties);
</pinp>

This looks impressive, or at least long, but it is actually pretty simple. The entire template consists of two nested switch statements. The first checks to see what the current state is, and depending on that it ‘knows’ which actions may be taken. These actions are in the second (nested) switch statement. For each action allowed for in each state, we can then ‘do’ the right thing. In this case that is very boring, we just update the state variable, and put the new state in a property. The property used here is actually a default Ariadne property intended for just this purpose.

The only slightly interesting thing is the checkgrant('publish') check when publishing a draft object. There is no grant ‘publish’ in Ariadne, or is there? In fact Ariadne doesn’t really care what grants you use, any string is fine. In this case all you need to do to ‘have’ the publish grant, is to enter it in the Ariadne grants dialog by hand, after selecting your user of course. If you logon with the admin user, you don’t need to do even that, it has all grants by default.

Using the workflow

By itself the workflow template doesn’t do anything usefull. You will need to use the information in your website templates. By default all objects, no matter what their state will act exactly the same. So, lets update your website templates.

Say your website template looks like this (I hope not, but hey, at least the code is short).

ppage::view.html

<html>
<head>
<title>Workflow Demo</title>
</head>
<body>
<h1><pinp> echo $nlsdata->name; </pinp></h1>
<p><pinp> echo $nlsdata->summary; </pinp></p>
<ul><pinp>
 ls('show.html');
</pinp></ul>
<pinp>
ShowPage();
</pinp>
</body>
</html>

Let’s add some checks so that only published objects are listed:

ppage::view.html

<html>
<head>
<title>Workflow Demo</title>
</head>
<body>
<h1><pinp> echo $nlsdata->name; </pinp></h1>
<p><pinp> echo $nlsdata->summary; </pinp></p>
<pinp>
  $query="state.value='published' and object.parent='$path'";
  if (count_find($query)) {
   echo "<ul>";
   find($query, 'show.html');
   echo "</ul>";
  }
 ShowPage();
</pinp>
</body>
</html>

What we did is to change the ls() function to a find(). The query here is very simple. The state.value='published' part is enough to make sure only published objects are shown, but find() searches not only through the immediate children of the current object, as ls() does, but in all children. So to limit the results to only immediate children, I’ve added the and object.parent='$path' part. This makes it work almost exactly like ls().

Now this is nice and all, and it will actually work, but there’s one problem. There’s no way to see if any objects need to be approved, in fact there is no way to publish an object at all. So first things first. We need a way to tell if you want your object published. For that I’m going to extend the normal wizards for all objects derived from ppage so that you can see the state of the object, and change it.

Changing state

First thing is to add an extra screen or step to all these wizards. I do that by creating a relatively simple template, which displays the state and lists the possible actions you can take from there.

ppage::user.wizard.workflow.html

<fieldset id="data">
<legend>Workflow</legend><img src="<pinp>
echo getSetting('dir:www'); </pinp>images/dot.gif" alt="">
<pinp>
  switch ($data->workflow['state']) {
    case 'deleted' :
      $state='Removed (archived)';
      $options['restore']="Restore from archive";
    break;
    case 'published' :
      $state='Active';
      $options['delete']="Remove (to archive)";
      $options['unpublish']="Retract (to draft)";
    break;
    case 'forapproval' :
      $state='Waiting for approval';
      $options['delete']="Remove (to archive)";
      $options['unpublish']="Retract (to draft)";
      $options['publish']="Publish";
    break;
    case 'draft' :
    default:
      $state='Inactive (draft)';
      $options['delete']="Remove (to archive)";
      $options['publish']="Publish";
  }
</pinp>
  <label for="workflow">State: <pinp> echo $state; </pinp></label>
  <select name="workflow" id="workflow">
    <option value="">No change</option>
    <pinp>
      foreach ($options as $option => $text) {
        echo "<option value='$option'>$text</option>";
      }
    </pinp>
  </select>
</fieldset>

Next, I’ll add this template to the list of screens for the wizards. There are two templates for this, one for the wizard which creates a new object from scratch, user.wizard.new.html, and one for the wizard which edits an existing object, user.wizard.edit.html:

ppage::user.wizard.edit.html

<pinp>
  $flow = getvar('wgWizFlow');


  $flow[] = Array(
    'title'  => 'Workflow',
    'image'  => getSetting('www').'images/wizard/workflow.png',
    'template' => 'user.wizard.workflow.html'
  );

  return $flow;
</pinp>

ppage::user.wizard.new.html

<pinp>
  return call('user.wizard.edit.html', getvar('arCallArgs'));
</pinp>

You will see an extra step called ‘Workflow’ in the wizards for each object derived from ppage. When creating a new object, you may have to wait till step two before it actually shows up, this is because of the way new objects are created in Ariadne. In a later version this may be fixed, but it shouldn’t be a problem.

Each time a wizard starts it will search for one of the above templates, and call it with one argument, wgWizFlow, which contains the list of steps or screens in the wizard. You can change this list by removing screens, adding or re-ordering them. The result returned from this template will be used as the actual list of screens for the wizard

When changing any wizard like this, the safe option is always to append the new screen at the end of the flow. Do not insert it as the first item, since $flow[0] has special meaning to the wizard, it must never contain a wizard screen as above.

The getSetting() method may be unfamiliar to you. Its a relatively new addition, which allows you to get to some Ariadne settings, like the location of the Ariadne www directory relative to the document root, or web root. It is in fact currently the only thing you can get back. It’s used here because Ariadne already comes with a nice workflow icon for the wizard, so why not use it.

Finishing up

So, you can create draft objects, change the state and see them show up in your website, what’s left? Well, its currently a bit difficult to see which objects are listed as ‘for approval’. It would be nice if you could logon to your site and automatically see which objects are awaiting approval.

Lets go back to the view template, and add some logic to do just that.

ppage::view.html

<html>
<head>
<title>Workflow Demo</title>
</head>
<body>
<h1><pinp> echo $nlsdata->name; </pinp></h1>
<p><pinp> echo $nlsdata->summary; </pinp></p>
<pinp>
 $query="state.value='published' and object.parent='$path'";
  if (count_find($query)) {
   echo "<ul>";
   find($query, 'show.html');
   echo "</ul>";
  }
 ShowPage();
  if (checkgrant('publish')) {
   $query="state.value='forapproval' and object.parent='$path'";
   if (count_find($query)) {
   echo "<h2>For approval:</h2>";
    echo "<ul>";
    find($query, 'show.html');
    echo "</ul>";
   }
 }
if (checkgrant('edit')) {
   $user=getuser();
   $query="state.value='draft' and owner.value='".
$user->data->login."' and object.parent='$path'";
   if (count_find($query)) {
    echo "<h2>Draft</h2>";
    echo "<ul>";
    find($query, 'show.html');
    echo "</ul>";
   }
  }
</pinp>
</body>
</html>

ppage::show.html

<pinp>
 if (($data->workflow['state']=='published') || checkgrant('edit')) {
</pinp>
<li><a href="<pinp> echo make_url(); </pinp>"><pinp>
echo $nlsdata->name; </pinp></a></li>
<pinp>
  }
</pinp>

This is one way of doing that. Below all the normal web content, if you are logged on and have sufficient grants, you get a list of unpublished content. If you have the publish grant, you’ll see a list of childrent which are waiting for approval. If you have the edit grant, you’ll see a list of your own objects which aren’t published yet.

It’s easy to extend this by creating a special view template, just for editors, which lists all objects waiting for approval in the entire site, or listing all your draft objects in the entire site. I’ll leave that as an exercise for the reader…