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