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