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 insert_period, period_overlaps, 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) 112 113 if freebusy: 114 remove_period(freebusy, self.uid) 115 conflict = True 116 for start, end in periods: 117 if period_overlaps(freebusy, (start, end)): 118 break 119 else: 120 conflict = False 121 else: 122 freebusy = [] 123 124 # If the event can be scheduled, it is registered and a reply sent 125 # accepting the event. (The attendee has PARTSTAT=ACCEPTED as an 126 # attribute.) 127 128 if not conflict: 129 for start, end in periods: 130 insert_period(freebusy, (start, end, self.uid)) 131 132 if self.get_value("TRANSP") in (None, "OPAQUE"): 133 self.store.set_freebusy(attendee, freebusy) 134 135 if self.publisher: 136 self.publisher.set_freebusy(attendee, freebusy) 137 138 self.store.set_event(attendee, self.uid, to_node( 139 {"VEVENT" : [(self.details, {})]} 140 )) 141 attendee_attr["PARTSTAT"] = "ACCEPTED" 142 143 # If the event cannot be scheduled, it is not registered and a reply 144 # sent declining the event. (The attendee has PARTSTAT=DECLINED as an 145 # attribute.) 146 147 else: 148 attendee_attr["PARTSTAT"] = "DECLINED" 149 150 self.details["ATTENDEE"] = [(attendee, attendee_attr)] 151 calendar.append(to_node( 152 {"VEVENT" : [(self.details, {})]} 153 )) 154 155 return "REPLY", to_part("REPLY", calendar) 156 157 class Freebusy(Handler): 158 159 "A free/busy handler." 160 161 def publish(self): 162 pass 163 164 def reply(self): 165 166 "Since this handler does not send requests, it will not handle replies." 167 168 pass 169 170 def request(self): 171 172 """ 173 Respond to a request by preparing a reply containing free/busy 174 information for each indicated attendee. 175 """ 176 177 oa = self.require_organiser_and_attendees() 178 if not oa: 179 return None 180 181 (organiser, organiser_attr), attendees = oa 182 183 # Construct an appropriate fragment. 184 185 calendar = [] 186 cwrite = calendar.append 187 188 # Get the details for each attendee. 189 190 for attendee, attendee_attr in attendees.items(): 191 freebusy = self.store.get_freebusy(attendee) 192 193 record = [] 194 rwrite = record.append 195 196 rwrite(("ORGANIZER", organiser_attr, organiser)) 197 rwrite(("ATTENDEE", attendee_attr, attendee)) 198 rwrite(("UID", {}, self.uid)) 199 200 if freebusy: 201 for start, end, uid in freebusy: 202 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, [start, end])) 203 204 cwrite(("VFREEBUSY", {}, record)) 205 206 # Return the reply. 207 208 return "REPLY", to_part("REPLY", calendar) 209 210 class Journal(Handler): 211 212 "A journal entry handler." 213 214 def add(self): 215 pass 216 217 def cancel(self): 218 pass 219 220 def publish(self): 221 pass 222 223 class Todo(Handler): 224 225 "A to-do item handler." 226 227 def add(self): 228 pass 229 230 def cancel(self): 231 pass 232 233 def counter(self): 234 235 "Since this handler does not send requests, it will not handle replies." 236 237 pass 238 239 def declinecounter(self): 240 241 """ 242 Since this handler does not send counter proposals, it will not handle 243 replies to such proposals. 244 """ 245 246 pass 247 248 def publish(self): 249 pass 250 251 def refresh(self): 252 pass 253 254 def reply(self): 255 256 "Since this handler does not send requests, it will not handle replies." 257 258 pass 259 260 def request(self): 261 pass 262 263 # Handler registry. 264 265 handlers = [ 266 ("VFREEBUSY", Freebusy), 267 ("VEVENT", Event), 268 ("VTODO", Todo), 269 ("VJOURNAL", Journal), 270 ] 271 272 # vim: tabstop=4 expandtab shiftwidth=4