1.1 --- a/imiptools/handlers/scheduling.py Fri Jan 29 17:02:06 2016 +0100
1.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
1.3 @@ -1,255 +0,0 @@
1.4 -#!/usr/bin/env python
1.5 -
1.6 -"""
1.7 -Common 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 apply_scheduling_functions(functions, handler):
1.32 -
1.33 - """
1.34 - Apply the given scheduling 'functions' in the current object of the given
1.35 - 'handler'.
1.36 - """
1.37 -
1.38 - response = "ACCEPTED"
1.39 -
1.40 - for fn in functions:
1.41 -
1.42 - # NOTE: Should signal an error for incorrectly configured resources.
1.43 -
1.44 - if not fn:
1.45 - return "DECLINED"
1.46 -
1.47 - # Keep evaluating scheduling functions, stopping only if one
1.48 - # declines or gives a null response.
1.49 -
1.50 - else:
1.51 - result = fn(handler)
1.52 -
1.53 - # Return a negative result immediately.
1.54 -
1.55 - if not result or result == "DECLINED":
1.56 - return result
1.57 -
1.58 - # Modify the eventual response from acceptance if a countering
1.59 - # result is obtained.
1.60 -
1.61 - elif response == "ACCEPTED":
1.62 - response = result
1.63 -
1.64 - return response
1.65 -
1.66 -def schedule_in_freebusy(handler, freebusy=None):
1.67 -
1.68 - """
1.69 - Attempt to schedule the current object of the given 'handler' in the
1.70 - free/busy schedule of a resource, returning an indication of the kind of
1.71 - response to be returned.
1.72 -
1.73 - If 'freebusy' is specified, the given collection of busy periods will be
1.74 - used to determine whether any conflicts occur. Otherwise, the current user's
1.75 - free/busy records will be used.
1.76 - """
1.77 -
1.78 - # If newer than any old version, discard old details from the
1.79 - # free/busy record and check for suitability.
1.80 -
1.81 - periods = handler.get_periods(handler.obj)
1.82 -
1.83 - freebusy = freebusy or handler.store.get_freebusy(handler.user)
1.84 - offers = handler.store.get_freebusy_offers(handler.user)
1.85 -
1.86 - # Check the periods against any scheduled events and against
1.87 - # any outstanding offers.
1.88 -
1.89 - scheduled = handler.can_schedule(freebusy, periods)
1.90 - scheduled = scheduled and handler.can_schedule(offers, periods)
1.91 -
1.92 - return scheduled and "ACCEPTED" or "DECLINED"
1.93 -
1.94 -def schedule_corrected_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,
1.99 - returning an indication of the kind of response to be returned.
1.100 - """
1.101 -
1.102 - obj = handler.obj.copy()
1.103 -
1.104 - # Check any constraints on the request.
1.105 -
1.106 - try:
1.107 - corrected = handler.correct_object()
1.108 -
1.109 - # Refuse to schedule obviously invalid requests.
1.110 -
1.111 - except ValidityError:
1.112 - return None
1.113 -
1.114 - # With a valid request, determine whether the event can be scheduled.
1.115 -
1.116 - scheduled = schedule_in_freebusy(handler)
1.117 -
1.118 - # Restore the original object if it was corrected but could not be
1.119 - # scheduled.
1.120 -
1.121 - if scheduled == "DECLINED" and corrected:
1.122 - handler.set_object(obj)
1.123 -
1.124 - # Where the corrected object can be scheduled, issue a counter
1.125 - # request.
1.126 -
1.127 - return scheduled == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED"
1.128 -
1.129 -def schedule_next_available_in_freebusy(handler):
1.130 -
1.131 - """
1.132 - Attempt to schedule the current object of the given 'handler', correcting
1.133 - specified datetimes according to the configuration of a resource, then
1.134 - suggesting the next available period in the free/busy records if scheduling
1.135 - cannot occur for the requested period, returning an indication of the kind
1.136 - of response to be returned.
1.137 - """
1.138 -
1.139 - scheduled = schedule_corrected_in_freebusy(handler)
1.140 -
1.141 - if scheduled in ("ACCEPTED", "COUNTER"):
1.142 - return scheduled
1.143 -
1.144 - # There should already be free/busy information for the user.
1.145 -
1.146 - user_freebusy = handler.store.get_freebusy(handler.user)
1.147 - busy = user_freebusy
1.148 -
1.149 - # Subtract any periods from this event from the free/busy collections.
1.150 -
1.151 - event_periods = remove_event_periods(user_freebusy, handler.uid, handler.recurrenceid)
1.152 -
1.153 - # Find busy periods for the other attendees.
1.154 -
1.155 - for attendee in uri_values(handler.obj.get_values("ATTENDEE")):
1.156 - if attendee != handler.user:
1.157 - freebusy = handler.store.get_freebusy_for_other(handler.user, attendee)
1.158 - if freebusy:
1.159 - remove_periods(freebusy, event_periods)
1.160 - busy += freebusy
1.161 -
1.162 - # Obtain the combined busy periods.
1.163 -
1.164 - busy.sort()
1.165 - busy = coalesce_freebusy(busy)
1.166 -
1.167 - # Obtain free periods.
1.168 -
1.169 - free = invert_freebusy(busy)
1.170 - permitted_values = handler.get_permitted_values()
1.171 - periods = []
1.172 -
1.173 - # Do not attempt to redefine rule-based periods.
1.174 -
1.175 - last = None
1.176 -
1.177 - for period in handler.get_periods(handler.obj, explicit_only=True):
1.178 - duration = period.get_duration()
1.179 -
1.180 - # Try and schedule periods normally since some of them may be
1.181 - # compatible with the schedule.
1.182 -
1.183 - if permitted_values:
1.184 - period = period.get_corrected(permitted_values)
1.185 -
1.186 - scheduled = handler.can_schedule(freebusy, [period])
1.187 -
1.188 - if scheduled == "ACCEPTED":
1.189 - periods.append(period)
1.190 - last = period.get_end()
1.191 - continue
1.192 -
1.193 - # Get free periods from the time of each period.
1.194 -
1.195 - for found in periods_from(free, period):
1.196 -
1.197 - # Skip any periods before the last period.
1.198 -
1.199 - if last:
1.200 - if last > found.get_end():
1.201 - continue
1.202 -
1.203 - # Adjust the start of the free period to exclude the last period.
1.204 -
1.205 - found = found.make_corrected(max(found.get_start(), last), found.get_end())
1.206 -
1.207 - # Only test free periods long enough to hold the requested period.
1.208 -
1.209 - if found.get_duration() >= duration:
1.210 -
1.211 - # Obtain a possible period, starting at the found point and
1.212 - # with the requested duration. Then, correct the period if
1.213 - # necessary.
1.214 -
1.215 - start = to_timezone(found.get_start(), period.get_tzid())
1.216 - possible = period.make_corrected(start, start + period.get_duration())
1.217 - if permitted_values:
1.218 - possible = possible.get_corrected(permitted_values)
1.219 -
1.220 - # Only if the possible period is still within the free period
1.221 - # can it be used.
1.222 -
1.223 - if possible.within(found):
1.224 - periods.append(possible)
1.225 - break
1.226 -
1.227 - # Where no period can be found, decline the invitation.
1.228 -
1.229 - else:
1.230 - return "DECLINED"
1.231 -
1.232 - # Use the found period to set the start of the next window to search.
1.233 -
1.234 - last = periods[-1].get_end()
1.235 -
1.236 - # Replace the periods in the object.
1.237 -
1.238 - obj = handler.obj.copy()
1.239 - changed = handler.obj.set_periods(periods)
1.240 -
1.241 - # Check one last time, reverting the change if not scheduled.
1.242 -
1.243 - scheduled = schedule_in_freebusy(handler, busy)
1.244 -
1.245 - if scheduled == "DECLINED":
1.246 - handler.set_object(obj)
1.247 -
1.248 - return scheduled == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED"
1.249 -
1.250 -# Registry of scheduling functions.
1.251 -
1.252 -scheduling_functions = {
1.253 - "schedule_in_freebusy" : schedule_in_freebusy,
1.254 - "schedule_corrected_in_freebusy" : schedule_corrected_in_freebusy,
1.255 - "schedule_next_available_in_freebusy" : schedule_next_available_in_freebusy,
1.256 - }
1.257 -
1.258 -# vim: tabstop=4 expandtab shiftwidth=4
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2.2 +++ b/imiptools/handlers/scheduling/__init__.py Fri Jan 29 17:08:46 2016 +0100
2.3 @@ -0,0 +1,255 @@
2.4 +#!/usr/bin/env python
2.5 +
2.6 +"""
2.7 +Common scheduling functionality.
2.8 +
2.9 +Copyright (C) 2015, 2016 Paul Boddie <paul@boddie.org.uk>
2.10 +
2.11 +This program is free software; you can redistribute it and/or modify it under
2.12 +the terms of the GNU General Public License as published by the Free Software
2.13 +Foundation; either version 3 of the License, or (at your option) any later
2.14 +version.
2.15 +
2.16 +This program is distributed in the hope that it will be useful, but WITHOUT
2.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
2.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
2.19 +details.
2.20 +
2.21 +You should have received a copy of the GNU General Public License along with
2.22 +this program. If not, see <http://www.gnu.org/licenses/>.
2.23 +"""
2.24 +
2.25 +from imiptools.data import uri_values
2.26 +from imiptools.dates import ValidityError, to_timezone
2.27 +from imiptools.period import coalesce_freebusy, invert_freebusy, \
2.28 + periods_from, remove_event_periods, \
2.29 + remove_periods
2.30 +
2.31 +def apply_scheduling_functions(functions, handler):
2.32 +
2.33 + """
2.34 + Apply the given scheduling 'functions' in the current object of the given
2.35 + 'handler'.
2.36 + """
2.37 +
2.38 + response = "ACCEPTED"
2.39 +
2.40 + for fn in functions:
2.41 +
2.42 + # NOTE: Should signal an error for incorrectly configured resources.
2.43 +
2.44 + if not fn:
2.45 + return "DECLINED"
2.46 +
2.47 + # Keep evaluating scheduling functions, stopping only if one
2.48 + # declines or gives a null response.
2.49 +
2.50 + else:
2.51 + result = fn(handler)
2.52 +
2.53 + # Return a negative result immediately.
2.54 +
2.55 + if not result or result == "DECLINED":
2.56 + return result
2.57 +
2.58 + # Modify the eventual response from acceptance if a countering
2.59 + # result is obtained.
2.60 +
2.61 + elif response == "ACCEPTED":
2.62 + response = result
2.63 +
2.64 + return response
2.65 +
2.66 +def schedule_in_freebusy(handler, freebusy=None):
2.67 +
2.68 + """
2.69 + Attempt to schedule the current object of the given 'handler' in the
2.70 + free/busy schedule of a resource, returning an indication of the kind of
2.71 + response to be returned.
2.72 +
2.73 + If 'freebusy' is specified, the given collection of busy periods will be
2.74 + used to determine whether any conflicts occur. Otherwise, the current user's
2.75 + free/busy records will be used.
2.76 + """
2.77 +
2.78 + # If newer than any old version, discard old details from the
2.79 + # free/busy record and check for suitability.
2.80 +
2.81 + periods = handler.get_periods(handler.obj)
2.82 +
2.83 + freebusy = freebusy or handler.store.get_freebusy(handler.user)
2.84 + offers = handler.store.get_freebusy_offers(handler.user)
2.85 +
2.86 + # Check the periods against any scheduled events and against
2.87 + # any outstanding offers.
2.88 +
2.89 + scheduled = handler.can_schedule(freebusy, periods)
2.90 + scheduled = scheduled and handler.can_schedule(offers, periods)
2.91 +
2.92 + return scheduled and "ACCEPTED" or "DECLINED"
2.93 +
2.94 +def schedule_corrected_in_freebusy(handler):
2.95 +
2.96 + """
2.97 + Attempt to schedule the current object of the given 'handler', correcting
2.98 + specified datetimes according to the configuration of a resource,
2.99 + returning an indication of the kind of response to be returned.
2.100 + """
2.101 +
2.102 + obj = handler.obj.copy()
2.103 +
2.104 + # Check any constraints on the request.
2.105 +
2.106 + try:
2.107 + corrected = handler.correct_object()
2.108 +
2.109 + # Refuse to schedule obviously invalid requests.
2.110 +
2.111 + except ValidityError:
2.112 + return None
2.113 +
2.114 + # With a valid request, determine whether the event can be scheduled.
2.115 +
2.116 + scheduled = schedule_in_freebusy(handler)
2.117 +
2.118 + # Restore the original object if it was corrected but could not be
2.119 + # scheduled.
2.120 +
2.121 + if scheduled == "DECLINED" and corrected:
2.122 + handler.set_object(obj)
2.123 +
2.124 + # Where the corrected object can be scheduled, issue a counter
2.125 + # request.
2.126 +
2.127 + return scheduled == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED"
2.128 +
2.129 +def schedule_next_available_in_freebusy(handler):
2.130 +
2.131 + """
2.132 + Attempt to schedule the current object of the given 'handler', correcting
2.133 + specified datetimes according to the configuration of a resource, then
2.134 + suggesting the next available period in the free/busy records if scheduling
2.135 + cannot occur for the requested period, returning an indication of the kind
2.136 + of response to be returned.
2.137 + """
2.138 +
2.139 + scheduled = schedule_corrected_in_freebusy(handler)
2.140 +
2.141 + if scheduled in ("ACCEPTED", "COUNTER"):
2.142 + return scheduled
2.143 +
2.144 + # There should already be free/busy information for the user.
2.145 +
2.146 + user_freebusy = handler.store.get_freebusy(handler.user)
2.147 + busy = user_freebusy
2.148 +
2.149 + # Subtract any periods from this event from the free/busy collections.
2.150 +
2.151 + event_periods = remove_event_periods(user_freebusy, handler.uid, handler.recurrenceid)
2.152 +
2.153 + # Find busy periods for the other attendees.
2.154 +
2.155 + for attendee in uri_values(handler.obj.get_values("ATTENDEE")):
2.156 + if attendee != handler.user:
2.157 + freebusy = handler.store.get_freebusy_for_other(handler.user, attendee)
2.158 + if freebusy:
2.159 + remove_periods(freebusy, event_periods)
2.160 + busy += freebusy
2.161 +
2.162 + # Obtain the combined busy periods.
2.163 +
2.164 + busy.sort()
2.165 + busy = coalesce_freebusy(busy)
2.166 +
2.167 + # Obtain free periods.
2.168 +
2.169 + free = invert_freebusy(busy)
2.170 + permitted_values = handler.get_permitted_values()
2.171 + periods = []
2.172 +
2.173 + # Do not attempt to redefine rule-based periods.
2.174 +
2.175 + last = None
2.176 +
2.177 + for period in handler.get_periods(handler.obj, explicit_only=True):
2.178 + duration = period.get_duration()
2.179 +
2.180 + # Try and schedule periods normally since some of them may be
2.181 + # compatible with the schedule.
2.182 +
2.183 + if permitted_values:
2.184 + period = period.get_corrected(permitted_values)
2.185 +
2.186 + scheduled = handler.can_schedule(freebusy, [period])
2.187 +
2.188 + if scheduled == "ACCEPTED":
2.189 + periods.append(period)
2.190 + last = period.get_end()
2.191 + continue
2.192 +
2.193 + # Get free periods from the time of each period.
2.194 +
2.195 + for found in periods_from(free, period):
2.196 +
2.197 + # Skip any periods before the last period.
2.198 +
2.199 + if last:
2.200 + if last > found.get_end():
2.201 + continue
2.202 +
2.203 + # Adjust the start of the free period to exclude the last period.
2.204 +
2.205 + found = found.make_corrected(max(found.get_start(), last), found.get_end())
2.206 +
2.207 + # Only test free periods long enough to hold the requested period.
2.208 +
2.209 + if found.get_duration() >= duration:
2.210 +
2.211 + # Obtain a possible period, starting at the found point and
2.212 + # with the requested duration. Then, correct the period if
2.213 + # necessary.
2.214 +
2.215 + start = to_timezone(found.get_start(), period.get_tzid())
2.216 + possible = period.make_corrected(start, start + period.get_duration())
2.217 + if permitted_values:
2.218 + possible = possible.get_corrected(permitted_values)
2.219 +
2.220 + # Only if the possible period is still within the free period
2.221 + # can it be used.
2.222 +
2.223 + if possible.within(found):
2.224 + periods.append(possible)
2.225 + break
2.226 +
2.227 + # Where no period can be found, decline the invitation.
2.228 +
2.229 + else:
2.230 + return "DECLINED"
2.231 +
2.232 + # Use the found period to set the start of the next window to search.
2.233 +
2.234 + last = periods[-1].get_end()
2.235 +
2.236 + # Replace the periods in the object.
2.237 +
2.238 + obj = handler.obj.copy()
2.239 + changed = handler.obj.set_periods(periods)
2.240 +
2.241 + # Check one last time, reverting the change if not scheduled.
2.242 +
2.243 + scheduled = schedule_in_freebusy(handler, busy)
2.244 +
2.245 + if scheduled == "DECLINED":
2.246 + handler.set_object(obj)
2.247 +
2.248 + return scheduled == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED"
2.249 +
2.250 +# Registry of scheduling functions.
2.251 +
2.252 +scheduling_functions = {
2.253 + "schedule_in_freebusy" : schedule_in_freebusy,
2.254 + "schedule_corrected_in_freebusy" : schedule_corrected_in_freebusy,
2.255 + "schedule_next_available_in_freebusy" : schedule_next_available_in_freebusy,
2.256 + }
2.257 +
2.258 +# vim: tabstop=4 expandtab shiftwidth=4