From 150e700d024516d77caf4cc62e8878fbfd413014 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sun, 1 Oct 2023 23:36:09 +0200 Subject: [PATCH] WIP: a check_reverse_relation method --- caldav/__init__.py | 2 +- caldav/objects.py | 46 +++++++++++++++++++++++++++++++++++++++++++- tests/test_caldav.py | 18 +++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/caldav/__init__.py b/caldav/__init__.py index b7ea935c..c1bc367b 100644 --- a/caldav/__init__.py +++ b/caldav/__init__.py @@ -3,7 +3,7 @@ import vobject.icalendar -__version__ = "1.3.6" +__version__ = "1.4.0dev" from .davclient import DAVClient from .objects import * diff --git a/caldav/objects.py b/caldav/objects.py index 01c19794..2afa602b 100644 --- a/caldav/objects.py +++ b/caldav/objects.py @@ -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 @@ -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 @@ -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) @@ -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", []) @@ -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 diff --git a/tests/test_caldav.py b/tests/test_caldav.py index c5068b93..fe51a5b5 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -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")