# HG changeset patch # User Paul Boddie # Date 1454083726 -3600 # Node ID 4a0226da2137312cf9c54c3f2284387bb592474c # Parent 39efcf72d853346eaa2489768b729265323652b5 Make a scheduling package to potentially support multiple scheduling modules. diff -r 39efcf72d853 -r 4a0226da2137 imiptools/handlers/scheduling.py --- a/imiptools/handlers/scheduling.py Fri Jan 29 17:02:06 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,255 +0,0 @@ -#!/usr/bin/env python - -""" -Common scheduling functionality. - -Copyright (C) 2015, 2016 Paul Boddie - -This program is free software; you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation; either version 3 of the License, or (at your option) any later -version. - -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -details. - -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -from imiptools.data import uri_values -from imiptools.dates import ValidityError, to_timezone -from imiptools.period import coalesce_freebusy, invert_freebusy, \ - periods_from, remove_event_periods, \ - remove_periods - -def apply_scheduling_functions(functions, handler): - - """ - Apply the given scheduling 'functions' in the current object of the given - 'handler'. - """ - - response = "ACCEPTED" - - for fn in functions: - - # NOTE: Should signal an error for incorrectly configured resources. - - if not fn: - return "DECLINED" - - # Keep evaluating scheduling functions, stopping only if one - # declines or gives a null response. - - else: - result = fn(handler) - - # Return a negative result immediately. - - if not result or result == "DECLINED": - return result - - # Modify the eventual response from acceptance if a countering - # result is obtained. - - elif response == "ACCEPTED": - response = result - - return response - -def schedule_in_freebusy(handler, freebusy=None): - - """ - Attempt to schedule the current object of the given 'handler' in the - free/busy schedule of a resource, returning an indication of the kind of - response to be returned. - - If 'freebusy' is specified, the given collection of busy periods will be - used to determine whether any conflicts occur. Otherwise, the current user's - free/busy records will be used. - """ - - # If newer than any old version, discard old details from the - # free/busy record and check for suitability. - - periods = handler.get_periods(handler.obj) - - freebusy = freebusy or handler.store.get_freebusy(handler.user) - offers = handler.store.get_freebusy_offers(handler.user) - - # Check the periods against any scheduled events and against - # any outstanding offers. - - scheduled = handler.can_schedule(freebusy, periods) - scheduled = scheduled and handler.can_schedule(offers, periods) - - return scheduled and "ACCEPTED" or "DECLINED" - -def schedule_corrected_in_freebusy(handler): - - """ - Attempt to schedule the current object of the given 'handler', correcting - specified datetimes according to the configuration of a resource, - returning an indication of the kind of response to be returned. - """ - - obj = handler.obj.copy() - - # Check any constraints on the request. - - try: - corrected = handler.correct_object() - - # Refuse to schedule obviously invalid requests. - - except ValidityError: - return None - - # With a valid request, determine whether the event can be scheduled. - - scheduled = schedule_in_freebusy(handler) - - # Restore the original object if it was corrected but could not be - # scheduled. - - if scheduled == "DECLINED" and corrected: - handler.set_object(obj) - - # Where the corrected object can be scheduled, issue a counter - # request. - - return scheduled == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED" - -def schedule_next_available_in_freebusy(handler): - - """ - Attempt to schedule the current object of the given 'handler', correcting - specified datetimes according to the configuration of a resource, then - suggesting the next available period in the free/busy records if scheduling - cannot occur for the requested period, returning an indication of the kind - of response to be returned. - """ - - scheduled = schedule_corrected_in_freebusy(handler) - - if scheduled in ("ACCEPTED", "COUNTER"): - return scheduled - - # There should already be free/busy information for the user. - - user_freebusy = handler.store.get_freebusy(handler.user) - busy = user_freebusy - - # Subtract any periods from this event from the free/busy collections. - - event_periods = remove_event_periods(user_freebusy, handler.uid, handler.recurrenceid) - - # Find busy periods for the other attendees. - - for attendee in uri_values(handler.obj.get_values("ATTENDEE")): - if attendee != handler.user: - freebusy = handler.store.get_freebusy_for_other(handler.user, attendee) - if freebusy: - remove_periods(freebusy, event_periods) - busy += freebusy - - # Obtain the combined busy periods. - - busy.sort() - busy = coalesce_freebusy(busy) - - # Obtain free periods. - - free = invert_freebusy(busy) - permitted_values = handler.get_permitted_values() - periods = [] - - # Do not attempt to redefine rule-based periods. - - last = None - - for period in handler.get_periods(handler.obj, explicit_only=True): - duration = period.get_duration() - - # Try and schedule periods normally since some of them may be - # compatible with the schedule. - - if permitted_values: - period = period.get_corrected(permitted_values) - - scheduled = handler.can_schedule(freebusy, [period]) - - if scheduled == "ACCEPTED": - periods.append(period) - last = period.get_end() - continue - - # Get free periods from the time of each period. - - for found in periods_from(free, period): - - # Skip any periods before the last period. - - if last: - if last > found.get_end(): - continue - - # Adjust the start of the free period to exclude the last period. - - found = found.make_corrected(max(found.get_start(), last), found.get_end()) - - # Only test free periods long enough to hold the requested period. - - if found.get_duration() >= duration: - - # Obtain a possible period, starting at the found point and - # with the requested duration. Then, correct the period if - # necessary. - - start = to_timezone(found.get_start(), period.get_tzid()) - possible = period.make_corrected(start, start + period.get_duration()) - if permitted_values: - possible = possible.get_corrected(permitted_values) - - # Only if the possible period is still within the free period - # can it be used. - - if possible.within(found): - periods.append(possible) - break - - # Where no period can be found, decline the invitation. - - else: - return "DECLINED" - - # Use the found period to set the start of the next window to search. - - last = periods[-1].get_end() - - # Replace the periods in the object. - - obj = handler.obj.copy() - changed = handler.obj.set_periods(periods) - - # Check one last time, reverting the change if not scheduled. - - scheduled = schedule_in_freebusy(handler, busy) - - if scheduled == "DECLINED": - handler.set_object(obj) - - return scheduled == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED" - -# Registry of scheduling functions. - -scheduling_functions = { - "schedule_in_freebusy" : schedule_in_freebusy, - "schedule_corrected_in_freebusy" : schedule_corrected_in_freebusy, - "schedule_next_available_in_freebusy" : schedule_next_available_in_freebusy, - } - -# vim: tabstop=4 expandtab shiftwidth=4 diff -r 39efcf72d853 -r 4a0226da2137 imiptools/handlers/scheduling/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/imiptools/handlers/scheduling/__init__.py Fri Jan 29 17:08:46 2016 +0100 @@ -0,0 +1,255 @@ +#!/usr/bin/env python + +""" +Common scheduling functionality. + +Copyright (C) 2015, 2016 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from imiptools.data import uri_values +from imiptools.dates import ValidityError, to_timezone +from imiptools.period import coalesce_freebusy, invert_freebusy, \ + periods_from, remove_event_periods, \ + remove_periods + +def apply_scheduling_functions(functions, handler): + + """ + Apply the given scheduling 'functions' in the current object of the given + 'handler'. + """ + + response = "ACCEPTED" + + for fn in functions: + + # NOTE: Should signal an error for incorrectly configured resources. + + if not fn: + return "DECLINED" + + # Keep evaluating scheduling functions, stopping only if one + # declines or gives a null response. + + else: + result = fn(handler) + + # Return a negative result immediately. + + if not result or result == "DECLINED": + return result + + # Modify the eventual response from acceptance if a countering + # result is obtained. + + elif response == "ACCEPTED": + response = result + + return response + +def schedule_in_freebusy(handler, freebusy=None): + + """ + Attempt to schedule the current object of the given 'handler' in the + free/busy schedule of a resource, returning an indication of the kind of + response to be returned. + + If 'freebusy' is specified, the given collection of busy periods will be + used to determine whether any conflicts occur. Otherwise, the current user's + free/busy records will be used. + """ + + # If newer than any old version, discard old details from the + # free/busy record and check for suitability. + + periods = handler.get_periods(handler.obj) + + freebusy = freebusy or handler.store.get_freebusy(handler.user) + offers = handler.store.get_freebusy_offers(handler.user) + + # Check the periods against any scheduled events and against + # any outstanding offers. + + scheduled = handler.can_schedule(freebusy, periods) + scheduled = scheduled and handler.can_schedule(offers, periods) + + return scheduled and "ACCEPTED" or "DECLINED" + +def schedule_corrected_in_freebusy(handler): + + """ + Attempt to schedule the current object of the given 'handler', correcting + specified datetimes according to the configuration of a resource, + returning an indication of the kind of response to be returned. + """ + + obj = handler.obj.copy() + + # Check any constraints on the request. + + try: + corrected = handler.correct_object() + + # Refuse to schedule obviously invalid requests. + + except ValidityError: + return None + + # With a valid request, determine whether the event can be scheduled. + + scheduled = schedule_in_freebusy(handler) + + # Restore the original object if it was corrected but could not be + # scheduled. + + if scheduled == "DECLINED" and corrected: + handler.set_object(obj) + + # Where the corrected object can be scheduled, issue a counter + # request. + + return scheduled == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED" + +def schedule_next_available_in_freebusy(handler): + + """ + Attempt to schedule the current object of the given 'handler', correcting + specified datetimes according to the configuration of a resource, then + suggesting the next available period in the free/busy records if scheduling + cannot occur for the requested period, returning an indication of the kind + of response to be returned. + """ + + scheduled = schedule_corrected_in_freebusy(handler) + + if scheduled in ("ACCEPTED", "COUNTER"): + return scheduled + + # There should already be free/busy information for the user. + + user_freebusy = handler.store.get_freebusy(handler.user) + busy = user_freebusy + + # Subtract any periods from this event from the free/busy collections. + + event_periods = remove_event_periods(user_freebusy, handler.uid, handler.recurrenceid) + + # Find busy periods for the other attendees. + + for attendee in uri_values(handler.obj.get_values("ATTENDEE")): + if attendee != handler.user: + freebusy = handler.store.get_freebusy_for_other(handler.user, attendee) + if freebusy: + remove_periods(freebusy, event_periods) + busy += freebusy + + # Obtain the combined busy periods. + + busy.sort() + busy = coalesce_freebusy(busy) + + # Obtain free periods. + + free = invert_freebusy(busy) + permitted_values = handler.get_permitted_values() + periods = [] + + # Do not attempt to redefine rule-based periods. + + last = None + + for period in handler.get_periods(handler.obj, explicit_only=True): + duration = period.get_duration() + + # Try and schedule periods normally since some of them may be + # compatible with the schedule. + + if permitted_values: + period = period.get_corrected(permitted_values) + + scheduled = handler.can_schedule(freebusy, [period]) + + if scheduled == "ACCEPTED": + periods.append(period) + last = period.get_end() + continue + + # Get free periods from the time of each period. + + for found in periods_from(free, period): + + # Skip any periods before the last period. + + if last: + if last > found.get_end(): + continue + + # Adjust the start of the free period to exclude the last period. + + found = found.make_corrected(max(found.get_start(), last), found.get_end()) + + # Only test free periods long enough to hold the requested period. + + if found.get_duration() >= duration: + + # Obtain a possible period, starting at the found point and + # with the requested duration. Then, correct the period if + # necessary. + + start = to_timezone(found.get_start(), period.get_tzid()) + possible = period.make_corrected(start, start + period.get_duration()) + if permitted_values: + possible = possible.get_corrected(permitted_values) + + # Only if the possible period is still within the free period + # can it be used. + + if possible.within(found): + periods.append(possible) + break + + # Where no period can be found, decline the invitation. + + else: + return "DECLINED" + + # Use the found period to set the start of the next window to search. + + last = periods[-1].get_end() + + # Replace the periods in the object. + + obj = handler.obj.copy() + changed = handler.obj.set_periods(periods) + + # Check one last time, reverting the change if not scheduled. + + scheduled = schedule_in_freebusy(handler, busy) + + if scheduled == "DECLINED": + handler.set_object(obj) + + return scheduled == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED" + +# Registry of scheduling functions. + +scheduling_functions = { + "schedule_in_freebusy" : schedule_in_freebusy, + "schedule_corrected_in_freebusy" : schedule_corrected_in_freebusy, + "schedule_next_available_in_freebusy" : schedule_next_available_in_freebusy, + } + +# vim: tabstop=4 expandtab shiftwidth=4