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