Skip to content

Commit

Permalink
Workflow: Notifications & improvement to M.sendEmail()
Browse files Browse the repository at this point in the history
Notifications make it easier to send 'notification' type emails during
a workflow.

They're defined using Workflow.notifications(), and automatically sent
when a state matching their name is entered. Or manually with
M.sendNotification().

Devtools lists defined notifications, and has a 'sent test email'
button.

specification for M.sendEmail() is improved to make it easier to define
notifications without using lots of functions.

* view property can be a function.
* strings for recipients can refer to entity names, if they have a
entity style _suffix
* entity names are rewritten to use ref & maybe for efficiency and
reliability
  • Loading branch information
bensummers committed Jan 8, 2018
1 parent 84ab19a commit d1dbcb6
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 12 deletions.
11 changes: 11 additions & 0 deletions example_usage/js/egu_workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,14 @@ ExampleUsageWorkflow.states({
finish: true
}
});

ExampleUsageWorkflow.notifications({
finished: {
to: ["user_list"],
view: function(M) {
return {
value: M.entities.user_maybe ? M.entities.user.title : "No user"
};
}
}
});
5 changes: 5 additions & 0 deletions example_usage/template/notification/finished.hsvt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
emailSubject("Example notification")

<p> "The workflow has finished" </p>

<p> "Value is '" value "'" </p>
4 changes: 4 additions & 0 deletions std_workflow/js/std_workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ var WorkflowInstanceBase = P.WorkflowInstanceBase = function() {
instance[list] = [impl];
});
this.$textLookup = {};
this.$notifications = {};
};

