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 period involved. 79 80 dtstart = self.obj.get_datetime("DTSTART") 81 dtend = self.obj.get_datetime("DTEND") 82 period = dtstart and dtend and Period(dtstart, dtend, self.get_tzid()) or None 83 84 # Get the details for each attendee. 85 86 responses = [] 87 rwrite = responses.append 88 89 # For replies, the organiser and attendee are preserved. 90 91 for attendee, attendee_attr in attendees.items(): 92 freebusy = self.store.get_freebusy(attendee) 93 94 # Indicate the actual sender of the reply. 95 96 self.update_sender_attr(attendee_attr) 97 98 # Produce a free/busy reply. 99 100 rwrite(make_freebusy(freebusy, self.uid, organiser, organiser_attr, 101 [(attendee, attendee_attr)], period)) 102 103 # Return the reply. 104 105 self.add_result("REPLY", [get_address(organiser)], to_part("REPLY", responses)) 106 107 class CommonEvent: 108 109 "Common outgoing message handling functionality mix-in." 110 111 def is_usable(self, method=None): 112 113 "Return whether the current object is usable with the given 'method'." 114 115 return self.obj and ( 116 method in ("CANCEL", "REFRESH") or 117 self.obj.get_datetime("DTSTART") and 118 (self.obj.get_datetime("DTEND") or self.obj.get_duration("DURATION"))) 119 120 def will_refresh(self): 121 122 """ 123 Indicate whether a REFRESH message should be used to respond to an ADD 124 message. 125 """ 126 127 return not self.get_stored_object_version() or self.get_add_method_response() == "refresh" 128 129 def make_refresh(self): 130 131 "Make a REFRESH message." 132 133 organiser = self.obj.get_uri("ORGANIZER") 134 attendees = self.obj.get_uri_map("ATTENDEE") 135 136 # Indicate the actual sender of the message. 137 138 attendee_attr = attendees[self.user] 139 self.update_sender_attr(attendee_attr) 140 141 # Make a new object with a minimal property selection. 142 143 obj = self.obj.copy() 144 obj.preserve(("ORGANIZER", "DTSTAMP", "UID", "RECURRENCE-ID")) 145 obj["ATTENDEE"] = [(self.user, attendee_attr)] 146 147 # Send a REFRESH message in response. 148 149 self.add_result("REFRESH", [get_address(organiser)], obj.to_part("REFRESH")) 150 151 def is_newly_separated_occurrence(self): 152 153 "Return whether the current object is a newly-separated occurrence." 154 155 # Obtain any stored object. 156 157 obj = self.get_stored_object_version() 158 159 # Handle any newly-separated, valid occurrence. 160 161 return not obj and self.describes_recurrence_period() 162 163 def make_separate_occurrence(self): 164 165 """ 166 Set the current object as a separate occurrence and redefine free/busy 167 records in terms of this new occurrence for other participants. This 168 really only makes sense when receiving a new recurrence from an attendee 169 whose details are then to be combined with the general event details for 170 other participants, for whom the attendee responsible for the recurrence 171 cannot specify attendance. 172 """ 173 174 parent = self.get_parent_object() 175 if not parent: 176 return False 177 178 # Transfer attendance information from the parent. 179 180 parent_attendees = parent.get_uri_map("ATTENDEE") 181 attendee_map = self.obj.get_uri_map("ATTENDEE") 182 183 for attendee, attendee_attr in parent_attendees.items(): 184 if not attendee_map.has_key(attendee): 185 attendee_map[attendee] = attendee_attr 186 187 # Separated occurrences should only have principal time details. 188 189 self.obj["ATTENDEE"] = attendee_map.items() 190 self.obj.remove_all(["EXDATE", "RDATE", "RRULE"]) 191 192 # Create or revive the occurrence. 193 194 self.store.remove_cancellation(self.user, self.uid, self.recurrenceid) 195 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) 196 197 # Update free/busy details for the current object for all attendees. 198 199 self.update_freebusy_from_attendees(attendee_map.keys()) 200 201 return True 202 203 # vim: tabstop=4 expandtab shiftwidth=4