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