WorkflowInstanceBase.prototype = {
Expand Down Expand Up @@ -199,6 +200,7 @@ WorkflowInstanceBase.prototype = {
this._callHandler('$observeExit', transition);
this.workUnit.tags.state = destination;
this.workUnit.tags.target = destinationTarget;
this._maybeSendNotificationOnEnterState(destination);

// Dispatch states are used to make decisions which skip other states
var safety = 256;
Expand All @@ -219,6 +221,7 @@ WorkflowInstanceBase.prototype = {
stateDefinition = this.$states[destination];
if(!stateDefinition) { throw new Error("Workflow does not have destination state after dispatch: "+destination); }
this.workUnit.tags.state = destination;
this._maybeSendNotificationOnEnterState(destination);
}
if(safety <= 0) { throw new Error("Went through too many dispatch states when attempting transition (possible loop)"); }

Expand Down Expand Up @@ -271,6 +274,7 @@ WorkflowInstanceBase.prototype = {
this._setPendingTransition(undefined);
}
this._callHandler('$observeEnter', entry.action, entry.previousState);
this._maybeSendNotificationOnEnterState(entry.state);
this._saveWorkUnit();
this.$timeline.create({
workUnitId: this.workUnit.id,
Expand Down
97 changes: 88 additions & 9 deletions std_workflow/js/std_workflow_emails.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ P.WorkflowInstanceBase.prototype.$emailTemplate = "std:email-template:workflow-n

// --------------------------------------------------------------------------

// USE OF M.sendEmail()
//
// specification has keys:
// template - Template object, or name of template within consuming plugin
// When used as a notification, template need not be specified and defaults
// to "notification/NAME" where NAME is the name of the notification.
// view - view for rendering template
// If view is a function, the function is called to generate the view
// with M as the single argument.
// to - list of recipients
// cc - CC list, only sent if the to list includes at least one entry
// except - list of recipients to *not* send stuff to
Expand All @@ -44,7 +50,13 @@ P.WorkflowInstanceBase.prototype.$emailTemplate = "std:email-template:workflow-n
// components.
//
// Recipients lists can contains:
// Strings as actionableBy names resolved by M.getActionableBy()
// Strings,
// when ending _refMaybe, _refList, _ref, _maybe, _list: recipients from
// M.entities (which will be adjusted to use 'ref'/'maybe' variants
// for efficiency)
// otherwise: as actionableBy names resolved by M.getActionableBy(),
// which for entity names, will give as result as using entities.
// When used standalone, will use entity lookup for this case too.
// SecurityPrincipal objects (users or groups)
// numeric user/group IDs (eg from the Group schema dictionary)
// Ref of a user, looked up with O.user()
Expand All @@ -54,6 +66,37 @@ P.WorkflowInstanceBase.prototype.$emailTemplate = "std:email-template:workflow-n
// Note that if there's a single recipient, it can be specified without enclosing it in an array.
//
// Email subject should be set in view as emailSubject, or preferably use the emailSubject() template function
//
//
// NOTIFICATIONS
//
// Notifications are pre-defined 'notification' emails, as a lookup of name to
// sendEmail() specification. The template name can be left out of these
// specifications, as it will default to templates in the notification/
// directory with the same name as the notification.
//
// Use Workflow.notifications() to create one or more notifications, then
// M.sendNotification() to send one.
//
// When the workflow enters a state (or passes through a dispatch state),
// the notification with the same name as the state is automatically sent.
//
// Remember that the sendEmail() spec is created when the plugin is loaded,
// so view will have to be a function, so ideally use strings to specific
// recipients. If necessary use a function for recipients.
//
// devtools has a feature to send test emails from all defined notifications.

// --------------------------------------------------------------------------

const IS_ENTITY_NAME = /_(refMaybe|refList|ref|maybe|list)$/;
const MATCH_ENTITY_SUFFIX = /(|_[a-zA-Z]+)$/;
const ENTITY_SUFFIX_REPLACEMENTS = {
"": "_refMaybe",
"_ref": "_refMaybe",
"_maybe": "_refMaybe",
"_list": "_refList"
};

var toId = function(u) { return u.id; };

Expand Down Expand Up @@ -83,12 +126,15 @@ var sendEmail = function(specification, entities, M) {
if(M) {
template = M.$plugin.template(template);
} else {
throw new Error("A template formed using P.template() should be passed to send_email, not a string");
throw new Error("template property for sendEmail can only be a string when called on a workflow object. Use P.template() to obtain a Template object.");
}
}

// Set up the initial template
var view = Object.create(specification.view || {});
var view = Object.create(
// view property is a template view, or a function which generates the view
((typeof(specification.view) === "function") ? specification.view(M) : specification.view) || {}
);
if(M) {
view.M = M;
}
Expand Down Expand Up @@ -142,13 +188,17 @@ var _generateEmailRecipientList = function(givenList, except, entities, M) {
if(recipient) {
switch(typeof(recipient)) {
case "string":
if(M) {
pushRecipient(M.getActionableBy(recipient));
} else {
var entityList = entities[recipient];
_.each(_.flatten([entityList]), function (entity) {
pushRecipient(O.user(entity.ref));
if(!M || IS_ENTITY_NAME.test(recipient)) {
// Adjust given entity name to 1) use maybe variants, so missing entities don't exception
// and 2) use ref variants, to avoid loading objects from the store unnecessarily.
var recipientEntityName = recipient.replace(MATCH_ENTITY_SUFFIX, function(_, suffix) {
return ENTITY_SUFFIX_REPLACEMENTS[suffix] || suffix;
});
_.each(_.flatten([entities[recipientEntityName]]), function(ref) {
if(ref) { pushRecipient(O.user(ref)); }
});
} else {
pushRecipient(M.getActionableBy(recipient));
}
break;
case "number":
Expand Down Expand Up @@ -191,3 +241,32 @@ P.WorkflowInstanceBase.prototype.sendEmail = function(specification) {
P.implementService("std:workflow_emails:send_email", function(specification, entities) {
sendEmail(specification, entities);
});

// --------------------------------------------------------------------------

// $notifications = {} in WorkflowInstanceBase constructor function

// Define notifications
P.Workflow.prototype.notifications = function(notifications) {
_.extend(this.$instanceClass.prototype.$notifications, notifications);
return this;
};

// Explicitly send a notification email
P.WorkflowInstanceBase.prototype.sendNotification = function(name) {
var specification = this.$notifications[name];
if(!specification) { throw new Error("Notification "+name+" is not defined"); }
// Notification template names can be implicit
if(!specification.template) {
specification = Object.create(specification);
specification.template = "notification/"+name;
}
this.sendEmail(specification);
};

// Automatically send a notification when entering state
P.WorkflowInstanceBase.prototype._maybeSendNotificationOnEnterState = function(state) {
if(state in this.$notifications) {
this.sendNotification(state);
}
};
36 changes: 33 additions & 3 deletions std_workflow_dev/js/std_workflow_debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ if(O.PLUGIN_DEBUGGING_ENABLED) {

P.workflow.registerOnLoadCallback(function(workflows) {

var getCheckedInstanceForDebugging = function(workUnit) {
if(!showDebugTools()) { return; }
var getCheckedInstanceForDebugging = function(workUnit, always) {
if(!(showDebugTools() || always)) { O.stop("Debug tools are not enabled"); }
var workflow = workflows.getWorkflow(workUnit.workType);
if(!workflow) { O.stop("Workflow not implemented"); }
return workflow.instance(workUnit);
Expand All @@ -50,7 +50,9 @@ if(O.PLUGIN_DEBUGGING_ENABLED) {
var plugin = workflow.plugin;

workflow.actionPanel({}, function(M, builder) {
var adminPanel = builder.panel(8888888);
var adminPanel = builder.panel(8888889);

adminPanel.link(98, "/do/workflow-dev/workflow-notifications/"+this.workUnit.id, "Notifications");

if(!showDebugTools()) {
if(O.currentUser.isSuperUser) {
Expand Down Expand Up @@ -141,6 +143,34 @@ if(O.PLUGIN_DEBUGGING_ENABLED) {
}, "std:ui:confirm");
}
});

// --------------------------------------------------------------------------

P.respond("GET,POST", "/do/workflow-dev/workflow-notifications", [
{pathElement:0, as:"workUnit", allUsers:true} // Security check below
], function(E, workUnit) {
var M = getCheckedInstanceForDebugging(workUnit, true);
var testSend;
if(E.request.method === "POST") {
testSend = (E.request.parameters.notification || '').split(/:\s+/)[1];
if(testSend) {
M.sendNotification(testSend);
}
}
var notifications = [];
_.each(M.$notifications, function(spec, name) {
notifications.push({
name: name,
testSend: name === testSend,
spec: spec
});
});
E.render({
M: M,
notifications: notifications
});
});

});

}
24 changes: 24 additions & 0 deletions std_workflow_dev/template/workflow-notifications.hsvt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
pageTitle("Notifications: " M.title)
backLink(M.url)

unless(notifications.length) {
std:ui:notice("There are no notifications defined for this workflow")
} else {
<form method="POST"> std:form:token()
<table>
<tr>
<th> "Name" </th>
<th> "Test?" </th>
</tr>
each(notifications) {
<td style="width:100px"> name </td>
<td>
<input type="submit" name="notification" value=["Test send: " name]> // value must end with ': <name of notification>'
if(testSend) {
" (email sent)"
}
</td>
}
</table>
</form>
}

0 comments on commit d1dbcb6

Please sign in to comment.