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