Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions caldav/calendarobjectresource.py
Original file line number Diff line number Diff line change
Expand Up @@ -2652,6 +2652,10 @@ def _next(self, ts=None, i=None, dtstart=None, rrule=None, by=None, no_count=Tru
dtstart = dtstart.astimezone(timezone.utc)
if not ts:
ts = dtstart
elif isinstance(ts, datetime) and isinstance(dtstart, datetime) and ts < dtstart:
## Completed before DTSTART (case 1A): the occurrence at
## DTSTART is the one done, so look past it (issue #782)
ts = dtstart
## Counting is taken care of other places
if no_count and "COUNT" in rrule:
rrule = rrule.copy()
Expand Down
1 change: 1 addition & 0 deletions changes/782.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`complete(handle_rrule=True)` on a recurring task with fixed times (`BY*` parts in the `RRULE`, e.g. `FREQ=YEARLY;BYMONTH=4,9`) did not advance the task when it was completed before its `DTSTART`: the next occurrence was looked for after the completion time, which found the occurrence just completed. It is now looked for after `DTSTART`.
4 changes: 4 additions & 0 deletions docs/design/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

`search()` decides the `!=` / `<>`, `!contains` and `def` operators client-side and leaves them out of the server query (https://github.com/python-caldav/caldav/issues/568). A caldav-server-tester probe for `negate-condition` support, and the compatibility_hints flag it would feed, would let `search()` send `!contains` as a negated `text-match` to the servers that honour it.

## Completing an all-day fixed-time recurring task

`Todo._next()` in `caldav/calendarobjectresource.py` raises `TypeError` (naive vs aware) for a task with `DTSTART;VALUE=DATE` and `BY*` parts in its `RRULE`: dateutil turns the date into a naive datetime and compares it with the UTC completion timestamp, so `complete(handle_rrule=True)` fails for such tasks. Fix: convert a date DTSTART to an aware datetime (or the timestamp to a date) before both the comparison and `rrulestr()`, with a unit test. Found in the review of the fix for https://github.com/python-caldav/caldav/issues/782.

## Calendar cleanup in integration tests

For servers where calendars cannot be created, the test will take the first available calendar it finds and wipe it. That is not acceptable without the user explicitly configuring that this is OK.
Expand Down
43 changes: 43 additions & 0 deletions tests/test_caldav_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -5972,6 +5972,49 @@ def test_build_safe_completed_count_one_skips_next(self) -> None:
)
assert todo.icalendar_component["RRULE"]["COUNT"] == [1]

## Issue https://github.com/python-caldav/caldav/issues/782: a
## fixed-time task (BY* in the RRULE) done before its DTSTART
EARLY = todo6.replace("RRULE:FREQ=YEARLY", "RRULE:FREQ=YEARLY;BYMONTH=4,9")

def test_next_fixed_time_completed_early(self) -> None:
todo = self._make_todo(self.EARLY)
## done two days before the 1992-04-15 13:30 occurrence
early = datetime(1992, 4, 13, 10, tzinfo=timezone.utc)
assert todo._next(early) == datetime(1992, 9, 15, 13, 30, tzinfo=timezone.utc)
## done after DTSTART: unchanged
late = datetime(1992, 4, 16, 10, tzinfo=timezone.utc)
assert todo._next(late) == datetime(1992, 9, 15, 13, 30, tzinfo=timezone.utc)

def test_next_floating_fixed_time_completed_early(self) -> None:
"""A floating DTSTART is taken as local time before the comparison."""
todo = self._make_todo(
self.EARLY.replace("DTSTART:19920415T133000Z", "DTSTART:19920415T133000").replace(
"DUE:19920516T045959Z", "DUE:19920516T045959"
)
)
dtstart = datetime(1992, 4, 15, 13, 30).astimezone(timezone.utc)
assert todo._next(dtstart - timedelta(days=2)) == datetime(1992, 9, 15, 13, 30).astimezone(
timezone.utc
)

def test_safe_completion_completed_early_advances(self) -> None:
todo = self._make_todo(self.EARLY)
completed = todo._build_recurring_safe_completed(
datetime(1992, 4, 13, 10, tzinfo=timezone.utc)
)
assert completed is not None
assert todo.icalendar_component["DTSTART"].dt == datetime(
1992, 9, 15, 13, 30, tzinfo=timezone.utc
)

def test_thisandfuture_completed_early_advances(self) -> None:
todo = self._make_todo(self.EARLY)
todo._prepare_recurring_thisandfuture(datetime(1992, 4, 13, 10, tzinfo=timezone.utc))
thisandfuture = todo.icalendar_instance.subcomponents[-1]
nxt = datetime(1992, 9, 15, 13, 30, tzinfo=timezone.utc)
assert thisandfuture["RECURRENCE-ID"].dt == nxt
assert thisandfuture["DTSTART"].dt == nxt

def test_safe_completion_issues_two_puts(self, monkeypatch: Any) -> None:
"""The standalone completed copy must not be PUT twice.

Expand Down
Loading