imip-agent

Annotated imiptools/handlers/scheduling/freebusy.py

1085:ab557e83c712
2016-03-09 Paul Boddie Support question mark placeholders in column details. 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@1071 106
paul@1071 107
    # Maintain a separate copy of the data.
paul@1071 108
paul@1071 109
    busy = user_freebusy.copy()
paul@1028 110
paul@1028 111
    # Subtract any periods from this event from the free/busy collections.
paul@1028 112
paul@1071 113
    event_periods = handler.remove_from_freebusy(busy)
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@1071 119
paul@1071 120
            # Get a copy of the attendee's free/busy data.
paul@1071 121
paul@1071 122
            freebusy = handler.get_store().get_freebusy_for_other(handler.user, attendee).copy()
paul@1028 123
            if freebusy:
paul@1062 124
                freebusy.remove_periods(event_periods)
paul@1028 125
                busy += freebusy
paul@1028 126
paul@1028 127
    # Obtain the combined busy periods.
paul@1028 128
paul@1062 129
    busy = busy.coalesce_freebusy()
paul@1028 130
paul@1028 131
    # Obtain free periods.
paul@1028 132
paul@1062 133
    free = busy.invert_freebusy()
paul@1028 134
    permitted_values = handler.get_permitted_values()
paul@1028 135
    periods = []
paul@1028 136
paul@1028 137
    # Do not attempt to redefine rule-based periods.
paul@1028 138
paul@1028 139
    last = None
paul@1028 140
paul@1028 141
    for period in handler.get_periods(handler.obj, explicit_only=True):
paul@1028 142
        duration = period.get_duration()
paul@1028 143
paul@1028 144
        # Try and schedule periods normally since some of them may be
paul@1028 145
        # compatible with the schedule.
paul@1028 146
paul@1028 147
        if permitted_values:
paul@1028 148
            period = period.get_corrected(permitted_values)
paul@1028 149
paul@1028 150
        scheduled = handler.can_schedule(freebusy, [period])
paul@1028 151
paul@1028 152
        if scheduled == "ACCEPTED":
paul@1028 153
            periods.append(period)
paul@1028 154
            last = period.get_end()
paul@1028 155
            continue
paul@1028 156
paul@1028 157
        # Get free periods from the time of each period.
paul@1028 158
paul@1062 159
        for found in free.periods_from(period):
paul@1028 160
paul@1028 161
            # Skip any periods before the last period.
paul@1028 162
paul@1028 163
            if last:
paul@1028 164
                if last > found.get_end():
paul@1028 165
                    continue
paul@1028 166
paul@1028 167
                # Adjust the start of the free period to exclude the last period.
paul@1028 168
paul@1028 169
                found = found.make_corrected(max(found.get_start(), last), found.get_end())
paul@1028 170
paul@1028 171
            # Only test free periods long enough to hold the requested period.
paul@1028 172
paul@1028 173
            if found.get_duration() >= duration:
paul@1028 174
paul@1028 175
                # Obtain a possible period, starting at the found point and
paul@1028 176
                # with the requested duration. Then, correct the period if
paul@1028 177
                # necessary.
paul@1028 178
paul@1028 179
                start = to_timezone(found.get_start(), period.get_tzid())
paul@1028 180
                possible = period.make_corrected(start, start + period.get_duration())
paul@1028 181
                if permitted_values:
paul@1028 182
                    possible = possible.get_corrected(permitted_values)
paul@1028 183
paul@1028 184
                # Only if the possible period is still within the free period
paul@1028 185
                # can it be used.
paul@1028 186
paul@1028 187
                if possible.within(found):
paul@1028 188
                    periods.append(possible)
paul@1028 189
                    break
paul@1028 190
paul@1028 191
        # Where no period can be found, decline the invitation.
paul@1028 192
paul@1028 193
        else:
paul@1028 194
            return "DECLINED"
paul@1028 195
paul@1028 196
        # Use the found period to set the start of the next window to search.
paul@1028 197
paul@1028 198
        last = periods[-1].get_end()
paul@1028 199
paul@1028 200
    # Replace the periods in the object.
paul@1028 201
paul@1028 202
    obj = handler.obj.copy()
paul@1028 203
    changed = handler.obj.set_periods(periods)
paul@1028 204
paul@1028 205
    # Check one last time, reverting the change if not scheduled.
paul@1028 206
paul@1031 207
    scheduled = schedule_in_freebusy(handler, args, busy)
paul@1028 208
paul@1028 209
    if scheduled == "DECLINED":
paul@1028 210
        handler.set_object(obj)
paul@1028 211
paul@1028 212
    return scheduled == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED"
paul@1028 213
paul@1028 214
# Registry of scheduling functions.
paul@1028 215
paul@1028 216
scheduling_functions = {
paul@1028 217
    "schedule_in_freebusy" : schedule_in_freebusy,
paul@1028 218
    "schedule_corrected_in_freebusy" : schedule_corrected_in_freebusy,
paul@1028 219
    "schedule_next_available_in_freebusy" : schedule_next_available_in_freebusy,
paul@1028 220
    }
paul@1028 221
paul@1040 222
# Registries of locking and unlocking functions.
paul@1040 223
paul@1040 224
locking_functions = {}
paul@1040 225
unlocking_functions = {}
paul@1040 226
paul@1039 227
# Registries of listener functions.
paul@1039 228
paul@1039 229
confirmation_functions = {}
paul@1039 230
retraction_functions = {}
paul@1039 231
paul@1028 232
# vim: tabstop=4 expandtab shiftwidth=4