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 # Obtain valid attendees, merging their attendance with the stored 124 # object. 125 126 attendees = self.require_attendees(from_organiser) 127 self.merge_attendance(attendees) 128 129 # Remove any associated request. 130 131 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 132 self.store.remove_counters(self.user, self.uid, self.recurrenceid) 133 134 # Update free/busy information. 135 136 if not counter: 137 self.update_event_in_freebusy(from_organiser) 138 139 # For countered proposals, record the offer in the resource's 140 # free/busy collection. 141 142 else: 143 self.update_event_in_freebusy_offers() 144 145 return True 146 147 def _remove(self): 148 149 """ 150 Remove details from the current object given a message originating 151 from an organiser if 'from_organiser' is set to a true value. 152 """ 153 154 if not Client.is_participating(self): 155 return False 156 157 # Check for event using UID. 158 159 if not self.have_new_object(): 160 return False 161 162 # Obtain any stored object, using parent object details if a newly- 163 # indicated occurrence is referenced. 164 165 obj = self.get_stored_object_version() 166 old = not obj and self.get_parent_object() or obj 167 168 if not old: 169 return False 170 171 # Only cancel the event completely if all attendees are given. 172 173 attendees = uri_dict(old.get_value_map("ATTENDEE")) 174 all_attendees = set(attendees.keys()) 175 given_attendees = set(uri_values(self.obj.get_values("ATTENDEE"))) 176 cancel_entire_event = not all_attendees.difference(given_attendees) 177 178 # Update the recipient's record of the organiser's schedule. 179 180 self.remove_freebusy_from_organiser(self.obj.get_value("ORGANIZER")) 181 182 # Otherwise, remove the given attendees and update the event. 183 184 if not cancel_entire_event and obj: 185 for attendee in given_attendees: 186 if attendees.has_key(attendee): 187 del attendees[attendee] 188 obj["ATTENDEE"] = attendees.items() 189 190 # Update the stored object with sequence information. 191 192 if obj: 193 obj["SEQUENCE"] = self.obj.get_items("SEQUENCE") or [] 194 obj["DTSTAMP"] = self.obj.get_items("DTSTAMP") or [] 195 196 # Update free/busy information. 197 198 if cancel_entire_event or self.user in given_attendees: 199 self.remove_event_from_freebusy() 200 self.remove_freebusy_from_attendees(given_attendees) 201 202 # Set the complete event if not an additional occurrence. For any newly- 203 # indicated occurrence, use the received event details. 204 205 self.store.set_event(self.user, self.uid, self.recurrenceid, (obj or self.obj).to_node()) 206 207 # Perform any cancellation after recording the latest state of the 208 # event. 209 210 if cancel_entire_event: 211 self.store.cancel_event(self.user, self.uid, self.recurrenceid) 212 213 # Remove any associated request. 214 215 self.store.dequeue_request(self.user, self.uid, self.recurrenceid) 216 self.store.remove_counters(self.user, self.uid, self.recurrenceid) 217 218 return True 219 220 def _declinecounter(self): 221 222 "Remove any counter-proposals for the given event." 223 224 if not Client.is_participating(self): 225 return False 226 227 # Check for event using UID. 228 229 if not self.have_new_object(): 230 return False 231 232 self.remove_counters(uri_values(self.obj.get_values("ATTENDEE"))) 233 234 class Event(PersonHandler): 235 236 "An event handler." 237 238 def add(self): 239 240 "Record the addition of a recurrence to an event." 241 242 self._add() 243 244 def cancel(self): 245 246 "Remove an event or a recurrence." 247 248 self._remove() 249 250 def counter(self): 251 252 "Record an offer made by a counter-proposal." 253 254 self._record(False, True) 255 256 def declinecounter(self): 257 258 "Expire any offer made by a counter-proposal." 259 260 self._declinecounter() 261 262 def publish(self): 263 264 "Published events are recorded." 265 266 self._record(True) 267 268 def refresh(self): 269 270 "Requests to refresh events do not provide event information." 271 272 pass 273 274 def reply(self): 275 276 "Replies to requests are inspected for attendee information." 277 278 self._record(False) 279 280 def request(self): 281 282 "Record events sent for potential scheduling." 283 284 self._record(True) 285 286 # Handler registry. 287 288 handlers = [ 289 ("VEVENT", Event), 290 ] 291 292 # vim: tabstop=4 expandtab shiftwidth=4