imip-agent

tools/make_freebusy.py

1253:333740ca50b6
2017-09-12 Paul Boddie Consider period replacement status when comparing form periods.
     1 #!/usr/bin/env python     2      3 """     4 Construct free/busy records for a user, either recording that user's own     5 availability schedule or the schedule of another user (using details provided     6 when scheduling events with that user).     7      8 Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk>     9     10 This program is free software; you can redistribute it and/or modify it under    11 the terms of the GNU General Public License as published by the Free Software    12 Foundation; either version 3 of the License, or (at your option) any later    13 version.    14     15 This program is distributed in the hope that it will be useful, but WITHOUT    16 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    17 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    18 details.    19     20 You should have received a copy of the GNU General Public License along with    21 this program.  If not, see <http://www.gnu.org/licenses/>.    22 """    23     24 from os.path import abspath, split    25 import sys    26     27 # Find the modules.    28     29 try:    30     import imiptools    31 except ImportError:    32     parent = abspath(split(split(__file__)[0])[0])    33     if split(parent)[1] == "imip-agent":    34         sys.path.append(parent)    35     36 from codecs import getwriter    37 from imiptools.config import settings    38 from imiptools.client import Client    39 from imiptools.data import get_window_end    40 from imiptools.dates import get_default_timezone, to_utc_datetime    41 from imiptools.freebusy import FreeBusyCollection, FreeBusyGroupCollection, \    42                                FreeBusyGroupPeriod    43 from imiptools.period import Period    44 from imiptools.stores import get_store, get_publisher, get_journal    45     46 def make_freebusy(client, participants, storage, store_and_publish,    47     include_needs_action, reset_updated_list, verbose):    48     49     """    50     Using the given 'client' representing a user, make free/busy details for the    51     records of the user, generating details for 'participants' if not indicated    52     as None; otherwise, generating free/busy details concerning the given user.    53     54     The 'storage' is the specific store or journal object used to access data.    55     56     If 'store_and_publish' is set, the stored details will be updated;    57     otherwise, the details will be written to standard output.    58     59     If 'include_needs_action' is set, details of objects whose participation    60     status is set to "NEEDS-ACTION" for the participant will be included in the    61     details.    62     63     If 'reset_updated_list' is set, all objects will be inspected for periods;    64     otherwise, only those in the stored free/busy providers file will be    65     inspected.    66     67     If 'verbose' is set, messages will be written to standard error.    68     """    69     70     user = client.user    71     journal = client.get_journal()    72     publisher = client.get_publisher()    73     preferences = client.get_preferences()    74     tzid = client.get_tzid()    75     76     # Get the start and end of the window. Note that the start is normally the    77     # current moment in time, but for testing we may choose a specific point in    78     # time instead.    79     80     window_start = client.get_window_start()    81     window_end = client.get_window_end()    82     83     providers = []    84     85     # Iterate over participants, with None being a special null participant    86     # value.    87     88     for participant in participants or [None]:    89     90         # Get identifiers for uncancelled events either from a list of events    91         # providing free/busy periods at the end of the given time window, or from    92         # a list of all events.    93     94         all_events = not reset_updated_list and storage.get_freebusy_providers(user, window_end)    95     96         if not all_events:    97             all_events = storage.get_all_events(user)    98             if storage is journal:    99                 fb = FreeBusyGroupCollection()   100             else:   101                 fb = FreeBusyCollection()   102    103         # With providers of additional periods, append to the existing collection.   104    105         else:   106             if participants is None:   107                 fb = storage.get_freebusy_for_update(user)   108             else:   109                 fb = storage.get_freebusy_for_other_for_update(user, participant)   110    111         # Remove periods before the window start.   112    113         fb.remove_periods_before(Period(window_start, None))   114    115         # Obtain event objects.   116    117         objs = []   118         for uid, recurrenceid in all_events:   119             if verbose:   120                 print >>sys.stderr, uid, recurrenceid   121             event = storage.get_event(user, uid, recurrenceid)   122             if event:   123                 objs.append(event)   124    125         # Build a free/busy collection for the given user.   126    127         for obj in objs:   128             recurrenceids = not obj.get_recurrenceid() and storage.get_recurrences(user, obj.get_uid())   129    130             # Obtain genuine attendees.   131    132             if storage is journal:   133                 attendees = storage.get_delegates(user)   134             else:   135                 attendees = [participant]   136    137             # Generate records for each attendee (applicable to consolidated   138             # journal data).   139    140             for attendee in attendees:   141                 partstat = obj.get_participation_status(attendee)   142    143                 # Only include objects where the attendee actually participates.   144    145                 if obj.get_participation(partstat, include_needs_action):   146    147                     # Add each active period to the collection.   148    149                     for p in obj.get_active_periods(recurrenceids, tzid,   150                         start=window_start, end=window_end):   151    152                         # Obtain a suitable period object.   153    154                         fbp = obj.get_freebusy_period(p, partstat == "ORG")   155    156                         if storage is journal:   157                             fbp = FreeBusyGroupPeriod(*fbp.as_tuple(), attendee=attendee)   158    159                         fb.insert_period(fbp)   160    161         # Store and publish the free/busy collection.   162    163         if store_and_publish:   164    165             # Set the user's own free/busy information.   166    167             if participant is None:   168                 storage.set_freebusy(user, fb)   169    170                 if client.is_sharing() and client.is_publishing():   171                     publisher.set_freebusy(user, fb)   172    173             # Set free/busy information concerning another user.   174    175             else:   176                 storage.set_freebusy_for_other(user, fb, participant)   177    178             # Update the list of objects providing periods on future occasions.   179    180             if participant is None or storage is journal:   181                 providers += [obj for obj in objs if obj.possibly_active_from(window_end, tzid)]   182    183         # Alternatively, just write the collection to standard output.   184    185         else:   186             f = getwriter("utf-8")(sys.stdout)   187             for item in fb:   188                 print >>f, "\t".join(item.as_tuple(strings_only=True))   189    190     # Update free/busy providers if storing.   191    192     if store_and_publish:   193         storage.set_freebusy_providers(user, to_utc_datetime(window_end, tzid), providers)   194    195 # Main program.   196    197 if __name__ == "__main__":   198    199     # Interpret the command line arguments.   200    201     participants = []   202     args = []   203     store_type = []   204     store_dir = []   205     publishing_dir = []   206     journal_dir = []   207     preferences_dir = []   208     ignored = []   209    210     # Collect user details first, switching to other arguments when encountering   211     # switches.   212    213     l = participants   214    215     for arg in sys.argv[1:]:   216         if arg in ("-n", "-s", "-v", "-r", "-q"):   217             args.append(arg)   218             l = ignored   219         elif arg == "-T":   220             l = store_type   221         elif arg == "-S":   222             l = store_dir   223         elif arg == "-P":   224             l = publishing_dir   225         elif arg == "-j":   226             l = journal_dir   227         elif arg == "-p":   228             l = preferences_dir   229         else:   230             l.append(arg)   231    232     try:   233         user = participants[0]   234     except IndexError:   235         print >>sys.stderr, """\   236 Usage: %s <user> [ <other user> ... ] [ <options> ]   237    238 Need a user and optional participants (if different from the user),   239 along with the -s option if updating the store and the published details.   240    241 Specific options:   242    243 -q  Access quotas in the journal instead of users in the store   244 -s  Update the store and published details (write details to standard output   245     otherwise)   246 -n  Include objects with PARTSTAT of NEEDS-ACTION   247 -r  Inspect all objects, not just those expected to provide details   248 -v  Show additional messages on standard error   249    250 General options:   251    252 -j  Indicates the journal directory location   253 -p  Indicates the preferences directory location   254 -P  Indicates the publishing directory location   255 -S  Indicates the store directory location   256 -T  Indicates the store type (the configured value if omitted)   257 """ % split(sys.argv[0])[1]   258         sys.exit(1)   259    260     # Define any other participant of interest plus options.   261    262     participants = participants[1:]   263     using_journal = "-q" in args   264     store_and_publish = "-s" in args   265     include_needs_action = "-n" in args   266     reset_updated_list = "-r" in args   267     verbose = "-v" in args   268    269     # Override defaults if indicated.   270    271     getvalue = lambda value, default=None: value and value[0] or default   272    273     store_type = getvalue(store_type, settings["STORE_TYPE"])   274     store_dir = getvalue(store_dir)   275     publishing_dir = getvalue(publishing_dir)   276     journal_dir = getvalue(journal_dir)   277     preferences_dir = getvalue(preferences_dir)   278    279     # Obtain store-related objects or delegate this to the Client initialiser.   280    281     store = get_store(store_type, store_dir)   282     publisher = get_publisher(publishing_dir)   283     journal = get_journal(store_type, journal_dir)   284    285     # Determine which kind of object will be accessed.   286    287     if using_journal:   288         storage = journal   289     else:   290         storage = store   291    292     # Obtain a list of users for processing.   293    294     if user in ("*", "all"):   295         users = storage.get_users()   296     else:   297         users = [user]   298    299     # Obtain a list of participants for processing.   300    301     if participants and participants[0] in ("*", "all"):   302         participants = storage.get_freebusy_others(user)   303    304     # Provide a participants list to iterate over even if no specific   305     # participant is involved. This updates a user's own records, but only for   306     # the general data store.   307    308     elif not participants:   309         if not using_journal:   310             participants = None   311         else:   312             print >>sys.stderr, "Participants must be indicated when updating quota records."   313             sys.exit(1)   314    315     # Process the given users.   316    317     for user in users:   318         if verbose:   319             print >>sys.stderr, user   320         make_freebusy(   321             Client(user, None, store, publisher, journal, preferences_dir),   322             participants, storage, store_and_publish, include_needs_action,   323             reset_updated_list, verbose)   324    325 # vim: tabstop=4 expandtab shiftwidth=4