public void IsBitSetIsCorrectForDailyMap() { // Day Offset 3210 9876 5432 1098 7654 3210 var map = new byte[] { 0b_1000_0000, 0b_0010_0000, 0b_0000_0100 }; var mapStartDate = new DateTime(2019, 1, 1); for (var dayOffset = -5; dayOffset < 100; dayOffset++) { var date = mapStartDate.AddDays(dayOffset); var isSet = StreakTypeService.IsBitSet(map, mapStartDate, date, StreakOccurrenceFrequency.Daily, out var errorMessage); if (dayOffset < 0) { // Should get error about checking a bit that is pre-start-date Assert.NotEmpty(errorMessage); } else { Assert.Empty(errorMessage); // The day of the month is the offset + 1 since offset 0 is Jan 1, 2019 if (date.Year == 2019 && date.Month == 1 && (date.Day == 3 || date.Day == 14 || date.Day == 24)) { Assert.True(isSet); } else { Assert.False(isSet); } } } }
public void GetFrequencyUnitDifferenceExclusiveDaily() { var frequency = StreakOccurrenceFrequency.Daily; var isInclusive = false; // Month of January is 31 days long var startDate = new DateTime(2019, 1, 1); var endDate = new DateTime(2019, 1, 31); var result = StreakTypeService.GetFrequencyUnitDifference(startDate, endDate, frequency, isInclusive); Assert.Equal(30, result); // Year of 2019 is 365 days long startDate = new DateTime(2019, 1, 1); endDate = new DateTime(2019, 12, 31); result = StreakTypeService.GetFrequencyUnitDifference(startDate, endDate, frequency, isInclusive); Assert.Equal(364, result); // Negative calculation is okay startDate = new DateTime(2019, 1, 1); endDate = new DateTime(2018, 12, 31); result = StreakTypeService.GetFrequencyUnitDifference(startDate, endDate, frequency, isInclusive); Assert.Equal(-1, result); // Same day calculation is 0 result = StreakTypeService.GetFrequencyUnitDifference(startDate, startDate, frequency, isInclusive); Assert.Equal(0, result); }
/// <summary> /// Get the map being edited /// </summary> /// <returns></returns> private byte[] GetTargetMap() { var savedMapState = ViewState[ViewStateKey.Map].ToStringSafe(); if (!savedMapState.IsNullOrWhiteSpace()) { return(StreakTypeService.GetMapFromHexDigitString(savedMapState)); } if (IsTargetingEngagementMap()) { var isEngagementExclusionMap = GetAttributeValue(AttributeKey.IsEngagementExclusion).AsBoolean(); return(isEngagementExclusionMap ? GetStreak().ExclusionMap : GetStreak().EngagementMap); } if (IsTargetingExclusionMap()) { return(GetStreakTypeExclusion().ExclusionMap); } if (IsTargetingOccurrenceMap()) { return(GetStreakType().OccurrenceMap); } return(null); }
public void GetHexDigitStringFromMapWorksCorrectly() { var map = new byte[] { 0x_ab, 0x_cd, 0x_ef, 0x_01, 0x_23, 0x_45, 0x_67, 0x_89 }; var expectedString = "ABCDEF0123456789"; var result = StreakTypeService.GetHexDigitStringFromMap(map); Assert.That.AreEqual(expectedString, result); }
/// <summary> /// Executes this instance. /// </summary> /// <exception cref="System.NotImplementedException"></exception> public void Execute() { if (StreakTypeId == default) { return; } StreakTypeService.HandlePostSaveChanges(StreakTypeId); }
/// <summary> /// Executes this instance. /// </summary> /// <param name="message"></param> public override void Execute(Message message) { StreakTypeService.RebuildStreakType(null, message.StreakTypeId, out var errorMessage); if (!errorMessage.IsNullOrWhiteSpace()) { ExceptionLogService.LogException(errorMessage); } }
public void ResetBitWorksForDailyMap() { // Offset 7654 3210 const byte byte2 = 0b_0000_0100; // Offset 5432 1098 const byte byte1 = 0b_0010_0000; // Offset 3210 9876 const byte byte0 = 0b_1000_0000; var map = new byte[] { byte0, byte1, byte2 }; var startDate = new DateTime(2019, 1, 1); var frequency = StreakOccurrenceFrequency.Daily; // Reset a bit before the start date and get an error var result = StreakTypeService.SetBit(map, startDate, startDate.AddDays(-1), frequency, false, out var errorMessage); Assert.NotEmpty(errorMessage); // Verify error occurred Assert.Same(result, map); // Verify in-place operation Assert.Equal(byte0, result[0]); // Verify no changes Assert.Equal(byte1, result[1]); // Verify no changes Assert.Equal(byte2, result[2]); // Verify no changes // Reset a bit that is already 0 result = StreakTypeService.SetBit(map, startDate, startDate.AddDays(0), frequency, false, out errorMessage); Assert.Empty(errorMessage); // Verify no error Assert.Same(result, map); // Verify in-place operation Assert.Equal(byte0, result[0]); // Verify no changes Assert.Equal(byte1, result[1]); // Verify no changes Assert.Equal(byte2, result[2]); // Verify no changes // Reset the first set bit result = StreakTypeService.SetBit(map, startDate, startDate.AddDays(2), frequency, false, out errorMessage); Assert.Empty(errorMessage); // Verify no errors Assert.Same(result, map); // Verify in-place operation Assert.Equal(0, result[2]); // Verify change Assert.Equal(byte1, result[1]); // Verify no changes Assert.Equal(byte0, result[0]); // Verify no changes // Reset a bit beyond the array and force it to grow result = StreakTypeService.SetBit(map, startDate, startDate.AddDays(24), frequency, false, out errorMessage); Assert.Empty(errorMessage); // Verify no errors Assert.NotSame(result, map); // Verify memory allocation occurred for new array var newLength = 128; Assert.True(result.Length == newLength); // Verify the array grew to the next multiple of 128 Assert.Equal(0, result[newLength - 1]); // Verify no additional changes Assert.Equal(byte1, result[newLength - 2]); // Verify no changes Assert.Equal(byte0, result[newLength - 3]); // Verify no changes // Verify all other bytes are unset for (var i = 0; i < (newLength - 3); i++) { Assert.Equal(0, result[i]); } }
/// <summary> /// Executes this instance. /// </summary> /// <param name="message"></param> public override void Execute(Message message) { if (message.StreakTypeId == default) { return; } StreakTypeService.HandlePostSaveChanges(message.StreakTypeId); }
/// <summary> /// Get the streak type service /// </summary> /// <returns></returns> private StreakTypeService GetStreakTypeService() { if (_streakTypeService == null) { var rockContext = GetRockContext(); _streakTypeService = new StreakTypeService(rockContext); } return(_streakTypeService); }
/// <summary> /// Shows the controls needed /// </summary> private void RenderCheckboxes() { nbMessage.Text = string.Empty; var streakType = GetStreakType(); if (streakType == null) { ShowBlockError(nbMessage, "A streak type is required."); return; } if (!CanEdit()) { SetVisible(false); return; } lTitle.Text = GetTargetMapTitle(); var map = GetTargetMap(); var errorMessage = string.Empty; var isDaily = streakType.OccurrenceFrequency == StreakOccurrenceFrequency.Daily; var dateRange = GetDateRange(); var startDate = dateRange.Start.Value; var endDate = dateRange.End.Value; if (!isDaily) { startDate = startDate.SundayDate(); endDate = endDate.SundayDate(); } cblCheckboxes.Label = isDaily ? "Days" : "Weeks"; cblCheckboxes.Items.Clear(); var minDate = GetMinDate(); var maxDate = isDaily ? RockDateTime.Today : RockDateTime.Today.SundayDate(); var checkboxCount = StreakTypeService.GetFrequencyUnitDifference(startDate, endDate, streakType.OccurrenceFrequency, true); for (var i = 0; i < checkboxCount; i++) { var representedDate = startDate.AddDays(isDaily ? i : (i * DaysPerWeek)); cblCheckboxes.Items.Add(new ListItem { Enabled = representedDate >= minDate && representedDate <= maxDate, Selected = StreakTypeService.IsBitSet(map, streakType.StartDate, representedDate, streakType.OccurrenceFrequency, out errorMessage), Text = GetLabel(isDaily, representedDate), Value = representedDate.ToISO8601DateString() }); } cblCheckboxes.DataBind(); }
/// <summary> /// Executes this instance. /// </summary> public void Execute() { ReportProgress(0); StreakTypeService.RebuildStreakType(Progress, StreakTypeId, out var errorMessage); if (!errorMessage.IsNullOrWhiteSpace()) { ExceptionLogService.LogException(errorMessage); } ReportProgress(100); }
public void GetMapFromHexDigitStringWorksCorrectly() { var expectedBytes = new byte[] { 0x_ab, 0x_cd, 0x_ef, 0x_01, 0x_23, 0x_45, 0x_67, 0x_89 }; var hexString = "ABCDEF0123456789"; var result = StreakTypeService.GetMapFromHexDigitString(hexString); Assert.That.AreEqual(expectedBytes.Length, result.Length); for (var i = 0; i < expectedBytes.Length; i++) { Assert.That.AreEqual(expectedBytes[i], result[i]); } }
/// <summary> /// Logs the interactions. /// </summary> /// <param name="interactionTransactionInfos">The interaction transaction infos to process.</param> /// <param name="rockContext">The rock context.</param> private void LogInteractions(List <InteractionTransactionInfo> interactionTransactionInfos, RockContext rockContext) { List <Interaction> interactionsToInsert = new List <Interaction>(); var interactionService = new InteractionService(rockContext); foreach (var info in interactionTransactionInfos.Where(a => a.InteractionComponentId.HasValue)) { /* * 2020-06-29 - JH * * The 'CreateInteraction(...)' method called below sets the following properties on the Interaction object: * * - InteractionComponentId * - InteractionDateTime (but with the wrong value) * - InteractionSessionId * - Source * - Medium * - Campaign * - Content * - Term */ var interaction = interactionService.CreateInteraction(info.InteractionComponentId.Value, info.UserAgent, info.InteractionData, info.IPAddress, info.BrowserSessionId); // The rest of the properties need to be manually set. interaction.EntityId = info.InteractionEntityId; interaction.Operation = info.InteractionOperation.IsNotNullOrWhiteSpace() ? info.InteractionOperation.Trim() : "View"; interaction.InteractionSummary = info.InteractionSummary?.Trim(); interaction.PersonAliasId = info.PersonAliasId; interaction.InteractionDateTime = info.InteractionDateTime; interaction.InteractionTimeToServe = info.InteractionTimeToServe; interaction.RelatedEntityTypeId = info.InteractionRelatedEntityTypeId; interaction.RelatedEntityId = info.InteractionRelatedEntityId; interaction.ChannelCustom1 = info.InteractionChannelCustom1?.Trim(); interaction.ChannelCustom2 = info.InteractionChannelCustom2?.Trim(); interaction.ChannelCustomIndexed1 = info.InteractionChannelCustomIndexed1?.Trim(); interaction.SetInteractionData(info.InteractionData?.Trim()); interactionsToInsert.Add(interaction); } rockContext.BulkInsert(interactionsToInsert); // This logic is normally handled in the Interaction.PostSave method, but since the BulkInsert bypasses those // model hooks, streaks need to be updated here. Also, it is not necessary for this logic to complete before this // transaction can continue processing and exit, so update the streak using a task. interactionsToInsert.ForEach(i => Task.Run(() => StreakTypeService.HandleInteractionRecord(i))); }
/// <summary> /// Executes the specified context. /// </summary> /// <param name="context">The context.</param> public void Execute(IJobExecutionContext context) { var streakTypeId = context.JobDetail.JobDataMap.GetString(DataMapKey.StreakTypeId).AsInteger(); StreakTypeService.RebuildStreakTypeFromAttendance(streakTypeId, out var errorMessage); if (errorMessage.IsNullOrWhiteSpace()) { context.Result = string.Format("Streak maps have been rebuilt for streak type id {0}", streakTypeId); return; } var exception = new Exception(errorMessage); var context2 = HttpContext.Current; ExceptionLogService.LogException(exception, context2); throw exception; }
/// <summary> /// Write the map to the view state but not to the database /// </summary> private void SaveMapState() { var streakType = GetStreakType(); var map = GetTargetMap(); var errorMessage = string.Empty; foreach (ListItem checkbox in cblCheckboxes.Items) { var representedDate = checkbox.Value.AsDateTime(); if (representedDate.HasValue) { map = StreakTypeService.SetBit(map, streakType.StartDate, representedDate.Value, streakType.OccurrenceFrequency, checkbox.Selected, out errorMessage); } } ViewState[ViewStateKey.Map] = StreakTypeService.GetHexDigitStringFromMap(map); }
/// <summary> /// Get the map being edited /// </summary> /// <returns></returns> private DateTime GetMinDate() { var streakType = GetStreakType(); var streakTypeCache = GetStreakTypeCache(); var minDate = StreakTypeService.AlignDate(streakType.StartDate, streakTypeCache); if (IsTargetingEngagementMap()) { var enrollment = GetStreak(); var enrollmentDate = StreakTypeService.AlignDate(enrollment.EnrollmentDate, streakTypeCache); if (enrollmentDate > minDate) { minDate = enrollmentDate; } } return(minDate); }
/// <summary> /// Rebuild the enrollment data /// </summary> private void RebuildData() { var rockContext = GetRockContext(); var streak = GetStreak(); if (!streak.IsAuthorized(Authorization.ADMINISTRATE, CurrentPerson)) { mdDeleteWarning.Show("You are not authorized to rebuild this item.", ModalAlertType.Information); return; } var errorMessage = string.Empty; StreakTypeService.RebuildStreakFromAttendance(streak.StreakTypeId, streak.PersonAlias.PersonId, out errorMessage); if (!errorMessage.IsNullOrWhiteSpace()) { ShowBlockError(nbEditModeMessage, errorMessage); return; } ShowBlockSuccess(nbEditModeMessage, "The streak rebuild was successful!"); }
public void GetAggregateMap() { var map1 = new byte[] { 0b_1000_0000, 0b_0010_0000, 0b_1000_0100 }; var map2 = new byte[] { 0b_1001_0000, 0b_0010_0100, 0b_0000_0100, 0b_1111_0101 }; var map3 = new byte[] { }; var result = StreakTypeService.GetAggregateMap(new byte[][] { map1, map2, map3 }); // Verify map 1 didn't change Assert.That.AreEqual(3, map1.Length); Assert.That.AreEqual(0b_1000_0000, map1[0]); Assert.That.AreEqual(0b_0010_0000, map1[1]); Assert.That.AreEqual(0b_1000_0100, map1[2]); // Verify map 2 didn't change Assert.That.AreEqual(4, map2.Length); Assert.That.AreEqual(0b_1001_0000, map2[0]); Assert.That.AreEqual(0b_0010_0100, map2[1]); Assert.That.AreEqual(0b_0000_0100, map2[2]); Assert.That.AreEqual(0b_1111_0101, map2[3]); // Verify map3 didn't change Assert.That.AreEqual(0, map3.Length); // Verify that the result is a new array, consisting of bytes OR'ed together Assert.That.AreEqual(128, result.Length); Assert.That.AreEqual(map1[2] | map2[3], result[128 - 1]); Assert.That.AreEqual(map1[1] | map2[2], result[128 - 2]); Assert.That.AreEqual(map1[0] | map2[1], result[128 - 3]); Assert.That.AreEqual(map2[0], result[128 - 4]); // Check all the other bytes are 0 for (var i = 0; i < (128 - 4); i++) { Assert.That.AreEqual(0, result[i]); } }
/// <summary> /// Get the default end date that should be used for the date picker /// </summary> /// <returns></returns> private DateTime GetDefaultEndDate() { var streakTypeCache = GetStreakTypeCache(); return(StreakTypeService.AlignDate(RockDateTime.Now, streakTypeCache)); }
/// <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> /// 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); // 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) { // 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; } } // 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 = true; openAttempt.IsSuccessful = progress >= 1m; } } 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($"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; } }
/// <summary> /// Logs the interactions. /// </summary> /// <param name="interactionTransactionInfos">The interaction transaction infos to process.</param> /// <param name="rockContext">The rock context.</param> private void LogInteractions(List <InteractionTransactionInfo> interactionTransactionInfos, RockContext rockContext) { List <Interaction> interactionsToInsert = new List <Interaction>(); var interactionService = new InteractionService(rockContext); foreach (var info in interactionTransactionInfos.Where(a => a.InteractionComponentId.HasValue)) { /* * 2020-06-29 - JH * * The 'CreateInteraction(...)' method called below sets the following properties on the Interaction object: * * - InteractionComponentId * - InteractionDateTime (but with the wrong value) * - InteractionSessionId * - Source * - Medium * - Campaign * - Content * - Term */ var interaction = interactionService.CreateInteraction(info.InteractionComponentId.Value, info.UserAgent, info.InteractionData, info.IPAddress, info.BrowserSessionId); // The rest of the properties need to be manually set. interaction.EntityId = info.InteractionEntityId; interaction.Operation = info.InteractionOperation.IsNotNullOrWhiteSpace() ? info.InteractionOperation.Trim() : "View"; interaction.InteractionSummary = info.InteractionSummary?.Trim(); interaction.PersonAliasId = info.PersonAliasId; interaction.InteractionDateTime = info.InteractionDateTime; interaction.InteractionTimeToServe = info.InteractionTimeToServe; interaction.RelatedEntityTypeId = info.InteractionRelatedEntityTypeId; interaction.RelatedEntityId = info.InteractionRelatedEntityId; interaction.ChannelCustom1 = info.InteractionChannelCustom1?.Trim(); interaction.ChannelCustom2 = info.InteractionChannelCustom2?.Trim(); interaction.ChannelCustomIndexed1 = info.InteractionChannelCustomIndexed1?.Trim(); interaction.Source = interaction.Source ?? info.InteractionSource?.Trim(); interaction.Medium = interaction.Medium ?? info.InteractionMedium?.Trim(); interaction.Campaign = interaction.Campaign ?? info.InteractionCampaign?.Trim(); interaction.Content = interaction.Content ?? info.InteractionContent?.Trim(); interaction.Term = interaction.Term ?? info.InteractionTerm?.Trim(); interaction.InteractionLength = info.InteractionLength; interaction.InteractionEndDateTime = info.InteractionEndDateTime; interaction.SetInteractionData(info.InteractionData?.Trim()); interactionsToInsert.Add(interaction); } rockContext.BulkInsert(interactionsToInsert); // This logic is normally handled in the Interaction.PostSave method, but since the BulkInsert bypasses those // model hooks, streaks need to be updated here. Also, it is not necessary for this logic to complete before this // transaction can continue processing and exit, so update the streak using a task. // Only launch this task if there are StreakTypes configured that have interactions. Otherwise several // database calls are made only to find out there are no streak types defined. if (StreakTypeCache.All().Any(s => s.IsInteractionRelated)) { // Ids do not exit for the interactions in the collection since they were bulk imported. // Read their ids from their guids and append the id. var insertedGuids = interactionsToInsert.Select(i => i.Guid).ToList(); var interactionIds = new InteractionService(new RockContext()).Queryable() .Where(i => insertedGuids.Contains(i.Guid)) .Select(i => new { i.Id, i.Guid }) .ToList(); foreach (var interactionId in interactionIds) { var interaction = interactionsToInsert.Where(i => i.Guid == interactionId.Guid).FirstOrDefault(); if (interaction != null) { interaction.Id = interactionId.Id; } } // Launch task interactionsToInsert.ForEach(i => Task.Run(() => StreakTypeService.HandleInteractionRecord(i.Id))); } }