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 busy = user_freebusy 107 108 # Subtract any periods from this event from the free/busy collections. 109 110 event_periods = handler.remove_from_freebusy(user_freebusy) 111 112 # Find busy periods for the other attendees. 113 114 for attendee in uri_values(handler.obj.get_values("ATTENDEE")): 115 if attendee != handler.user: 116 freebusy = handler.get_store().get_freebusy_for_other(handler.user, attendee) 117 if freebusy: 118 freebusy.remove_periods(event_periods) 119 busy += freebusy 120 121 # Obtain the combined busy periods. 122 123 busy = busy.coalesce_freebusy() 124 125 # Obtain free periods. 126 127 free = busy.invert_freebusy() 128 permitted_values = handler.get_permitted_values() 129 periods = [] 130 131 # Do not attempt to redefine rule-based periods. 132 133 last = None 134 135 for period in handler.get_periods(handler.obj, explicit_only=True): 136 duration = period.get_duration() 137 138 # Try and schedule periods normally since some of them may be 139 # compatible with the schedule. 140 141 if permitted_values: 142 period = period.get_corrected(permitted_values) 143 144 scheduled = handler.can_schedule(freebusy, [period]) 145 146 if scheduled == "ACCEPTED": 147 periods.append(period) 148 last = period.get_end() 149 continue 150 151 # Get free periods from the time of each period. 152 153 for found in free.periods_from(period): 154 155 # Skip any periods before the last period. 156 157 if last: 158 if last > found.get_end(): 159 continue 160 161 # Adjust the start of the free period to exclude the last period. 162 163 found = found.make_corrected(max(found.get_start(), last), found.get_end()) 164 165 # Only test free periods long enough to hold the requested period. 166 167 if found.get_duration() >= duration: 168 169 # Obtain a possible period, starting at the found point and 170 # with the requested duration. Then, correct the period if 171 # necessary. 172 173 start = to_timezone(found.get_start(), period.get_tzid()) 174 possible = period.make_corrected(start, start + period.get_duration()) 175 if permitted_values: 176 possible = possible.get_corrected(permitted_values) 177 178 # Only if the possible period is still within the free period 179 # can it be used. 180 181 if possible.within(found): 182 periods.append(possible) 183 break 184 185 # Where no period can be found, decline the invitation. 186 187 else: 188 return "DECLINED" 189 190 # Use the found period to set the start of the next window to search. 191 192 last = periods[-1].get_end() 193 194 # Replace the periods in the object. 195 196 obj = handler.obj.copy() 197 changed = handler.obj.set_periods(periods) 198 199 # Check one last time, reverting the change if not scheduled. 200 201 scheduled = schedule_in_freebusy(handler, args, busy) 202 203 if scheduled == "DECLINED": 204 handler.set_object(obj) 205 206 return scheduled == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED" 207 208 # Registry of scheduling functions. 209 210 scheduling_functions = { 211 "schedule_in_freebusy" : schedule_in_freebusy, 212 "schedule_corrected_in_freebusy" : schedule_corrected_in_freebusy, 213 "schedule_next_available_in_freebusy" : schedule_next_available_in_freebusy, 214 } 215 216 # Registries of locking and unlocking functions. 217 218 locking_functions = {} 219 unlocking_functions = {} 220 221 # Registries of listener functions. 222 223 confirmation_functions = {} 224 retraction_functions = {} 225 226 # vim: tabstop=4 expandtab shiftwidth=4