imip-agent

Annotated imiptools/handlers/scheduling/freebusy.py

1039:a12150034cbd
2016-02-08 Paul Boddie Added a journal storage area, maintaining quota and collective scheduling data for scheduling decisions. Introduced confirmation and retraction functions for resource scheduling so that quotas and collective schedules can be maintained and thus queried by scheduling functions. Updated the documentation, tools and tests.
paul@1028 1
#!/usr/bin/env python
paul@1028 2
paul@1028 3
"""
paul@1028 4
Free/busy-related scheduling functionality.
paul@1028 5
paul@1028 6
Copyright (C) 2015, 2016 Paul Boddie <paul@boddie.org.uk>
paul@1028 7
paul@1028 8
This program is free software; you can redistribute it and/or modify it under
paul@1028 9
the terms of the GNU General Public License as published by the Free Software
paul@1028 10
Foundation; either version 3 of the License, or (at your option) any later
paul@1028 11
version.
paul@1028 12
paul@1028 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@1028 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@1028 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@1028 16
details.
paul@1028 17
paul@1028 18
You should have received a copy of the GNU General Public License along with
paul@1028 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@1028 20
"""
paul@1028 21
paul@1028 22
from imiptools.data import uri_values
paul@1028 23
from imiptools.dates import ValidityError, to_timezone
paul@1028 24
from imiptools.period import coalesce_freebusy, invert_freebusy, \
paul@1028 25
                             periods_from, remove_event_periods, \
paul@1028 26
                             remove_periods
paul@1028 27
paul@1031 28
def schedule_in_freebusy(handler, args, freebusy=None):
paul@1028 29
paul@1028 30
    """
paul@1028 31
    Attempt to schedule the current object of the given 'handler' in the
paul@1028 32
    free/busy schedule of a resource, returning an indication of the kind of
paul@1028 33
    response to be returned.
paul@1028 34
paul@1028 35
    If 'freebusy' is specified, the given collection of busy periods will be
paul@1028 36
    used to determine whether any conflicts occur. Otherwise, the current user's
paul@1028 37
    free/busy records will be used.
paul@1028 38
    """
paul@1028 39
paul@1028 40
    # If newer than any old version, discard old details from the
paul@1028 41
    # free/busy record and check for suitability.
paul@1028 42
paul@1028 43
    periods = handler.get_periods(handler.obj)
paul@1028 44
paul@1039 45
    freebusy = freebusy or handler.get_store().get_freebusy(handler.user)
paul@1039 46
    offers = handler.get_store().get_freebusy_offers(handler.user)
paul@1028 47
paul@1028 48
    # Check the periods against any scheduled events and against
paul@1028 49
    # any outstanding offers.
paul@1028 50
paul@1028 51
    scheduled = handler.can_schedule(freebusy, periods)
paul@1028 52
    scheduled = scheduled and handler.can_schedule(offers, periods)
paul@1028 53
paul@1028 54
    return scheduled and "ACCEPTED" or "DECLINED"
paul@1028 55
paul@1031 56
def schedule_corrected_in_freebusy(handler, args):
paul@1028 57
paul@1028 58
    """
paul@1028 59
    Attempt to schedule the current object of the given 'handler', correcting
paul@1028 60
    specified datetimes according to the configuration of a resource,
paul@1028 61
    returning an indication of the kind of response to be returned.
paul@1028 62
    """
paul@1028 63
paul@1028 64
    obj = handler.obj.copy()
paul@1028 65
paul@1028 66
    # Check any constraints on the request.
paul@1028 67
paul@1028 68
    try:
paul@1028 69
        corrected = handler.correct_object()
paul@1028 70
paul@1028 71
    # Refuse to schedule obviously invalid requests.
paul@1028 72
paul@1028 73
    except ValidityError:
paul@1028 74
        return None
paul@1028 75
paul@1028 76
    # With a valid request, determine whether the event can be scheduled.
paul@1028 77
paul@1034 78
    scheduled = schedule_in_freebusy(handler, args)
paul@1028 79
paul@1028 80
    # Restore the original object if it was corrected but could not be
paul@1028 81
    # scheduled.
paul@1028 82
paul@1028 83
    if scheduled == "DECLINED" and corrected:
paul@1028 84
        handler.set_object(obj)
paul@1028 85
    
paul@1028 86
    # Where the corrected object can be scheduled, issue a counter
paul@1028 87
    # request.
paul@1028 88
paul@1028 89
    return scheduled == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED"
paul@1028 90
paul@1031 91
def schedule_next_available_in_freebusy(handler, args):
paul@1028 92
paul@1028 93
    """
paul@1028 94
    Attempt to schedule the current object of the given 'handler', correcting
paul@1028 95
    specified datetimes according to the configuration of a resource, then
paul@1028 96
    suggesting the next available period in the free/busy records if scheduling
paul@1028 97
    cannot occur for the requested period, returning an indication of the kind
paul@1028 98
    of response to be returned.
paul@1028 99
    """
paul@1028 100
paul@1034 101
    scheduled = schedule_corrected_in_freebusy(handler, args)
paul@1028 102
paul@1028 103
    if scheduled in ("ACCEPTED", "COUNTER"):
paul@1028 104
        return scheduled
paul@1028 105
paul@1028 106
    # There should already be free/busy information for the user.
