From 7be636149cea398aa4098f423159a19975224427 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Fri, 8 Dec 2023 19:47:46 +0100 Subject: [PATCH] * fix for issue https://github.com/python-caldav/caldav/issues/352 * refactored some usage of vobject into icalendar. * lots more test code for the sorting * fixed some corner cases where sorting would throw a runtime error * creating a task with some explicit status set didn't work --- CHANGELOG.md | 6 ++++- caldav/lib/vcal.py | 4 ++-- caldav/objects.py | 31 +++++++++++++++---------- tests/test_caldav.py | 55 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b700f77b..4471717f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,11 @@ Some bugfixes. * Bugfix that a 500 internal server error could cause an recursion loop, credits to github user @bchardin in https://github.com/python-caldav/caldav/pull/344 * Compatibility-fix for Google calendar, credits to github user @e-katov in https://github.com/python-caldav/caldav/pull/344 * Spelling, grammar and removing a useless regexp, credits to github user @scop in https://github.com/python-caldav/caldav/pull/337 -* Faulty icalendar code caused the code for fixing faulty icalendar code to break, credits to github user @yuwash in https://github.com/python-caldav/caldav/pull/347 +* Faulty icalendar code caused the code for fixing faulty icalendar code to break, credits to github user @yuwash in https://github.com/python-caldav/caldav/pull/347 and https://github.com/python-caldav/caldav/pull/350 +* Sorting on uppercase attributes didn't work, ref issue https://github.com/python-caldav/caldav/issues/352 - credits to github user @ArtemIsmagilov. +* The sorting algorithm was dependent on vobject library - refactored to use icalendar library instead +* Lots more test code on the sorting, and fixed some corner cases +* Creating a task with a status didn't work ## [1.3.6] - 2023-07-20 diff --git a/caldav/lib/vcal.py b/caldav/lib/vcal.py index 764fcc2b..8ef08fa3 100644 --- a/caldav/lib/vcal.py +++ b/caldav/lib/vcal.py @@ -165,11 +165,11 @@ def create_ical(ical_fragment=None, objtype=None, language="en_DK", **props): ## (otherwise we cannot easily add a task to a davical calendar and ## then find it again - ref https://gitlab.com/davical-project/davical/-/issues/281 if ( - not props.get("STATUS") + not props.get("status") and not "\nSTATUS:" in (ical_fragment or "") and objtype == "VTODO" ): - props["STATUS"] = "NEEDS-ACTION" + props["status"] = "NEEDS-ACTION" else: if not ical_fragment.strip().startswith("BEGIN:VCALENDAR"): diff --git a/caldav/objects.py b/caldav/objects.py index 58283e2c..1ca092ec 100644 --- a/caldav/objects.py +++ b/caldav/objects.py @@ -1111,33 +1111,40 @@ def search( def sort_key_func(x): ret = [] - for objtype in ("vtodo", "vevent", "vjournal"): - if hasattr(x.instance, objtype): - vobj = getattr(x.instance, objtype) - break + comp = x.icalendar_component defaults = { + ## TODO: all possible non-string sort attributes needs to be listed here, otherwise we will get type errors when comparing objects with the property defined vs undefined (or maybe we should make an "undefined" object that always will compare below any other type? Perhaps there exists such an object already?) "due": "2050-01-01", "dtstart": "1970-01-01", - "priority": "0", + "priority": 0, + "status": { + "VTODO": "NEEDS-ACTION", + "VJOURNAL": "FINAL", + "VEVENT": "TENTATIVE", + }[comp.name], + "category": "", ## Usage of strftime is a simple way to ensure there won't be ## problems if comparing dates with timestamps "isnt_overdue": not ( - hasattr(vobj, "due") - and vobj.due.value.strftime("%F%H%M%S") + "due" in comp + and comp["due"].dt.strftime("%F%H%M%S") < datetime.now().strftime("%F%H%M%S") ), "hasnt_started": ( - hasattr(vobj, "dtstart") - and vobj.dtstart.value.strftime("%F%H%M%S") + "dtstart" in comp + and comp["dtstart"].dt.strftime("%F%H%M%S") > datetime.now().strftime("%F%H%M%S") ), } for sort_key in sort_keys: - val = getattr(vobj, sort_key, None) + val = comp.get(sort_key, None) if val is None: - ret.append(defaults.get(sort_key, "0")) + ret.append(defaults.get(sort_key.lower(), "")) continue - val = val.value + if hasattr(val, "dt"): + val = val.dt + elif hasattr(val, "cats"): + val = ",".join(val.cats) if hasattr(val, "strftime"): ret.append(val.strftime("%F%H%M%S")) else: diff --git a/tests/test_caldav.py b/tests/test_caldav.py index 7b9d1586..14892379 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -1327,6 +1327,61 @@ def testSearchEvent(self): assert len(all_events) == 3 assert all_events[0].instance.vevent.summary.value == "Bastille Day Jitsi Party" + ## Sorting by upper case should also wor + all_events = c.search(sort_keys=("SUMMARY", "DTSTAMP")) + assert len(all_events) == 3 + assert all_events[0].instance.vevent.summary.value == "Bastille Day Jitsi Party" + + def testSearchSortTodo(self): + self.skip_on_compatibility_flag("read_only") + self.skip_on_compatibility_flag("no_todo") + c = self._fixCalendar(supported_calendar_component_set=["VTODO"]) + t1 = c.save_todo( + summary="1 task overdue", + due=date(2022, 12, 12), + dtstart=date(2022, 10, 11), + uid="1", + ) + t2 = c.save_todo( + summary="2 task future", + due=datetime.now() + timedelta(hours=15), + dtstart=datetime.now() + timedelta(minutes=15), + uid="2", + ) + t3 = c.save_todo( + summary="3 task future due", + due=datetime.now() + timedelta(hours=15), + dtstart=datetime(2022, 12, 11, 10, 9, 8), + uid="3", + ) + t4 = c.save_todo(summary="4 task priority low", priority=9, uid="4") + t5 = c.save_todo(summary="5 task status completed", status="COMPLETED", uid="5") + t6 = c.save_todo( + summary="6 task has categories", categories="home,garden,sunshine", uid="6" + ) + + def check_order(tasks, order): + assert [str(x.icalendar_component["uid"]) for x in tasks] == [ + str(x) for x in order + ] + + all_tasks = c.search(todo=True, sort_keys=("uid",)) + check_order(all_tasks, (1, 2, 3, 4, 6)) + + all_tasks = c.search(sort_keys=("summary",)) + check_order(all_tasks, (1, 2, 3, 4, 5, 6)) + + all_tasks = c.search( + sort_keys=("isnt_overdue", "categories", "dtstart", "priority", "status") + ) + ## This is difficult ... + ## * 1 is the only one that is overdue, and False sorts before True, so 1 comes first + ## * categories, empty string sorts before a non-empty string, so 6 is at the end of the list + ## So we have 2-5 still to worry about ... + ## * dtstart - default is "long ago", so 4,5 or 5,4 should be first, followed by 3,2 + ## * priority - default is 0, so 5 comes before 4 + check_order(all_tasks, (1, 5, 4, 3, 2, 6)) + def testSearchTodos(self): self.skip_on_compatibility_flag("read_only") self.skip_on_compatibility_flag("no_todo")