1 #!/usr/bin/env python 2 3 """ 4 Free/busy-related scheduling functionality. 5 6 Copyright (C) 2015, 2016 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 uri_values 23 from imiptools.dates import ValidityError, to_timezone 24 25 def schedule_in_freebusy(handler, args, freebusy=None): 26 27 """ 28 Attempt to schedule the current object of the given 'handler' in the 29 free/busy schedule of a resource, returning an indication of the kind of 30 response to be returned. 31 32 If 'freebusy' is specified, the given collection of busy periods will be 33 used to determine whether any conflicts occur. Otherwise, the current user's 34 free/busy records will be used. 35 """ 36 37 _ = handler.get_translator() 38 39 # If newer than any old version, discard old details from the 40 # free/busy record and check for suitability. 41 42 periods = handler.get_periods(handler.obj) 43 44 freebusy = freebusy or handler.get_store().get_freebusy(handler.user) 45 offers = handler.get_store().get_freebusy_offers(handler.user) 46 47 # Check the periods against any scheduled events and against 48 # any outstanding offers. 49 50 scheduled = handler.can_schedule(freebusy, periods) 51 scheduled = scheduled and handler.can_schedule(offers, periods) 52 53 return standard_responses(handler, scheduled and "ACCEPTED" or "DECLINED") 54 55 def schedule_corrected_in_freebusy(handler, args): 56 57 """ 58 Attempt to schedule the current object of the given 'handler', correcting 59 specified datetimes according to the configuration of a resource, 60 returning an indication of the kind of response to be returned. 61 """ 62 63 _ = handler.get_translator() 64 65 obj = handler.obj.copy() 66 67 # Check any constraints on the request. 68 69 try: 70 corrected = handler.correct_object() 71 72 # Refuse to schedule obviously invalid requests. 73 74 except ValidityError: 75 return None 76 77 # With a valid request, determine whether the event can be scheduled. 78 79 scheduled = schedule_in_freebusy(handler, args) 80 response, description = scheduled or ("DECLINED", None) 81 82 # Restore the original object if it was corrected but could not be 83 # scheduled. 84 85 if response == "DECLINED" and corrected: 86 handler.set_object(obj) 87 88 # Where the corrected object can be scheduled, issue a counter 89 # request. 90 91 response = response == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED" 92 return standard_responses(handler, response) 93 94 def schedule_next_available_in_freebusy(handler, args): 95 96 """ 97 Attempt to schedule the current object of the given 'handler', correcting 98 specified datetimes according to the configuration of a resource, then 99 suggesting the next available period in the free/busy records if scheduling 100 cannot occur for the requested period, returning an indication of the kind 101 of response to be returned. 102 """ 103 104 _ = handler.get_translator() 105 106 scheduled = schedule_corrected_in_freebusy(handler, args) 107 response, description = scheduled or ("DECLINED", None) 108 109 if response in ("ACCEPTED", "COUNTER"): 110 return scheduled 111 112 # There should already be free/busy information for the user. 113 114 user_freebusy = handler.get_store().get_freebusy(handler.user) 115 116 # Maintain a separate copy of the data. 117 118 busy = user_freebusy.copy() 119 120 # Subtract any periods from this event from the free/busy collections. 121 122 event_periods = handler.remove_from_freebusy(busy) 123 124 # Find busy periods for the other attendees. 125 126 for attendee in uri_values(handler.obj.get_values("ATTENDEE")): 127 if attendee != handler.user: 128 129 # Get a copy of the attendee's free/busy data. 130 131 freebusy = handler.get_store().get_freebusy_for_other(handler.user, attendee).copy() 132 if freebusy: 133 freebusy.remove_periods(event_periods) 134 busy += freebusy 135 136 # Obtain the combined busy periods. 137 138 busy = busy.coalesce_freebusy() 139 140 # Obtain free periods. 141 142 free = busy.invert_freebusy() 143 permitted_values = handler.get_permitted_values() 144 periods = [] 145 146 # Do not attempt to redefine rule-based periods. 147 148 last = None 149 150 for period in handler.get_periods(handler.obj, explicit_only=True): 151 duration = period.get_duration() 152 153 # Try and schedule periods normally since some of them may be 154 # compatible with the schedule. 155 156 if permitted_values: 157 period = period.get_corrected(permitted_values) 158 159 scheduled = handler.can_schedule(busy, [period]) 160 161 if scheduled: 162 periods.append(period) 163 last = period.get_end() 164 continue 165 166 # Get free periods from the time of each period. 167 168 for found in free.periods_from(period): 169 170 # Skip any periods before the last period. 171 172 if last: 173 if last > found.get_end(): 174 continue 175 176 # Adjust the start of the free period to exclude the last period. 177 178 found = found.make_corrected(max(found.get_start(), last), found.get_end()) 179 180 # Only test free periods long enough to hold the requested period. 181 182 if found.get_duration() >= duration: 183 184 # Obtain a possible period, starting at the found point and 185 # with the requested duration. Then, correct the period if 186 # necessary. 187 188 start = to_timezone(found.get_start(), period.get_tzid()) 189 possible = period.make_corrected(start, start + period.get_duration()) 190 if permitted_values: 191 possible = possible.get_corrected(permitted_values) 192 193 # Only if the possible period is still within the free period 194 # can it be used. 195 196 if possible.within(found): 197 periods.append(possible) 198 break 199 200 # Where no period can be found, decline the invitation. 201 202 else: 203 return "DECLINED", _("The recipient is unavailable in the requested period.") 204 205 # Use the found period to set the start of the next window to search. 206 207 last = periods[-1].get_end() 208 209 # Replace the periods in the object. 210 211 obj = handler.obj.copy() 212 changed = handler.obj.set_periods(periods) 213 214 # Check one last time, reverting the change if not scheduled. 215 216 scheduled = schedule_in_freebusy(handler, args, busy) 217 response, description = scheduled or ("DECLINED", None) 218 219 if response == "DECLINED": 220 handler.set_object(obj) 221 222 response = response == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED" 223 return standard_responses(handler, response) 224 225 def standard_responses(handler, response): 226 227 """ 228 Using 'handler' to translate descriptions, return a tuple containing the 229 'response' and a suitable description. 230 """ 231 232 _ = handler.get_translator() 233 234 if response == "ACCEPTED": 235 return response, _("The recipient has scheduled the requested period.") 236 elif response == "COUNTER": 237 return response, _("The recipient has suggested a different period.") 238 else: 239 return response, _("The recipient is unavailable in the requested period.") 240 241 # Registry of scheduling functions. 242 243 scheduling_functions = { 244 "schedule_in_freebusy" : schedule_in_freebusy, 245 "schedule_corrected_in_freebusy" : schedule_corrected_in_freebusy, 246 "schedule_next_available_in_freebusy" : schedule_next_available_in_freebusy, 247 } 248 249 # Registries of locking and unlocking functions. 250 251 locking_functions = {} 252 unlocking_functions = {} 253 254 # Registries of listener functions. 255 256 confirmation_functions = {} 257 retraction_functions = {} 258 259 # vim: tabstop=4 expandtab shiftwidth=4