Handling workflows triggered by multiple events

--HG--
branch : 1.x
extra : rebase_source : 65f8932e645c67f3fb5220d4314aaaea01c93719
This commit is contained in:
Sebastien Ros
2013-01-24 17:55:15 -08:00
parent 32bbae193d
commit 56b656747d
37 changed files with 500 additions and 225 deletions

View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.Localization;
using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Activities {
public class BranchActivity : Task {
public BranchActivity() {
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public override bool CanExecute(ActivityContext context) {
return true;
}
public override IEnumerable<LocalizedString> GetPossibleOutcomes(ActivityContext context) {
return GetBranches(context).Select(x => T(x));
}
public override IEnumerable<LocalizedString> Execute(ActivityContext context) {
return GetBranches(context).Select(x => T(x));
}
public override string Name {
get { return "Branch"; }
}
public override LocalizedString Category {
get { return T("Flow"); }
}
public override LocalizedString Description {
get { return T("Splits the workflow on two different branches."); }
}
public override string Form {
get {
return "ActivityBranch";
}
}
private IEnumerable<string> GetBranches(ActivityContext context) {
if (context.State == null) {
return Enumerable.Empty<string>();
}
string branches = context.State.Branches;
if (String.IsNullOrEmpty(branches)) {
return Enumerable.Empty<string>();
}
return branches.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
}
}
}

View File

@@ -7,10 +7,14 @@ using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Activities {
public abstract class ContentActivity : BlockingActivity {
public abstract class ContentActivity : Event {
public Localizer T { get; set; }
public override bool CanStartWorkflow {
get { return true; }
}
public override bool CanExecute(ActivityContext context) {
try {
@@ -40,8 +44,8 @@ namespace Orchard.Workflows.Activities {
return new[] { T("Done") };
}
public override LocalizedString Execute(ActivityContext context) {
return T("Done");
public override IEnumerable<LocalizedString> Execute(ActivityContext context) {
yield return T("Done");
}
public override string Form {
@@ -60,7 +64,6 @@ namespace Orchard.Workflows.Activities {
get { return "ContentCreated"; }
}
public override LocalizedString Description {
get { return T("Content is created."); }
}
@@ -93,7 +96,6 @@ namespace Orchard.Workflows.Activities {
get { return "ContentRemoved"; }
}
public override LocalizedString Description {
get { return T("Content is removed."); }
}

View File

@@ -4,7 +4,7 @@ using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Activities {
public class DecisionActivity : BaseActivity {
public class DecisionActivity : Task {
public DecisionActivity() {
T = NullLocalizer.Instance;
@@ -28,8 +28,8 @@ namespace Orchard.Workflows.Activities {
return new[] { T("True"), T("False") };
}
public override LocalizedString Execute(ActivityContext context) {
return T("True");
public override IEnumerable<LocalizedString> Execute(ActivityContext context) {
yield return T("True");
}
}
}

View File

@@ -7,7 +7,7 @@ using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Activities {
public class IsInRoleActivity : BaseActivity {
public class IsInRoleActivity : Task {
private readonly IWorkContextAccessor _workContextAccessor;
public IsInRoleActivity(IWorkContextAccessor workContextAccessor) {
@@ -41,13 +41,13 @@ namespace Orchard.Workflows.Activities {
return true;
}
public override LocalizedString Execute(ActivityContext context) {
public override IEnumerable<LocalizedString> Execute(ActivityContext context) {
if (UserIsInRole(context)) {
return T("Yes");
yield return T("Yes");
}
return T("No");
yield return T("No");
}
private bool UserIsInRole(ActivityContext context) {

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using Orchard.Core.Common.Models;
using Orchard.Events;
using Orchard.Messaging.Events;
using Orchard.Messaging.Models;
using Orchard.Messaging.Services;
@@ -12,9 +11,9 @@ using Orchard.Security;
using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Activities.Mail {
namespace Orchard.Workflows.Activities {
public class MailActions : BaseActivity {
public class MailActions : Task {
private readonly IMessageManager _messageManager;
private readonly IOrchardServices _orchardServices;
private readonly IMembershipService _membershipService;
@@ -55,12 +54,13 @@ namespace Orchard.Workflows.Activities.Mail {
get { return T("Sends an e-mail to a specific user."); }
}
public override LocalizedString Execute(ActivityContext context) {
public override IEnumerable<LocalizedString> Execute(ActivityContext context) {
string recipient = context.State.Recipient;
var properties = new Dictionary<string, string>(); // context.State.Properties
properties.Add("Body", context.State.Body.ToString());
properties.Add("Subject", context.State.Subject.ToString());
var properties = new Dictionary<string, string> {
{"Body", context.State.Body.ToString()},
{"Subject", context.State.Subject.ToString()}
};
if (recipient == "owner") {
var content = context.Tokens["Content"] as IContent;
@@ -95,7 +95,7 @@ namespace Orchard.Workflows.Activities.Mail {
_messageManager.Send(SplitEmail(email), MessageType, "email", properties);
}
return T("Sent");
yield return T("Sent");
}
private static IEnumerable<string> SplitEmail(string commaSeparated) {

View File

@@ -7,7 +7,7 @@ using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Activities {
public class NotificationActivity : BaseActivity {
public class NotificationActivity : Task {
private readonly INotifier _notifier;
private readonly ITokenizer _tokenizer;
@@ -39,7 +39,7 @@ namespace Orchard.Workflows.Activities {
yield return T("Done");
}
public override LocalizedString Execute(ActivityContext context) {
public override IEnumerable<LocalizedString> Execute(ActivityContext context) {
string notification = context.State.Notification;
string message = context.State.Message;
@@ -48,7 +48,7 @@ namespace Orchard.Workflows.Activities {
var notificationType = (NotifyType)Enum.Parse(typeof(NotifyType), notification);
_notifier.Add(notificationType, T(message));
return T("Done");
yield return T("Done");
}
}
}

View File

@@ -5,7 +5,7 @@ using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Activities {
public class PublishActivity : BaseActivity {
public class PublishActivity : Task {
private readonly IContentManager _contentManager;
public PublishActivity(IContentManager contentManager) {
@@ -22,9 +22,9 @@ namespace Orchard.Workflows.Activities {
return new[] { T("Published") };
}
public override LocalizedString Execute(ActivityContext context) {
public override IEnumerable<LocalizedString> Execute(ActivityContext context) {
_contentManager.Publish(context.Content.ContentItem);
return T("Published");
yield return T("Published");
}
public override string Name {

View File

@@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.ContentManagement;
using Orchard.Data;
using Orchard.Forms.Services;
using Orchard.Localization;
using Orchard.Services;
using Orchard.Tasks;
using Orchard.Workflows.Models;
using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Activities {
public class TimerActivity : Event {
private readonly IClock _clock;
private readonly IWorkContextAccessor _workContextAccessor;
public TimerActivity(
IClock clock,
IWorkContextAccessor workContextAccessor) {
_clock = clock;
_workContextAccessor = workContextAccessor;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public override string Name {
get { return "Timer"; }
}
public override LocalizedString Category {
get { return T("Tasks"); }
}
public override LocalizedString Description {
get { return T("Wait for a specific time has passed."); }
}
public override string Form {
get { return "ActivityTimer"; }
}
public override IEnumerable<LocalizedString> GetPossibleOutcomes(ActivityContext context) {
yield return T("Done");
}
public override bool CanExecute(ActivityContext context) {
return _clock.UtcNow > When(context);
}
public override void Touch(dynamic workflowState) {
workflowState.TimerActivity_StartedUtc = _clock.UtcNow;
}
public override IEnumerable<LocalizedString> Execute(ActivityContext context) {
if(_clock.UtcNow > When(context)) {
yield return T("Done");
}
}
public static DateTime When(ActivityContext context) {
try {
int amount = context.State.Amount;
string type = context.State.Unity;
DateTime started = context.WorkflowState.TimerActivity_StartedUtc;
var when = started;
switch (type) {
case "Minute":
when = when.AddMinutes(amount);
break;
case "Hour":
when = when.AddHours(amount);
break;
case "Day":
when = when.AddDays(amount);
break;
case "Week":
when = when.AddDays(7*amount);
break;
case "Month":
when = when.AddMonths(amount);
break;
case "Year":
when = when.AddYears(amount);
break;
}
return when;
}
catch {
return DateTime.MaxValue;
}
}
}
public class TimerBackgroundTask : IBackgroundTask {
private readonly IClock _clock;
private readonly IContentManager _contentManager;
private readonly IWorkflowManager _workflowManager;
private readonly IRepository<AwaitingActivityRecord> _awaitingActivityRepository;
public TimerBackgroundTask(
IClock clock,
IContentManager contentManager,
IWorkflowManager workflowManager,
IRepository<AwaitingActivityRecord> awaitingActivityRepository) {
_clock = clock;
_contentManager = contentManager;
_workflowManager = workflowManager;
_awaitingActivityRepository = awaitingActivityRepository;
}
public void Sweep() {
var awaiting = _awaitingActivityRepository.Table.Where(x => x.ActivityRecord.Name == "Timer").ToList();
var actions = awaiting.Where(x => {
var contentItem = _contentManager.Get(x.ContentItemRecord.Id, VersionOptions.Latest);
var state = FormParametersHelper.FromJsonString(x.ActivityRecord.State);
var workflowState = FormParametersHelper.FromJsonString(x.WorkflowRecord.State);
return _clock.UtcNow > TimerActivity.When(new ActivityContext {
State = state,
WorkflowState = workflowState,
Content = contentItem
});
});
foreach (var action in actions) {
var contentItem = _contentManager.Get(action.ContentItemRecord.Id, VersionOptions.Latest);
_workflowManager.TriggerEvent("Timer", contentItem, () => new Dictionary<string, object> { { "Content", contentItem } });
}
}
}
}

View File

@@ -7,7 +7,7 @@ using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Activities {
public class UserTaskActivity : BlockingActivity {
public class UserTaskActivity : Event {
private readonly IWorkContextAccessor _workContextAccessor;
public UserTaskActivity(IWorkContextAccessor workContextAccessor) {
@@ -43,13 +43,11 @@ namespace Orchard.Workflows.Activities {
return ActionIsValid(context) && UserIsInRole(context);
}
public override LocalizedString Execute(ActivityContext context) {
public override IEnumerable<LocalizedString> Execute(ActivityContext context) {
if (ActionIsValid(context) && UserIsInRole(context)) {
return T(context.Tokens["UserTask.Action"].ToString());
yield return T(context.Tokens["UserTask.Action"].ToString());
}
return null;
}
private bool UserIsInRole(ActivityContext context) {

View File

@@ -0,0 +1,36 @@
using System;
using Orchard.DisplayManagement;
using Orchard.Forms.Services;
using Orchard.Localization;
namespace Orchard.Workflows.Forms {
public class BranchForms : IFormProvider {
protected dynamic Shape { get; set; }
public Localizer T { get; set; }
public BranchForms(IShapeFactory shapeFactory) {
Shape = shapeFactory;
T = NullLocalizer.Instance;
}
public void Describe(DescribeContext context) {
Func<IShapeFactory, dynamic> form =
shape => {
var f = Shape.Form(
Id: "BranchNames",
_Message: Shape.Textbox(
Id: "branches", Name: "Branches",
Title: T("Available branches."),
Description: T("A comma separated list of names."),
Classes: new[] {"textMedium"})
);
return f;
};
context.Form("ActivityBranch", form);
}
}
}

View File

@@ -6,7 +6,7 @@ using Orchard.DisplayManagement;
using Orchard.Forms.Services;
using Orchard.Localization;
namespace Orchard.Workflows.Providers {
namespace Orchard.Workflows.Forms {
public class ContentForms : IFormProvider {
private readonly IContentDefinitionManager _contentDefinitionManager;
protected dynamic Shape { get; set; }

View File

@@ -3,7 +3,7 @@ using Orchard.DisplayManagement;
using Orchard.Forms.Services;
using Orchard.Localization;
namespace Orchard.Workflows.Activities.Mail {
namespace Orchard.Workflows.Forms {
public class MailForms : IFormProvider {
protected dynamic Shape { get; set; }

View File

@@ -3,7 +3,7 @@ using Orchard.DisplayManagement;
using Orchard.Forms.Services;
using Orchard.Localization;
namespace Orchard.Workflows.Providers {
namespace Orchard.Workflows.Forms {
public class NotificationActivityForms : IFormProvider {
protected dynamic Shape { get; set; }
public Localizer T { get; set; }

View File

@@ -6,7 +6,7 @@ using Orchard.Forms.Services;
using Orchard.Localization;
using Orchard.Roles.Services;
namespace Orchard.Workflows.Providers {
namespace Orchard.Workflows.Forms {
public class SelectRolesForms : IFormProvider {
private readonly IRoleService _roleService;
protected dynamic Shape { get; set; }

View File

@@ -0,0 +1,57 @@
using System;
using System.Web.Mvc;
using Orchard.DisplayManagement;
using Orchard.Forms.Services;
using Orchard.Localization;
namespace Orchard.Workflows.Forms {
public class ScheduleForms : IFormProvider {
protected dynamic Shape { get; set; }
public Localizer T { get; set; }
public ScheduleForms(IShapeFactory shapeFactory) {
Shape = shapeFactory;
T = NullLocalizer.Instance;
}
public void Describe(DescribeContext context) {
context.Form("ActivityTimer",
shape => {
var form = Shape.Form(
Id: "ActionDelay",
_Amount: Shape.Textbox(
Id: "Amount", Name: "Amount",
Title: T("Amount"),
Classes: new[] { "text-small" }),
_Type: Shape.SelectList(
Id: "Unity", Name: "Unity",
Title: T("Amount type"))
.Add(new SelectListItem { Value = "Minute", Text = T("Minutes").Text, Selected = true })
.Add(new SelectListItem { Value = "Hour", Text = T("Hours").Text })
.Add(new SelectListItem { Value = "Day", Text = T("Days").Text })
.Add(new SelectListItem { Value = "Week", Text = T("Weeks").Text })
.Add(new SelectListItem { Value = "Month", Text = T("Months").Text })
);
return form;
}
);
}
}
public class ScheduleFormsValitator : FormHandler {
public Localizer T { get; set; }
public override void Validating(ValidatingContext context) {
if (context.FormName == "ActivityTimer") {
if (context.ValueProvider.GetValue("Amount").AttemptedValue == String.Empty) {
context.ModelState.AddModelError("Amount", T("You must provide an Amount").Text);
}
if (context.ValueProvider.GetValue("Unity").AttemptedValue == String.Empty) {
context.ModelState.AddModelError("Unity", T("You must provide a Type").Text);
}
}
}
}
}

View File

@@ -1,13 +1,12 @@
using System;
using System.Linq;
using System.Web.Mvc;
using Orchard.ContentManagement.MetaData;
using Orchard.DisplayManagement;
using Orchard.Forms.Services;
using Orchard.Localization;
using Orchard.Roles.Services;
namespace Orchard.Workflows.Providers {
namespace Orchard.Workflows.Forms {
public class UserTaskForms : IFormProvider {
private readonly IRoleService _roleService;
protected dynamic Shape { get; set; }

View File

@@ -71,6 +71,8 @@
<Content Include="Scripts\jquery.jsPlumb-1.3.16-all-min.js" />
<Content Include="Scripts\orchard-workflows-serialize.js" />
<Content Include="Scripts\orchard-workflows.js" />
<Content Include="Styles\workflows-activity-timer.css" />
<Content Include="Styles\workflows-activity-branch.css" />
<Content Include="Styles\orchard-workflows-admin.css" />
<Content Include="Styles\admin-usertask.css" />
<Content Include="Styles\workflows-activity-isinrole.css" />
@@ -114,9 +116,13 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Activities\ContentActivity.cs" />
<Compile Include="Activities\BranchActivity.cs" />
<Compile Include="Activities\IsInRoleActivity.cs" />
<Compile Include="Activities\Mail\MailActivity.cs" />
<Compile Include="Activities\Mail\MailForms.cs" />
<Compile Include="Activities\MailActivity.cs" />
<Compile Include="Activities\TimerActivity.cs" />
<Compile Include="Forms\TimerForms.cs" />
<Compile Include="Forms\BranchForms.cs" />
<Compile Include="Forms\MailForms.cs" />
<Compile Include="Activities\PublishActivity.cs" />
<Compile Include="Activities\UserTaskActivity.cs" />
<Compile Include="Activities\DecisionActivity.cs" />
@@ -138,12 +144,10 @@
<Compile Include="Models\WorkflowRecord.cs" />
<Compile Include="Migrations.cs" />
<Compile Include="Models\WorkflowDefinitionRecord.cs" />
<Compile Include="Providers\ContentActivityProvider.cs" />
<Compile Include="Providers\ContentActivityForms.cs" />
<Compile Include="Providers\SelectRolesForms.cs" />
<Compile Include="Providers\UserTaskForms.cs" />
<Compile Include="Providers\NotificationActivityForms.cs" />
<Compile Include="Providers\NotificationActivityProvider.cs" />
<Compile Include="Forms\ContentActivityForms.cs" />
<Compile Include="Forms\SelectRolesForms.cs" />
<Compile Include="Forms\UserTaskForms.cs" />
<Compile Include="Forms\NotificationActivityForms.cs" />
<Compile Include="ResourceManifest.cs" />
<Compile Include="Services\BaseActivity.cs" />
<Compile Include="Services\BlockingActivity.cs" />
@@ -169,9 +173,6 @@
<ItemGroup>
<Content Include="Views\Admin\Create.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Activity-ContentCreated.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Activity.cshtml" />
</ItemGroup>
@@ -205,6 +206,12 @@
<ItemGroup>
<Content Include="Views\Activity-SendEmail.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Activity-Branch.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Activity-Timer.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

View File

@@ -1,53 +0,0 @@
using System;
using System.Linq;
using Orchard.ContentManagement;
using Orchard.Localization;
using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Providers {
public class ContentActivityProvider : IActivityProvider {
public ContentActivityProvider() {
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public void Describe(DescribeActivityContext describe) {
describe.For("Content", T("Content Items"), T("Content Items"))
.Element("Created", T("Content Created"), T("Content is actually created."), ContentHasPart, context => T("When content with types ({0}) is created.", FormatPartsList(context)), "SelectContentTypes")
.Element("Versioned", T("Content Versioned"), T("Content is actually versioned."), ContentHasPart, context => T("When content with types ({0}) is versioned.", FormatPartsList(context)), "SelectContentTypes")
.Element("Published", T("Content Published"), T("Content is actually published."), ContentHasPart, context => T("When content with types ({0}) is published.", FormatPartsList(context)), "SelectContentTypes")
.Element("Removed", T("Content Removed"), T("Content is actually removed."), ContentHasPart, context => T("When content with types ({0}) is removed.", FormatPartsList(context)), "SelectContentTypes");
}
private string FormatPartsList(ActivityContext context) {
string contenttypes = context.State.ContentTypes;
if (String.IsNullOrEmpty(contenttypes)) {
return T("Any").Text;
}
return contenttypes;
}
private bool ContentHasPart(ActivityContext context) {
string contenttypes = context.State.ContentTypes;
var content = context.Tokens["Content"] as IContent;
// "" means 'any'
if (String.IsNullOrEmpty(contenttypes)) {
return true;
}
if (content == null) {
return false;
}
var contentTypes = contenttypes.Split(new[] { ',' });
return contentTypes.Any(contentType => content.ContentItem.TypeDefinition.Name == contentType);
}
}
}

View File

@@ -1,49 +0,0 @@
using System;
using Orchard.Localization;
using Orchard.Tokens;
using Orchard.UI.Notify;
using Orchard.Workflows.Models.Descriptors;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Providers {
public class NotificationActivityProvider : IActivityProvider {
private readonly INotifier _notifier;
private readonly ITokenizer _tokenizer;
public NotificationActivityProvider(INotifier notifier, ITokenizer tokenizer) {
_notifier = notifier;
_tokenizer = tokenizer;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public void Describe(DescribeActivityContext describe) {
describe.For("Notification", T("Notification"), T("Notifications"))
.Element(
"Notify",
T("Notify"),
T("Display a message."),
ExecuteActivity,
DisplayActivity,
"ActivityNotify"
);
}
private bool ExecuteActivity(ActivityContext context) {
string notification = context.State.Notification;
string message = context.State.Message;
message = _tokenizer.Replace(message, context.Tokens);
var notificationType = (NotifyType)Enum.Parse(typeof(NotifyType), notification);
_notifier.Add(notificationType, T(message));
return true;
}
private LocalizedString DisplayActivity(ActivityContext context) {
return T("Displays \"{1}\" as {0}", T(context.State.Notification).Text, T(context.State.Message).Text);
}
}
}

View File

@@ -215,7 +215,7 @@
target = $(target);
// start button
$('#activity-toolbar-start').toggle(target.hasClass('blocking'));
$('#activity-toolbar-start').toggle(target.hasClass('canStart'));
$('#activity-toolbar-start-checkbox').prop('checked', target.get(0).viewModel.start);
// edit button

View File

@@ -3,16 +3,18 @@ using Orchard.Localization;
using Orchard.Workflows.Models.Descriptors;
namespace Orchard.Workflows.Services {
public abstract class BaseActivity : IActivity {
public abstract class Task : IActivity {
public abstract string Name { get; }
public abstract LocalizedString Category { get; }
public abstract LocalizedString Description { get; }
public virtual bool IsBlocking {
public virtual bool IsEvent {
get { return false; }
}
public bool CanStartWorkflow { get { return false; } }
public virtual string Form {
get { return null; }
}
@@ -23,6 +25,11 @@ namespace Orchard.Workflows.Services {
return true;
}
public abstract LocalizedString Execute(ActivityContext context);
public abstract IEnumerable<LocalizedString> Execute(ActivityContext context);
public virtual void Touch(dynamic state) {
}
}
}

View File

@@ -3,13 +3,13 @@ using Orchard.Localization;
using Orchard.Workflows.Models.Descriptors;
namespace Orchard.Workflows.Services {
public abstract class BlockingActivity : IActivity {
public abstract class Event : IActivity {
public abstract string Name { get; }
public abstract LocalizedString Category { get; }
public abstract LocalizedString Description { get; }
public virtual bool IsBlocking {
public virtual bool IsEvent {
get { return true; }
}
@@ -17,12 +17,20 @@ namespace Orchard.Workflows.Services {
get { return null; }
}
public virtual bool CanStartWorkflow {
get { return false; }
}
public abstract IEnumerable<LocalizedString> GetPossibleOutcomes(ActivityContext context);
public virtual bool CanExecute(ActivityContext context) {
return true;
}
public abstract LocalizedString Execute(ActivityContext context);
public abstract IEnumerable<LocalizedString> Execute(ActivityContext context);
public virtual void Touch(dynamic workflowState) {
}
}
}

View File

@@ -8,11 +8,12 @@ namespace Orchard.Workflows.Services {
string Name { get; }
LocalizedString Category { get; }
LocalizedString Description { get; }
bool IsBlocking { get; }
bool IsEvent { get; }
bool CanStartWorkflow { get; }
string Form { get; }
/// <summary>
/// List of possible outcomes when the activity is executed
/// List of possible outcomes when the activity is executed.
/// </summary>
IEnumerable<LocalizedString> GetPossibleOutcomes(ActivityContext context);
@@ -25,6 +26,12 @@ namespace Orchard.Workflows.Services {
/// <summary>
/// Executes the current activity
/// </summary>
LocalizedString Execute(ActivityContext context);
/// <returns>The names of the resulting outcomes.</returns>
IEnumerable<LocalizedString> Execute(ActivityContext context);
/// <summary>
/// Called on blocking activities when they are reached
/// </summary>
void Touch(dynamic workflowState);
}
}

View File

@@ -15,7 +15,7 @@ namespace Orchard.Workflows.Services {
/// <param name="tokensContext">An object containing the tokens context</param>
void TriggerEvent(string name, IContent target, Func<Dictionary<string, object>> tokensContext);
ActivityRecord ExecuteWorkflow(WorkflowDefinitionRecord workflowDefinitionRecord, ActivityRecord activityRecord, IContent target, Dictionary<string, object> tokens, dynamic workflowState);
IEnumerable<ActivityRecord> ExecuteWorkflow(WorkflowDefinitionRecord workflowDefinitionRecord, ActivityRecord activityRecord, IContent target, Dictionary<string, object> tokens, dynamic workflowState);
}
}

View File

@@ -53,7 +53,9 @@ namespace Orchard.Workflows.Services {
var startedWorkflows = new List<ActivityRecord>();
// look for workflow definitions with a corresponding starting activity,
// look for workflow definitions with a corresponding starting activity
// it's important to return activities at this point and not workflows,
// as a workflow definition could have multiple entry points with the same type of activity
startedWorkflows.AddRange(_activityRepository.Table.Where(
x =>x.Name == name && x.Start && x.WorkflowDefinitionRecord.Enabled
)
@@ -62,6 +64,8 @@ namespace Orchard.Workflows.Services {
var awaitingActivities = new List<AwaitingActivityRecord>();
// and any running workflow paused on this kind of activity for this content
// it's important to return activities at this point as a workflow could be awaiting
// on several ones. When an activity is restarted, all the other ones of the same workflow are cancelled.
awaitingActivities.AddRange(_awaitingActivityRepository.Table.Where(
x => x.ActivityRecord.Name == name && x.ActivityRecord.Start == false && x.ContentItemRecord == target.ContentItem.Record
).ToList()
@@ -133,10 +137,10 @@ namespace Orchard.Workflows.Services {
private void StartWorkflow(ActivityRecord activityRecord, IContent target, Dictionary<string, object> tokens) {
var workflowState = FormParametersHelper.FromJsonString("{}");
var lastActivity = ExecuteWorkflow(activityRecord.WorkflowDefinitionRecord, activityRecord, target, tokens, workflowState);
IEnumerable<ActivityRecord> blockedOn = ExecuteWorkflow(activityRecord.WorkflowDefinitionRecord, activityRecord, target, tokens, workflowState);
// is the workflow halted on a blocking activity ?
if (lastActivity == null) {
if (blockedOn == null || !blockedOn.Any()) {
// no, nothing to do
}
else {
@@ -148,61 +152,83 @@ namespace Orchard.Workflows.Services {
_workflowRepository.Create(workflow);
workflow.AwaitingActivities.Add(new AwaitingActivityRecord {
ActivityRecord = lastActivity,
ContentItemRecord = target.ContentItem.Record
});
foreach (var blocking in blockedOn) {
workflow.AwaitingActivities.Add(new AwaitingActivityRecord {
ActivityRecord = blocking,
ContentItemRecord = target.ContentItem.Record
});
}
}
}
private void ResumeWorkflow(AwaitingActivityRecord awaitingActivityRecord, IContent target, Dictionary<string, object> tokens) {
var workflowState = FormParametersHelper.FromJsonString(awaitingActivityRecord.WorkflowRecord.State);
var lastActivity = ExecuteWorkflow(awaitingActivityRecord.WorkflowRecord.WorkflowDefinitionRecord, awaitingActivityRecord.ActivityRecord, target, tokens, workflowState);
IEnumerable<ActivityRecord> blockedOn = ExecuteWorkflow(awaitingActivityRecord.WorkflowRecord.WorkflowDefinitionRecord, awaitingActivityRecord.ActivityRecord, target, tokens, workflowState);
// is the workflow halted on a blocking activity ?
if (lastActivity == null) {
if (blockedOn == null || !blockedOn.Any()) {
// no, delete the workflow
_workflowRepository.Delete(awaitingActivityRecord.WorkflowRecord);
}
else {
// workflow halted, save state
awaitingActivityRecord.ActivityRecord = lastActivity;
// remove all previous awaiting activities
var workflow = awaitingActivityRecord.WorkflowRecord;
workflow.State = FormParametersHelper.ToJsonString(workflowState);
workflow.AwaitingActivities.Clear();
// add the new ones
foreach (var blocking in blockedOn) {
workflow.AwaitingActivities.Add(new AwaitingActivityRecord {
ActivityRecord = blocking,
ContentItemRecord = target.ContentItem.Record
});
}
}
}
public ActivityRecord ExecuteWorkflow(WorkflowDefinitionRecord workflowDefinitionRecord, ActivityRecord activityRecord, IContent target, Dictionary<string, object> tokens, dynamic workflowState) {
public IEnumerable<ActivityRecord> ExecuteWorkflow(WorkflowDefinitionRecord workflowDefinitionRecord, ActivityRecord activityRecord, IContent target, Dictionary<string, object> tokens, dynamic workflowState) {
var firstPass = true;
var pending = new Stack<ActivityRecord>();
pending.Push(activityRecord);
var blocking = new List<ActivityRecord>();
while (pending.Any()) {
activityRecord = pending.Pop();
while (true) {
// while there is an activity to process
var activity = _activitiesManager.GetActivityByName(activityRecord.Name);
if (!firstPass && activity.IsBlocking) {
return activityRecord;
if (!firstPass){
if(activity.IsEvent) {
activity.Touch(workflowState);
blocking.Add(activityRecord);
continue;
}
}
else {
firstPass = false;
}
var state = FormParametersHelper.FromJsonString(activityRecord.State);
var activityContext = new ActivityContext {Tokens = tokens, State = state, WorkflowState = workflowState, Content = target };
var outcome = activity.Execute(activityContext);
var activityContext = new ActivityContext { Tokens = tokens, State = state, WorkflowState = workflowState, Content = target };
var outcomes = activity.Execute(activityContext);
if (outcome != null) {
// look for next activity in the graph
var transition = workflowDefinitionRecord.TransitionRecords.FirstOrDefault(x => x.SourceActivityRecord == activityRecord && x.SourceEndpoint == outcome.TextHint);
if (outcomes != null) {
foreach (var outcome in outcomes) {
// look for next activity in the graph
var transition = workflowDefinitionRecord.TransitionRecords.FirstOrDefault(x => x.SourceActivityRecord == activityRecord && x.SourceEndpoint == outcome.TextHint);
if (transition == null) {
return null;
if (transition != null) {
pending.Push(transition.DestinationActivityRecord);
}
}
else {
activityRecord = transition.DestinationActivityRecord;
}
}
else {
return null;
}
}
// apply Distinct() as two paths could block on the same activity
return blocking.Distinct();
}
}
}

View File

@@ -22,9 +22,8 @@
box-shadow: 2px 2px 19px #aaa;
}
.blocking {
.event {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAATCAYAAACk9eypAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAANNJREFUOE+VkbENhDAMRcOtRElDRRsxB5MwAxNQ0yAGYAJqWiagASFfbMmRHXzcXfGk+Nvf+UocAPzDyxIfMcUnZJF94WYg0ziOkOc50TQNHMehZlQRyLqui4a+74Ok+jrSeZ5TXdc0XJYl7Psu+4Qssm3boKoqMrRtGyRwqHnvo1kZZH4JmxFpcDI/UxQFrOsa2okh5HecH19nGAY6y+1IPKTgbel2RA0xfNsv/0Dwa83zHErdUwWzLIu5HbkJCOa3tiM34bouMljbEVN8whQ/A+4NnH6HdIESjBQAAAAASUVORK5CYII=');
/*background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAKVJREFUOE+lkmENwyAQRusJA7WADiSgAA0oqAMUoAAr/QMh38oCbJQbLR3JS5rvjscFugD4CzKcgQxnKB/QWkMphRgjpJRgjJGs64p931uBMaY2bNuGEAI4583GQidwznVN1tqj9hkzcUiXJO0E1EnnpqFgVPzmsaDUfk15KUiM7qkRnJsSWYrzS+V1LUgIIeC9r/9KXu+9dcynkOEMZDgDGd4HywvvUq4US/BOrgAAAABJRU5ErkJggg==');*/
background-position: top left;
background-repeat: no-repeat;
}
@@ -130,11 +129,11 @@
border-right: 1px solid #ccc;
}
#activity-toolbar > div:last-child {
border-right: none;
}
#activity-toolbar > div:last-child {
border-right: none;
}
#activity-toolbar > div label {
#activity-toolbar > div label {
display: inline-block;
width: 22px;
height: 22px;
@@ -146,18 +145,19 @@
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
border-radius: 2px;
}
#activity-toolbar > div label:hover {
border-color: #333;
}
#activity-toolbar > div label:hover {
border-color: #333;
}
/* Start button */
#activity-toolbar #activity-toolbar-start-checkbox { /* hide the checkbox and rely on the label for two-state button */
display:none;
display: none;
}
#activity-toolbar #activity-toolbar-start-checkbox + label {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAMCAYAAABfnvydAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAIlJREFUKFOFjrERhBEUhF9PQolIrg4lqEAN+lGBWKoCCWP2/ueOc3MBM5u8/XYXAaDeO4wxEEJMKaVQa30sEJVSoLXe5pKUEjlnUAhhHrz3nJjvvBFXn5WsNcn3O3CduH7yCeyWU9ZatNZoAimlPyDGyGtvYIwB59w2P+kvwDpbVvoHWC1nGgC9AJ139gdKftEFAAAAAElFTkSuQmCC');
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAANCAYAAACdKY9CAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAK1JREFUKFN1ULsNxSAQY6eUNKnSM0dGyERswARMkJqWASIoQMgP8gTigBQuwJ/zHQNAkFJi13XhPE+EECZ++ogxMiEE9n2Hc27iiaikeu+b4Xmed1pvbhW2bfs0VK5UZEop8jFW6gOllGgPrXWeuN7hvu8W2pOvYXWlHIKqmwwrEMNYaQVSaVx6FE9L9x+ccxhjsu4vttbiOA4S+BL1MoVYob9YG11Qu/agu4H9AENqkP+wqtOOAAAAAElFTkSuQmCC');
background-position: center;
}
@@ -167,12 +167,13 @@
background-position: center;
}
/* Delete button */
#activity-toolbar #activity-toolbar-delete > label {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAJCAYAAADkZNYtAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAGVJREFUKFOVkLENwCAMBL2TS3p2YwbvwwSuaRnjEywZBRJHSXEFr3sDJgCkqmBmlFLOIywbiIjltVbLqfeOnLOF14KLg5QSWmvrhAgfcLsyEhd5sBf2P4TiU+FVdOabf2/j255BB4ch671zW3IBAAAAAElFTkSuQmCC');
background-position: center;
}
#activity-toolbar #activity-toolbar-start-checkbox:checked + label{
#activity-toolbar #activity-toolbar-start-checkbox:checked + label {
background-color: white;
}

View File

@@ -0,0 +1,7 @@
.branch {
width: 36px;
height: 36px;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAYCAYAAADpnJ2CAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAJlJREFUSEvtk8sJwCAQBe3JBmzBsqzBfqzAVjx52GhAkM0jagyC4GEum2dm/awgoi6stSSlhBhjUgSv48AiJ8YotNZQllFKUQgBruXAIucIj7AGFhFLx6Km7HZkVzWw+MbeQu89vKMZnHPp10DYeolf4SfxELaOajbXDHD2E/YO9F+5uwv0MVO6K92iTGYkd4RHeDMkXDsWhi7rGDeiuX27SQAAAABJRU5ErkJggg==');
background-repeat: no-repeat;
background-position: center;
}

View File

@@ -0,0 +1,7 @@
.timer {
width: 36px;
height: 36px;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAUNJREFUSEu1VsGRhCAQJCeffnz5Nw5DMAJjMAIzMAIj8O3XCPxoWXOMggfY4Fh111W9tYvN9DAMuIqIkhyGgbIsg+z7XkvwPEs4eByHapoGBkWs65q2bYOxHgPTNMEgEo7jqEP48bwfqXJIGZbt/hLLPM9zmudZS3wsy0JlWcI57krOj5jYCJNAiTlJXQZd18UEJ/Z9p6qqqCgKWtfVjP4CJdi2LT9SMPsw8zcDRrgSm+RjY03LmWkXJAa6tSlsbd7wR3lQ3SUGjHAVXCbvQMU6RmoQlpuroXiiHYgFkBpYnRvvk4HVuXTnQIMvJbI6l64BLJFkk6WAmyxpUwmibSo5aBJED5p+9npVvCFxVVwGSMD8s8uOiYSB2EMsKaZJ7Ix7GzD/9YVjGVuJhE5J73hecEvUcil+eukHJMHfFgaaq0nqBz88GIi8S10IAAAAAElFTkSuQmCC');
background-repeat: no-repeat;
background-position: center;
}

View File

@@ -0,0 +1,13 @@
@using Orchard.Utility.Extensions
@{
string name = Model.Name;
string branches = Model.State.Branches;
var outcomes = String.Join(",", branches == null ? new string[0] : branches.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => "'" + x.Trim() + "'").ToArray());
}
<div data-outcomes="@outcomes">
<div class="branch" ></div>
@*@name.CamelFriendly()*@
</div>

View File

@@ -1,11 +0,0 @@
@using Orchard.Utility.Extensions
@{
string name = Model.Name;
bool blocking = Model.IsBlocking;
string blockingClass = blocking ? "blocking" : null;
}
<div class="@blockingClass">
@name.CamelFriendly()
</div>

View File

@@ -2,10 +2,9 @@
@{
string name = Model.Name;
bool blocking = Model.IsBlocking;
}
<div class="@blocking">
<div>
<div class="diamond"></div>
@*@name.CamelFriendly()*@
</div>

View File

@@ -2,11 +2,9 @@
@{
string name = Model.Name;
bool blocking = Model.IsBlocking;
string blockingClass = blocking ? "blocking" : null;
}
<div class="@blockingClass" title="@Model.State.Message">
<div title="@Model.State.Message">
@name.CamelFriendly()
@if (Model.State.Notification != null) {
@:- @Model.State.Notification

View File

@@ -0,0 +1,17 @@
@using Orchard.Utility.Extensions
@{
string name = Model.Name;
string title = null;
if (Model.State != null && HasText(Model.State.Unity)) {
string amount = Model.State.Amount;
string unity = Model.State.Unity;
title = T("{0} {1} after", amount, T(unity).Text).Text;
}
}
<div class="event" title="@title">
<div class="timer" ></div>
@*@name.CamelFriendly()*@
</div>

View File

@@ -6,7 +6,7 @@
var outcomes = String.Join(",", actions == null ? new string[0] : actions.Split(new []{','}, StringSplitOptions.RemoveEmptyEntries).Select(x => "'" + x.Trim() + "'").ToArray());
}
<div class="blocking" data-outcomes="@outcomes">
<div class="event" data-outcomes="@outcomes">
@name.CamelFriendly()
</div>

View File

@@ -2,11 +2,11 @@
@{
string name = Model.Name;
bool blocking = Model.IsBlocking;
string blockingClass = blocking ? "blocking" : null;
string isEventClass = Model.IsEvent ? "event" : null;
string canStartClass = Model.CanStartWorkflow ? "canStart" : null;
}
<div class="@blockingClass">
<div class="@isEventClass @canStartClass">
@name.CamelFriendly()
</div>

View File

@@ -32,7 +32,7 @@
outcomes: [@Html.Raw(String.Join(",", activity.GetPossibleOutcomes(new ActivityContext()).Where(x => !String.IsNullOrEmpty(x.Text)).Select(x => "'" + HttpUtility.JavaScriptStringEncode(x.Text) + "'").ToArray()))],
category: '@HttpUtility.JavaScriptStringEncode(activity.Category.Text)',
description: '@HttpUtility.JavaScriptStringEncode(activity.Description.Text)',
isBlocking: @(activity.IsBlocking ? "true" : "false"),
IsEvent: @(activity.IsEvent ? "true" : "false"),
hasForm: @(!String.IsNullOrWhiteSpace(activity.Form) ? "true" : "false")
},</text>
}

View File

@@ -25,7 +25,7 @@
<div id="activity-toolbar">
<div id="activity-toolbar-start">
<input type="checkbox" id="activity-toolbar-start-checkbox"/>
<label for="activity-toolbar-start-checkbox" title="@T("Start")"></label>
<label for="activity-toolbar-start-checkbox" title="@T("Starts workflow")"></label>
</div>
<div id="activity-toolbar-edit">
<label title="@T("Edit")"></label>