imip-agent

imiptools/handlers/scheduling/freebusy.py

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