From 5de56e4f3818a7794f1bc575e24cab5a7ff730ba Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sun, 29 Jan 2023 00:19:11 +0100 Subject: [PATCH] Needed for plann: Make it possible to prevent a child objects due date to be pushed past the parents due date --- caldav/objects.py | 39 +++++++-- tests/test_caldav.py | 194 ++++++++++++++++++++++++------------------- 2 files changed, 143 insertions(+), 90 deletions(-) diff --git a/caldav/objects.py b/caldav/objects.py index e56686e4..f02f4ac4 100644 --- a/caldav/objects.py +++ b/caldav/objects.py @@ -2618,7 +2618,7 @@ def complete( * safe - see doc for _complete_recurring_safe for details """ if not completion_timestamp: - completion_timestamp = datetime.utcnow().astimezone(vobject.icalendar.utc) + completion_timestamp = datetime.utcnow().astimezone(timezone.utc) if hasattr(self.instance.vtodo, "rrule") and handle_rrule: return getattr(self, "_complete_recurring_%s" % rrule_mode)( @@ -2710,16 +2710,45 @@ def get_due(self): else: return None - def set_due(self, due, move_dtstart=False): + def set_due(self, due, move_dtstart=False, check_dependent=False): """The RFC specifies that a VTODO cannot have both due and duration, so when setting due, the duration field must be evicted - WARNING: this method is likely to be deprecated and moved to - the icalendar library. If you decide to use it, please put - caldav<2.0 in the requirements. + check_dependent=True will raise some error if there exists a + parent calendar component (through RELATED-TO), and the parents + due or dtend is before the new dtend). + + WARNING: this method is likely to be deprecated and parts of + it moved to the icalendar library. If you decide to use it, + please put caldav<2.0 in the requirements. + + WARNING: the check_dependent-logic may be rewritten to support + RFC9253 in 1.x already """ + if hasattr(due, "tzinfo") and not due.tzinfo: + due = due.astimezone(timezone.utc) i = self.icalendar_component + if check_dependent: + rels = i.get("RELATED-TO") + if rels is None: + rels = [] + if not isinstance(rels, list): + rels = [rels] + for rel in rels: + if rel.params.get("RELTYPE") == "PARENT": + parent = self.parent.object_by_uid(rel) + pend = parent.icalendar_component.get("DTEND") + if pend: + pend = pend.dt + else: + pend = parent.get_due() + if pend and pend.astimezone(timezone.utc) < due: + if check_dependent == "return": + return parent + raise error.ConsistencyError( + "parent object has due/end %s, cannot procrastinate child object without first procrastinating parent object" + ) duration = self.get_duration() i.pop("DURATION", None) i.pop("DUE", None) diff --git a/tests/test_caldav.py b/tests/test_caldav.py index f94f0430..d01c46d3 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -17,6 +17,8 @@ from collections import namedtuple from datetime import date from datetime import datetime +from datetime import timedelta +from datetime import timezone import pytest import requests @@ -1363,6 +1365,113 @@ def testCreateChildParent(self): assert rt == parent.id assert rt.params["RELTYPE"] == "PARENT" + def testSetDue(self): + self.skip_on_compatibility_flag("read_only") + + c = self._fixCalendar(supported_calendar_component_set=["VEVENT"]) + + utc = timezone.utc + + some_todo = c.save_todo( + dtstart=datetime(2022, 12, 26, 19, 15, tzinfo=utc), + due=datetime(2022, 12, 26, 20, 00, tzinfo=utc), + summary="Some task", + uid="ctuid1", + ) + + ## setting the due should ... set the due (surprise, surprise) + some_todo.set_due(datetime(2022, 12, 26, 20, 10, tzinfo=utc)) + assert some_todo.icalendar_component["DUE"].dt == datetime( + 2022, 12, 26, 20, 10, tzinfo=utc + ) + assert some_todo.icalendar_component["DTSTART"].dt == datetime( + 2022, 12, 26, 19, 15, tzinfo=utc + ) + + ## move_dtstart causes the duration to be unchanged + some_todo.set_due(datetime(2022, 12, 26, 20, 20, tzinfo=utc), move_dtstart=True) + assert some_todo.icalendar_component["DUE"].dt == datetime( + 2022, 12, 26, 20, 20, tzinfo=utc + ) + assert some_todo.icalendar_component["DTSTART"].dt == datetime( + 2022, 12, 26, 19, 25, tzinfo=utc + ) + + ## This task has duration set rather than due. Due should be implied to be 19:30. + some_other_todo = c.save_todo( + dtstart=datetime(2022, 12, 26, 19, 15, tzinfo=utc), + duration=timedelta(minutes=15), + summary="Some other task", + uid="ctuid2", + ) + some_other_todo.set_due( + datetime(2022, 12, 26, 19, 45, tzinfo=utc), move_dtstart=True + ) + assert some_other_todo.icalendar_component["DUE"].dt == datetime( + 2022, 12, 26, 19, 45, tzinfo=utc + ) + assert some_other_todo.icalendar_component["DTSTART"].dt == datetime( + 2022, 12, 26, 19, 30, tzinfo=utc + ) + + some_todo.save() + + self.skip_on_compatibility_flag("no_relships") + parent = c.save_todo( + dtstart=datetime(2022, 12, 26, 19, 00, tzinfo=utc), + dtend=datetime(2022, 12, 26, 21, 00, tzinfo=utc), + summary="this is a parent test task", + uid="ctuid3", + child=[some_todo.id], + ) + + ## The above updates the some_todo object on the server side, but the local object is not + ## updated ... until we reload it + some_todo.load() + + ## This should work out (set the childs due to some time before the parents due) + some_todo.set_due( + datetime(2022, 12, 26, 20, 30, tzinfo=utc), + move_dtstart=True, + check_dependent=True, + ) + assert some_todo.icalendar_component["DUE"].dt == datetime( + 2022, 12, 26, 20, 30, tzinfo=utc + ) + assert some_todo.icalendar_component["DTSTART"].dt == datetime( + 2022, 12, 26, 19, 35, tzinfo=utc + ) + + ## This should not work out (set the childs due to some time before the parents due) + with pytest.raises(error.ConsistencyError): + some_todo.set_due( + datetime(2022, 12, 26, 21, 30, tzinfo=utc), + move_dtstart=True, + check_dependent=True, + ) + + child = c.save_todo( + dtstart=datetime(2022, 12, 26, 19, 45), + due=datetime(2022, 12, 26, 19, 55), + summary="this is a test child task", + uid="ctuid4", + parent=[some_todo.id], + ) + + ## This should still work out (set the childs due to some time before the parents due) + ## (The fact that we now have a child does not affect it anyhow) + some_todo.set_due( + datetime(2022, 12, 26, 20, 31, tzinfo=utc), + move_dtstart=True, + check_dependent=True, + ) + assert some_todo.icalendar_component["DUE"].dt == datetime( + 2022, 12, 26, 20, 31, tzinfo=utc + ) + assert some_todo.icalendar_component["DTSTART"].dt == datetime( + 2022, 12, 26, 19, 36, tzinfo=utc + ) + def testCreateJournalListAndJournalEntry(self): """ This test demonstrates the support for journals. @@ -2137,91 +2246,6 @@ def testOffsetURL(self): principal = conn.principal() calendars = principal.calendars() - ## TODO: run this test, ref https://github.com/python-caldav/caldav/issues/91 - ## It should be removed prior to a 1.0-release. - def testBackwardCompatibility(self): - """ - Tobias Brox has done some API changes - but this thing should - still be backward compatible. - """ - self.skip_on_compatibility_flag("read_only") - if "backwards_compatibility_url" not in self.server_params: - pytest.skip( - "backward compatibility check skipped - needs an URL like it was supposed to be in 2013" - ) - caldav = DAVClient(self.server_params["backwards_compatibility_url"]) - principal = Principal(caldav, self.server_params["backwards_compatibility_url"]) - c = Calendar(caldav, name="Yep", parent=principal, id=self.testcal_id).save() - assert c.url is not None - - c.set_properties( - [ - dav.DisplayName("hooray"), - ] - ) - props = c.get_properties( - [ - dav.DisplayName(), - ] - ) - assert props[dav.DisplayName.tag] == "hooray" - - cc = Calendar(caldav, name="Yep", parent=principal).save() - assert cc.url is not None - cc.delete() - - e = Event(caldav, data=ev1, parent=c).save() - assert e.url is not None - ee = Event(caldav, url=url.make(e.url), parent=c) - ee.load() - assert e.instance.vevent.uid == ee.instance.vevent.uid - - r = c.date_search( - datetime(2006, 7, 13, 17, 00, 00), - datetime(2006, 7, 15, 17, 00, 00), - expand=False, - ) - assert e.instance.vevent.uid == r[0].instance.vevent.uid - assert len(r) == 1 - - all = c.events() - assert len(all) == 1 - - e2 = Event(caldav, data=ev2, parent=c).save() - assert e.url is not None - - tmp = c.event("20010712T182145Z-123401@example.com") - assert e2.instance.vevent.uid == tmp.instance.vevent.uid - - r = c.date_search( - datetime(2007, 7, 13, 17, 00, 00), - datetime(2007, 7, 15, 17, 00, 00), - expand=False, - ) - assert len(r) == 1 - - e.data = ev2 - e.save() - - r = c.date_search( - datetime(2007, 7, 13, 17, 00, 00), - datetime(2007, 7, 15, 17, 00, 00), - expand=False, - ) - # for e in r: print(e.data) - assert len(r) == 1 - - e.instance = e2.instance - e.save() - - r = c.date_search( - datetime(2007, 7, 13, 17, 00, 00), - datetime(2007, 7, 15, 17, 00, 00), - expand=False, - ) - # for e in r: print(e.data) - assert len(r) == 1 - def testObjects(self): # TODO: description ... what are we trying to test for here? o = DAVObject(self.caldav)