Finalizing branching activities

--HG--
branch : 1.x
extra : rebase_source : 51c51c157af384782d33300d94ed0690b03d070f
This commit is contained in:
Sebastien Ros
2013-01-31 12:53:30 -08:00
parent 436e7c3559
commit ffa4b52c87
11 changed files with 203 additions and 59 deletions

View File

@@ -69,7 +69,7 @@ namespace Orchard.Forms.Services {
return null;
}
return JsonConvert.DeserializeObject<dynamic>(state);
return JObject.Parse(state);
}
public static string ToJsonString(FormCollection formCollection) {

View File

@@ -1,8 +1,17 @@
using System.Linq;
using Orchard.Localization;
using Orchard.Workflows.Models;
namespace Orchard.Workflows.Activities {
public class ExclusiveBranchActivity : BranchActivity {
public override string Name {
get { return "ExclusiveBranch"; }
}
public override LocalizedString Description {
get { return T("Splits the workflow on different branches, activating the first event to occur."); }
}
public override void OnActivityExecuted(WorkflowContext workflowContext, ActivityContext activityContext) {
// for blocking activities only
@@ -16,7 +25,7 @@ namespace Orchard.Workflows.Activities {
// if a direct target of a Branch Activity is executed, then suppress all other direct waiting activities
var parentBranchActivities = inboundActivities
.Where(x => x.SourceActivityRecord.Name == typeof(ExclusiveBranchActivity).Name)
.Where(x => x.SourceActivityRecord.Name == this.Name)
.Select(x => x.SourceActivityRecord)
.ToList();

View File

@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.Localization;
using Orchard.Workflows.Models;
using Orchard.Workflows.Services;
namespace Orchard.Workflows.Activities {
public class MergeActivity : Task {
public MergeActivity() {
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public override bool CanExecute(WorkflowContext workflowContext, ActivityContext activityContext) {
return true;
}
public override IEnumerable<LocalizedString> GetPossibleOutcomes(WorkflowContext workflowContext, ActivityContext activityContext) {
yield return T("Done");
}
public override IEnumerable<LocalizedString> Execute(WorkflowContext workflowContext, ActivityContext activityContext) {
// wait for all incoming branches to trigger the Execute before returning the result
var branchesState = workflowContext.GetStateFor<string>(activityContext.Record, "Branches");
if (String.IsNullOrWhiteSpace(branchesState)) {
yield break;
}
var branches = GetBranches(branchesState);
var inboundActivities = workflowContext.GetInboundTransitions(activityContext.Record);
var done = inboundActivities
.All(x => branches.Contains(GetTransitionKey(x)));
if(done) {
yield return T("Done");
}
}
public override string Name {
get { return "MergeBranch"; }
}
public override LocalizedString Category {
get { return T("Flow"); }
}
public override LocalizedString Description {
get { return T("Merges multiple branches."); }
}
public override string Form {
get { return null; }
}
public override void OnActivityExecuted(WorkflowContext workflowContext, ActivityContext activityContext) {
// activity records pointed by the executed activity
var outboundActivities = workflowContext.GetOutboundTransitions(activityContext.Record);
// if a direct target of a Branch Activity is executed, then suppress all other direct waiting activities
var childBranches = outboundActivities
.Where(x => x.DestinationActivityRecord.Name == this.Name)
.ToList();
foreach (var childBranch in childBranches) {
var branchesState = workflowContext.GetStateFor<string>(childBranch.DestinationActivityRecord, "Branches");
var branches = GetBranches(branchesState);
branches = branches.Union(new[] { GetTransitionKey(childBranch)}).Distinct();
workflowContext.SetStateFor(childBranch.DestinationActivityRecord, "Branches", String.Join(",", branches.ToArray()));
}
}
private string GetTransitionKey(TransitionRecord transitionRecord) {
return "@" + transitionRecord.SourceActivityRecord.Id + "_" + transitionRecord.SourceEndpoint;
}
private IEnumerable<string> GetBranches(string branches) {
if (String.IsNullOrEmpty(branches)) {
return Enumerable.Empty<string>();
}
return branches.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
}
}
}

View File

@@ -62,7 +62,11 @@ namespace Orchard.Workflows.Activities {
// checking if user is in an accepted role
var workContext = _workContextAccessor.GetContext();
var user = workContext.CurrentUser;
var roles = GetRoles(context);
var roles = GetRoles(context).ToArray();
if (!roles.Any()) {
return true;
}
return UserIsInRole(user, roles);
}

View File

@@ -6,6 +6,7 @@ using Orchard.Data;
using Orchard.Forms.Services;
using Orchard.Localization;
using Orchard.Mvc;
using Orchard.Security;
using Orchard.UI.Notify;
using Orchard.Workflows.Activities;
using Orchard.Workflows.Models;
@@ -47,16 +48,7 @@ namespace Orchard.Workflows.Drivers {
var user = workContext.CurrentUser;
var awaiting = _awaitingActivityRepository.Table.Where(x => x.ContentItemRecord == part.ContentItem.Record && x.ActivityRecord.Name == "UserTask").ToList();
var actions = awaiting.Where(x => {
var state = FormParametersHelper.FromJsonString(x.ActivityRecord.State);
string rolesState = state.Roles ?? "";
var roles = rolesState.Split(',').Select(role => role.Trim());
return UserTaskActivity.UserIsInRole(user, roles);
}).SelectMany(x => {
var state = FormParametersHelper.FromJsonString(x.ActivityRecord.State);
string actionState = state.Actions ?? "";
return actionState.Split(',').Select(action => action.Trim());
}).ToList();
var actions = awaiting.Where(x => UserIsInRole(x, user)).SelectMany(ListAction).ToList();
return shapeHelper.UserTask_ActionButton().Actions(actions);
})
@@ -67,6 +59,26 @@ namespace Orchard.Workflows.Drivers {
return Combined(results.ToArray());
}
// returns all the actions associated with a specific state
private static IEnumerable<string> ListAction(AwaitingActivityRecord x) {
var state = FormParametersHelper.FromJsonString(x.ActivityRecord.State);
string actionState = state.Actions ?? "";
return actionState.Split(',').Select(action => action.Trim());
}
// whether a user is in an accepted role for this state
private static bool UserIsInRole(AwaitingActivityRecord x, IUser user) {
var state = FormParametersHelper.FromJsonString(x.ActivityRecord.State);
string rolesState = state.Roles ?? "";
// "Any" if string is empty
if (string.IsNullOrWhiteSpace(rolesState)) {
return true;
}
var roles = rolesState.Split(',').Select(role => role.Trim());
return UserTaskActivity.UserIsInRole(user, roles);
}
protected override DriverResult Editor(ContentPart part, IUpdateModel updater, dynamic shapeHelper) {
var httpContext = _httpContextAccessor.Current();
var name = httpContext.Request.Form["submit.Save"];
@@ -76,16 +88,7 @@ namespace Orchard.Workflows.Drivers {
var user = Services.WorkContext.CurrentUser;
var awaiting = _awaitingActivityRepository.Table.Where(x => x.ContentItemRecord == part.ContentItem.Record && x.ActivityRecord.Name == "UserTask").ToList();
var actions = awaiting.Where(x => {
var state = FormParametersHelper.FromJsonString(x.ActivityRecord.State);
string rolesState = state.Roles ?? "";
var roles = rolesState.Split(',').Select(role => role.Trim());
return UserTaskActivity.UserIsInRole(user, roles);
}).SelectMany(x => {
var state = FormParametersHelper.FromJsonString(x.ActivityRecord.State);
string actionState = state.Actions ?? "";
return actionState.Split(',').Select(action => action.Trim());
}).ToList();
var actions = awaiting.Where(x => UserIsInRole(x, user)).SelectMany(ListAction).ToList();
if (!actions.Contains(name)) {
Services.Notifier.Error(T("Not authorized to trigger {0}.", name));

View File

@@ -117,6 +117,7 @@
<ItemGroup>
<Compile Include="Activities\ContentActivity.cs" />
<Compile Include="Activities\BranchActivity.cs" />
<Compile Include="Activities\MergeBranchActivity.cs" />
<Compile Include="Activities\DeleteActivity.cs" />
<Compile Include="Activities\ExclusiveBranchActivity.cs" />
<Compile Include="Activities\IsInRoleActivity.cs" />
@@ -212,6 +213,12 @@
<ItemGroup>
<Content Include="Views\Activity-Timer.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Activity-ExclusiveBranch.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Activity-MergeBranch.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

View File

@@ -115,11 +115,6 @@ namespace Orchard.Workflows.Services {
return;
}
// load workflow definitions too for eager loading
_workflowDefinitionRepository.Table
.Where(x => x.Enabled && x.ActivityRecords.Any(e => e.Name == name))
.ToList();
// resume halted workflows
foreach (var awaitingActivityRecord in awaitingActivities) {
ResumeWorkflow(awaitingActivityRecord, workflowContext, tokens);
@@ -144,7 +139,7 @@ namespace Orchard.Workflows.Services {
// workflow halted, create a workflow state
var workflow = new WorkflowRecord {
WorkflowDefinitionRecord = activityRecord.WorkflowDefinitionRecord,
State = FormParametersHelper.ToJsonString("{}")
State = "{}"
};
workflowContext.Record = workflow;
@@ -196,10 +191,12 @@ namespace Orchard.Workflows.Services {
var workflow = awaitingActivityRecord.WorkflowRecord;
workflowContext.Record = workflow;
workflow.AwaitingActivities.Remove(awaitingActivityRecord);
var blockedOn = ExecuteWorkflow(workflowContext, awaitingActivityRecord.ActivityRecord, tokens).ToList();
// is the workflow halted on a blocking activity ?
if (!blockedOn.Any()) {
// is the workflow halted on a blocking activity, and there is no more awaiting activities
if (!blockedOn.Any() && !workflow.AwaitingActivities.Any()) {
// no, delete the workflow
_workflowRepository.Delete(awaitingActivityRecord.WorkflowRecord);
}
@@ -240,9 +237,20 @@ namespace Orchard.Workflows.Services {
firstPass = false;
}
var outcomes = activityContext.Activity.Execute(workflowContext, activityContext);
// signal every activity that the activity is about to be executed
var cancellationToken = new CancellationToken();
InvokeActivities(activity => activity.OnActivityExecuting(workflowContext, activityContext, cancellationToken));
if (cancellationToken.IsCancelled) {
// activity is aborted
continue;
}
var outcomes = activityContext.Activity.Execute(workflowContext, activityContext).ToList();
// signal every activity that the activity is executed
InvokeActivities(activity => activity.OnActivityExecuted(workflowContext, activityContext));
if (outcomes != null) {
foreach (var outcome in outcomes) {
// look for next activity in the graph
var transition = workflowContext.Record.WorkflowDefinitionRecord.TransitionRecords.FirstOrDefault(x => x.SourceActivityRecord == activityRecord && x.SourceEndpoint == outcome.TextHint);
@@ -252,7 +260,6 @@ namespace Orchard.Workflows.Services {
}
}
}
}
// apply Distinct() as two paths could block on the same activity
return blocking.Distinct();

View File

@@ -5,3 +5,20 @@
background-repeat: no-repeat;
background-position: center;
}
.exclusive-branch {
width: 36px;
height: 36px;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAYCAYAAADpnJ2CAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsIBFShKgAAAABp0RVh0U29mdHdhcmUAUGFpbnQuTkVUIHYzLjUuMTAw9HKhAAAAoklEQVRIS+2TzQnAIAyF3ckFXMGxnMF9nMBVPHlIa0GQ+Kh/baHg4bvEZ14SjSCiLqy1JKWEGGNOCb7HgUFOjFForaFZQilFIQR4lwODnG24DUtgEPHpWpTkbke6KoHBO/5t6L2Hb7SCc+5MDQxbP3EWPonKsDWqVV1TwPmfYe9CP6W7qkCHiVxdrhZpEiO6mUSCMaTbht2J3jSsGNF9vBaGDk3NF8LuD+uFAAAAAElFTkSuQmCC');
background-repeat: no-repeat;
background-position: center;
}
.merge-branch {
width: 36px;
height: 36px;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAYCAYAAADpnJ2CAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsIBFShKgAAAABp0RVh0U29mdHdhcmUAUGFpbnQuTkVUIHYzLjUuMTAw9HKhAAAAk0lEQVRIS+2T0QmAIBRF304u4AqO5Qzu4wSu4pcflsGjkmsqSlAYnJ/btWPiI611FEJAjDFxfygxq0dSSvgyoZSKIQTy3k/rLeESHnQJXx8LLvLueLec54z2qoWc7wvR2Y9QFCastXDRCM65/dOn4yZsofVIS8Dwif8KWwe/Bgxzaje4529hmLOES3gFhog5YxFpA8qKN6JZxNXVAAAAAElFTkSuQmCC');
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" title="First of @outcomes">
<div class="exclusive-branch" ></div>
@*@name.CamelFriendly()*@
</div>

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 title="Merge">
<div class="merge-branch" ></div>
@*@name.CamelFriendly()*@
</div>

View File

@@ -146,12 +146,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UpgradeTo16", "Orchard.Web\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Workflows", "Orchard.Web\Modules\Orchard.Workflows\Orchard.Workflows.csproj", "{7059493C-8251-4764-9C1E-2368B8B485BC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Caching", "Orchard.Web\Modules\Orchard.Caching\Orchard.Caching.csproj", "{7528BF74-25C7-4ABE-883A-443B4EEC4776}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Memcached", "Orchard.Web\Modules\Memcached\Memcached.csproj", "{4A037ACB-A79A-43A9-9E7D-E8F1BF7AEB91}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Caching.Memcached", "Orchard.Web\Modules\Orchard.Caching.Memcached\Orchard.Caching.Memcached.csproj", "{A2C5BAE0-E4A0-4B41-BC44-D4099E471111}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
CodeCoverage|Any CPU = CodeCoverage|Any CPU
@@ -823,13 +819,6 @@ Global
{7059493C-8251-4764-9C1E-2368B8B485BC}.FxCop|Any CPU.ActiveCfg = Release|Any CPU
{7059493C-8251-4764-9C1E-2368B8B485BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7059493C-8251-4764-9C1E-2368B8B485BC}.Release|Any CPU.Build.0 = Release|Any CPU
{7528BF74-25C7-4ABE-883A-443B4EEC4776}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
{7528BF74-25C7-4ABE-883A-443B4EEC4776}.Coverage|Any CPU.ActiveCfg = Release|Any CPU
{7528BF74-25C7-4ABE-883A-443B4EEC4776}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7528BF74-25C7-4ABE-883A-443B4EEC4776}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7528BF74-25C7-4ABE-883A-443B4EEC4776}.FxCop|Any CPU.ActiveCfg = Release|Any CPU
{7528BF74-25C7-4ABE-883A-443B4EEC4776}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7528BF74-25C7-4ABE-883A-443B4EEC4776}.Release|Any CPU.Build.0 = Release|Any CPU
{4A037ACB-A79A-43A9-9E7D-E8F1BF7AEB91}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
{4A037ACB-A79A-43A9-9E7D-E8F1BF7AEB91}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
{4A037ACB-A79A-43A9-9E7D-E8F1BF7AEB91}.Coverage|Any CPU.ActiveCfg = Release|Any CPU
@@ -840,13 +829,6 @@ Global
{4A037ACB-A79A-43A9-9E7D-E8F1BF7AEB91}.FxCop|Any CPU.Build.0 = Release|Any CPU
{4A037ACB-A79A-43A9-9E7D-E8F1BF7AEB91}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A037ACB-A79A-43A9-9E7D-E8F1BF7AEB91}.Release|Any CPU.Build.0 = Release|Any CPU
{A2C5BAE0-E4A0-4B41-BC44-D4099E471111}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
{A2C5BAE0-E4A0-4B41-BC44-D4099E471111}.Coverage|Any CPU.ActiveCfg = Release|Any CPU
{A2C5BAE0-E4A0-4B41-BC44-D4099E471111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A2C5BAE0-E4A0-4B41-BC44-D4099E471111}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A2C5BAE0-E4A0-4B41-BC44-D4099E471111}.FxCop|Any CPU.ActiveCfg = Release|Any CPU
{A2C5BAE0-E4A0-4B41-BC44-D4099E471111}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A2C5BAE0-E4A0-4B41-BC44-D4099E471111}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -902,9 +884,7 @@ Global
{3BD22132-D538-48C6-8854-F71333C798EB} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{8A9FDB57-342D-49C2-BAFC-D885AAE5CC7C} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{7059493C-8251-4764-9C1E-2368B8B485BC} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{7528BF74-25C7-4ABE-883A-443B4EEC4776} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{4A037ACB-A79A-43A9-9E7D-E8F1BF7AEB91} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{A2C5BAE0-E4A0-4B41-BC44-D4099E471111} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{ABC826D4-2FA1-4F2F-87DE-E6095F653810} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}
{F112851D-B023-4746-B6B1-8D2E5AD8F7AA} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}
{6CB3EB30-F725-45C0-9742-42599BA8E8D2} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}