1 #!/usr/bin/env python 2 3 """ 4 Common calendar client utilities. 5 6 Copyright (C) 2014, 2015 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 datetime import datetime 23 from imiptools.data import get_address, get_uri, get_window_end, uri_dict, uri_items, uri_values 24 from imiptools.period import update_freebusy 25 from imiptools.profile import Preferences 26 from imiptools.dates import format_datetime, get_default_timezone, \ 27 to_timezone 28 29 def update_attendees(obj, attendees, removed): 30 31 """ 32 Update the attendees in 'obj' with the given 'attendees' and 'removed' 33 attendee lists. A list is returned containing the attendees whose 34 attendance should be cancelled. 35 """ 36 37 to_cancel = [] 38 39 existing_attendees = uri_values(obj.get_values("ATTENDEE") or []) 40 added = set(attendees).difference(existing_attendees) 41 42 if added or removed: 43 attendees = uri_items(obj.get_items("ATTENDEE") or []) 44 sequence = obj.get_value("SEQUENCE") 45 46 if removed: 47 remaining = [] 48 49 for attendee, attendee_attr in attendees: 50 if attendee in removed: 51 52 # Without a sequence number, assume that the event has not 53 # been published and that attendees can be silently removed. 54 55 if sequence is not None: 56 to_cancel.append((attendee, attendee_attr)) 57 else: 58 remaining.append((attendee, attendee_attr)) 59 60 attendees = remaining 61 62 if added: 63 for attendee in added: 64 attendee = attendee.strip() 65 if attendee: 66 attendees.append((get_uri(attendee), {"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"})) 67 68 obj["ATTENDEE"] = attendees 69 70 return to_cancel 71 72 class Client: 73 74 "Common handler and manager methods." 75 76 default_window_size = 100 77 78 def __init__(self, user, messenger=None): 79 self.user = user 80 self.messenger = messenger 81 self.preferences = None 82 83 def get_preferences(self): 84 if not self.preferences and self.user: 85 self.preferences = Preferences(self.user) 86 return self.preferences 87 88 def get_tzid(self): 89 prefs = self.get_preferences() 90 return prefs and prefs.get("TZID") or get_default_timezone() 91 92 def get_window_size(self): 93 prefs = self.get_preferences() 94 try: 95 return prefs and int(prefs.get("window_size")) or self.default_window_size 96 except (TypeError, ValueError): 97 return self.default_window_size 98 99 def get_window_end(self): 100 return get_window_end(self.get_tzid(), self.get_window_size()) 101 102 def is_sharing(self): 103 prefs = self.get_preferences() 104 return prefs and prefs.get("freebusy_sharing") == "share" or False 105 106 def is_bundling(self): 107 prefs = self.get_preferences() 108 return prefs and prefs.get("freebusy_bundling") == "always" or False 109 110 def is_notifying(self): 111 prefs = self.get_preferences() 112 return prefs and prefs.get("freebusy_messages") == "notify" or False 113 114 # Common operations on calendar data. 115 116 def update_participation(self, obj, partstat=None): 117 118 """ 119 Update the participation in 'obj' of the user with the given 'partstat'. 120 """ 121 122 attendee_attr = uri_dict(obj.get_value_map("ATTENDEE")).get(self.user) 123 if not attendee_attr: 124 return None 125 if partstat: 126 attendee_attr["PARTSTAT"] = partstat 127 if attendee_attr.has_key("RSVP"): 128 del attendee_attr["RSVP"] 129 self.update_sender(attendee_attr) 130 return attendee_attr 131 132 def update_sender(self, attr): 133 134 "Update the SENT-BY attribute of the 'attr' sender metadata." 135 136 if self.messenger and self.messenger.sender != get_address(self.user): 137 attr["SENT-BY"] = get_uri(self.messenger.sender) 138 139 class ClientForObject(Client): 140 141 "A client maintaining a specific object." 142 143 def __init__(self, obj, user, messenger=None): 144 Client.__init__(self, user, messenger) 145 self.set_object(obj) 146 147 def set_object(self, obj): 148 self.obj = obj 149 self.uid = obj and self.obj.get_uid() 150 self.recurrenceid = obj and self.obj.get_recurrenceid() 151 self.sequence = obj and self.obj.get_value("SEQUENCE") 152 self.dtstamp = obj and self.obj.get_value("DTSTAMP") 153 154 def _update_freebusy(self, freebusy, periods, recurrenceid, transp=None): 155 156 """ 157 Update the 'freebusy' collection with the given 'periods', indicating an 158 explicit 'recurrenceid' to affect either a recurrence or the parent 159 event. 160 """ 161 162 update_freebusy(freebusy, periods, 163 transp or self.obj.get_value("TRANSP") or "OPAQUE", 164 self.uid, recurrenceid, 165 self.obj.get_value("SUMMARY"), 166 self.obj.get_value("ORGANIZER")) 167 168 def update_freebusy(self, freebusy, periods, transp=None): 169 170 """ 171 Update the 'freebusy' collection for this event with the given 172 'periods'. 173 """ 174 175 self._update_freebusy(freebusy, periods, self.recurrenceid, transp) 176 177 def update_freebusy_for_participant(self, freebusy, periods, attr, for_organiser=False): 178 179 """ 180 Update the 'freebusy' collection using the given 'periods', subject to 181 the 'attr' provided for the participant, indicating whether this is 182 being generated 'for_organiser' or not. 183 """ 184 185 # Organisers employ a special transparency if not attending. 186 187 if self.is_participating(attr, for_organiser): 188 self.update_freebusy(freebusy, periods, 189 transp=self.get_overriding_transparency(attr, for_organiser)) 190 else: 191 self.remove_from_freebusy(freebusy) 192 193 def is_participating(self, attr, as_organiser=False): 194 return as_organiser or not attr or attr.get("PARTSTAT") != "DECLINED" 195 196 def get_overriding_transparency(self, attr, as_organiser=False): 197 return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None 198 199 def update_dtstamp(self): 200 201 "Update the DTSTAMP in the current object." 202 203 dtstamp = self.obj.get_utc_datetime("DTSTAMP") 204 utcnow = to_timezone(datetime.utcnow(), "UTC") 205 self.obj["DTSTAMP"] = [(format_datetime(dtstamp > utcnow and dtstamp or utcnow), {})] 206 207 def set_sequence(self, increment=False): 208 209 "Update the SEQUENCE in the current object." 210 211 sequence = self.obj.get_value("SEQUENCE") or "0" 212 self.obj["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})] 213 214 # vim: tabstop=4 expandtab shiftwidth=4