1 #!/usr/bin/env python 2 3 """ 4 Handlers for a resource. 5 """ 6 7 from datetime import date, datetime, timedelta 8 from imiptools.content import Handler, format_datetime, to_part 9 from imiptools.period import have_conflict, insert_period, remove_period 10 from vCalendar import to_node 11 from vRecurrence import get_parameters, get_rule 12 13 class Event(Handler): 14 15 "An event handler." 16 17 def add(self): 18 pass 19 20 def cancel(self): 21 pass 22 23 def counter(self): 24 25 "Since this handler does not send requests, it will not handle replies." 26 27 pass 28 29 def declinecounter(self): 30 31 """ 32 Since this handler does not send counter proposals, it will not handle 33 replies to such proposals. 34 """ 35 36 pass 37 38 def publish(self): 39 pass 40 41 def refresh(self): 42 pass 43 44 def reply(self): 45 46 "Since this handler does not send requests, it will not handle replies." 47 48 pass 49 50 def request(self): 51 52 """ 53 Respond to a request by preparing a reply containing accept/decline 54 information for each indicated attendee. 55 56 No support for countering requests is implemented. 57 """ 58 59 oa = self.require_organiser_and_attendees() 60 if not oa: 61 return None 62 63 (organiser, organiser_attr), attendees = oa 64 65 # Process each attendee separately. 66 67 calendar = [] 68 69 for attendee, attendee_attr in attendees.items(): 70 71 # Check for event using UID. 72 73 if not self.have_new_object(attendee, "VEVENT"): 74 continue 75 76 # If newer than any old version, discard old details from the 77 # free/busy record and check for suitability. 78 79 dtstart = self.get_utc_datetime("DTSTART") 80 dtend = self.get_utc_datetime("DTEND") 81 82 # NOTE: Need also DURATION support. 83 84 duration = dtend - dtstart 85 86 # Recurrence rules create multiple instances to be checked. 87 # Conflicts may only be assessed within a period defined by policy 88 # for the agent, with instances outside that period being considered 89 # unchecked. 90 91 # NOTE: Need to expose the 100 day window in the configuration. 92 93 window_end = datetime.now() + timedelta(100) 94 95 # NOTE: Need also RDATE and EXDATE support. 96 97 rrule = self.get_value("RRULE") 98 99 if rrule: 100 selector = get_rule(dtstart, rrule) 101 parameters = get_parameters(rrule) 102 periods = [] 103 for start in selector.materialise(dtstart, window_end, parameters.get("COUNT"), parameters.get("BYSETPOS")): 104 start = datetime(*start, tzinfo=timezone("UTC")) 105 end = start + duration 106 periods.append((format_datetime(start), format_datetime(end))) 107 else: 108 periods = [(format_datetime(dtstart), format_datetime(dtend))] 109 110 conflict = False 111 freebusy = self.store.get_freebusy(attendee) or [] 112 113 if freebusy: 114 remove_period(freebusy, self.uid) 115 conflict = have_conflict(freebusy, periods) 116 117 # If the event can be scheduled, it is registered and a reply sent 118 # accepting the event. (The attendee has PARTSTAT=ACCEPTED as an 119 # attribute.) 120 121 if not conflict: 122 for start, end in periods: 123 insert_period(freebusy, (start, end, self.uid)) 124 125 if self.get_value("TRANSP") in (None, "OPAQUE"): 126 self.store.set_freebusy(attendee, freebusy) 127 128 if self.publisher: 129 self.publisher.set_freebusy(attendee, freebusy) 130 131 self.store.set_event(attendee, self.uid, to_node( 132 {"VEVENT" : [(self.details, {})]} 133 )) 134 attendee_attr["PARTSTAT"] = "ACCEPTED" 135 136 # If the event cannot be scheduled, it is not registered and a reply 137 # sent declining the event. (The attendee has PARTSTAT=DECLINED as an 138 # attribute.) 139 140 else: 141 attendee_attr["PARTSTAT"] = "DECLINED" 142 143 self.details["ATTENDEE"] = [(attendee, attendee_attr)] 144 calendar.append(to_node( 145 {"VEVENT" : [(self.details, {})]} 146 )) 147 148 return "REPLY", to_part("REPLY", calendar) 149 150 class Freebusy(Handler): 151 152 "A free/busy handler." 153 154 def publish(self): 155 pass 156 157 def reply(self): 158 159 "Since this handler does not send requests, it will not handle replies." 160 161 pass 162 163 def request(self): 164 165 """ 166 Respond to a request by preparing a reply containing free/busy 167 information for each indicated attendee. 168 """ 169 170 oa = self.require_organiser_and_attendees() 171 if not oa: 172 return None 173 174 (organiser, organiser_attr), attendees = oa 175 176 # Construct an appropriate fragment. 177 178 calendar = [] 179 cwrite = calendar.append 180 181 # Get the details for each attendee. 182 183 for attendee, attendee_attr in attendees.items(): 184 freebusy = self.store.get_freebusy(attendee) 185 186 record = [] 187 rwrite = record.append 188 189 rwrite(("ORGANIZER", organiser_attr, organiser)) 190 rwrite(("ATTENDEE", attendee_attr, attendee)) 191 rwrite(("UID", {}, self.uid)) 192 193 if freebusy: 194 for start, end, uid in freebusy: 195 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, [start, end])) 196 197 cwrite(("VFREEBUSY", {}, record)) 198 199 # Return the reply. 200 201 return "REPLY", to_part("REPLY", calendar) 202 203 class Journal(Handler): 204 205 "A journal entry handler." 206 207 def add(self): 208 pass 209 210 def cancel(self): 211 pass 212 213 def publish(self): 214 pass 215 216 class Todo(Handler): 217 218 "A to-do item handler." 219 220 def add(self): 221 pass 222 223 def cancel(self): 224 pass 225 226 def counter(self): 227 228 "Since this handler does not send requests, it will not handle replies." 229 230 pass 231 232 def declinecounter(self): 233 234 """ 235 Since this handler does not send counter proposals, it will not handle 236 replies to such proposals. 237 """ 238 239 pass 240 241 def publish(self): 242 pass 243 244 def refresh(self): 245 pass 246 247 def reply(self): 248 249 "Since this handler does not send requests, it will not handle replies." 250 251 pass 252 253 def request(self): 254 pass 255 256 # Handler registry. 257 258 handlers = [ 259 ("VFREEBUSY", Freebusy), 260 ("VEVENT", Event), 261 ("VTODO", Todo), 262 ("VJOURNAL", Journal), 263 ] 264 265 # vim: tabstop=4 expandtab shiftwidth=4