imip-agent

Annotated tools/make_freebusy.py

886:8a3994e54ea4
2015-10-20 Paul Boddie Permit the selection of a same-day ending while still allowing time adjustments.
paul@120 1
#!/usr/bin/env python
paul@120 2
paul@599 3
"""
paul@599 4
Construct free/busy records for a user, either recording that user's own
paul@599 5
availability schedule or the schedule of another user (using details provided
paul@599 6
when scheduling events with that user).
paul@600 7
paul@600 8
Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
paul@600 9
paul@600 10
This program is free software; you can redistribute it and/or modify it under
paul@600 11
the terms of the GNU General Public License as published by the Free Software
paul@600 12
Foundation; either version 3 of the License, or (at your option) any later
paul@600 13
version.
paul@600 14
paul@600 15
This program is distributed in the hope that it will be useful, but WITHOUT
paul@600 16
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@600 17
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@600 18
details.
paul@600 19
paul@600 20
You should have received a copy of the GNU General Public License along with
paul@600 21
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@599 22
"""
paul@599 23
paul@815 24
from os.path import split
paul@815 25
import sys
paul@815 26
paul@815 27
# Find the modules.
paul@815 28
paul@815 29
try:
paul@815 30
    import imiptools
paul@815 31
except ImportError:
paul@815 32
    parent = split(split(__file__)[0])[0]
paul@815 33
    if split(parent)[1] == "imip-agent":
paul@815 34
        sys.path.append(parent)
paul@815 35
paul@649 36
from codecs import getwriter
paul@748 37
from imiptools.client import Client
paul@367 38
from imiptools.data import get_window_end, Object
paul@654 39
from imiptools.dates import get_default_timezone, to_utc_datetime
paul@654 40
from imiptools.period import insert_period
paul@291 41
from imiptools.profile import Preferences
paul@120 42
from imip_store import FileStore, FilePublisher
paul@120 43
paul@672 44
def make_freebusy(store, publisher, preferences, user, participant,
paul@672 45
    store_and_publish, include_needs_action, reset_updated_list, verbose):
paul@377 46
paul@670 47
    """
paul@672 48
    Using the given 'store', 'publisher' and 'preferences', make free/busy
paul@672 49
    details for the records of the given 'user', generating details for
paul@672 50
    'participant' if not indicated as None; otherwise, generating free/busy
paul@672 51
    details concerning the given user.
paul@599 52
paul@670 53
    If 'store_and_publish' is set, the stored details will be updated;
paul@670 54
    otherwise, the details will be written to standard output.
paul@120 55
paul@670 56
    If 'include_needs_action' is set, details of objects whose participation
paul@670 57
    status is set to "NEEDS-ACTION" for the participant will be included in the
paul@670 58
    details.
paul@670 59
paul@670 60
    If 'reset_updated_list' is set, all objects will be inspected for periods;
paul@670 61
    otherwise, only those in the stored free/busy providers file will be
paul@670 62
    inspected.
paul@670 63
paul@670 64
    If 'verbose' is set, messages will be written to standard error.
paul@599 65
    """
paul@670 66
paul@670 67
    participant = participant or user
paul@599 68
    tzid = preferences.get("TZID") or get_default_timezone()
paul@599 69
paul@599 70
    # Get the size of the free/busy window.
paul@120 71
paul@599 72
    try:
paul@599 73
        window_size = int(preferences.get("window_size"))
paul@599 74
    except (TypeError, ValueError):
paul@599 75
        window_size = 100
paul@599 76
    window_end = get_window_end(tzid, window_size)
paul@367 77
paul@652 78
    # Get identifiers for uncancelled events either from a list of events
paul@652 79
    # providing free/busy periods at the end of the given time window, or from
paul@652 80
    # a list of all events.
paul@652 81
paul@654 82
    all_events = not reset_updated_list and store.get_freebusy_providers(user, window_end)
paul@349 83
paul@652 84
    if not all_events:
paul@694 85
        all_events = store.get_all_events(user)
paul@652 86
        fb = []
paul@652 87
paul@652 88
    # With providers of additional periods, append to the existing collection.
paul@652 89
paul@652 90
    else:
paul@652 91
        if user == participant:
paul@652 92
            fb = store.get_freebusy(user)
paul@652 93
        else:
paul@652 94
            fb = store.get_freebusy_for_other(user, participant)
paul@120 95
paul@599 96
    # Obtain event objects.
paul@367 97
paul@599 98
    objs = []
paul@599 99
    for uid, recurrenceid in all_events:
paul@599 100
        if verbose:
paul@599 101
            print >>sys.stderr, uid, recurrenceid
paul@599 102
        event = store.get_event(user, uid, recurrenceid)
paul@599 103
        if event:
paul@599 104
            objs.append(Object(event))
paul@367 105
paul@599 106
    # Build a free/busy collection for the given user.
paul@120 107
paul@599 108
    for obj in objs:
paul@648 109
        partstat = obj.get_participation_status(participant)
paul@648 110
        recurrenceids = not obj.get_recurrenceid() and store.get_recurrences(user, obj.get_uid())
