mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-21 19:34:40 +08:00
Implementing long-running workflows
--HG-- branch : 1.x extra : rebase_source : 34f5639603de6e06b4dcc60d82e42e5cfcf9b083
This commit is contained in:
BIN
src/Orchard.Web/Modules/Orchard.Workflows.zip
Normal file
BIN
src/Orchard.Web/Modules/Orchard.Workflows.zip
Normal file
Binary file not shown.
@@ -13,6 +13,7 @@ namespace Orchard.Workflows.Activities {
|
||||
|
||||
public override bool CanExecute(ActivityContext context) {
|
||||
try {
|
||||
|
||||
string contenttypes = context.State.ContentTypes;
|
||||
var content = context.Tokens["Content"] as IContent;
|
||||
|
||||
@@ -35,11 +36,11 @@ namespace Orchard.Workflows.Activities {
|
||||
}
|
||||
|
||||
public override IEnumerable<LocalizedString> GetPossibleOutcomes(ActivityContext context) {
|
||||
return new[] { T("Success") };
|
||||
return new[] { T("Done") };
|
||||
}
|
||||
|
||||
public override LocalizedString Execute(ActivityContext context) {
|
||||
return T("Success");
|
||||
return T("Done");
|
||||
}
|
||||
|
||||
public override string Form {
|
||||
@@ -47,6 +48,10 @@ namespace Orchard.Workflows.Activities {
|
||||
return "SelectContentTypes";
|
||||
}
|
||||
}
|
||||
|
||||
public override LocalizedString Category {
|
||||
get { return T("Content Items"); }
|
||||
}
|
||||
}
|
||||
|
||||
public class ContentCreatedActivity : ContentActivity {
|
||||
@@ -54,12 +59,42 @@ namespace Orchard.Workflows.Activities {
|
||||
get { return "ContentCreated"; }
|
||||
}
|
||||
|
||||
public override LocalizedString Category {
|
||||
get { return T("Content Items"); }
|
||||
}
|
||||
|
||||
public override LocalizedString Description {
|
||||
get { return T("Content is actually created."); }
|
||||
get { return T("Content is created."); }
|
||||
}
|
||||
}
|
||||
|
||||
public class ContentPublishedActivity : ContentActivity {
|
||||
public override string Name {
|
||||
get { return "ContentPublished"; }
|
||||
}
|
||||
|
||||
|
||||
public override LocalizedString Description {
|
||||
get { return T("Content is published."); }
|
||||
}
|
||||
}
|
||||
|
||||
public class ContentVersionedActivity : ContentActivity {
|
||||
public override string Name {
|
||||
get { return "ContentVersioned"; }
|
||||
}
|
||||
|
||||
|
||||
public override LocalizedString Description {
|
||||
get { return T("Content is versioned."); }
|
||||
}
|
||||
}
|
||||
|
||||
public class ContentRemovedActivity : ContentActivity {
|
||||
public override string Name {
|
||||
get { return "ContentRemoved"; }
|
||||
}
|
||||
|
||||
|
||||
public override LocalizedString Description {
|
||||
get { return T("Content is removed."); }
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
using System.Linq;
|
||||
using Orchard.ContentManagement.Drivers;
|
||||
using Orchard.Core.Common.Models;
|
||||
using Orchard.Data;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Workflows.Models;
|
||||
|
||||
namespace Orchard.Workflows.Drivers {
|
||||
public class WorkflowDriver : ContentPartDriver<CommonPart> {
|
||||
private readonly IRepository<AwaitingActivityRecord> _awaitingActivityRepository;
|
||||
|
||||
public WorkflowDriver(
|
||||
IOrchardServices services,
|
||||
IRepository<AwaitingActivityRecord> awaitingActivityRepository
|
||||
) {
|
||||
_awaitingActivityRepository = awaitingActivityRepository;
|
||||
T = NullLocalizer.Instance;
|
||||
Services = services;
|
||||
}
|
||||
|
||||
public Localizer T { get; set; }
|
||||
public IOrchardServices Services { get; set; }
|
||||
|
||||
protected override string Prefix {
|
||||
get { return "WorkflowDriver"; }
|
||||
}
|
||||
|
||||
protected override DriverResult Display(CommonPart part, string displayType, dynamic shapeHelper) {
|
||||
return ContentShape("Parts_Workflow_SummaryAdmin", () => {
|
||||
var awaiting = _awaitingActivityRepository.Table.Where(x => x.ContentItemRecord == part.ContentItem.Record).ToList();
|
||||
return shapeHelper.Parts_Workflow_SummaryAdmin().Activities(awaiting.Select(x => x.ActivityRecord));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -11,21 +11,25 @@ namespace Orchard.Workflows.Handlers {
|
||||
OnPublished<ContentPart>(
|
||||
(context, part) =>
|
||||
workflowManager.TriggerEvent("ContentPublished",
|
||||
context.ContentItem,
|
||||
() => new Dictionary<string, object> { { "Content", context.ContentItem } }));
|
||||
|
||||
OnRemoved<ContentPart>(
|
||||
OnRemoving<ContentPart>(
|
||||
(context, part) =>
|
||||
workflowManager.TriggerEvent("ContentRemoved",
|
||||
context.ContentItem,
|
||||
() => new Dictionary<string, object> { { "Content", context.ContentItem } }));
|
||||
|
||||
OnVersioned<ContentPart>(
|
||||
(context, part1, part2) =>
|
||||
workflowManager.TriggerEvent("ContentVersioned",
|
||||
() => new Dictionary<string, object> { { "Content", part1.ContentItem } }));
|
||||
context.BuildingContentItem,
|
||||
() => new Dictionary<string, object> { { "Content", context.BuildingContentItem } }));
|
||||
|
||||
OnCreated<ContentPart>(
|
||||
(context, part) =>
|
||||
workflowManager.TriggerEvent("ContentCreated",
|
||||
context.ContentItem,
|
||||
() => new Dictionary<string, object> { { "Content", context.ContentItem } }));
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Handlers;
|
||||
using Orchard.Data;
|
||||
using Orchard.Workflows.Models;
|
||||
|
||||
namespace Orchard.Workflows.Handlers {
|
||||
|
||||
public class WorkflowHandler : ContentHandler {
|
||||
|
||||
public WorkflowHandler(
|
||||
IRepository<AwaitingActivityRecord> awaitingActivityRepository,
|
||||
IRepository<WorkflowRecord> workflowRepository
|
||||
) {
|
||||
|
||||
// Delete any pending workflow related to a deleted content item
|
||||
OnRemoving<ContentPart>(
|
||||
(context, part) => {
|
||||
var awaiting = awaitingActivityRepository.Table.Where(x => x.ContentItemRecord == context.ContentItemRecord).ToList();
|
||||
|
||||
foreach (var item in awaiting) {
|
||||
workflowRepository.Delete(item.WorkflowRecord);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -32,6 +32,7 @@ namespace Orchard.Workflows {
|
||||
SchemaBuilder.CreateTable("AwaitingActivityRecord", table => table
|
||||
.Column<int>("Id", column => column.PrimaryKey().Identity())
|
||||
.Column<int>("ActivityRecord_id")
|
||||
.Column<int>("ContentItemRecord_id")
|
||||
.Column<int>("WorkflowRecord_id")
|
||||
);
|
||||
|
||||
@@ -48,6 +49,5 @@ namespace Orchard.Workflows {
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -1,11 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.ContentManagement.Records;
|
||||
|
||||
namespace Orchard.Workflows.Models {
|
||||
public class AwaitingActivityRecord {
|
||||
public virtual int Id { get; set; }
|
||||
|
||||
public virtual ActivityRecord ActivityRecord { get; set; }
|
||||
|
||||
|
||||
public virtual ContentItemRecord ContentItemRecord { get; set; }
|
||||
|
||||
// Parent property
|
||||
public virtual WorkflowRecord WorkflowRecord { get; set; }
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.ContentManagement;
|
||||
|
||||
namespace Orchard.Workflows.Models.Descriptors {
|
||||
public class ActivityContext {
|
||||
@@ -6,7 +7,13 @@ namespace Orchard.Workflows.Models.Descriptors {
|
||||
Tokens = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If set, represents the subject of the current workflow
|
||||
/// </summary>
|
||||
public IContent Content { get; set; }
|
||||
|
||||
public IDictionary<string, object> Tokens { get; set; }
|
||||
public dynamic State { get; set; }
|
||||
public dynamic WorkflowState { get; set; }
|
||||
}
|
||||
}
|
@@ -105,7 +105,9 @@
|
||||
<Compile Include="Activities\NotificationActivity.cs" />
|
||||
<Compile Include="AdminMenu.cs" />
|
||||
<Compile Include="Controllers\AdminController.cs" />
|
||||
<Compile Include="Drivers\WorkflowDriver.cs" />
|
||||
<Compile Include="Handlers\ContentHandler.cs" />
|
||||
<Compile Include="Handlers\WorkflowHandler.cs" />
|
||||
<Compile Include="Models\AwaitingActivityRecord.cs" />
|
||||
<Compile Include="Models\Descriptors\ActivityContext.cs" />
|
||||
<Compile Include="Models\Descriptors\ActivityDescriptor.cs" />
|
||||
@@ -133,7 +135,9 @@
|
||||
<Compile Include="ViewModels\AdminIndexViewModel.cs" />
|
||||
<Compile Include="ViewModels\WorkflowDefinitionViewModel.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<ItemGroup>
|
||||
<Content Include="Placement.info" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Views\Admin\Index.cshtml" />
|
||||
</ItemGroup>
|
||||
@@ -161,6 +165,9 @@
|
||||
<ItemGroup>
|
||||
<Content Include="Views\Activity-Decision.cshtml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Views\Parts.Workflow.SummaryAdmin.cshtml" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
|
6
src/Orchard.Web/Modules/Orchard.Workflows/Placement.info
Normal file
6
src/Orchard.Web/Modules/Orchard.Workflows/Placement.info
Normal file
@@ -0,0 +1,6 @@
|
||||
<Placement>
|
||||
<Match DisplayType="SummaryAdmin">
|
||||
<Place Parts_Workflow_SummaryAdmin="Meta:5"/>
|
||||
</Match>
|
||||
|
||||
</Placement>
|
@@ -1,20 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Events;
|
||||
using Orchard.Workflows.Models;
|
||||
|
||||
namespace Orchard.Workflows.Services {
|
||||
public interface IWorkflowManager : IEventHandler {
|
||||
|
||||
/// <summary>
|
||||
/// Triggers a specific Event, and provides the tokens context if the event is
|
||||
/// actually executed
|
||||
/// </summary>
|
||||
/// <param name="name">The type of the event to trigger, e.g. Publish</param>
|
||||
/// <param name="target">The <see cref="IContent"/> content item the event is related to</param>
|
||||
/// <param name="tokensContext">An object containing the tokens context</param>
|
||||
void TriggerEvent(string name, Func<Dictionary<string, object>> tokensContext);
|
||||
void TriggerEvent(string name, IContent target, Func<Dictionary<string, object>> tokensContext);
|
||||
|
||||
ActivityRecord ExecuteWorkflow(WorkflowDefinitionRecord workflowDefinitionRecord, ActivityRecord activityRecord, Dictionary<string, object> tokens);
|
||||
ActivityRecord ExecuteWorkflow(WorkflowDefinitionRecord workflowDefinitionRecord, ActivityRecord activityRecord, IContent target, Dictionary<string, object> tokens, dynamic workflowState);
|
||||
}
|
||||
|
||||
}
|
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Forms.Services;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Workflows.Models;
|
||||
@@ -40,7 +41,7 @@ namespace Orchard.Workflows.Services {
|
||||
public Localizer T { get; set; }
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public void TriggerEvent(string name, Func<Dictionary<string, object>> tokensContext) {
|
||||
public void TriggerEvent(string name, IContent target, Func<Dictionary<string, object>> tokensContext) {
|
||||
var tokens = tokensContext();
|
||||
|
||||
var activity = _activitiesManager.GetActivityByName(name);
|
||||
@@ -60,9 +61,9 @@ namespace Orchard.Workflows.Services {
|
||||
|
||||
var awaitingActivities = new List<AwaitingActivityRecord>();
|
||||
|
||||
// and any running workflow paused on this kind of activity
|
||||
// and any running workflow paused on this kind of activity for this content
|
||||
awaitingActivities.AddRange(_awaitingActivityRepository.Table.Where(
|
||||
x => x.ActivityRecord.Name == name && x.ActivityRecord.Start == false
|
||||
x => x.ActivityRecord.Name == name && x.ActivityRecord.Start == false && x.ContentItemRecord == target.ContentItem.Record
|
||||
).ToList()
|
||||
);
|
||||
|
||||
@@ -75,9 +76,10 @@ namespace Orchard.Workflows.Services {
|
||||
awaitingActivities = awaitingActivities.Where(a => {
|
||||
var formatted = JsonConvert.DeserializeXNode(a.ActivityRecord.State).ToString();
|
||||
var tokenized = _tokenizer.Replace(formatted, tokens);
|
||||
var serialized = JsonConvert.SerializeXNode(XElement.Parse(tokenized));
|
||||
var serialized = String.IsNullOrEmpty(tokenized) ? "{}" : JsonConvert.SerializeXNode(XElement.Parse(tokenized));
|
||||
var state = FormParametersHelper.FromJsonString(serialized);
|
||||
var context = new ActivityContext { Tokens = tokens, State = state };
|
||||
var workflowState = FormParametersHelper.FromJsonString(a.WorkflowRecord.State);
|
||||
var context = new ActivityContext { Tokens = tokens, State = state, WorkflowState = workflowState, Content = target};
|
||||
|
||||
// check the condition
|
||||
try {
|
||||
@@ -93,9 +95,10 @@ namespace Orchard.Workflows.Services {
|
||||
startedWorkflows = startedWorkflows.Where(a => {
|
||||
var formatted = JsonConvert.DeserializeXNode(a.State).ToString();
|
||||
var tokenized = _tokenizer.Replace(formatted, tokens);
|
||||
var serialized = JsonConvert.SerializeXNode(XElement.Parse(tokenized));
|
||||
var serialized = String.IsNullOrEmpty(tokenized) ? "{}" : JsonConvert.SerializeXNode(XElement.Parse(tokenized));
|
||||
var state = FormParametersHelper.FromJsonString(serialized);
|
||||
var context = new ActivityContext { Tokens = tokens, State = state };
|
||||
var workflowState = FormParametersHelper.FromJsonString("{}");
|
||||
var context = new ActivityContext { Tokens = tokens, State = state, WorkflowState = workflowState, Content = target };
|
||||
|
||||
// check the condition
|
||||
try {
|
||||
@@ -119,17 +122,18 @@ namespace Orchard.Workflows.Services {
|
||||
|
||||
// resume halted workflows
|
||||
foreach (var a in awaitingActivities) {
|
||||
ResumeWorkflow(a, tokens);
|
||||
ResumeWorkflow(a, target, tokens);
|
||||
}
|
||||
|
||||
// start new workflows
|
||||
foreach (var a in startedWorkflows) {
|
||||
StartWorkflow(a, tokens);
|
||||
StartWorkflow(a, target, tokens);
|
||||
}
|
||||
}
|
||||
|
||||
private void StartWorkflow(ActivityRecord activityRecord, Dictionary<string, object> tokens) {
|
||||
var lastActivity = ExecuteWorkflow(activityRecord.WorkflowDefinitionRecord, activityRecord, tokens);
|
||||
private void StartWorkflow(ActivityRecord activityRecord, IContent target, Dictionary<string, object> tokens) {
|
||||
var workflowState = FormParametersHelper.FromJsonString("{}");
|
||||
var lastActivity = ExecuteWorkflow(activityRecord.WorkflowDefinitionRecord, activityRecord, target, tokens, workflowState);
|
||||
|
||||
// is the workflow halted on a blocking activity ?
|
||||
if (lastActivity == null) {
|
||||
@@ -138,20 +142,22 @@ namespace Orchard.Workflows.Services {
|
||||
else {
|
||||
// workflow halted, create a workflow state
|
||||
var workflow = new WorkflowRecord {
|
||||
WorkflowDefinitionRecord = activityRecord.WorkflowDefinitionRecord
|
||||
WorkflowDefinitionRecord = activityRecord.WorkflowDefinitionRecord,
|
||||
State = FormParametersHelper.ToJsonString(workflowState)
|
||||
};
|
||||
|
||||
workflow.AwaitingActivities.Add(new AwaitingActivityRecord {
|
||||
ActivityRecord = activityRecord,
|
||||
WorkflowRecord = workflow
|
||||
});
|
||||
|
||||
_workflowRepository.Create(workflow);
|
||||
|
||||
workflow.AwaitingActivities.Add(new AwaitingActivityRecord {
|
||||
ActivityRecord = lastActivity,
|
||||
ContentItemRecord = target.ContentItem.Record
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ResumeWorkflow(AwaitingActivityRecord awaitingActivityRecord, Dictionary<string, object> tokens) {
|
||||
var lastActivity = ExecuteWorkflow(awaitingActivityRecord.WorkflowRecord.WorkflowDefinitionRecord, awaitingActivityRecord.ActivityRecord, tokens);
|
||||
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);
|
||||
|
||||
// is the workflow halted on a blocking activity ?
|
||||
if (lastActivity == null) {
|
||||
@@ -164,7 +170,7 @@ namespace Orchard.Workflows.Services {
|
||||
}
|
||||
}
|
||||
|
||||
public ActivityRecord ExecuteWorkflow(WorkflowDefinitionRecord workflowDefinitionRecord, ActivityRecord activityRecord, Dictionary<string, object> tokens) {
|
||||
public ActivityRecord ExecuteWorkflow(WorkflowDefinitionRecord workflowDefinitionRecord, ActivityRecord activityRecord, IContent target, Dictionary<string, object> tokens, dynamic workflowState) {
|
||||
var firstPass = true;
|
||||
|
||||
while (true) {
|
||||
@@ -179,7 +185,7 @@ namespace Orchard.Workflows.Services {
|
||||
}
|
||||
|
||||
var state = FormParametersHelper.FromJsonString(activityRecord.State);
|
||||
var activityContext = new ActivityContext {Tokens = tokens, State = state};
|
||||
var activityContext = new ActivityContext {Tokens = tokens, State = state, WorkflowState = workflowState, Content = target };
|
||||
var outcome = activity.Execute(activityContext);
|
||||
|
||||
if (outcome != null) {
|
||||
|
@@ -3,9 +3,10 @@
|
||||
@{
|
||||
string name = Model.Name;
|
||||
bool blocking = Model.IsBlocking;
|
||||
string blockingClass = blocking ? "blocking" : null;
|
||||
}
|
||||
|
||||
<div class="@blocking">
|
||||
<div class="@blockingClass">
|
||||
@name.CamelFriendly()
|
||||
</div>
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
@using Orchard.DisplayManagement
|
||||
@using Orchard.Utility.Extensions
|
||||
@using Orchard.Workflows.Models.Descriptors
|
||||
@using Orchard.Workflows.Services
|
||||
|
||||
@@ -11,7 +12,7 @@
|
||||
<ul>
|
||||
@foreach (var activity in allActivities) {
|
||||
<li class="activity-toolbox-item" data-activity-name="@activity.Name">
|
||||
<h2>@activity.Name</h2>
|
||||
<h2>@activity.Name.CamelFriendly()</h2>
|
||||
</li>
|
||||
|
||||
Style.Include("workflows-activity-" + activity.Name.ToLower());
|
||||
|
@@ -31,10 +31,10 @@
|
||||
using (Script.Head()) {
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
var renderActivityUrl = '@Url.Action("RenderActivity", "Admin", new { area = "Orchard.Workflows" })';
|
||||
var editActivityUrl = '@Url.Action("EditActivity", "Admin", new { area = "Orchard.Workflows" })';
|
||||
var requestAntiForgeryToken = '@Html.AntiForgeryTokenValueOrchard()';
|
||||
var localId = '@Model.LocalId';
|
||||
var renderActivityUrl = '@HttpUtility.JavaScriptStringEncode(Url.Action("RenderActivity", "Admin", new { area = "Orchard.Workflows" }))';
|
||||
var editActivityUrl = '@HttpUtility.JavaScriptStringEncode(Url.Action("EditActivity", "Admin", new { area = "Orchard.Workflows" }))';
|
||||
var requestAntiForgeryToken = '@HttpUtility.JavaScriptStringEncode(Html.AntiForgeryTokenValueOrchard().ToString())';
|
||||
var localId = '@HttpUtility.JavaScriptStringEncode(Model.LocalId)';
|
||||
var updatedActivityClientId = null;
|
||||
var updatedActivityState = null;
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
if (model != null) {
|
||||
<text>
|
||||
updatedActivityClientId = '@(model.ClientId)';
|
||||
updatedActivityState = '@Html.Raw(FormParametersHelper.ToJsonString(model.Data))';
|
||||
updatedActivityState = '@Html.Raw(HttpUtility.JavaScriptStringEncode(FormParametersHelper.ToJsonString(model.Data)))';
|
||||
</text>
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,10 @@
|
||||
@{
|
||||
IEnumerable<Orchard.Workflows.Models.ActivityRecord> activities = Model.Activities;
|
||||
var count = activities.Count();
|
||||
}
|
||||
|
||||
@if (count > 0) {
|
||||
<div>
|
||||
@T("{0} active workflow(s)", count)
|
||||
</div>
|
||||
}
|
Reference in New Issue
Block a user