1 #!/usr/bin/env python 2 3 """ 4 Common handler functionality for different entities. 5 6 Copyright (C) 2014, 2015, 2016, 2017, 2018 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 imiptools.data import get_address, make_freebusy, to_part 23 from imiptools.dates import format_datetime 24 from imiptools.freebusy import FreeBusyPeriod 25 from imiptools.period import Period 26 27 class CommonFreebusy: 28 29 "Common free/busy mix-in." 30 31 def _record_freebusy(self, from_organiser=True): 32 33 """ 34 Record free/busy information for a message originating from an organiser 35 if 'from_organiser' is set to a true value. 36 """ 37 38 if from_organiser: 39 organiser_item = self.require_organiser(from_organiser) 40 if not organiser_item: 41 return 42 43 senders = [organiser_item] 44 else: 45 oa = self.require_organiser_and_attendees(from_organiser) 46 if not oa: 47 return 48 49 organiser_item, attendees = oa 50 senders = attendees.items() 51 52 if not senders: 53 return 54 55 freebusy = [FreeBusyPeriod(p.get_start_point(), p.get_end_point()) for p in self.obj.get_period_values("FREEBUSY")] 56 dtstart = self.obj.get_datetime("DTSTART") 57 dtend = self.obj.get_datetime("DTEND") 58 period = Period(dtstart, dtend, self.get_tzid()) 59 60 for sender, sender_attr in senders: 61 stored_freebusy = self.store.get_freebusy_for_other_for_update(self.user, sender) 62 stored_freebusy.replace_overlapping(period, freebusy) 63 self.store.set_freebusy_for_other(self.user, stored_freebusy, sender) 64 65 def request(self): 66 67 """ 68 Respond to a request by preparing a reply containing free/busy 69 information for each indicated attendee. 70 """ 71 72 oa = self.require_organiser_and_attendees() 73 if not oa: 74 return 75 76 (organiser, organiser_attr), attendees = oa 77 78 # Get the details for each attendee. 79 80 responses = [] 81 rwrite = responses.append 82 83 # For replies, the organiser and attendee are preserved. 84 85 for attendee, attendee_attr in attendees.items(): 86 freebusy = self.store.get_freebusy(attendee) 87 88 # Indicate the actual sender of the reply. 89 90 self.update_sender_attr(attendee_attr) 91 92 dtstart = self.obj.get_datetime("DTSTART") 93 dtend = self.obj.get_datetime("DTEND") 94 period = dtstart and dtend and Period(dtstart, dtend, self.get_tzid()) or None 95 96 rwrite(make_freebusy(freebusy, self.uid, organiser, organiser_attr, attendee, attendee_attr, period)) 97 98 # Return the reply. 99 100 self.add_result("REPLY", [get_address(organiser)], to_part("REPLY", responses)) 101 102 class CommonEvent: 103 104 "Common outgoing message handling functionality mix-in." 105 106 def is_usable(self, method=None): 107 108 "Return whether the current object is usable with the given 'method'." 109 110 return self.obj and ( 111 method in ("CANCEL", "REFRESH") or 112 self.obj.get_datetime("DTSTART") and 113 (self.obj.get_datetime("DTEND") or self.obj.get_duration("DURATION"))) 114 115 def will_refresh(self): 116 117 """ 118 Indicate whether a REFRESH message should be used to respond to an ADD 119 message. 120 """ 121 122 return not self.get_stored_object_version() or self.get_add_method_response() == "refresh" 123 124 def make_refresh(self): 125 126 "Make a REFRESH message." 127 128 organiser = self.obj.get_uri("ORGANIZER") 129 attendees = self.obj.get_uri_map("ATTENDEE") 130 131 # Indicate the actual sender of the message. 132 133 attendee_attr = attendees[self.user] 134 self.update_sender_attr(attendee_attr) 135 136 # Make a new object with a minimal property selection. 137 138 obj = self.obj.copy() 139 obj.preserve(("ORGANIZER", "DTSTAMP", "UID", "RECURRENCE-ID")) 140 obj["ATTENDEE"] = [(self.user, attendee_attr)] 141 142 # Send a REFRESH message in response. 143 144 self.add_result("REFRESH", [get_address(organiser)], obj.to_part("REFRESH")) 145 146 def is_newly_separated_occurrence(self): 147 148 "Return whether the current object is a newly-separated occurrence." 149 150 # Obtain any stored object. 151 152 obj = self.get_stored_object_version() 153 154 # Handle any newly-separated, valid occurrence. 155 156 return not obj and self.describes_recurrence_period() 157 158 def make_separate_occurrence(self): 159 160 """ 161 Set the current object as a separate occurrence and redefine free/busy 162 records in terms of this new occurrence for other participants. This 163 really only makes sense when receiving a new recurrence from an attendee 164 whose details are then to be combined with the general event details for 165 other participants, for whom the attendee responsible for the recurrence 166 cannot specify attendance. 167 """ 168 169 parent = self.get_parent_object() 170 if not parent: 171 return False 172 173 # Transfer attendance information from the parent. 174 175 parent_attendees = parent.get_uri_map("ATTENDEE") 176 attendee_map = self.obj.get_uri_map("ATTENDEE") 177 178 for attendee, attendee_attr in parent_attendees.items(): 179 if not attendee_map.has_key(attendee): 180 attendee_map[attendee] = attendee_attr 181 182 # Separated occurrences should only have principal time details. 183 184 self.obj["ATTENDEE"] = attendee_map.items() 185 self.obj.remove_all(["EXDATE", "RDATE", "RRULE"]) 186 187 # Create or revive the occurrence. 188 189 self.store.remove_cancellation(self.user, self.uid, self.recurrenceid) 190 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) 191 192 # Update free/busy details for the current object for all attendees. 193 194 self.update_freebusy_from_attendees(attendee_map.keys()) 195 196 return True 197 198 # vim: tabstop=4 expandtab shiftwidth=4