Skip to content

Commit

Permalink
WIP: a check_reverse_relation method
Browse files Browse the repository at this point in the history
  • Loading branch information
tobixen committed Oct 1, 2023
1 parent 373e86d commit 150e700
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 2 deletions.
2 changes: 1 addition & 1 deletion caldav/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import vobject.icalendar

__version__ = "1.3.6"
__version__ = "1.4.0dev"

from .davclient import DAVClient
from .objects import *
Expand Down
46 changes: 45 additions & 1 deletion caldav/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,6 @@ def _handle_relations(self, uid, ical_data):
## TODO: think more through this - is `save_foo` better than `add_foo`?
## `save_foo` should not be used for updating existing content on the
## calendar!

add_event = save_event
add_todo = save_todo
add_journal = save_journal
Expand Down Expand Up @@ -1709,10 +1708,18 @@ class CalendarObjectResource(DAVObject):
event, a todo-item, a journal entry, or a free/busy entry
"""

## There is also STARTTOFINISH in RFC9253, it does not have any reverse
RELTYPE_REVERSER: ClassVar = {
"PARENT": "CHILD",
"CHILD": "PARENT",
"SIBLING": "SIBLING",
## this is how Tobias Brox inteprets RFC9253:
"DEPENDENT": "FINISHTOSTART",
"FINISHTOSTART": "DEPENDENT",
## next/first is a special case, linked list
## it needs special handling when length of list<>2
"NEXT": "FIRST",
"FIRST": "NEXT",
}

_ENDPARAM = None
Expand Down Expand Up @@ -1827,6 +1834,8 @@ def set_relation(
if set_reverse:
other = self.parent.object_by_uid(uid)
if set_reverse:
## TODO: special handling of NEXT/FIRST.
## STARTTOFINISH does not have any equivalent "reverse".
reltype_reverse = self.RELTYPE_REVERSER[reltype]
other.set_relation(other=self, reltype=reltype_reverse, set_reverse=False)

Expand Down Expand Up @@ -1870,6 +1879,10 @@ def get_relatives(
TODO: this is partially overlapped by plann.lib._relships_by_type
in the plann tool. Should consolidate the code.
TODO: should probably return some kind of object instead of a weird dict structure.
(but due to backward compatibility requirement, such an object should behave like
the current dict)
"""
ret = defaultdict(set)
relations = self.icalendar_component.get("RELATED-TO", [])
Expand All @@ -1895,6 +1908,37 @@ def get_relatives(
raise
return ret


def check_reverse_relations(self, pdb: bool = False) -> list:
"""
Goes through all relations and verifies that the return relation is set
Returns a list of objects missing a reverse (or an empty list if everything is OK)
"""
ret = []
relations = self.get_relatives()
for reltype in relations:
for other in relations[reltype]:
revreltype = self.RELTYPE_REVERSER[reltype]
## TODO: special case FIRST/NEXT needs special handling
other_relations = other.get_relatives(
fetch_objects=False, reltypes={revreltype}
)
if (
not str(self.icalendar_component["uid"])
in other_relations[revreltype]
):
if pdb:
import pdb

pdb.set_trace()
ret.append((other, revreltype))
return ret

## TODO: fix this (and consolidate with _handle_relations / set_relation?)
# def ensure_reverse_relations(self):
# missing_relations = self.check_reverse_relations()
# ...

def _get_icalendar_component(self, assert_one=False):
"""Returns the icalendar subcomponent - which should be an
Event, Journal, Todo or FreeBusy from the icalendar class
Expand Down
18 changes: 18 additions & 0 deletions tests/test_caldav.py
Original file line number Diff line number Diff line change
Expand Up @@ -1498,6 +1498,24 @@ def testCreateChildParent(self):
assert len(foo["PARENT"]) == 1
foo = parent_.get_relatives(relfilter=lambda x: x.params.get("GAP"))

## verify the check_reverse_relations method (TODO: move to a separate test)
assert parent_.check_reverse_relations() == []
assert child_.check_reverse_relations() == []
assert grandparent_.check_reverse_relations() == []

## My grandchild is also my child ... that sounds fishy
grandparent_.set_relation(child, reltype='CHILD', set_reverse=False)

## The check_reverse should tell that something is amiss
missing_parent = grandparent_.check_reverse_relations()
assert len(missing_parent) == 1
assert missing_parent[0][0].icalendar_component['uid'] == 'ctuid2'
assert missing_parent[0][1] == 'PARENT'
## But only when run on the grandparent. The child is blissfully
## unaware who the second parent is (even if reloading it).
child_.load()
assert child_.check_reverse_relations() == []

def testSetDue(self):
self.skip_on_compatibility_flag("read_only")

Expand Down

0 comments on commit 150e700

Please sign in to comment.