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