imip-agent

Annotated imiptools/handlers/scheduling/freebusy.py

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