diff --git a/ambition_utils/rrule/models.py b/ambition_utils/rrule/models.py index 2a4a794..2912ef4 100644 --- a/ambition_utils/rrule/models.py +++ b/ambition_utils/rrule/models.py @@ -351,9 +351,11 @@ def set_date_objects(self): """ Ensure that all the date keys are properly set on all rrule params """ + # A cloned object will not have a primary key but does have a next_occurrence that should not be reset + # to the first date in a series. + is_new = self.pk is None and self.last_occurrence is None # Convert the rrule and exclusion rrule params to properly set date keys - is_new = self.pk is None self.set_date_objects_for_params(self.rrule_params, is_new=is_new) self.set_date_objects_for_params(self.rrule_exclusion_params, is_new=is_new) @@ -470,11 +472,13 @@ def clone_with_day_offset(self, day_offset: int) -> RRule: """ Creates a clone of a passed RRule object with day_offset set. clone() is not called to ensure .id is not set before .save() so offset is applied. + The clone's next_occurrence is set to the offset of this object. :param day_offset: The number of days to offset the clone's start date. Can be negative. """ clone = copy.deepcopy(self) clone.id = None clone.day_offset = day_offset + clone.next_occurrence = clone.offset(clone.next_occurrence) clone.save() return clone diff --git a/ambition_utils/rrule/tests/model_tests.py b/ambition_utils/rrule/tests/model_tests.py index 4a6f665..04ceb17 100644 --- a/ambition_utils/rrule/tests/model_tests.py +++ b/ambition_utils/rrule/tests/model_tests.py @@ -1614,6 +1614,37 @@ def test_next_occurrences_day_offset(self): self.assertEqual(rrule1.next_occurrence, datetime.datetime(2017, 1, 10)) self.assertEqual(rrule2.next_occurrence, datetime.datetime(2017, 1, 15)) + def test_next_occurrence_clone_with_offset(self): + """ + Cloning an established object with an offset should yield a next_occurrence of the object's with the offset. + """ + # A daily recurrence with a multi-day offset to highlight differences clearly. + rrule1 = G( + RRule, + rrule_params={ + 'freq': rrule.DAILY, + 'interval': 1, + 'dtstart': datetime.datetime(2023, 1, 1), + }, + occurrence_handler_path='ambition_utils.rrule.tests.model_tests.HandlerOne', + ) + + # Many days into the future + with freeze_time('1-10-2023'): + # Catch the recurrence object up. + while rrule1.next_occurrence <= datetime.datetime.now(): + RRule.objects.handle_overdue() + rrule1.refresh_from_db() + self.assertEqual(rrule1.next_occurrence, datetime.datetime(2023, 1, 11)) + + # Create a clone with an offset and assert that the clone's next occurrence is 1/11 -5 = 1/6. + rrule2 = rrule1.clone_with_day_offset(day_offset=-5) + self.assertEqual(rrule2.next_occurrence, datetime.datetime(2023, 1, 6)) + + # First object is unchanged + rrule1.refresh_from_db() + self.assertEqual(rrule1.next_occurrence, datetime.datetime(2023, 1, 11)) + class RRuleWithExclusionTest(TestCase): def test_exclusion(self):