diff --git a/Examples/Projects/Project/Steps/Start.yaml b/Examples/Projects/Project/Steps/Start.yaml index 947e749..6cbe26b 100644 --- a/Examples/Projects/Project/Steps/Start.yaml +++ b/Examples/Projects/Project/Steps/Start.yaml @@ -1,5 +1,8 @@ name: Start +condition: + deadline: addMonths(CreateDate, 2) + actions: - roles: [Student] type: Submit diff --git a/UvA.Workflow.Api/WorkflowInstances/Dtos/WorkflowInstanceDto.cs b/UvA.Workflow.Api/WorkflowInstances/Dtos/WorkflowInstanceDto.cs index dcec11c..3b36389 100644 --- a/UvA.Workflow.Api/WorkflowInstances/Dtos/WorkflowInstanceDto.cs +++ b/UvA.Workflow.Api/WorkflowInstances/Dtos/WorkflowInstanceDto.cs @@ -24,15 +24,22 @@ RoleAction[] Permissions public record FieldDto(); -public record StepDto(string Id, BilingualString Title, string? Event, DateTime? DateCompleted, StepDto[]? Children) +public record StepDto( + string Id, + BilingualString Title, + string? Event, + DateTime? DateCompleted, + DateTime? Deadline, + StepDto[]? Children) { - public static StepDto Create(Step step, WorkflowInstance instance) + public static StepDto Create(Step step, WorkflowInstance instance, ModelService modelService) => new( step.Name, step.DisplayTitle, step.EndEvent, step.GetEndDate(instance), - step.Children.Length != 0 ? step.Children.Select(s => Create(s, instance)).ToArray() : null + step.GetDeadline(instance, modelService), + step.Children.Length != 0 ? step.Children.Select(s => Create(s, instance, modelService)).ToArray() : null ); } diff --git a/UvA.Workflow.Api/WorkflowInstances/Dtos/WorkflowInstanceDtoFactory.cs b/UvA.Workflow.Api/WorkflowInstances/Dtos/WorkflowInstanceDtoFactory.cs index 26ad20f..cec5b9f 100644 --- a/UvA.Workflow.Api/WorkflowInstances/Dtos/WorkflowInstanceDtoFactory.cs +++ b/UvA.Workflow.Api/WorkflowInstances/Dtos/WorkflowInstanceDtoFactory.cs @@ -27,7 +27,7 @@ public async Task Create(WorkflowInstance instance, Cancell instance.ParentId, actions.Select(ActionDto.Create).ToArray(), [], - entityType.Steps.Select(s => StepDto.Create(s, instance)).ToArray(), + entityType.Steps.Select(s => StepDto.Create(s, instance, modelService)).ToArray(), submissions .Select(s => submissionDtoFactory.Create(instance, s.Form, s.Event, s.QuestionStatus)) .ToArray(), diff --git a/UvA.Workflow.Tests/Builders/EventBuilder.cs b/UvA.Workflow.Tests/Builders/EventBuilder.cs index afd1909..11a45e2 100644 --- a/UvA.Workflow.Tests/Builders/EventBuilder.cs +++ b/UvA.Workflow.Tests/Builders/EventBuilder.cs @@ -38,7 +38,7 @@ public EventBuilder AsCompleted(string completionDateString) /// The updated instance with the completion date set. public EventBuilder AsCompleted(int daysAgo = 1) { - date = DateTime.UtcNow.AddDays(daysAgo); + date = DateTime.Now.AddDays(daysAgo); return this; } diff --git a/UvA.Workflow.Tests/ExpressionTests.cs b/UvA.Workflow.Tests/ExpressionTests.cs index 01b6345..c7b1409 100644 --- a/UvA.Workflow.Tests/ExpressionTests.cs +++ b/UvA.Workflow.Tests/ExpressionTests.cs @@ -60,4 +60,56 @@ public void TestCondition() Assert.Single(exp.Properties, p => p is ComplexLookup); } + + [Fact] + public void TestDate_DaysAfter() + { + var exp = ExpressionParser.Parse("addDays(now, 5)"); + var context = new ObjectContext(new Dictionary()); + + var res = exp.Execute(context); + + Assert.IsType(res); + var date = (DateTime)res; + Assert.Equal(DateTime.Now.AddDays(5).Date, date.Date); + } + + [Fact] + public void TestDate_WeeksAfter() + { + var exp = ExpressionParser.Parse("addWeeks(now, 5)"); + var context = new ObjectContext(new Dictionary()); + + var res = exp.Execute(context); + + Assert.IsType(res); + var date = (DateTime)res; + Assert.Equal(DateTime.Now.AddDays(5 * 7).Date, date.Date); + } + + [Fact] + public void TestDate_MonthsAfter() + { + var exp = ExpressionParser.Parse("addMonths(now, 5)"); + var context = new ObjectContext(new Dictionary()); + + var res = exp.Execute(context); + + Assert.IsType(res); + var date = (DateTime)res; + Assert.Equal(DateTime.Now.AddMonths(5).Date, date.Date); + } + + [Fact] + public void TestDate_CombinedAfter() + { + var exp = ExpressionParser.Parse("addDays(addMonths(now, 5), 3)"); + var context = new ObjectContext(new Dictionary()); + + var res = exp.Execute(context); + + Assert.IsType(res); + var date = (DateTime)res; + Assert.Equal(DateTime.Now.AddMonths(5).AddDays(3).Date, date.Date); + } } \ No newline at end of file diff --git a/UvA.Workflow/Expressions/Expression.cs b/UvA.Workflow/Expressions/Expression.cs index 1debae0..d8fe42b 100644 --- a/UvA.Workflow/Expressions/Expression.cs +++ b/UvA.Workflow/Expressions/Expression.cs @@ -23,6 +23,8 @@ public record Expression private static readonly Dictionary Functions = new() { ["addDays"] = new Function((d, i) => d?.AddDays(i)), + ["addMonths"] = new Function((d, i) => d?.AddMonths(i)), + ["addWeeks"] = new Function((d, i) => d?.AddDays(7 * i)), ["if"] = new Function((b, t1, t2) => b ? t1 : t2), ["contains"] = new Function, object, bool>((a, o) => a?.Contains(o) == true), ["and"] = new Function((a, b) => a && b), diff --git a/UvA.Workflow/WorkflowInstances/WorkflowInstance.cs b/UvA.Workflow/WorkflowInstances/WorkflowInstance.cs index 24beb1b..0d7c4c0 100644 --- a/UvA.Workflow/WorkflowInstances/WorkflowInstance.cs +++ b/UvA.Workflow/WorkflowInstances/WorkflowInstance.cs @@ -16,6 +16,8 @@ public class WorkflowInstance public string EntityType { get; set; } = null!; public string? CurrentStep { get; set; } + public DateTime CreatedOn { get; set; } + // public List LogEntries { get; set; } = []; public Dictionary Properties { get; set; } = null!; public Dictionary Events { get; set; } = null!; @@ -75,7 +77,7 @@ public void RecordEvent(string eventId, DateTime? date = null) Events[eventId] = new InstanceEvent { Id = eventId, - Date = date ?? DateTime.UtcNow + Date = date ?? DateTime.Now }; } diff --git a/UvA.Workflow/WorkflowInstances/WorkflowInstanceService.cs b/UvA.Workflow/WorkflowInstances/WorkflowInstanceService.cs index 84fef5d..7f193b5 100644 --- a/UvA.Workflow/WorkflowInstances/WorkflowInstanceService.cs +++ b/UvA.Workflow/WorkflowInstances/WorkflowInstanceService.cs @@ -22,6 +22,7 @@ public async Task Create( { EntityType = entityType, ParentId = parentId, + CreatedOn = DateTime.Now, Properties = initialProperties ?? new Dictionary(), Events = new Dictionary() }; diff --git a/UvA.Workflow/WorkflowModel/Conditions/Condition.cs b/UvA.Workflow/WorkflowModel/Conditions/Condition.cs index 30381b1..2fd947e 100644 --- a/UvA.Workflow/WorkflowModel/Conditions/Condition.cs +++ b/UvA.Workflow/WorkflowModel/Conditions/Condition.cs @@ -32,6 +32,11 @@ public class Condition /// public Date? Date { get; set; } + /// + /// Check if a deadline has passed + /// + public Date? Deadline { get; set; } + /// /// Use a named reusable condition /// @@ -44,7 +49,7 @@ public class Condition [JsonIgnore] [YamlIgnore] public Condition? NamedCondition { get; set; } - public ConditionPart Part => Value ?? Logical ?? Date ?? Event ?? NamedCondition?.Part!; + public ConditionPart Part => Value ?? Logical ?? Date ?? Deadline ?? Event ?? NamedCondition?.Part!; public IEnumerable Properties => Part?.Properties ?? []; diff --git a/UvA.Workflow/WorkflowModel/ObjectContext.cs b/UvA.Workflow/WorkflowModel/ObjectContext.cs index 040ca92..239d1ea 100644 --- a/UvA.Workflow/WorkflowModel/ObjectContext.cs +++ b/UvA.Workflow/WorkflowModel/ObjectContext.cs @@ -39,6 +39,8 @@ public static ObjectContext Create(WorkflowInstance instance, ModelService model ); dict.Add("Id", instance.Id); dict.Add("CurrentStep", instance.CurrentStep); + dict.Add("CreateDate", instance.CreatedOn); + foreach (var ev in instance.Events.Values.Where(e => e.Date != null)) dict.Add(ev.Id + "Event", ev.Date); return new ObjectContext(dict); diff --git a/UvA.Workflow/WorkflowModel/Step.cs b/UvA.Workflow/WorkflowModel/Step.cs index 899f077..1cfa7e3 100644 --- a/UvA.Workflow/WorkflowModel/Step.cs +++ b/UvA.Workflow/WorkflowModel/Step.cs @@ -1,3 +1,6 @@ +using UvA.Workflow.Expressions; +using Date = UvA.Workflow.Entities.Domain.Conditions.Date; + namespace UvA.Workflow.Entities.Domain; public enum StepHierarchyMode @@ -77,6 +80,15 @@ public class Step return null; } + public DateTime? GetDeadline(WorkflowInstance instance, ModelService modelService) + { + if (Condition?.Deadline == null) + return null; + var exp = ExpressionParser.Parse(Condition.Deadline.Source); + var context = ObjectContext.Create(instance, modelService); + return exp.Execute(context) as DateTime?; + } + public bool HasEnded(ObjectContext context) { if (Ends != null)