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