imip-agent

imiptools/handlers/scheduling/freebusy.py

1359:8cb064fcd9f1
2017-10-22 Paul Boddie Reworked various aspects of the recurrence computation implementation, removing explicit sort operations and changing day selection to produce results in order.
     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