Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow checking DUE on dependent before moving DUE #275

Merged
merged 1 commit into from
Feb 15, 2023
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
39 changes: 34 additions & 5 deletions caldav/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -2615,7 +2615,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 @@ -2707,16 +2707,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