public IHttpActionResult GetNodePendingTasks(int id) { // id will be 0 when creating a new page - id is assigned after save if (id == 0) { return(Json(new { settings = false, noFlow = false }, ViewHelpers.CamelCase)); } try { WorkflowTaskPoco currentTask = _tasksService.GetTasksByNodeId(id).FirstOrDefault(); return(Json(new { items = currentTask != null && currentTask.TaskStatus.In(TaskStatus.PendingApproval, TaskStatus.Rejected) ? _tasksService.ConvertToWorkflowTaskList(new List <WorkflowTaskPoco> { currentTask }) : new List <WorkflowTaskViewModel>() }, ViewHelpers.CamelCase)); } catch (Exception ex) { string msg = Constants.ErrorGettingPendingTasksForNode.Replace("{id}", id.ToString()); Log.Error(msg, ex); return(Content(HttpStatusCode.InternalServerError, ViewHelpers.ApiException(ex, msg))); } }
/// <summary> /// Create simple html markup for an inactive workflow task. /// </summary> /// <param name="taskInstance">The task instance.</param> /// <param name="index"></param> /// <returns>HTML markup describing an active task instance.</returns> public static string BuildTaskSummary(this WorkflowTaskPoco taskInstance) { var result = ""; switch (taskInstance.Status) { case (int)TaskStatus.Approved: case (int)TaskStatus.Rejected: case (int)TaskStatus.Cancelled: if (taskInstance.CompletedDate != null) { result += $"Stage {taskInstance.ApprovalStep + 1}: {taskInstance.StatusName} by {taskInstance.ActionedByUser.Name} on {taskInstance.CompletedDate.Value:dd/MM/yy}"; } if (taskInstance.Comment.HasValue()) { result += $"<br/> Comment: <i>{taskInstance.Comment}</i>"; } break; case (int)TaskStatus.NotRequired: result += $"Stage {taskInstance.ApprovalStep + 1}: Not required"; break; } return(result); }
/// <summary> /// Maps Users to the UserGroup property of a WorkflowTaskInstance /// </summary> /// <param name="task"></param> /// <param name="instance"></param> /// <param name="userGroup"></param> /// <returns></returns> public WorkflowInstancePoco MapIt(WorkflowInstancePoco instance, WorkflowTaskPoco task, UserGroupPoco userGroup) { if (instance == null) { return(_current); } if (userGroup.GroupId == task.GroupId) { task.UserGroup = userGroup; } if (_current != null && _current.Guid == instance.Guid) { if (_current.TaskInstances.All(t => t.ApprovalStep != task.ApprovalStep)) { _current.TaskInstances.Add(task); } return(null); } WorkflowInstancePoco prev = _current; _current = instance; _current.TaskInstances.Add(task); return(prev); }
public static EmailType?ProcessApproval(this WorkflowTaskPoco taskInstance, WorkflowAction action, int userId, string comment) { EmailType?emailAction = null; switch (action) { case WorkflowAction.Approve: if (taskInstance.TaskStatus != TaskStatus.NotRequired) { taskInstance.Status = (int)TaskStatus.Approved; emailAction = EmailType.ApprovalRequest; } break; case WorkflowAction.Reject: taskInstance.Status = (int)TaskStatus.Rejected; emailAction = EmailType.ApprovalRejection; break; } taskInstance.CompletedDate = DateTime.Now; taskInstance.Comment = comment; taskInstance.ActionedByUserId = userId; taskInstance.ActionedByAdmin = ActionedByAdmin(taskInstance, userId); return(emailAction); }
/// <summary> /// Set the appropriate properties to indicate the task has been cancelled /// </summary> /// <param name="taskInstance"></param> /// <param name="userId"></param> /// <param name="reason"></param> /// <param name="completedDate"></param> public static void Cancel(this WorkflowTaskPoco taskInstance, int userId, string reason, DateTime?completedDate) { taskInstance.Status = (int)TaskStatus.Cancelled; taskInstance.ActionedByUserId = userId; taskInstance.Comment = reason; taskInstance.CompletedDate = completedDate; taskInstance.ActionedByAdmin = ActionedByAdmin(taskInstance, userId); }
/// <summary> /// Adds an approval task to this workflow instance, setting the approval step and instance guid /// </summary> /// <param name="instance"></param> public static WorkflowTaskPoco CreateApprovalTask(this WorkflowInstancePoco instance) { var taskInstance = new WorkflowTaskPoco(TaskType.Approve) { ApprovalStep = instance.TaskInstances.Count(x => x.TaskStatus.In(TaskStatus.Approved, TaskStatus.NotRequired)), WorkflowInstanceGuid = instance.Guid }; instance.TaskInstances.Add(taskInstance); return(taskInstance); }
public void Can_Get_Summary_String(TaskStatus status) { var taskInstance = new WorkflowTaskPoco { WorkflowInstanceGuid = Guid.NewGuid(), ApprovalStep = 1, CreatedDate = DateTime.Now.AddDays(-10), CompletedDate = DateTime.Now, Comment = Utility.RandomString(), Status = (int)status, ActionedByUserId = 0 }; string summary = taskInstance.BuildTaskSummary(true); Assert.NotNull(summary); }
public void Can_Process_Task(WorkflowAction action, int userId, string comment, EmailType expected) { var taskInstance = new WorkflowTaskPoco { WorkflowInstanceGuid = Guid.NewGuid(), ApprovalStep = 1, CreatedDate = DateTime.Now, Status = (int)TaskStatus.PendingApproval }; EmailType?emailType = taskInstance.ProcessApproval(action, userId, comment); Assert.Equal(expected, emailType.Value); Assert.Equal(comment, taskInstance.Comment); Assert.Equal(userId, taskInstance.ActionedByUserId); }
public void Can_Cancel_Task(int userId, string comment) { var taskInstance = new WorkflowTaskPoco { WorkflowInstanceGuid = Guid.NewGuid(), ApprovalStep = 1, CreatedDate = DateTime.Now.AddDays(-2), Status = (int)TaskStatus.PendingApproval }; DateTime cancelledAt = DateTime.Now.AddDays(-1); taskInstance.Cancel(userId, comment, cancelledAt); Assert.Equal(comment, taskInstance.Comment); Assert.Equal(userId, taskInstance.ActionedByUserId); Assert.Equal(TaskStatus.Cancelled, taskInstance.TaskStatus); }
public void Can_Update_Task_And_Raise_Event() { const string comment = "Comment has been updated"; TasksService.Updated += (sender, args) => { Assert.NotNull(args); Assert.IsAssignableFrom <WorkflowTaskPoco>(args.Task); Assert.Equal(comment, args.Task.Comment); }; WorkflowTaskPoco task = Scaffold.Task(); _service.InsertTask(task); task.Comment = comment; _service.UpdateTask(task); }
public async void Cannot_Validate_Request_When_Last_Task_Not_Pending() { Guid guid = Guid.NewGuid(); const int userId = 446; const int nodeId = 3456; UserGroupPoco group = await AddGroupWithPermissionAndUser(userId, nodeId); // create a task on an instance WorkflowTaskPoco task = Scaffold.Task(guid, groupId: group.GroupId, status: (int)TaskStatus.NotRequired); _tasksService.InsertTask(task); _instancesService.InsertInstance(Scaffold.Instance(guid, 1, nodeId)); bool isValid = await _previewService.Validate(nodeId, userId, task.Id, guid); Assert.False(isValid); }
public async void Can_Validate_Request() { Guid guid = Guid.NewGuid(); const int userId = 11; const int nodeId = 1089; UserGroupPoco group = await AddGroupWithPermissionAndUser(userId, nodeId); // create a task on an instance WorkflowTaskPoco task = Scaffold.Task(guid, groupId: group.GroupId); _tasksService.InsertTask(task); _instancesService.InsertInstance(Scaffold.Instance(guid, 1, nodeId)); // is valid when the user is in the group responsible for the task with the given id // and the task belongs to the given instance by guid // and both the task and instance are related to the given node id bool isValid = await _previewService.Validate(nodeId, userId, task.Id, guid); Assert.True(isValid); // invalid user id isValid = await _previewService.Validate(nodeId, 99, task.Id, guid); Assert.False(isValid); // invalid task id isValid = await _previewService.Validate(nodeId, userId, 11111, guid); Assert.False(isValid); // invalid guid isValid = await _previewService.Validate(nodeId, userId, task.Id, Guid.NewGuid()); Assert.False(isValid); // invalid node id isValid = await _previewService.Validate(43535, userId, task.Id, guid); Assert.False(isValid); }
/// <summary> /// Delete from /app_plugins/workflow/preview /// </summary> /// <param name="nodeId"></param> /// <param name="userId"></param> /// <param name="taskId"></param> /// <param name="guid"></param> public async Task <bool> Validate(int nodeId, int userId, int taskId, Guid guid) { List <WorkflowTaskPoco> taskInstances = _tasksService.GetTasksByNodeId(nodeId); if (!taskInstances.Any() || taskInstances.Last().TaskStatus == TaskStatus.Cancelled) { return(false); } // only interested in last active task WorkflowTaskPoco activeTask = taskInstances.OrderBy(t => t.Id).LastOrDefault(t => t.TaskStatus.In(TaskStatus.PendingApproval, TaskStatus.Rejected)); if (activeTask == null) { return(false); } UserGroupPoco group = await _groupService.GetPopulatedUserGroupAsync(activeTask.GroupId); // only valid if the task belongs to the current workflow, and the user is in the current group, and the task id is correct return(activeTask.WorkflowInstanceGuid == guid && group.Users.Any(u => u.UserId == userId) && activeTask.Id == taskId); }
public override void Up() { //Don't exeucte if the column is already there ColumnInfo[] columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); if (columns.Any(x => x.TableName.InvariantEquals("WorkflowInstance") && x.ColumnName.InvariantEquals("CompletedDate"))) { return; } // column doesn't exist, add it and populate the completed date for any existing instances Create.Column("CompletedDate").OnTable("WorkflowInstance").AsDateTime().Nullable(); // once the column has been added, check for any instances where status is not active, find the last task, and set complete date to match // this only impacts on charting, but allows more complete history as instances didn't previously store a completion date List <WorkflowInstancePoco> instances = InstancesService.GetAll() .Where(x => x.Status == (int)WorkflowStatus.Approved || x.Status == (int)WorkflowStatus.Cancelled) .ToList(); if (!instances.Any()) { return; } foreach (WorkflowInstancePoco instance in instances) { if (!instance.TaskInstances.Any()) { continue; } WorkflowTaskPoco finalTask = instance.TaskInstances.OrderBy(x => x.Id).Last(); instance.CompletedDate = finalTask.CompletedDate; Context.Database.Update(instance); } }
/// <summary> /// /// </summary> /// <param name="poco"></param> public void UpdateTask(WorkflowTaskPoco poco) { _database.Update(poco); }
/// <summary> /// check if user is a member of the group, or is acting as an admin, then set flag /// If the usergroup doesn't exist, just return false - if that's the case, we have bigger problems... /// </summary> /// <param name="taskInstance"></param> /// <param name="userId"></param> /// <returns></returns> private static bool ActionedByAdmin(WorkflowTaskPoco taskInstance, int userId) { return(!taskInstance.UserGroup?.UsersSummary.Contains($"|{userId}|") ?? false); }
/// <summary> /// /// </summary> /// <param name="poco"></param> /// <returns></returns> public void UpdateTask(WorkflowTaskPoco poco) { _tasksRepo.UpdateTask(poco); Updated?.Invoke(this, new TaskEventArgs(poco)); }
/// <summary> /// /// </summary> /// <param name="poco"></param> public void InsertTask(WorkflowTaskPoco poco) { _tasksRepo.InsertTask(poco); Created?.Invoke(this, new TaskEventArgs(poco)); }
/// <summary> /// /// </summary> /// <param name="id"></param> /// <returns></returns> public WorkflowTaskViewModel GetTask(int id) { WorkflowTaskPoco task = _tasksRepo.Get(id); return(ConvertToWorkflowTaskList(task.AsEnumerableOfOne().ToList()).FirstOrDefault()); }
/// <summary> /// Sends an email notification out for the workflow process /// </summary> /// <param name="instance"></param> /// <param name="emailType">the type of email to be sent</param> public async void Send(WorkflowInstancePoco instance, EmailType emailType) { WorkflowSettingsPoco settings = _settingsService.GetSettings(); if (!settings.SendNotifications) { return; } if (!instance.TaskInstances.Any()) { instance.TaskInstances = _tasksService.GetTasksWithGroupByInstanceGuid(instance.Guid); } if (!instance.TaskInstances.Any()) { Log.Error($"Notifications not sent - no tasks exist for instance { instance.Id }"); return; } WorkflowTaskPoco finalTask = null; try { string docTitle = instance.Node.Name; string docUrl = UrlHelpers.GetFullyQualifiedContentEditorUrl(instance.NodeId); WorkflowTaskPoco[] flowTasks = instance.TaskInstances.OrderBy(t => t.ApprovalStep).ToArray(); // always take get the emails for all previous users, sometimes they will be discarded later // easier to just grab em all, rather than doing so conditionally List <string> emailsForAllTaskUsers = new List <string>(); // in the loop, also store the last task to a variable, and keep the populated group var taskIndex = 0; int taskCount = flowTasks.Length; foreach (WorkflowTaskPoco task in flowTasks) { taskIndex += 1; UserGroupPoco group = await _groupService.GetPopulatedUserGroupAsync(task.GroupId); if (group == null) { continue; } emailsForAllTaskUsers.AddRange(group.PreferredEmailAddresses()); if (taskIndex != taskCount) { continue; } finalTask = task; finalTask.UserGroup = group; } if (finalTask == null) { Log.Error("No valid task found for email notifications"); return; } List <string> to = new List <string>(); var body = ""; string typeDescription = instance.WorkflowType.Description(instance.ScheduledDate); string typeDescriptionPast = instance.WorkflowType.DescriptionPastTense(instance.ScheduledDate); switch (emailType) { case EmailType.ApprovalRequest: to = finalTask.UserGroup.PreferredEmailAddresses(); body = string.Format(EmailApprovalRequestString, to.Count > 1 ? "Umbraco user" : finalTask.UserGroup.Name, docUrl, docTitle, instance.AuthorComment, instance.AuthorUser.Name, typeDescription, string.Empty); break; case EmailType.ApprovalRejection: to = emailsForAllTaskUsers; to.Add(instance.AuthorUser.Email); body = string.Format(EmailRejectedString, "Umbraco user", docUrl, docTitle, finalTask.Comment, finalTask.ActionedByUser.Name, typeDescription.ToLower()); break; case EmailType.ApprovedAndCompleted: to = emailsForAllTaskUsers; to.Add(instance.AuthorUser.Email); //Notify web admins to.Add(settings.Email); if (instance.WorkflowType == WorkflowType.Publish) { IPublishedContent n = _utility.GetPublishedContent(instance.NodeId); docUrl = UrlHelpers.GetFullyQualifiedSiteUrl(n.Url); } body = string.Format(EmailApprovedString, "Umbraco user", docUrl, docTitle, typeDescriptionPast.ToLower()) + "<br/>"; body += instance.BuildProcessSummary(); break; case EmailType.ApprovedAndCompletedForScheduler: to = emailsForAllTaskUsers; to.Add(instance.AuthorUser.Email); body = string.Format(EmailApprovedString, "Umbraco user", docUrl, docTitle, typeDescriptionPast.ToLower()) + "<br/>"; body += instance.BuildProcessSummary(); break; case EmailType.WorkflowCancelled: to = emailsForAllTaskUsers; // include the initiator email to.Add(instance.AuthorUser.Email); body = string.Format(EmailCancelledString, "Umbraco user", typeDescription, docUrl, docTitle, finalTask.ActionedByUser.Name, finalTask.Comment); break; case EmailType.SchedulerActionCancelled: break; default: throw new ArgumentOutOfRangeException(nameof(emailType), emailType, null); } if (!to.Any()) { return; } var client = new SmtpClient(); var msg = new MailMessage { Subject = $"{emailType.ToString().ToTitleCase()} - {instance.Node.Name} ({typeDescription})", IsBodyHtml = true, }; if (settings.Email.HasValue()) { msg.From = new MailAddress(settings.Email); } // if offline is permitted, email group members individually as we need the user id in the url if (emailType == EmailType.ApprovalRequest && finalTask.UserGroup.OfflineApproval) { foreach (User2UserGroupPoco user in finalTask.UserGroup.Users) { string offlineString = string.Format(EmailOfflineApprovalString, settings.SiteUrl, instance.NodeId, user.UserId, finalTask.Id, instance.Guid); body = string.Format(EmailApprovalRequestString, user.User.Name, docUrl, docTitle, instance.AuthorComment, instance.AuthorUser.Name, typeDescription, offlineString); msg.To.Clear(); msg.To.Add(user.User.Email); msg.Body = string.Format(EmailBody, msg.Subject, body); client.Send(msg); } } else { msg.To.Add(string.Join(",", to.Distinct())); msg.Body = string.Format(EmailBody, msg.Subject, body); client.Send(msg); } Log.Info($"Email notifications sent for task { finalTask.Id }, to { msg.To }"); } catch (Exception e) { Log.Error($"Error sending notifications for task { finalTask.Id }", e); } }
/// <summary> /// Sends an email notification out for the workflow process /// </summary> /// <param name="instance"></param> /// <param name="emailType">the type of email to be sent</param> /// <param name="errorDetail"></param> public async Task <string> Send(WorkflowInstancePoco instance, EmailType emailType, string errorDetail = "") { var msg = new MailMessage(); if (!_settings.SendNotifications) { return(null); } if (!instance.TaskInstances.Any()) { instance.TaskInstances = _tasksService.GetTasksWithGroupByInstanceGuid(instance.Guid); } if (!instance.TaskInstances.Any()) { Log.Error($"Notifications not sent - no tasks exist for instance { instance.Id }"); return(null); } try { WorkflowTaskPoco[] flowTasks = instance.TaskInstances.OrderBy(t => t.ApprovalStep).ToArray(); // always take get the emails for all previous users, sometimes they will be discarded later // easier to just grab em all, rather than doing so conditionally List <string> emailsForAllTaskUsers = new List <string>(); // in the loop, also store the last task to a variable, and keep the populated group var taskIndex = 0; int taskCount = flowTasks.Length; foreach (WorkflowTaskPoco task in flowTasks) { taskIndex += 1; UserGroupPoco group = await _groupService.GetPopulatedUserGroupAsync(task.GroupId); if (group == null) { continue; } emailsForAllTaskUsers.AddRange(group.PreferredEmailAddresses()); if (taskIndex != taskCount) { continue; } _finalTask = task; _finalTask.UserGroup = group; } if (_finalTask == null) { Log.Error("No valid task found for email notifications"); return(null); } // populate list of recipients List <string> to = GetRecipients(emailType, instance, emailsForAllTaskUsers); if (!to.Any()) { return(null); } string body = GetBody(emailType, instance, out string typeDescription, errorDetail); var client = new SmtpClient(); msg = new MailMessage { Subject = $"{emailType.ToString().ToTitleCase()} - {instance.Node.Name} ({typeDescription})", IsBodyHtml = true, Body = string.Format(EmailBody, msg.Subject, body) }; if (_settings.Email.HasValue()) { msg.From = new MailAddress(_settings.Email); } // if offline is permitted, email group members individually as we need the user id in the url if (emailType == EmailType.ApprovalRequest && _finalTask.UserGroup.OfflineApproval) { string docTitle = instance.Node.Name; string docUrl = UrlHelpers.GetFullyQualifiedContentEditorUrl(instance.NodeId); foreach (User2UserGroupPoco user in _finalTask.UserGroup.Users) { var msgBody = body + string.Format(EmailOfflineApprovalString, _settings.SiteUrl, instance.NodeId, user.UserId, _finalTask.Id, instance.Guid); msg.Body = string.Format(EmailBody, msg.Subject, msgBody); msg.To.Clear(); msg.To.Add(user.User.Email); client.Send(msg); } } else { msg.To.Add(string.Join(",", to.Distinct())); client.Send(msg); } Log.Info($"Email notifications sent for task { _finalTask.Id }, to { msg.To }"); } catch (Exception e) { Log.Error($"Error sending notifications for task { _finalTask.Id }", e); } return(msg.Body); }
public TaskEventArgs(WorkflowTaskPoco task) { Task = task; }
/// <summary> /// /// </summary> /// <param name="poco"></param> public void InsertTask(WorkflowTaskPoco poco) { _database.Insert(poco); }