Skip to content

Commit

Permalink
* fix for issue #352
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
tobixen committed Dec 8, 2023
1 parent fcef569 commit 7be6361
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 15 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions caldav/lib/vcal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
31 changes: 19 additions & 12 deletions caldav/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
55 changes: 55 additions & 0 deletions tests/test_caldav.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 7be6361

Please sign in to comment.