imip-agent

Annotated imiptools/handlers/scheduling/freebusy.py

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