Skip to content

Commit

Permalink
Needed for plann: Make it possible to prevent a child objects due dat…
Browse files Browse the repository at this point in the history
…e to be pushed past the parents due date
  • Loading branch information
tobixen committed Feb 15, 2023
1 parent 640a314 commit 5de56e4
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 90 deletions.
39 changes: 34 additions & 5 deletions caldav/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)(
Expand Down Expand Up @@ -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)
Expand Down
194 changes: 109 additions & 85 deletions tests/test_caldav.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 5de56e4

Please sign in to comment.