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