paul@367 111
paul@648 112
        if obj.get_participation(partstat, include_needs_action):
paul@648 113
            for p in obj.get_active_periods(recurrenceids, tzid, window_end):
paul@652 114
                fbp = obj.get_freebusy_period(p, partstat == "ORG")
paul@654 115
                insert_period(fb, fbp)
paul@120 116
paul@599 117
    # Store and publish the free/busy collection.
paul@367 118
paul@599 119
    if store_and_publish:
paul@599 120
        if user == participant:
paul@599 121
            store.set_freebusy(user, fb)
paul@748 122
paul@748 123
            if Client(user).is_sharing() and Client(user).is_publishing():
paul@748 124
                publisher.set_freebusy(user, fb)
paul@670 125
paul@670 126
            # Update the list of objects providing periods on future occasions.
paul@670 127
paul@670 128
            store.set_freebusy_providers(user, to_utc_datetime(window_end, tzid),
paul@670 129
                [obj for obj in objs if obj.possibly_active_from(window_end, tzid)])
paul@599 130
        else:
paul@599 131
            store.set_freebusy_for_other(user, fb, participant)
paul@670 132
paul@670 133
    # Alternatively, just write the collection to standard output.
paul@670 134
paul@395 135
    else:
paul@649 136
        f = getwriter("utf-8")(sys.stdout)
paul@599 137
        for item in fb:
paul@649 138
            print >>f, "\t".join(item.as_tuple(strings_only=True))
paul@120 139
paul@670 140
# Main program.
paul@670 141
paul@670 142
if __name__ == "__main__":
paul@670 143
paul@670 144
    # Interpret the command line arguments.
paul@670 145
paul@672 146
    participants = []
paul@672 147
    args = []
paul@672 148
    store_dir = []
paul@672 149
    publishing_dir = []
paul@672 150
    preferences_dir = []
paul@672 151
    ignored = []
paul@672 152
paul@672 153
    # Collect user details first, switching to other arguments when encountering
paul@672 154
    # switches.
paul@672 155
paul@672 156
    l = participants
paul@672 157
paul@672 158
    for arg in sys.argv[1:]:
paul@672 159
        if arg in ("-n", "-s", "-v", "-r"):
paul@672 160
            args.append(arg)
paul@672 161
            l = ignored
paul@672 162
        elif arg == "-S":
paul@672 163
            l = store_dir
paul@672 164
        elif arg == "-P":
paul@672 165
            l = publishing_dir
paul@672 166
        elif arg == "-p":
paul@672 167
            l = preferences_dir
paul@672 168
        else:
paul@672 169
            l.append(arg)
paul@672 170
paul@670 171
    try:
paul@672 172
        user = participants[0]
paul@670 173
    except IndexError:
paul@670 174
        print >>sys.stderr, """\
paul@815 175
Usage: %s <user> [ <other user> ] <options>
paul@815 176
paul@670 177
Need a user and an optional participant (if different from the user),
paul@670 178
along with the -s option if updating the store and the published details.
paul@670 179
Specify -n to include objects with PARTSTAT of NEEDS-ACTION.
paul@670 180
Specify -r to inspect all objects, not just those expected to provide details.
paul@670 181
Specify -v for additional messages on standard error.
paul@815 182
""" % split(sys.argv[0])[1]
paul@670 183
        sys.exit(1)
paul@670 184
paul@672 185
    # Define any other participant of interest plus options.
paul@672 186
paul@672 187
    participant = participants[1:] and participants[1] or None
paul@672 188
    store_and_publish = "-s" in args
paul@672 189
    include_needs_action = "-n" in args
paul@672 190
    reset_updated_list = "-r" in args
paul@672 191
    verbose = "-v" in args
paul@672 192
paul@672 193
    # Override defaults if indicated.
paul@672 194
paul@672 195
    store_dir = store_dir and store_dir[0] or None
paul@672 196
    publishing_dir = publishing_dir and publishing_dir[0] or None
paul@672 197
    preferences_dir = preferences_dir and preferences_dir[0] or None
paul@672 198
paul@672 199
    # Obtain store-related objects.
paul@672 200
paul@672 201
    store = FileStore(store_dir)
paul@672 202
    publisher = FilePublisher(publishing_dir)
paul@672 203
    preferences = Preferences(user, preferences_dir)
paul@672 204
paul@672 205
    # Obtain a list of users for processing.
paul@672 206
paul@670 207
    if user in ("*", "all"):
paul@672 208
        users = store.get_users()
paul@670 209
    else:
paul@670 210
        users = [user]
paul@670 211
paul@672 212
    # Process the given users.
paul@672 213
paul@670 214
    for user in users:
paul@670 215
        if verbose:
paul@670 216
            print >>sys.stderr, user
paul@672 217
        make_freebusy(store, publisher, preferences, user, participant,
paul@672 218
            store_and_publish, include_needs_action, reset_updated_list, verbose)
paul@670 219
paul@120 220
# vim: tabstop=4 expandtab shiftwidth=4