paul@1028 107
paul@1039 108
    user_freebusy = handler.get_store().get_freebusy(handler.user)
paul@1028 109
    busy = user_freebusy
paul@1028 110
paul@1028 111
    # Subtract any periods from this event from the free/busy collections.
paul@1028 112
paul@1028 113
    event_periods = remove_event_periods(user_freebusy, handler.uid, handler.recurrenceid)
paul@1028 114
paul@1028 115
    # Find busy periods for the other attendees.
paul@1028 116
paul@1028 117
    for attendee in uri_values(handler.obj.get_values("ATTENDEE")):
paul@1028 118
        if attendee != handler.user:
paul@1039 119
            freebusy = handler.get_store().get_freebusy_for_other(handler.user, attendee)
paul@1028 120
            if freebusy:
paul@1028 121
                remove_periods(freebusy, event_periods)
paul@1028 122
                busy += freebusy
paul@1028 123
paul@1028 124
    # Obtain the combined busy periods.
paul@1028 125
paul@1028 126
    busy.sort()
paul@1028 127
    busy = coalesce_freebusy(busy)
paul@1028 128
paul@1028 129
    # Obtain free periods.
paul@1028 130
paul@1028 131
    free = invert_freebusy(busy)
paul@1028 132
    permitted_values = handler.get_permitted_values()
paul@1028 133
    periods = []
paul@1028 134
paul@1028 135
    # Do not attempt to redefine rule-based periods.
paul@1028 136
paul@1028 137
    last = None
paul@1028 138
paul@1028 139
    for period in handler.get_periods(handler.obj, explicit_only=True):
paul@1028 140
        duration = period.get_duration()
paul@1028 141
paul@1028 142
        # Try and schedule periods normally since some of them may be
paul@1028 143
        # compatible with the schedule.
paul@1028 144
paul@1028 145
        if permitted_values:
paul@1028 146
            period = period.get_corrected(permitted_values)
paul@1028 147
paul@1028 148
        scheduled = handler.can_schedule(freebusy, [period])
paul@1028 149
paul@1028 150
        if scheduled == "ACCEPTED":
paul@1028 151
            periods.append(period)
paul@1028 152
            last = period.get_end()
paul@1028 153
            continue
paul@1028 154
paul@1028 155
        # Get free periods from the time of each period.
paul@1028 156
paul@1028 157
        for found in periods_from(free, period):
paul@1028 158
paul@1028 159
            # Skip any periods before the last period.
paul@1028 160
paul@1028 161
            if last:
paul@1028 162
                if last > found.get_end():
paul@1028 163
                    continue
paul@1028 164
paul@1028 165
                # Adjust the start of the free period to exclude the last period.
paul@1028 166
paul@1028 167
                found = found.make_corrected(max(found.get_start(), last), found.get_end())
paul@1028 168
paul@1028 169
            # Only test free periods long enough to hold the requested period.
paul@1028 170
paul@1028 171
            if found.get_duration() >= duration:
paul@1028 172
paul@1028 173
                # Obtain a possible period, starting at the found point and
paul@1028 174
                # with the requested duration. Then, correct the period if
paul@1028 175
                # necessary.
paul@1028 176
paul@1028 177
                start = to_timezone(found.get_start(), period.get_tzid())
paul@1028 178
                possible = period.make_corrected(start, start + period.get_duration())
paul@1028 179
                if permitted_values:
paul@1028 180
                    possible = possible.get_corrected(permitted_values)
paul@1028 181
paul@1028 182
                # Only if the possible period is still within the free period
paul@1028 183
                # can it be used.
paul@1028 184
paul@1028 185
                if possible.within(found):
paul@1028 186
                    periods.append(possible)
paul@1028 187
                    break
paul@1028 188
paul@1028 189
        # Where no period can be found, decline the invitation.
paul@1028 190
paul@1028 191
        else:
paul@1028 192
            return "DECLINED"
paul@1028 193
paul@1028 194
        # Use the found period to set the start of the next window to search.
paul@1028 195
paul@1028 196
        last = periods[-1].get_end()
paul@1028 197
paul@1028 198
    # Replace the periods in the object.
paul@1028 199
paul@1028 200
    obj = handler.obj.copy()
paul@1028 201
    changed = handler.obj.set_periods(periods)
paul@1028 202
paul@1028 203
    # Check one last time, reverting the change if not scheduled.
paul@1028 204
paul@1031 205
    scheduled = schedule_in_freebusy(handler, args, busy)
paul@1028 206
paul@1028 207
    if scheduled == "DECLINED":
paul@1028 208
        handler.set_object(obj)
paul@1028 209
paul@1028 210
    return scheduled == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED"
paul@1028 211
paul@1028 212
# Registry of scheduling functions.
paul@1028 213
paul@1028 214
scheduling_functions = {
paul@1028 215
    "schedule_in_freebusy" : schedule_in_freebusy,
paul@1028 216
    "schedule_corrected_in_freebusy" : schedule_corrected_in_freebusy,
paul@1028 217
    "schedule_next_available_in_freebusy" : schedule_next_available_in_freebusy,
paul@1028 218
    }
paul@1028 219
paul@1039 220
# Registries of listener functions.
paul@1039 221
paul@1039 222
confirmation_functions = {}
paul@1039 223
retraction_functions = {}
paul@1039 224
paul@1028 225
# vim: tabstop=4 expandtab shiftwidth=4