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