1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/imiptools/handlers/scheduling/freebusy.py Fri Jan 29 22:25:02 2016 +0100
1.3 @@ -0,0 +1,220 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +"""
1.7 +Free/busy-related scheduling functionality.
1.8 +
1.9 +Copyright (C) 2015, 2016 Paul Boddie <paul@boddie.org.uk>
1.10 +
1.11 +This program is free software; you can redistribute it and/or modify it under
1.12 +the terms of the GNU General Public License as published by the Free Software
1.13 +Foundation; either version 3 of the License, or (at your option) any later
1.14 +version.
1.15 +
1.16 +This program is distributed in the hope that it will be useful, but WITHOUT
1.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
1.19 +details.
1.20 +
1.21 +You should have received a copy of the GNU General Public License along with
1.22 +this program. If not, see <http://www.gnu.org/licenses/>.
1.23 +"""
1.24 +
1.25 +from imiptools.data import uri_values
1.26 +from imiptools.dates import ValidityError, to_timezone
1.27 +from imiptools.period import coalesce_freebusy, invert_freebusy, \
1.28 + periods_from, remove_event_periods, \
1.29 + remove_periods
1.30 +
1.31 +def schedule_in_freebusy(handler, freebusy=None):
1.32 +
1.33 + """
1.34 + Attempt to schedule the current object of the given 'handler' in the
1.35 + free/busy schedule of a resource, returning an indication of the kind of
1.36 + response to be returned.
1.37 +
1.38 + If 'freebusy' is specified, the given collection of busy periods will be
1.39 + used to determine whether any conflicts occur. Otherwise, the current user's
1.40 + free/busy records will be used.
1.41 + """
1.42 +
1.43 + # If newer than any old version, discard old details from the
1.44 + # free/busy record and check for suitability.
1.45 +
1.46 + periods = handler.get_periods(handler.obj)
1.47 +
1.48 + freebusy = freebusy or handler.store.get_freebusy(handler.user)
1.49 + offers = handler.store.get_freebusy_offers(handler.user)
1.50 +
1.51 + # Check the periods against any scheduled events and against
1.52 + # any outstanding offers.
1.53 +
1.54 + scheduled = handler.can_schedule(freebusy, periods)
1.55 + scheduled = scheduled and handler.can_schedule(offers, periods)
1.56 +
1.57 + return scheduled and "ACCEPTED" or "DECLINED"
1.58 +
1.59 +def schedule_corrected_in_freebusy(handler):
1.60 +
1.61 + """
1.62 + Attempt to schedule the current object of the given 'handler', correcting
1.63 + specified datetimes according to the configuration of a resource,
1.64 + returning an indication of the kind of response to be returned.
1.65 + """
1.66 +
1.67 + obj = handler.obj.copy()
1.68 +
1.69 + # Check any constraints on the request.
1.70 +
1.71 + try:
1.72 + corrected = handler.correct_object()
1.73 +
1.74 + # Refuse to schedule obviously invalid requests.
1.75 +
1.76 + except ValidityError:
1.77 + return None
1.78 +
1.79 + # With a valid request, determine whether the event can be scheduled.
1.80 +
1.81 + scheduled = schedule_in_freebusy(handler)
1.82 +
1.83 + # Restore the original object if it was corrected but could not be
1.84 + # scheduled.
1.85 +
1.86 + if scheduled == "DECLINED" and corrected:
1.87 + handler.set_object(obj)
1.88 +
1.89 + # Where the corrected object can be scheduled, issue a counter
1.90 + # request.
1.91 +
1.92 + return scheduled == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED"
1.93 +
1.94 +def schedule_next_available_in_freebusy(handler):
1.95 +
1.96 + """
1.97 + Attempt to schedule the current object of the given 'handler', correcting
1.98 + specified datetimes according to the configuration of a resource, then
1.99 + suggesting the next available period in the free/busy records if scheduling
1.100 + cannot occur for the requested period, returning an indication of the kind
1.101 + of response to be returned.
1.102 + """
1.103 +
1.104 + scheduled = schedule_corrected_in_freebusy(handler)
1.105 +
1.106 + if scheduled in ("ACCEPTED", "COUNTER"):
1.107 + return scheduled
1.108 +
1.109 + # There should already be free/busy information for the user.
1.110 +
1.111 + user_freebusy = handler.store.get_freebusy(handler.user)
1.112 + busy = user_freebusy
1.113 +
1.114 + # Subtract any periods from this event from the free/busy collections.
1.115 +
1.116 + event_periods = remove_event_periods(user_freebusy, handler.uid, handler.recurrenceid)
1.117 +
1.118 + # Find busy periods for the other attendees.
1.119 +
1.120 + for attendee in uri_values(handler.obj.get_values("ATTENDEE")):
1.121 + if attendee != handler.user:
1.122 + freebusy = handler.store.get_freebusy_for_other(handler.user, attendee)
1.123 + if freebusy:
1.124 + remove_periods(freebusy, event_periods)
1.125 + busy += freebusy
1.126 +
1.127 + # Obtain the combined busy periods.
1.128 +
1.129 + busy.sort()
1.130 + busy = coalesce_freebusy(busy)
1.131 +
1.132 + # Obtain free periods.
1.133 +
1.134 + free = invert_freebusy(busy)
1.135 + permitted_values = handler.get_permitted_values()
1.136 + periods = []
1.137 +
1.138 + # Do not attempt to redefine rule-based periods.
1.139 +
1.140 + last = None
1.141 +
1.142 + for period in handler.get_periods(handler.obj, explicit_only=True):
1.143 + duration = period.get_duration()
1.144 +
1.145 + # Try and schedule periods normally since some of them may be
1.146 + # compatible with the schedule.
1.147 +
1.148 + if permitted_values:
1.149 + period = period.get_corrected(permitted_values)
1.150 +
1.151 + scheduled = handler.can_schedule(freebusy, [period])
1.152 +
1.153 + if scheduled == "ACCEPTED":
1.154 + periods.append(period)
1.155 + last = period.get_end()
1.156 + continue
1.157 +
1.158 + # Get free periods from the time of each period.
1.159 +
1.160 + for found in periods_from(free, period):
1.161 +
1.162 + # Skip any periods before the last period.
1.163 +
1.164 + if last:
1.165 + if last > found.get_end():
1.166 + continue
1.167 +
1.168 + # Adjust the start of the free period to exclude the last period.
1.169 +
1.170 + found = found.make_corrected(max(found.get_start(), last), found.get_end())
1.171 +
1.172 + # Only test free periods long enough to hold the requested period.
1.173 +
1.174 + if found.get_duration() >= duration:
1.175 +
1.176 + # Obtain a possible period, starting at the found point and
1.177 + # with the requested duration. Then, correct the period if
1.178 + # necessary.
1.179 +
1.180 + start = to_timezone(found.get_start(), period.get_tzid())
1.181 + possible = period.make_corrected(start, start + period.get_duration())
1.182 + if permitted_values:
1.183 + possible = possible.get_corrected(permitted_values)
1.184 +
1.185 + # Only if the possible period is still within the free period
1.186 + # can it be used.
1.187 +
1.188 + if possible.within(found):
1.189 + periods.append(possible)
1.190 + break
1.191 +
1.192 + # Where no period can be found, decline the invitation.
1.193 +
1.194 + else:
1.195 + return "DECLINED"
1.196 +
1.197 + # Use the found period to set the start of the next window to search.
1.198 +
1.199 + last = periods[-1].get_end()
1.200 +
1.201 + # Replace the periods in the object.
1.202 +
1.203 + obj = handler.obj.copy()
1.204 + changed = handler.obj.set_periods(periods)
1.205 +
1.206 + # Check one last time, reverting the change if not scheduled.
1.207 +
1.208 + scheduled = schedule_in_freebusy(handler, busy)
1.209 +
1.210 + if scheduled == "DECLINED":
1.211 + handler.set_object(obj)
1.212 +
1.213 + return scheduled == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED"
1.214 +
1.215 +# Registry of scheduling functions.
1.216 +
1.217 +scheduling_functions = {
1.218 + "schedule_in_freebusy" : schedule_in_freebusy,
1.219 + "schedule_corrected_in_freebusy" : schedule_corrected_in_freebusy,
1.220 + "schedule_next_available_in_freebusy" : schedule_next_available_in_freebusy,
1.221 + }
1.222 +
1.223 +# vim: tabstop=4 expandtab shiftwidth=4