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