33A "DAV object" is anything we get from the caldav server or push into the
44caldav server, notably principal, calendars and calendar events.
55
6- (This file has become huge and will be split up prior to the next
7- release . I think it makes sense moving the CalendarObjectResource
8- class hierarchy into a separate file)
6+ (This file has become huge and will be split up when the list of
7+ pull requests have been cleared . I think it makes sense moving the
8+ CalendarObjectResource class hierarchy into a separate file)
99"""
1010import re
1111import sys
@@ -43,7 +43,7 @@ class hierarchy into a separate file)
4343from caldav .lib .python_utilities import to_wire
4444
4545try :
46- from typing import ClassVar , Optional , Union
46+ from typing import ClassVar , Optional , Union , Type
4747
4848 TimeStamp = Optional [Union [date , datetime ]]
4949except :
@@ -849,14 +849,14 @@ def _use_or_create_ics(self, ical, objtype, **ical_data):
849849 return vcal .create_ical (objtype = objtype , ** ical_data )
850850 return ical
851851
852- ## TODO: consolidate save_* - too much code duplication here
853- def save_event (
852+ def save_object (
854853 self ,
854+ objclass : Type [DAVObject ],
855855 ical : Optional [str ] = None ,
856856 no_overwrite : bool = False ,
857857 no_create : bool = False ,
858858 ** ical_data ,
859- ) -> "Event " :
859+ ) -> "CalendarResourceObject " :
860860 """
861861 Add a new event to the calendar, with the given ical.
862862
@@ -866,73 +866,48 @@ def save_event(
866866 * no_create - don't create a new object, existing calendar objects should be updated
867867 * ical_data - passed to lib.vcal.create_ical
868868 """
869- e = Event (
869+ o = objclass (
870870 self .client ,
871- data = self ._use_or_create_ics (ical , objtype = "VEVENT" , ** ical_data ),
871+ data = self ._use_or_create_ics (
872+ ical , objtype = f"V{ objclass .__name__ .upper ()} " , ** ical_data
873+ ),
872874 parent = self ,
873875 )
874- e .save (no_overwrite = no_overwrite , no_create = no_create , obj_type = "event" )
875- self ._handle_relations (e .id , ical_data )
876- return e
876+ o = o .save (no_overwrite = no_overwrite , no_create = no_create )
877+ ## TODO: Saving nothing is currently giving an object with None as URL.
878+ ## This should probably be changed in some future version to raise an error
879+ ## See also CalendarObjectResource.save()
880+ if o .url is not None :
881+ o ._handle_reverse_relations (fix = True )
882+ return o
877883
878- def save_todo (
879- self ,
880- ical : Optional [str ] = None ,
881- no_overwrite : bool = False ,
882- no_create : bool = False ,
883- ** ical_data ,
884- ) -> "Todo" :
884+ ## It could still be possible to refactor even more, but
885+ ## readability would be harder
886+ def save_event (self , * largs , ** kwargs ) -> "Event" :
885887 """
886- Add a new task to the calendar, with the given ical.
887-
888- Parameters:
889- * ical - ical object (text)
888+ See save_object
890889 """
891- t = Todo (
892- self .client ,
893- data = self ._use_or_create_ics (ical , objtype = "VTODO" , ** ical_data ),
894- parent = self ,
895- )
896- t .save (no_overwrite = no_overwrite , no_create = no_create , obj_type = "todo" )
897- self ._handle_relations (t .id , ical_data )
898- return t
890+ return self .save_object (Event , * largs , ** kwargs )
899891
900- def save_journal (
901- self ,
902- ical : Optional [str ] = None ,
903- no_overwrite : bool = False ,
904- no_create : bool = False ,
905- ** ical_data ,
906- ) -> "Journal" :
892+ def save_todo (self , * largs , ** kwargs ) -> "Todo" :
907893 """
908- Add a new journal entry to the calendar, with the given ical.
909-
910- Parameters:
911- * ical - ical object (text)
894+ See save_object
912895 """
913- j = Journal (
914- self .client ,
915- data = self ._use_or_create_ics (ical , objtype = "VJOURNAL" , ** ical_data ),
916- parent = self ,
917- )
918- j .save (no_overwrite = no_overwrite , no_create = no_create , obj_type = "journal" )
919- self ._handle_relations (j .id , ical_data )
920- return j
896+ return self .save_object (Todo , * largs , ** kwargs )
921897
922- def _handle_relations (self , uid , ical_data ) -> None :
923- for reverse_reltype , other_uid in [
924- ("parent" , x ) for x in ical_data .get ("child" , ())
925- ] + [("child" , x ) for x in ical_data .get ("parent" , ())]:
926- other = self .object_by_uid (other_uid )
927- other .set_relation (other = uid , reltype = reverse_reltype , set_reverse = False )
898+ def save_journal (self , * largs , ** kwargs ) -> "Journal" :
899+ """
900+ See save_object
901+ """
902+ return self .save_object (Journal , * largs , ** kwargs )
928903
929904 ## legacy aliases
930905 ## TODO: should be deprecated
931906
932907 ## TODO: think more through this - is `save_foo` better than `add_foo`?
933908 ## `save_foo` should not be used for updating existing content on the
934909 ## calendar!
935-
910+ add_object = save_object
936911 add_event = save_event
937912 add_todo = save_todo
938913 add_journal = save_journal
@@ -1938,10 +1913,24 @@ class CalendarObjectResource(DAVObject):
19381913 event, a todo-item, a journal entry, or a free/busy entry
19391914 """
19401915
1941- RELTYPE_REVERSER : ClassVar = {
1916+ ## There is also STARTTOFINISH, STARTTOSTART and FINISHTOFINISH in RFC9253,
1917+ ## those do not seem to have any reverse
1918+ ## (FINISHTOSTART and STARTTOFINISH may seem like reverse relations, but
1919+ ## as I read the RFC, FINISHTOSTART seems like the reverse of DEPENDS-ON)
1920+ ## (STARTTOSTART and FINISHTOFINISH may also seem like symmetric relations,
1921+ ## meaning they are their own reverse, but as I read the RFC they are
1922+ ## asymmetric)
1923+ RELTYPE_REVERSE_MAP : ClassVar = {
19421924 "PARENT" : "CHILD" ,
19431925 "CHILD" : "PARENT" ,
19441926 "SIBLING" : "SIBLING" ,
1927+ ## this is how Tobias Brox inteprets RFC9253:
1928+ "DEPENDS-ON" : "FINISHTOSTART" ,
1929+ "FINISHTOSTART" : "DEPENDENT" ,
1930+ ## next/first is a special case, linked list
1931+ ## it needs special handling when length of list<>2
1932+ # "NEXT": "FIRST",
1933+ # "FIRST": "NEXT",
19451934 }
19461935
19471936 _ENDPARAM = None
@@ -2072,7 +2061,9 @@ def set_relation(
20722061 if set_reverse :
20732062 other = self .parent .object_by_uid (uid )
20742063 if set_reverse :
2075- reltype_reverse = self .RELTYPE_REVERSER [reltype ]
2064+ ## TODO: special handling of NEXT/FIRST.
2065+ ## STARTTOFINISH does not have any equivalent "reverse".
2066+ reltype_reverse = self .RELTYPE_REVERSE_MAP [reltype ]
20762067 other .set_relation (other = self , reltype = reltype_reverse , set_reverse = False )
20772068
20782069 existing_relation = self .icalendar_component .get ("related-to" , None )
@@ -2119,6 +2110,10 @@ def get_relatives(
21192110
21202111 TODO: this is partially overlapped by plann.lib._relships_by_type
21212112 in the plann tool. Should consolidate the code.
2113+
2114+ TODO: should probably return some kind of object instead of a weird dict structure.
2115+ (but due to backward compatibility requirement, such an object should behave like
2116+ the current dict)
21222117 """
21232118 ret = defaultdict (set )
21242119 relations = self .icalendar_component .get ("RELATED-TO" , [])
@@ -2156,6 +2151,88 @@ def get_relatives(
21562151
21572152 return ret
21582153
2154+ def _set_reverse_relation (self , other , reltype ):
2155+ ## TODO: handle RFC9253 better! Particularly next/first-lists
2156+ reverse_reltype = self .RELTYPE_REVERSE_MAP .get (reltype )
2157+ if not reverse_reltype :
2158+ logging .error (
2159+ "Reltype %s not supported in object uid %s" % (reltype , self .id )
2160+ )
2161+ return
2162+ other .set_relation (self , reverse_reltype , other )
2163+
2164+ def _verify_reverse_relation (self , other , reltype ) -> tuple :
2165+ revreltype = self .RELTYPE_REVERSE_MAP [reltype ]
2166+ ## TODO: special case FIRST/NEXT needs special handling
2167+ other_relations = other .get_relatives (
2168+ fetch_objects = False , reltypes = {revreltype }
2169+ )
2170+ if not str (self .icalendar_component ["uid" ]) in other_relations [revreltype ]:
2171+ ## I don't remember why we need to return a tuple
2172+ ## but it's propagated through the "public" methods, so we'll
2173+ ## have to leave it like this.
2174+ return (other , revreltype )
2175+ return False
2176+
2177+ def _handle_reverse_relations (
2178+ self , verify : bool = False , fix : bool = False , pdb : bool = False
2179+ ) -> list :
2180+ """
2181+ Goes through all relations and verifies that the return relation is set
2182+ if verify is set:
2183+ Returns a list of objects missing a reverse.
2184+ Use public method check_reverse_relations instead
2185+ if verify and fix is set:
2186+ Fixup all objects missing a reverse.
2187+ Use public method fix_reverse_relations instead.
2188+ If fix but not verify is set:
2189+ Assume all reverse relations are missing.
2190+ Used internally when creating new objects.
2191+ """
2192+ ret = []
2193+ assert verify or fix
2194+ relations = self .get_relatives ()
2195+ for reltype in relations :
2196+ for other in relations [reltype ]:
2197+ if verify :
2198+ foobar = self ._verify_reverse_relation (other , reltype )
2199+ if foobar :
2200+ ret .append (foobar )
2201+ if pdb :
2202+ breakpoint ()
2203+ if fix :
2204+ self ._set_reverse_relation (other , reltype )
2205+ elif fix :
2206+ self ._set_reverse_relation (other , reltype )
2207+ return ret
2208+
2209+ def check_reverse_relations (self , pdb : bool = False ) -> List [tuple ]:
2210+ """
2211+ Will verify that for all the objects we point at though
2212+ the RELATED-TO property, the other object points back to us as
2213+ well.
2214+
2215+ Returns a list of tuples. Each tuple contains an object that
2216+ do not point back as expected, and the expected reltype
2217+ """
2218+ return self ._handle_reverse_relations (verify = True , fix = False , pdb = pdb )
2219+
2220+ def fix_reverse_relations (self , pdb : bool = False ) -> list :
2221+ """
2222+ Will ensure that for all the objects we point at though
2223+ the RELATED-TO property, the other object points back to us as
2224+ well.
2225+
2226+ Returns a list of tuples. Each tuple contains an object that
2227+ did not point back as expected, and the expected reltype
2228+ """
2229+ return self ._handle_reverse_relations (verify = True , fix = True , pdb = pdb )
2230+
2231+ ## TODO: fix this (and consolidate with _handle_relations / set_relation?)
2232+ # def ensure_reverse_relations(self):
2233+ # missing_relations = self.check_reverse_relations()
2234+ # ...
2235+
21592236 def _get_icalendar_component (self , assert_one = False ):
21602237 """Returns the icalendar subcomponent - which should be an
21612238 Event, Journal, Todo or FreeBusy from the icalendar class
@@ -2526,11 +2603,15 @@ def save(
25262603 * self
25272604
25282605 """
2606+ if not obj_type :
2607+ obj_type = self .__class__ .__name__ .lower ()
25292608 if (
25302609 self ._vobject_instance is None
25312610 and self ._data is None
25322611 and self ._icalendar_instance is None
25332612 ):
2613+ ## TODO: This makes no sense. We should probably raise an error.
2614+ ## But the behaviour should be officially deprecated first.
25342615 return self
25352616
25362617 path = self .url .path if self .url else None
0 commit comments