/// <summary> /// Update the open attempt record if there are changes. /// </summary> /// <param name="openAttempt"></param> /// <param name="achievementTypeCache">The achievement type cache.</param> /// <param name="transaction">The financial transaction.</param> private void UpdateOpenAttempt(AchievementAttempt openAttempt, AchievementTypeCache achievementTypeCache, FinancialTransaction transaction) { // Validate the attribute values var numberToAccumulate = GetAttributeValue(achievementTypeCache, AttributeKey.NumberToAccumulate).AsInteger(); if (numberToAccumulate <= 0) { ExceptionLogService.LogException($"{GetType().Name}.UpdateOpenAttempt cannot process because the NumberToAccumulate attribute is less than 1"); return; } // Calculate the date range where the open attempt can be validly fulfilled var attributeMaxDate = GetAttributeValue(achievementTypeCache, AttributeKey.EndDateTime).AsDateTime(); var minDate = openAttempt.AchievementAttemptStartDateTime; var maxDate = CalculateMaxDateForAchievementAttempt(minDate, attributeMaxDate); // Get the transaction dates var transactionDates = GetOrderedFinancialTransactionDatesByPerson(achievementTypeCache, transaction.AuthorizedPersonAlias.PersonId, minDate, maxDate); var newCount = transactionDates.Count(); if (newCount == 0) { return; } var lastInteractionDate = transactionDates.LastOrDefault(); var progress = CalculateProgress(newCount, numberToAccumulate); var isSuccessful = progress >= 1m; openAttempt.AchievementAttemptEndDateTime = lastInteractionDate; openAttempt.Progress = progress; openAttempt.IsClosed = isSuccessful && !achievementTypeCache.AllowOverAchievement; openAttempt.IsSuccessful = isSuccessful; }
/// <summary> /// Gets the attempt from the streak. /// </summary> /// <param name="computedStreak">The computed streak.</param> /// <param name="targetCount">The target count.</param> /// <param name="isClosed">if set to <c>true</c> [is closed].</param> /// <returns></returns> private static AchievementAttempt GetAttemptFromStreak(ComputedStreak computedStreak, int targetCount, bool isClosed) { var attempt = new AchievementAttempt(); ApplyStreakToAttempt(computedStreak, attempt, targetCount, isClosed); return(attempt); }
/// <summary> /// Initializes a new instance of the <see cref="MarkEngagementResponse"/> class. /// </summary> public AttemptListItem(AchievementAttempt achievementAttempt) { AchievementAttemptId = achievementAttempt.Id; AchievementTypeId = achievementAttempt.AchievementTypeId; AchievementTypeGuid = achievementAttempt.AchievementType?.Guid ?? AchievementTypeCache.Get(AchievementTypeId)?.Guid; Progress = achievementAttempt.Progress; IsComplete = Progress >= 1; }
/// <summary> /// Initializes a new instance of the <see cref="PersonAchievementType"/> class. /// </summary> /// <param name="person">The person.</param> /// <param name="achievementType">Type of the achievement.</param> /// <param name="achievementAttempts">The achievement attempts.</param> /// <param name="justCompletedAchievementAttempt">The just completed achievement attempt.</param> public PersonAchievementType(Person person, AchievementTypeCache achievementType, AchievementAttempt[] achievementAttempts, AchievementAttempt justCompletedAchievementAttempt) { Person = person; AchievementType = achievementType; JustCompletedAchievementAttempt = justCompletedAchievementAttempt; CurrentInProgressAchievement = achievementAttempts.Where(a => !a.IsSuccessful && !a.IsClosed).FirstOrDefault(); AchievementAttempts = achievementAttempts; }
/// <summary> /// Gets the attempt from the streak /// </summary> /// <param name="computedStreak">The computed streak.</param> /// <param name="attempt">The attempt.</param> /// <param name="targetCount">The target count.</param> /// <param name="isClosed">if set to <c>true</c> [is closed].</param> private static void ApplyStreakToAttempt(ComputedStreak computedStreak, AchievementAttempt attempt, int targetCount, bool isClosed) { var progress = CalculateProgress(computedStreak.Count, targetCount); attempt.AchievementAttemptStartDateTime = computedStreak.StartDate; attempt.AchievementAttemptEndDateTime = computedStreak.EndDate; attempt.Progress = progress; attempt.IsClosed = isClosed; attempt.IsSuccessful = progress >= 1m; }
/// <summary> /// Gets the person achievement types. /// </summary> /// <param name="includeJustCompleted">if set to <c>true</c> [include just completed].</param> /// <returns></returns> public PersonAchievementType[] GetPersonAchievementTypes(bool includeJustCompleted) { // For each checkin, we only want to show one achievement per AchievementType // If there is one in Progress, include that // otherwise add the first Completed one of each AchievementType List <AchievementAttempt> achievementAttempts = new List <AchievementAttempt>(); var checkinResult = this; if (checkinResult.InProgressAchievementAttempts?.Any() == true) { achievementAttempts.AddRange(checkinResult.InProgressAchievementAttempts); } if (checkinResult.CompletedAchievementAttempts?.Any() == true) { achievementAttempts.AddRange(checkinResult.CompletedAchievementAttempts); } if (achievementAttempts.Any() == true) { PersonAchievementType[] personAchievementTypes = achievementAttempts .GroupBy(a => a.AchievementTypeId) .Select(a => { var achievmentTypeId = a.Key; var achievementType = AchievementTypeCache.Get(achievmentTypeId); var person = checkinResult.Person.Person; AchievementAttempt justCompleted = null; if (includeJustCompleted) { justCompleted = this.JustCompletedAchievementAttempts.Where(j => j.AchievementTypeId == achievmentTypeId).FirstOrDefault(); } return(new PersonAchievementType(person, achievementType, a.ToArray(), justCompleted)); }).ToArray(); return(personAchievementTypes); } return(new PersonAchievementType[0]); }
/// <summary> /// Save the current record. /// </summary> /// <returns></returns> private void SaveRecord() { var attempt = GetAttempt(); var achievementType = GetAchievementTypeCache(); // Add the new attempt if we are adding if (attempt == null) { var attemptService = GetAttemptService(); var achievementTypeId = achievementType == null?atpAchievementType.SelectedValue.AsInteger() : achievementType.Id; attempt = new AchievementAttempt { AchievementTypeId = achievementTypeId, AchieverEntityId = nbAchieverEntityId.IntegerValue ?? 0 }; attemptService.Add(attempt); } var progress = tbProgress.Text.AsDecimal(); if (attempt.Progress < 0m) { attempt.Progress = 0m; } if (attempt.Progress > 1m && !achievementType.AllowOverAchievement) { attempt.Progress = 1m; } var isSuccess = progress >= 1m; var startDate = dpStart.SelectedDate ?? RockDateTime.Today; var endDate = dpEnd.SelectedDate; if (!endDate.HasValue && isSuccess && !achievementType.AllowOverAchievement) { endDate = RockDateTime.Today; } if (endDate.HasValue && endDate < startDate) { endDate = startDate; } attempt.IsClosed = (endDate.HasValue && endDate.Value < RockDateTime.Today) || (isSuccess && !achievementType.AllowOverAchievement); attempt.AchievementAttemptStartDateTime = startDate; attempt.AchievementAttemptEndDateTime = endDate; attempt.Progress = progress; attempt.IsSuccessful = isSuccess; if (!attempt.IsValid) { // Controls will render the error messages return; } try { var rockContext = GetRockContext(); rockContext.SaveChanges(); if (!attempt.IsAuthorized(Authorization.VIEW, CurrentPerson)) { attempt.AllowPerson(Authorization.VIEW, CurrentPerson, rockContext); } if (!attempt.IsAuthorized(Authorization.EDIT, CurrentPerson)) { attempt.AllowPerson(Authorization.EDIT, CurrentPerson, rockContext); } if (!attempt.IsAuthorized(Authorization.ADMINISTRATE, CurrentPerson)) { attempt.AllowPerson(Authorization.ADMINISTRATE, CurrentPerson, rockContext); } } catch (Exception ex) { ShowBlockException(nbEditModeMessage, ex); return; } // If the save was successful, reload the page using the new record Id. NavigateToPage(RockPage.Guid, new Dictionary <string, string> { { PageParameterKey.AchievementAttemptId, attempt.Id.ToString() } }); }
/// <summary> /// Create new attempt records and return them in a list. All new attempts should be after the most recent successful attempt. /// </summary> /// <param name="achievementTypeCache">The achievement type cache.</param> /// <param name="streak">The streak.</param> /// <param name="mostRecentClosedAttempt">The most recent closed attempt.</param> /// <returns></returns> protected override List <AchievementAttempt> CreateNewAttempts(AchievementTypeCache achievementTypeCache, Streak streak, AchievementAttempt mostRecentClosedAttempt) { var rockContext = new RockContext(); var streakTypeService = new StreakTypeService(rockContext); var streakTypeCache = StreakTypeCache.Get(streak.StreakTypeId); // Validate the attribute values var numberToAchieve = GetAttributeValue(achievementTypeCache, AttributeKey.NumberToAchieve).AsInteger(); if (numberToAchieve <= 0) { ExceptionLogService.LogException($"StreakAchievement.CreateNewAttempts cannot process because the NumberToAchieve attribute is less than 1"); return(null); } var attributeTimespanDays = GetAttributeValue(achievementTypeCache, AttributeKey.TimespanInDays).AsIntegerOrNull(); if (attributeTimespanDays.HasValue && attributeTimespanDays.Value <= 0) { ExceptionLogService.LogException($"StreakAchievement.CreateNewAttempts cannot process because the TimespanInDays attribute is less than 1"); return(null); } // Calculate the date range where new achievements can be validly found var attributeMinDate = GetAttributeValue(achievementTypeCache, AttributeKey.StartDateTime).AsDateTime(); var attributeMaxDate = GetAttributeValue(achievementTypeCache, AttributeKey.EndDateTime).AsDateTime(); var minDate = CalculateMinDateForAchievementAttempt(streak.EnrollmentDate, mostRecentClosedAttempt, attributeMinDate, numberToAchieve); var maxDate = CalculateMaxDateForAchievementAttempt(minDate, attributeMaxDate); // Get the max date that streaks can be broken. This is to avoid breaking streaks while people still have time to // engage in that day or week (because it is the current day or week) var maxDateForStreakBreaking = StreakTypeService.GetMaxDateForStreakBreaking(streakTypeCache); // Track the attempts in a list that will be returned. The int is the streak count for that attempt var attempts = new List <AchievementAttempt>(); var streaks = new List <ComputedStreak>(); // Define what happens for each bit in the date range bool iterationAction(int currentUnit, DateTime currentDate, bool hasOccurrence, bool hasEngagement, bool hasExclusion) { // If there is an engagement and a timespan, then this is a possible attempt. If there is no timespan then only one // attempt needs to be tracked at a time if (hasOccurrence && hasEngagement && (attributeTimespanDays.HasValue || !streaks.Any())) { streaks.Add(new ComputedStreak(currentDate)); } else if (hasOccurrence && !hasEngagement && !hasExclusion && streaks.Any()) { // Break the streaks and close an attempt if there is an unexcused absence var longestStreak = streaks.First(); attempts.Add(GetAttemptFromStreak(longestStreak, numberToAchieve, currentDate <= maxDateForStreakBreaking)); streaks.Clear(); return(false); } for (var i = streaks.Count - 1; i >= 0; i--) { var computedStreak = streaks[i]; if (hasOccurrence && hasEngagement) { // Increment the streak computedStreak.Count++; computedStreak.EndDate = currentDate; // Check for a fulfilled attempt if (computedStreak.Count >= numberToAchieve) { streaks.Clear(); if (achievementTypeCache.AllowOverAchievement) { streaks.Add(computedStreak); i = 0; } else { attempts.Add(GetAttemptFromStreak(computedStreak, numberToAchieve, !achievementTypeCache.AllowOverAchievement)); break; } } } // If there is a timespan and this streak is too old, then the attempt is closed if (attributeTimespanDays.HasValue) { var inclusiveAge = (currentDate - computedStreak.StartDate).Days + 1; if (inclusiveAge >= attributeTimespanDays.Value) { var timedOutAttempt = GetAttemptFromStreak(computedStreak, numberToAchieve, true); attempts.Add(timedOutAttempt); streaks.RemoveAt(i); // Remove more recently started streaks that started before the next valid start date (based // on the deficiency of this timed out attempt) var nextValidStartDate = CalculateMinDateForAchievementAttempt(streak.EnrollmentDate, timedOutAttempt, attributeMinDate, numberToAchieve); for (var j = streaks.Count - 1; j >= i; j--) { var moreRecentStreak = streaks[j]; if (moreRecentStreak.StartDate < nextValidStartDate) { streaks.RemoveAt(j); } } } } } return(false); } // Iterate through the streak date for the date range specified streakTypeService.IterateStreakMap(streakTypeCache, streak.PersonAliasId, minDate, maxDate, iterationAction, out var errorMessage); if (!errorMessage.IsNullOrWhiteSpace()) { ExceptionLogService.LogException($"StreakAchievement.CreateNewAttempts got an error calling StreakTypeService.IterateStreakMap: {errorMessage}"); return(null); } // The longest leftover streak is an open attempt if (streaks.Any()) { var longestStreak = streaks.First(); attempts.Add(GetAttemptFromStreak(longestStreak, numberToAchieve, false)); } return(attempts); }
/// <summary> /// Update the open attempt record if there are changes. /// </summary> /// <param name="openAttempt"></param> /// <param name="achievementTypeCache">The achievement type cache.</param> /// <param name="streak">The streak.</param> protected override void UpdateOpenAttempt(AchievementAttempt openAttempt, AchievementTypeCache achievementTypeCache, Streak streak) { var rockContext = new RockContext(); var streakTypeService = new StreakTypeService(rockContext); var streakTypeCache = GetStreakTypeCache(achievementTypeCache); // Validate the attribute values var numberToAchieve = GetAttributeValue(achievementTypeCache, AttributeKey.NumberToAchieve).AsInteger(); if (numberToAchieve <= 0) { ExceptionLogService.LogException($"StreakAchievement.UpdateOpenAttempt cannot process because the numberToAchieve attribute is less than 1"); return; } var attributeTimespanDays = GetAttributeValue(achievementTypeCache, AttributeKey.TimespanInDays).AsIntegerOrNull(); if (attributeTimespanDays.HasValue && attributeTimespanDays.Value <= 0) { ExceptionLogService.LogException($"StreakAchievement.UpdateOpenAttempt cannot process because the TimespanInDays attribute is less than 1"); return; } // Calculate the date range where the open attempt can be validly fulfilled var attributeMaxDate = GetAttributeValue(achievementTypeCache, AttributeKey.EndDateTime).AsDateTime(); var minDate = openAttempt.AchievementAttemptStartDateTime; var maxDate = CalculateMaxDateForAchievementAttempt(minDate, attributeMaxDate); // Get the max date that streaks can be broken. This is to avoid breaking streaks while people still have time to // engage in that day or week (because it is the current day or week) var maxDateForStreakBreaking = StreakTypeService.GetMaxDateForStreakBreaking(streakTypeCache); // Track the streak var computedStreak = new ComputedStreak(minDate) { EndDate = minDate }; // Define what happens for each bit in the date range bool iterationAction(int currentUnit, DateTime currentDate, bool hasOccurrence, bool hasEngagement, bool hasExclusion) { var iterationCanStop = false; // If there is an engagement, then increment the streak if (hasOccurrence && hasEngagement) { computedStreak.Count++; computedStreak.EndDate = currentDate; // Check for a fulfilled attempt if (computedStreak.Count >= numberToAchieve) { ApplyStreakToAttempt(computedStreak, openAttempt, numberToAchieve, !achievementTypeCache.AllowOverAchievement); iterationCanStop = !achievementTypeCache.AllowOverAchievement; } } else if (hasOccurrence && !hasEngagement && !hasExclusion) { // Break the streak and close the attempt if there is an unexcused absence ApplyStreakToAttempt(computedStreak, openAttempt, numberToAchieve, currentDate <= maxDateForStreakBreaking); iterationCanStop = true; } // If there is a timespan and this streak is too old, then the attempt is closed if (attributeTimespanDays.HasValue) { var inclusiveAge = (currentDate - computedStreak.StartDate).Days + 1; if (inclusiveAge >= attributeTimespanDays.Value) { ApplyStreakToAttempt(computedStreak, openAttempt, numberToAchieve, currentDate <= maxDateForStreakBreaking); iterationCanStop = true; } } return(iterationCanStop); } // Iterate through the streak date for the date range specified streakTypeService.IterateStreakMap(streakTypeCache, streak.PersonAliasId, minDate, maxDate, iterationAction, out var errorMessage); if (!errorMessage.IsNullOrWhiteSpace()) { ExceptionLogService.LogException($"StreakAchievement.UpdateOpenAttempt got an error calling StreakTypeService.IterateStreakMap: {errorMessage}"); return; } // If the attempt wasn't closed in the iteration, then it will remain open if (!openAttempt.IsClosed) { var progress = CalculateProgress(computedStreak.Count, numberToAchieve); openAttempt.Progress = progress; openAttempt.IsSuccessful = progress >= 1m; } }
/// <summary> /// Create new attempt records and return them in a list. All new attempts should be after the most recent successful attempt. /// </summary> /// <param name="achievementTypeCache">The achievement type cache.</param> /// <param name="transaction">The financial transaction.</param> /// <param name="mostRecentSuccess">The most recent successful attempt.</param> /// <returns></returns> private List <AchievementAttempt> CreateNewAttempts(AchievementTypeCache achievementTypeCache, FinancialTransaction transaction, AchievementAttempt mostRecentSuccess) { // Validate the attribute values var numberToAccumulate = GetAttributeValue(achievementTypeCache, AttributeKey.NumberToAccumulate).AsInteger(); if (numberToAccumulate <= 0) { ExceptionLogService.LogException($"{GetType().Name}. CreateNewAttempts cannot process because the NumberToAccumulate attribute is less than 1"); return(null); } // Calculate the date range where new achievements can be validly found var attributeMinDate = GetAttributeValue(achievementTypeCache, AttributeKey.StartDateTime).AsDateTime(); var attributeMaxDate = GetAttributeValue(achievementTypeCache, AttributeKey.EndDateTime).AsDateTime(); var minDate = CalculateMinDateForAchievementAttempt(DateTime.MinValue, mostRecentSuccess, attributeMinDate, numberToAccumulate); var maxDate = CalculateMaxDateForAchievementAttempt(minDate, attributeMaxDate); // Track the attempts in a list that will be returned var attempts = new List <AchievementAttempt>(); ComputedStreak accumulation = null; // Get the transaction dates and begin calculating attempts var transactionDates = GetOrderedFinancialTransactionDatesByPerson(achievementTypeCache, transaction.AuthorizedPersonAlias.PersonId, minDate, maxDate); foreach (var transactionDate in transactionDates) { if (!transactionDate.HasValue) { // Nothing we can do without a date continue; } if (accumulation == null) { accumulation = new ComputedStreak(transactionDate.Value); } // Increment the accumulation accumulation.Count++; accumulation.EndDate = transactionDate; // Check for a fulfilled attempt if (accumulation.Count >= numberToAccumulate) { attempts.Add(GetAttempt(accumulation, numberToAccumulate, true)); if (!achievementTypeCache.AllowOverAchievement) { accumulation = null; } } } // The leftover accumulation is an open attempt if (accumulation != null) { var openAttempt = GetAttempt(accumulation, numberToAccumulate, false); var lastAttempt = attempts.LastOrDefault(); if (null == lastAttempt || openAttempt.Progress != lastAttempt.Progress || openAttempt.AchievementAttemptStartDateTime != lastAttempt.AchievementAttemptStartDateTime || openAttempt.AchievementAttemptEndDateTime != lastAttempt.AchievementAttemptEndDateTime) { attempts.Add(openAttempt); } } return(attempts); }
/// <summary> /// Processes the specified achievement type cache for the source entity. /// </summary> /// <param name="rockContext">The rock context.</param> /// <param name="achievementTypeCache">The achievement type cache.</param> /// <param name="sourceEntity">The source entity.</param> /// <returns>The set of attempts that were created or updated</returns> public override HashSet <AchievementAttempt> Process(RockContext rockContext, AchievementTypeCache achievementTypeCache, IEntity sourceEntity) { var step = sourceEntity as Step; var updatedAttempts = new HashSet <AchievementAttempt>(); // If we cannot link the step to a person, then there is nothing to do if (step == null) { return(updatedAttempts); } // If the achievement type is not active (or null) then there is nothing to do if (achievementTypeCache?.IsActive != true) { return(updatedAttempts); } // If there are unmet prerequisites, then there is nothing to do var achievementTypeService = new AchievementTypeService(rockContext); var unmetPrerequisites = achievementTypeService.GetUnmetPrerequisites(achievementTypeCache.Id, step.PersonAliasId); if (unmetPrerequisites.Any()) { return(updatedAttempts); } // Get all of the attempts for this program and achievement combo, ordered by start date DESC so that // the most recent attempts can be found with FirstOrDefault var achievementAttemptService = new AchievementAttemptService(rockContext); var attempts = achievementAttemptService.Queryable() .Where(aa => aa.AchievementTypeId == achievementTypeCache.Id && aa.AchieverEntityId == step.PersonAliasId) .ToList() .OrderByDescending(aa => aa.AchievementAttemptStartDateTime) .ToList(); var mostRecentSuccess = attempts.FirstOrDefault(saa => saa.AchievementAttemptEndDateTime.HasValue && saa.IsSuccessful); // This component does not allow more than one success if (mostRecentSuccess != null) { return(updatedAttempts); } var currentAttempt = attempts.LastOrDefault(); if (currentAttempt == null) { currentAttempt = new AchievementAttempt { AchieverEntityId = step.PersonAliasId, AchievementTypeId = achievementTypeCache.Id }; achievementAttemptService.Add(currentAttempt); } var attributeMinDate = GetAttributeValue(achievementTypeCache, AttributeKey.StartDateTime).AsDateTime(); var attributeMaxDate = GetAttributeValue(achievementTypeCache, AttributeKey.EndDateTime).AsDateTime(); var completedStepTypeDates = GetCompletedStepTypeDates(achievementTypeCache, step.PersonAliasId, attributeMinDate, attributeMaxDate); var stepProgram = GetStepProgramCache(achievementTypeCache); var stepTypeCount = stepProgram.StepTypes.Count; var progress = CalculateProgress(completedStepTypeDates.Count, stepTypeCount); var isSuccessful = progress >= 1m; currentAttempt.AchievementAttemptStartDateTime = completedStepTypeDates.Any() ? completedStepTypeDates.First() : RockDateTime.Today; currentAttempt.AchievementAttemptEndDateTime = completedStepTypeDates.Any() ? completedStepTypeDates.Last() : RockDateTime.Today; currentAttempt.Progress = progress; currentAttempt.IsClosed = isSuccessful; currentAttempt.IsSuccessful = isSuccessful; return(updatedAttempts); }
/// <summary> /// Create new attempt records and return them in a list. All new attempts should be after the most recent successful attempt. /// </summary> /// <param name="achievementTypeCache">The achievement type cache.</param> /// <param name="streak">The streak.</param> /// <param name="mostRecentSuccess">The most recent successful attempt.</param> /// <returns></returns> protected abstract List <AchievementAttempt> CreateNewAttempts(AchievementTypeCache achievementTypeCache, Streak streak, AchievementAttempt mostRecentSuccess);
/// <summary> /// Update the open attempt record if there are changes. Be sure to close the attempt if it is no longer possible to make /// progress on this open attempt. /// </summary> /// <param name="openAttempt">The open attempt.</param> /// <param name="achievementTypeCache">The achievement type cache.</param> /// <param name="streak">The streak.</param> protected abstract void UpdateOpenAttempt(AchievementAttempt openAttempt, AchievementTypeCache achievementTypeCache, Streak streak);