1 #!/usr/bin/env python 2 3 """ 4 Handlers for a resource. 5 6 Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from imiptools.data import get_address, to_part, uri_dict 23 from imiptools.handlers import Handler 24 from imiptools.handlers.common import CommonFreebusy, CommonEvent 25 from imiptools.handlers.scheduling import apply_scheduling_functions, \ 26 confirm_scheduling, \ 27 finish_scheduling, \ 28 retract_scheduling 29 30 class ResourceHandler(CommonEvent, Handler): 31 32 "Handling mechanisms specific to resources." 33 34 def _process(self, handle_for_attendee): 35 36 """ 37 Record details from the incoming message, using the given 38 'handle_for_attendee' callable to process any valid message 39 appropriately. 40 """ 41 42 oa = self.require_organiser_and_attendees() 43 if not oa: 44 return None 45 46 organiser_item, attendees = oa 47 48 # Process for the current user, a resource as attendee. 49 50 if not self.have_new_object(): 51 return None 52 53 # Collect response objects produced when handling the request. 54 55 handle_for_attendee() 56 57 def _add_for_attendee(self): 58 59 """ 60 Attempt to add a recurrence to an existing object for the current user. 61 This does not request a response concerning participation, apparently. 62 """ 63 64 # Request details where configured, doing so for unknown objects anyway. 65 66 if self.will_refresh(): 67 self.make_refresh() 68 return 69 70 # Record the event as a recurrence of the parent object. 71 72 self.update_recurrenceid() 73 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) 74 75 # Remove any previous cancellations involving this event. 76 77 self.store.remove_cancellation(self.user, self.uid, self.recurrenceid) 78 79 # Update free/busy information. 80 81 self.update_event_in_freebusy(for_organiser=False) 82 83 # Confirm the scheduling of the recurrence. 84 85 self.confirm_scheduling() 86 87 def _schedule_for_attendee(self): 88 89 "Attempt to schedule the current object for the current user." 90 91 attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[self.user] 92 93 # Attempt to schedule the event. 94 95 scheduled = self.schedule() 96 97 try: 98 # Update the participation of the resource in the object. 99 # Update free/busy information. 100 101 if scheduled in ("ACCEPTED", "DECLINED"): 102 method = "REPLY" 103 attendee_attr = self.update_participation(scheduled) 104 105 self.update_event_in_freebusy(for_organiser=False) 106 self.remove_event_from_freebusy_offers() 107 108 # Set the complete event or an additional occurrence. 109 110 event = self.obj.to_node() 111 self.store.set_event(self.user, self.uid, self.recurrenceid, event) 112 113 # Remove additional recurrences if handling a complete event. 114 # Also remove any previous cancellations involving this event. 115 116 if not self.recurrenceid: 117 self.store.remove_recurrences(self.user, self.uid) 118 self.store.remove_cancellations(self.user, self.uid) 119 else: 120 self.store.remove_cancellation(self.user, self.uid, self.recurrenceid) 121 122 if scheduled == "ACCEPTED": 123 self.confirm_scheduling() 124 125 # For countered proposals, record the offer in the resource's 126 # free/busy collection. 127 128 elif scheduled == "COUNTER": 129 method = "COUNTER" 130 self.update_event_in_freebusy_offers() 131 132 # For inappropriate periods, reply declining participation. 133 134 else: 135 method = "REPLY" 136 attendee_attr = self.update_participation("DECLINED") 137 138 # Confirm any scheduling. 139 140 finally: 141 self.finish_scheduling() 142 143 # Make a version of the object with just this attendee, update the 144 # DTSTAMP in the response, and return the object for sending. 145 146 self.update_sender(attendee_attr) 147 self.obj["ATTENDEE"] = [(self.user, attendee_attr)] 148 self.update_dtstamp() 149 self.add_result(method, map(get_address, self.obj.get_values("ORGANIZER")), to_part(method, [self.obj.to_node()])) 150 151 def _cancel_for_attendee(self): 152 153 """ 154 Cancel for the current user their attendance of the event described by 155 the current object. 156 """ 157 158 # Update free/busy information. 159 160 self.remove_event_from_freebusy() 161 162 # Update the stored event and cancel it. 163 164 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) 165 self.store.cancel_event(self.user, self.uid, self.recurrenceid) 166 167 # Retract the scheduling of the event. 168 169 self.retract_scheduling() 170 171 def _revoke_for_attendee(self): 172 173 "Revoke any counter-proposal recorded as a free/busy offer." 174 175 self.remove_event_from_freebusy_offers() 176 177 # Scheduling details. 178 179 def get_scheduling_functions(self): 180 181 "Return the scheduling functions for the resource." 182 183 return self.get_preferences().get("scheduling_function", 184 "schedule_in_freebusy").split("\n") 185 186 def schedule(self): 187 188 """ 189 Attempt to schedule the current object, returning an indication of the 190 kind of response to be returned: "COUNTER" for counter-proposals, 191 "ACCEPTED" for acceptances, "DECLINED" for rejections, and None for 192 invalid requests. 193 """ 194 195 return apply_scheduling_functions(self) 196 197 def confirm_scheduling(self): 198 199 "Confirm that this event has been scheduled." 200 201 confirm_scheduling(self) 202 203 def finish_scheduling(self): 204 205 "Finish the scheduling, unlocking resources where appropriate." 206 207 finish_scheduling(self) 208 209 def retract_scheduling(self): 210 211 "Retract this event from scheduling records." 212 213 retract_scheduling(self) 214 215 class Event(ResourceHandler): 216 217 "An event handler." 218 219 def add(self): 220 221 "Add a new occurrence to an existing event." 222 223 self._process(self._add_for_attendee) 224 225 def cancel(self): 226 227 "Cancel attendance for attendees." 228 229 self._process(self._cancel_for_attendee) 230 231 def counter(self): 232 233 "Since this handler does not send requests, it will not handle replies." 234 235 pass 236 237 def declinecounter(self): 238 239 "Revoke any counter-proposal." 240 241 self._process(self._revoke_for_attendee) 242 243 def publish(self): 244 245 """ 246 Resources only consider events sent as requests, not generally published 247 events. 248 """ 249 250 pass 251 252 def refresh(self): 253 254 """ 255 Refresh messages are typically sent to event organisers, but resources 256 do not act as organisers themselves. 257 """ 258 259 pass 260 261 def reply(self): 262 263 "Since this handler does not send requests, it will not handle replies." 264 265 pass 266 267 def request(self): 268 269 """ 270 Respond to a request by preparing a reply containing accept/decline 271 information for the recipient. 272 273 No support for countering requests is implemented. 274 """ 275 276 self._process(self._schedule_for_attendee) 277 278 class Freebusy(CommonFreebusy, Handler): 279 280 "A free/busy handler." 281 282 def publish(self): 283 284 "Resources ignore generally published free/busy information." 285 286 self._record_freebusy(from_organiser=True) 287 288 def reply(self): 289 290 "Since this handler does not send requests, it will not handle replies." 291 292 pass 293 294 # request provided by CommonFreeBusy.request 295 296 # Handler registry. 297 298 handlers = [ 299 ("VFREEBUSY", Freebusy), 300 ("VEVENT", Event), 301 ] 302 303 # vim: tabstop=4 expandtab shiftwidth=4