/// <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 StreakAchievementAttempt GetAttemptFromStreak(ComputedStreak computedStreak, int targetCount, bool isClosed) { var attempt = new StreakAchievementAttempt(); ApplyStreakToAttempt(computedStreak, attempt, targetCount, isClosed); return(attempt); }
/// <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, StreakAchievementAttempt 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> /// 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 streak = GetOrAddStreak(); var achievementTypeId = achievementType == null?atpAchievementType.SelectedValue.AsInteger() : achievementType.Id; attempt = new StreakAchievementAttempt { StreakTypeAchievementTypeId = achievementTypeId, StreakId = streak.Id }; 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.StreakAchievementAttemptId, 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="streakTypeAchievementTypeCache">The streak type achievement type cache.</param> /// <param name="streak">The streak.</param> /// <param name="mostRecentClosedAttempt">The most recent closed attempt.</param> /// <returns></returns> protected override List <StreakAchievementAttempt> CreateNewAttempts(StreakTypeAchievementTypeCache streakTypeAchievementTypeCache, Streak streak, StreakAchievementAttempt mostRecentClosedAttempt) { var rockContext = new RockContext(); var streakTypeService = new StreakTypeService(rockContext); var streakTypeCache = streakTypeAchievementTypeCache.StreakTypeCache; // Validate the attribute values var numberToAchieve = GetAttributeValue(streakTypeAchievementTypeCache, 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(streakTypeAchievementTypeCache, 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(streakTypeAchievementTypeCache, AttributeKey.StartDateTime).AsDateTime(); var attributeMaxDate = GetAttributeValue(streakTypeAchievementTypeCache, AttributeKey.EndDateTime).AsDateTime(); var minDate = CalculateMinDateForAchievementAttempt(streak.EnrollmentDate, mostRecentClosedAttempt, attributeMinDate, numberToAchieve); var maxDate = CalculateMaxDateForAchievementAttempt(minDate, attributeMaxDate); // Track the attempts in a list that will be returned. The int is the streak count for that attempt var attempts = new List <StreakAchievementAttempt>(); 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, true)); 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 (streakTypeAchievementTypeCache.AllowOverAchievement) { streaks.Add(computedStreak); i = 0; } else { attempts.Add(GetAttemptFromStreak(computedStreak, numberToAchieve, !streakTypeAchievementTypeCache.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="streakTypeAchievementTypeCache">The streak type achievement type cache.</param> /// <param name="streak">The streak.</param> protected override void UpdateOpenAttempt(StreakAchievementAttempt openAttempt, StreakTypeAchievementTypeCache streakTypeAchievementTypeCache, Streak streak) { var rockContext = new RockContext(); var streakTypeService = new StreakTypeService(rockContext); var streakTypeCache = streakTypeAchievementTypeCache.StreakTypeCache; // Validate the attribute values var numberToAchieve = GetAttributeValue(streakTypeAchievementTypeCache, AttributeKey.NumberToAchieve).AsInteger(); if (numberToAchieve <= 0) { ExceptionLogService.LogException($"StreakAchievement.UpdateOpenAttempt cannot process because the numberToAchieve attribute is less than 1"); return; } var attributeTimespanDays = GetAttributeValue(streakTypeAchievementTypeCache, 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(streakTypeAchievementTypeCache, AttributeKey.EndDateTime).AsDateTime(); var minDate = openAttempt.AchievementAttemptStartDateTime; var maxDate = CalculateMaxDateForAchievementAttempt(minDate, attributeMaxDate); // 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) { // 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, !streakTypeAchievementTypeCache.AllowOverAchievement); } } else if (hasOccurrence && !hasEngagement && !hasExclusion) { // Break the streak and close the attempt if there is an unexcused absence ApplyStreakToAttempt(computedStreak, openAttempt, numberToAchieve, 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, true); } } return(openAttempt.IsClosed); } // 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> /// Update the open attempt record if there are changes. /// </summary> /// <param name="openAttempt"></param> /// <param name="streakTypeAchievementTypeCache">The streak type achievement type cache.</param> /// <param name="streak">The streak.</param> protected override void UpdateOpenAttempt( StreakAchievementAttempt openAttempt, StreakTypeAchievementTypeCache streakTypeAchievementTypeCache, Streak streak ) { var rockContext = new RockContext(); var streakTypeService = new StreakTypeService( rockContext ); var streakTypeCache = streakTypeAchievementTypeCache.StreakTypeCache; // Validate the attribute values var numberToAccumulate = GetAttributeValue( streakTypeAchievementTypeCache, AttributeKey.NumberToAccumulate ).AsInteger(); if ( numberToAccumulate <= 0 ) { ExceptionLogService.LogException( $"AccumulativeAchievement.UpdateOpenAttempt cannot process because the NumberToAccumulate attribute is less than 1" ); return; } var attributeTimespanDays = GetAttributeValue( streakTypeAchievementTypeCache, AttributeKey.TimespanInDays ).AsIntegerOrNull(); if ( attributeTimespanDays.HasValue && attributeTimespanDays.Value <= 0 ) { ExceptionLogService.LogException( $"AccumulativeAchievement.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( streakTypeAchievementTypeCache, 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 accumulation var accumulation = 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 accumulation if ( hasOccurrence && hasEngagement ) { accumulation.Count++; accumulation.EndDate = currentDate; // Check for a fulfilled attempt if ( accumulation.Count >= numberToAccumulate ) { var progress = CalculateProgress( accumulation.Count, numberToAccumulate ); openAttempt.AchievementAttemptEndDateTime = accumulation.EndDate; openAttempt.Progress = progress; openAttempt.IsClosed = !streakTypeAchievementTypeCache.AllowOverAchievement; openAttempt.IsSuccessful = progress >= 1m; iterationCanStop = !streakTypeAchievementTypeCache.AllowOverAchievement; } } // If there is a timespan and this accumulation is too old, then the attempt is closed if ( attributeTimespanDays.HasValue ) { var inclusiveAge = ( currentDate - accumulation.StartDate ).Days + 1; if ( inclusiveAge >= attributeTimespanDays.Value ) { var progress = CalculateProgress( accumulation.Count, numberToAccumulate ); openAttempt.AchievementAttemptEndDateTime = accumulation.EndDate; openAttempt.Progress = progress; openAttempt.IsClosed = currentDate <= maxDateForStreakBreaking; openAttempt.IsSuccessful = progress >= 1m; 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( $"AccumulativeAchievement.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( accumulation.Count, numberToAccumulate ); openAttempt.Progress = progress; openAttempt.IsSuccessful = progress >= 1m; } }