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