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