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