/// <summary> /// Validates the parent object. /// </summary> public WatchShiftValidator() { RuleFor(x => x.Points).GreaterThanOrEqualTo(0); RuleFor(x => x.ShiftType).NotEmpty(); RuleFor(x => x.Title).NotEmpty().Length(1, 50); #pragma warning disable CS0618 // Type or member is obsolete Custom(watchShift => { if (watchShift.Range.Start == default(DateTime) || watchShift.Range.End == default(DateTime)) { return(new FluentValidation.Results.ValidationFailure(PropertySelector.SelectPropertyFrom <WatchShift>(x => x.Range).Name, "The watch shift's range dates must make sense. Please.")); } if (watchShift.Range.Start >= watchShift.Range.End) { return(new FluentValidation.Results.ValidationFailure(PropertySelector.SelectPropertyFrom <WatchShift>(x => x.Range).Name, "The watch shift's range dates must make sense. Please.")); } if (watchShift.Range.End <= watchShift.Range.Start) { return(new FluentValidation.Results.ValidationFailure(PropertySelector.SelectPropertyFrom <WatchShift>(x => x.Range).Name, "The watch shift's range dates must make sense. Please.")); } return(null); }); #pragma warning restore CS0618 // Type or member is obsolete }
/// <summary> /// This method is responsible for advancing the watchbill into different states. /// At each state, different actions must be taken. /// </summary> /// <param name="desiredState"></param> /// <param name="setTime"></param> /// <param name="person"></param> /// <param name="session"></param> public virtual void SetState(WatchbillStatus desiredState, DateTime setTime, Person person, ISession session) { //Don't allow same changes. if (this.CurrentState == desiredState) { throw new Exception("Can't set the state to its same value."); } //If we set a watchbill's state to initial, then remove all the assignments from it, //leaving a watchbill with only its days and shifts. if (desiredState == ReferenceLists.ReferenceListHelper <WatchbillStatus> .Find("Initial")) { this.InputRequirements.Clear(); this.WatchInputs.Clear(); foreach (var shift in this.WatchShifts) { if (shift.WatchAssignment != null) { session.Delete(shift.WatchAssignment); shift.WatchAssignment = null; } } //We also need to remove the job. if (FluentScheduler.JobManager.RunningSchedules.Any(x => x.Name == this.Id.ToString())) { FluentScheduler.JobManager.RemoveJob(this.Id.ToString()); } } //Inform all the people who need to provide inputs along with all the people who are in its chain of command. else if (desiredState == ReferenceLists.ReferenceListHelper <WatchbillStatus> .Find("Open for Inputs")) { if (this.CurrentState == null || this.CurrentState != ReferenceLists.ReferenceListHelper <WatchbillStatus> .Find("Initial")) { throw new Exception("You may not move to the open for inputs state from anything other than the initial state."); } foreach (var elPerson in this.EligibilityGroup.EligiblePersons) { this.InputRequirements.Add(new WatchInputRequirement { Id = Guid.NewGuid(), Person = elPerson }); } var emailAddressesByPerson = this.EligibilityGroup.EligiblePersons .Select(x => new KeyValuePair <string, List <System.Net.Mail.MailAddress> >(x.ToString(), x.EmailAddresses.Where(y => y.IsPreferred).Select(y => new System.Net.Mail.MailAddress(y.Address, x.ToString())).ToList())).ToList(); //Start a new task to send all the emails. Task.Run(() => { foreach (var group in emailAddressesByPerson) { var model = new Email.Models.WatchbillInputRequiredEmailModel { FriendlyName = group.Key, Watchbill = this.Title }; Email.EmailInterface.CCEmailMessage .CreateDefault() .To(group.Value) .CC(Email.EmailInterface.CCEmailMessage.DeveloperAddress) .Subject("Watchbill Inputs Required") .HTMLAlternateViewUsingTemplateFromEmbedded("CommandCentral.Email.Templates.WatchbillInputRequired_HTML.html", model) .SendWithRetryAndFailure(TimeSpan.FromSeconds(1)); } }).ConfigureAwait(false); //We now also need to load all persons in the watchbill's chain of command var groups = new Authorization.Groups.PermissionGroup[] { new Authorization.Groups.Definitions.CommandQuarterdeckWatchbill(), new Authorization.Groups.Definitions.DepartmentQuarterdeckWatchbill(), new Authorization.Groups.Definitions.DivisionQuarterdeckWatchbill() } .Select(x => x.GroupName) .ToList(); using (var internalSession = DataAccess.DataProvider.CreateStatefulSession()) using (var transaction = internalSession.BeginTransaction()) { try { var queryString = "from Person as person where ("; for (var x = 0; x < groups.Count; x++) { queryString += " '{0}' in elements(person.{1}) ".With(groups[x], PropertySelector.SelectPropertyFrom <Person>(y => y.PermissionGroupNames).Name); if (x + 1 != groups.Count) { queryString += " or "; } } queryString += " ) and person.Command = :command"; var persons = internalSession.CreateQuery(queryString) .SetParameter("command", this.Command) .List <Person>(); //Now with these people who are the duty holders. var collateralEmailAddresses = persons.Select(x => x.EmailAddresses.Where(y => y.IsPreferred).Select(y => new System.Net.Mail.MailAddress(y.Address, x.ToString())).ToList()).ToList(); var uniqueWatchQuals = this.WatchShifts.SelectMany(x => x.ShiftType.RequiredWatchQualifications).Distinct(); var personNamesWithoutWatchQualifications = this.EligibilityGroup.EligiblePersons.Where(p => !p.WatchQualifications.Any(qual => uniqueWatchQuals.Contains(qual))).Select(x => x.ToString()).ToList(); Task.Run(() => { var model = new Email.Models.WatchbillOpenForInputsEmailModel { WatchbillTitle = this.Title, NotQualledPersonsFriendlyNames = personNamesWithoutWatchQualifications }; foreach (var addressGroup in collateralEmailAddresses) { Email.EmailInterface.CCEmailMessage .CreateDefault() .To(addressGroup) .CC(Email.EmailInterface.CCEmailMessage.DeveloperAddress) .Subject("Watchbill Open For Inputs") .HTMLAlternateViewUsingTemplateFromEmbedded("CommandCentral.Email.Templates.WatchbillOpenForInputs_HTML.html", model) .SendWithRetryAndFailure(TimeSpan.FromSeconds(1)); } }).ConfigureAwait(false); transaction.Commit(); } catch { transaction.Rollback(); throw; } } //We now need to register the job that will send emails every day to alert people to the inputs they are responsible for. FluentScheduler.JobManager.AddJob(() => SendWatchInputRequirementsAlertEmail(this.Id), s => s.WithName(this.Id.ToString()).ToRunNow().AndEvery(1).Days().At(4, 0)); } //Inform everyone in the chain of command that the watchbill is closed for inputs. else if (desiredState == ReferenceLists.ReferenceListHelper <WatchbillStatus> .Find("Closed for Inputs")) { if (this.CurrentState == null || this.CurrentState != ReferenceLists.ReferenceListHelper <WatchbillStatus> .Find("Open for Inputs")) { throw new Exception("You may not move to the closed for inputs state from anything other than the open for inputs state."); } //We now also need to load all persons in the watchbill's chain of command. var groups = new Authorization.Groups.PermissionGroup[] { new Authorization.Groups.Definitions.CommandQuarterdeckWatchbill(), new Authorization.Groups.Definitions.DepartmentQuarterdeckWatchbill(), new Authorization.Groups.Definitions.DivisionQuarterdeckWatchbill() } .Select(x => x.GroupName) .ToList(); using (var internalSession = DataAccess.DataProvider.CreateStatefulSession()) using (var transaction = internalSession.BeginTransaction()) { try { var queryString = "from Person as person where ("; for (var x = 0; x < groups.Count; x++) { queryString += " '{0}' in elements(person.{1}) ".With(groups[x], PropertySelector.SelectPropertyFrom <Person>(y => y.PermissionGroupNames).Name); if (x + 1 != groups.Count) { queryString += " or "; } } queryString += " ) and person.Command = :command"; var persons = internalSession.CreateQuery(queryString) .SetParameter("command", this.Command) .List <Person>(); //Now with these people who are the duty holders. var collateralEmailAddresses = persons.Select(x => x.EmailAddresses.Where(y => y.IsPreferred).Select(y => new System.Net.Mail.MailAddress(y.Address, x.ToString())).ToList()).ToList(); Task.Run(() => { var model = new Email.Models.WatchbillClosedForInputsEmailModel { Watchbill = this.Title }; foreach (var addressGroup in collateralEmailAddresses) { Email.EmailInterface.CCEmailMessage .CreateDefault() .To(addressGroup) .CC(Email.EmailInterface.CCEmailMessage.DeveloperAddress) .Subject("Watchbill Closed For Inputs") .HTMLAlternateViewUsingTemplateFromEmbedded("CommandCentral.Email.Templates.WatchbillClosedForInputs_HTML.html", model) .SendWithRetryAndFailure(TimeSpan.FromSeconds(1)); } }).ConfigureAwait(false); transaction.Commit(); } catch { transaction.Rollback(); throw; } } if (FluentScheduler.JobManager.RunningSchedules.Any(x => x.Name == this.Id.ToString())) { FluentScheduler.JobManager.RemoveJob(this.Id.ToString()); } } //Make sure there are assignments for each shift. //Inform the chain of command that the watchbill is open for review. else if (desiredState == ReferenceLists.ReferenceListHelper <WatchbillStatus> .Find("Under Review")) { if (this.CurrentState == null || this.CurrentState != ReferenceLists.ReferenceListHelper <WatchbillStatus> .Find("Closed for Inputs")) { throw new Exception("You may not move to the under review state from anything other than the closed for inputs state."); } if (!this.WatchShifts.All(y => y.WatchAssignment != null)) { throw new Exception("A watchbill may not move into the 'Under Review' state unless the all watch shifts have been assigned."); } //We now also need to load all persons in the watchbill's chain of command. var groups = new Authorization.Groups.PermissionGroup[] { new Authorization.Groups.Definitions.CommandQuarterdeckWatchbill(), new Authorization.Groups.Definitions.DepartmentQuarterdeckWatchbill(), new Authorization.Groups.Definitions.DivisionQuarterdeckWatchbill() } .Select(x => x.GroupName) .ToList(); using (var internalSession = DataAccess.DataProvider.CreateStatefulSession()) using (var transaction = internalSession.BeginTransaction()) { try { var queryString = "from Person as person where ("; for (var x = 0; x < groups.Count; x++) { queryString += " '{0}' in elements(person.{1}) ".With(groups[x], PropertySelector.SelectPropertyFrom <Person>(y => y.PermissionGroupNames).Name); if (x + 1 != groups.Count) { queryString += " or "; } } queryString += " ) and person.Command = :command"; var persons = internalSession.CreateQuery(queryString) .SetParameter("command", this.Command) .List <Person>(); //Now with these people who are the duty holders. var collateralEmailAddresses = persons.Select(x => x.EmailAddresses.Where(y => y.IsPreferred).Select(y => new System.Net.Mail.MailAddress(y.Address, x.ToString())).ToList()).ToList(); Task.Run(() => { var model = new Email.Models.WatchbillUnderReviewEmailModel { Watchbill = this.Title }; foreach (var addressGroup in collateralEmailAddresses) { Email.EmailInterface.CCEmailMessage .CreateDefault() .To(addressGroup) .CC(Email.EmailInterface.CCEmailMessage.DeveloperAddress) .Subject("Watchbill Under Review") .HTMLAlternateViewUsingTemplateFromEmbedded("CommandCentral.Email.Templates.WatchbillUnderReview_HTML.html", model) .SendWithRetryAndFailure(TimeSpan.FromSeconds(1)); } }).ConfigureAwait(false); transaction.Commit(); } catch { transaction.Rollback(); throw; } } } //Move the watchbill into its published state, tell everyone who has watch which watches they have. //Tell the chain of command the watchbill is published. else if (desiredState == ReferenceLists.ReferenceListHelper <WatchbillStatus> .Find("Published")) { if (this.CurrentState == null || this.CurrentState != ReferenceLists.ReferenceListHelper <WatchbillStatus> .Find("Under Review")) { throw new Exception("You may not move to the published state from anything other than the under review state."); } //Let's send an email to each person who is on watch, informing them of their watches. var assignmentsByPerson = this.WatchShifts.Select(x => x.WatchAssignment) .GroupBy(x => x.PersonAssigned); foreach (var assignments in assignmentsByPerson) { var model = new Email.Models.WatchAssignedEmailModel { FriendlyName = assignments.Key.ToString(), WatchAssignments = assignments.ToList(), Watchbill = this.Title }; var emailAddresses = assignments.Key.EmailAddresses.Where(x => x.IsPreferred).Select(x => new System.Net.Mail.MailAddress(x.Address, assignments.Key.ToString())).ToList(); Task.Run(() => { Email.EmailInterface.CCEmailMessage .CreateDefault() .To(emailAddresses) .CC(Email.EmailInterface.CCEmailMessage.DeveloperAddress) .Subject("Watch Assigned") .HTMLAlternateViewUsingTemplateFromEmbedded("CommandCentral.Email.Templates.WatchAssigned_HTML.html", model) .SendWithRetryAndFailure(TimeSpan.FromSeconds(1)); }).ConfigureAwait(false); } //Let's send emails to all the coordinators. //We now also need to load all persons in the watchbill's chain of command. var groups = new Authorization.Groups.PermissionGroup[] { new Authorization.Groups.Definitions.CommandQuarterdeckWatchbill(), new Authorization.Groups.Definitions.DepartmentQuarterdeckWatchbill(), new Authorization.Groups.Definitions.DivisionQuarterdeckWatchbill() } .Select(x => x.GroupName) .ToList(); using (var internalSession = DataAccess.DataProvider.CreateStatefulSession()) using (var transaction = internalSession.BeginTransaction()) { try { var queryString = "from Person as person where ("; for (var x = 0; x < groups.Count; x++) { queryString += " '{0}' in elements(person.{1}) ".With(groups[x], PropertySelector.SelectPropertyFrom <Person>(y => y.PermissionGroupNames).Name); if (x + 1 != groups.Count) { queryString += " or "; } } queryString += " ) and person.Command = :command"; var persons = internalSession.CreateQuery(queryString) .SetParameter("command", this.Command) .List <Person>(); //Now with these people who are the duty holders, get their preferred email addresses. var collateralEmailAddresses = persons.Select(x => x.EmailAddresses.Where(y => y.IsPreferred).Select(y => new System.Net.Mail.MailAddress(y.Address, x.ToString())).ToList()).ToList(); Task.Run(() => { var model = new Email.Models.WatchbillPublishedEmailModel { Watchbill = this.Title }; foreach (var addressGroup in collateralEmailAddresses) { Email.EmailInterface.CCEmailMessage .CreateDefault() .To(addressGroup) .CC(Email.EmailInterface.CCEmailMessage.DeveloperAddress) .Subject("Watchbill Published") .HTMLAlternateViewUsingTemplateFromEmbedded("CommandCentral.Email.Templates.WatchbillPublished_HTML.html", model) .SendWithRetryAndFailure(TimeSpan.FromSeconds(1)); } }).ConfigureAwait(false); transaction.Commit(); } catch { transaction.Rollback(); throw; } } } else { throw new NotImplementedException("Not implemented default case in the set watchbill state method."); } this.CurrentState = desiredState; this.LastStateChange = setTime; this.LastStateChangedBy = person; }