1 #!/usr/bin/env python 2 3 """ 4 Handlers for a person for whom scheduling is performed, inspecting outgoing 5 messages to obtain scheduling done externally. 6 7 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk> 8 9 This program is free software; you can redistribute it and/or modify it under 10 the terms of the GNU General Public License as published by the Free Software 11 Foundation; either version 3 of the License, or (at your option) any later 12 version. 13 14 This program is distributed in the hope that it will be useful, but WITHOUT 15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 16 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 17 details. 18 19 You should have received a copy of the GNU General Public License along with 20 this program. If not, see <http://www.gnu.org/licenses/>. 21 """ 22 23 from imiptools.client import Client 24 from imiptools.data import get_uri, uri_dict, uri_values 25 from imiptools.handlers import Handler 26 from imiptools.handlers.common import CommonEvent 27 28 class PersonHandler(CommonEvent, Handler): 29 30 "Handling mechanisms specific to people." 31 32 def set_identity(self, method): 33 34 """ 35 Set the current user for the current object in the context of the given 36 'method'. It is usually set when initialising the handler, using the 37 recipient details, but outgoing messages do not reference the recipient 38 in this way. 39 """ 40 41 if self.obj and not self.user: 42 from_organiser = method in self.organiser_methods 43 if from_organiser: 44 self.user = get_uri(self.obj.get_value("ORGANIZER")) 45 46 # Since there may be many attendees in an attendee-provided outgoing 47 # message, because counter-proposals can have more than one 48 # attendee, the attendee originating from the calendar system is 49 # chosen. 50 51 else: 52 self.user = self.get_sending_attendee() 53 54 def _add(self): 55 56 "Add a recurrence for the current object." 57 58 if not Client.is_participating(self): 59 return False 60 61 # Check for event using UID. 62 63 if not self.have_new_object(): 64 return False 65 66 # Ignore unknown objects. 67 68 if not self.get_stored_object_version(): 69 return 70 71 # Record the event as a recurrence of the parent object. 72 73 self.update_recurrenceid() 74 75 # Set the additional occurrence. 76 77 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) 78 79 # Remove any previous cancellations involving this event. 80 81 self.store.remove_cancellation(self.user, self.uid, self.recurrenceid) 82 83 # Update free/busy information. 84 85 self.update_event_in_freebusy() 86 87 return True 88 89 def _record(self, from_organiser=True, counter=False): 90 91 """ 92 Record details from the current object given a message originating 93 from an organiser if 'from_organiser' is set to a true value. 94 """ 95 96 if not Client.is_participating(self): 97 return False 98 99 # Check for a new event, tolerating not-strictly-new events if the 100 # attendee is responding. 101 102 if not self.have_new_object(strict=from_organiser): 103 return False 104 105 # Update the object. 106 107 if from_organiser: 108 109 # Set the complete event or an additional occurrence. 110 111 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) 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 else: 123 if not self.ensure_occurrence(): 124 return False 125 126 # Obtain valid attendees, merging their attendance with the stored 127 # object. 128 129 attendees = self.require_attendees(from_organiser) 130 self.merge_attendance(attendees) 131 132 # Remove any associated request. 133 134 if from_organiser or self.has_indicated_attendance(): 135 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 136 self.store.remove_counters(self.user, self.uid, self.recurrenceid) 137 138 # Update free/busy information. 139 140 if not counter: 141 self.update_event_in_freebusy(from_organiser) 142 143 # For countered proposals, record the offer in the resource's 144 # free/busy collection. 145 146 else: 147 self.update_event_in_freebusy_offers() 148 149 return True 150 151 def _remove(self): 152 153 """ 154 Remove details from the current object given a message originating 155 from an organiser if 'from_organiser' is set to a true value. 156 """ 157 158 if not Client.is_participating(self): 159 return False 160 161 # Check for event using UID. 162 163 if not self.have_new_object(): 164 return False 165 166 # Obtain any stored object, using parent object details if a newly- 167 # indicated occurrence is referenced. 168 169 obj = self.get_stored_object_version() 170 old = not obj and self.get_parent_object() or obj 171 172 if not old: 173 return False 174 175 # Only cancel the event completely if all attendees are given. 176 177 attendees = uri_dict(old.get_value_map("ATTENDEE")) 178 all_attendees = set(attendees.keys()) 179 given_attendees = set(uri_values(self.obj.get_values("ATTENDEE"))) 180 cancel_entire_event = not all_attendees.difference(given_attendees) 181 182 # Update the recipient's record of the organiser's schedule. 183 184 self.remove_freebusy_from_organiser(self.obj.get_value("ORGANIZER")) 185 186 # Otherwise, remove the given attendees and update the event. 187 188 if not cancel_entire_event and obj: 189 for attendee in given_attendees: 190 if attendees.has_key(attendee): 191 del attendees[attendee] 192 obj["ATTENDEE"] = attendees.items() 193 194 # Update the stored object with sequence information. 195 196 if obj: 197 obj["SEQUENCE"] = self.obj.get_items("SEQUENCE") or [] 198 obj["DTSTAMP"] = self.obj.get_items("DTSTAMP") or [] 199 200 # Update free/busy information. 201 202 if cancel_entire_event or self.user in given_attendees: 203 self.remove_event_from_freebusy() 204 self.remove_freebusy_from_attendees(attendees) 205 206 # Set the complete event if not an additional occurrence. For any newly- 207 # indicated occurrence, use the received event details. 208 209 self.store.set_event(self.user, self.uid, self.recurrenceid, (obj or self.obj).to_node()) 210 211 # Perform any cancellation after recording the latest state of the 212 # event. 213 214 if cancel_entire_event: 215 self.store.cancel_event(self.user, self.uid, self.recurrenceid) 216 217 # Remove any associated request. 218 219 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 220 self.store.remove_counters(self.user, self.uid, self.recurrenceid) 221 222 return True 223 224 def _declinecounter(self): 225 226 "Remove any counter-proposals for the given event." 227 228 if not Client.is_participating(self): 229 return False 230 231 # Check for event using UID. 232 233 if not self.have_new_object(): 234 return False 235 236 self.remove_counters(uri_values(self.obj.get_values("ATTENDEE"))) 237 238 class Event(PersonHandler): 239 240 "An event handler." 241 242 def add(self): 243 244 "Record the addition of a recurrence to an event." 245 246 self._add() 247 248 def cancel(self): 249 250 "Remove an event or a recurrence." 251 252 self._remove() 253 254 def counter(self): 255 256 "Record an offer made by a counter-proposal." 257 258 self._record(False, True) 259 260 def declinecounter(self): 261 262 "Expire any offer made by a counter-proposal." 263 264 self._declinecounter() 265 266 def publish(self): 267 268 "Published events are recorded." 269 270 self._record(True) 271 272 def refresh(self): 273 274 "Requests to refresh events do not provide event information." 275 276 pass 277 278 def reply(self): 279 280 "Replies to requests are inspected for attendee information." 281 282 self._record(False) 283 284 def request(self): 285 286 "Record events sent for potential scheduling." 287 288 self._record(True) 289 290 # Handler registry. 291 292 handlers = [ 293 ("VEVENT", Event), 294 ] 295 296 # vim: tabstop=4 expandtab shiftwidth=4