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, Object 40 from imiptools.dates import get_default_timezone, to_utc_datetime 41 from imiptools.period import FreeBusyCollection, FreeBusyGroupCollection, \ 42 FreeBusyGroupPeriod 43 from imiptools.stores import get_store, get_publisher, get_journal 44 45 def make_freebusy(client, participants, storage, store_and_publish, 46 include_needs_action, reset_updated_list, verbose): 47 48 """ 49 Using the given 'client' representing a user, make free/busy details for the 50 records of the user, generating details for 'participants' if not indicated 51 as None; otherwise, generating free/busy details concerning the given user. 52 53 The 'storage' is the specific store or journal object used to access data. 54 55 If 'store_and_publish' is set, the stored details will be updated; 56 otherwise, the details will be written to standard output. 57 58 If 'include_needs_action' is set, details of objects whose participation 59 status is set to "NEEDS-ACTION" for the participant will be included in the 60 details. 61 62 If 'reset_updated_list' is set, all objects will be inspected for periods; 63 otherwise, only those in the stored free/busy providers file will be 64 inspected. 65 66 If 'verbose' is set, messages will be written to standard error. 67 """ 68 69 user = client.user 70 journal = client.get_journal() 71 publisher = client.get_publisher() 72 preferences = client.get_preferences() 73 74 tzid = preferences.get("TZID") or get_default_timezone() 75 76 # Get the size of the free/busy window. 77 78 try: 79 window_size = int(preferences.get("window_size")) 80 except (TypeError, ValueError): 81 window_size = 100 82 window_end = get_window_end(tzid, window_size) 83 84 providers = [] 85 86 # Iterate over participants, with None being a special null participant 87 # value. 88 89 for participant in participants or [None]: 90 91 # Get identifiers for uncancelled events either from a list of events 92 # providing free/busy periods at the end of the given time window, or from 93 # a list of all events. 94 95 all_events = not reset_updated_list and storage.get_freebusy_providers(user, window_end) 96 97 if not all_events: 98 all_events = storage.get_all_events(user) 99 if storage is journal: 100 fb = FreeBusyGroupCollection() 101 else: 102 fb = FreeBusyCollection() 103 104 # With providers of additional periods, append to the existing collection. 105 106 else: 107 if participants is None: 108 fb = storage.get_freebusy_for_update(user) 109 else: 110 fb = storage.get_freebusy_for_other_for_update(user, participant) 111 112 # Obtain event objects. 113 114 objs = [] 115 for uid, recurrenceid in all_events: 116 if verbose: 117 print >>sys.stderr, uid, recurrenceid 118 event = storage.get_event(user, uid, recurrenceid) 119 if event: 120 objs.append(Object(event)) 121 122 # Build a free/busy collection for the given user. 123 124 for obj in objs: 125 recurrenceids = not obj.get_recurrenceid() and storage.get_recurrences(user, obj.get_uid()) 126 127 # Obtain genuine attendees. 128 129 if storage is journal: 130 attendees = storage.get_delegates(user) 131 else: 132 attendees = [participant] 133 134 # Generate records for each attendee (applicable to consolidated 135 # journal data). 136 137 for attendee in attendees: 138 partstat = obj.get_participation_status(attendee) 139 140 # Only include objects where the attendee actually participates. 141 142 if obj.get_participation(partstat, include_needs_action): 143 144 # Add each active period to the collection. 145 146 for p in obj.get_active_periods(recurrenceids, tzid, window_end): 147 148 # Obtain a suitable period object. 149 150 fbp = obj.get_freebusy_period(p, partstat == "ORG") 151 152 if storage is journal: 153 fbp = FreeBusyGroupPeriod(*fbp.as_tuple(), attendee=attendee) 154 155 fb.insert_period(fbp) 156 157 # Store and publish the free/busy collection. 158 159 if store_and_publish: 160 161 # Set the user's own free/busy information. 162 163 if participant is None: 164 storage.set_freebusy(user, fb) 165 166 if client.is_sharing() and client.is_publishing(): 167 publisher.set_freebusy(user, fb) 168 169 # Set free/busy information concerning another user. 170 171 else: 172 storage.set_freebusy_for_other(user, fb, participant) 173 174 # Update the list of objects providing periods on future occasions. 175 176 if participant is None or storage is journal: 177 providers += [obj for obj in objs if obj.possibly_active_from(window_end, tzid)] 178 179 # Alternatively, just write the collection to standard output. 180 181 else: 182 f = getwriter("utf-8")(sys.stdout) 183 for item in fb: 184 print >>f, "\t".join(item.as_tuple(strings_only=True)) 185 186 # Update free/busy providers if storing. 187 188 if store_and_publish: 189 storage.set_freebusy_providers(user, to_utc_datetime(window_end, tzid), providers) 190 191 # Main program. 192 193 if __name__ == "__main__": 194 195 # Interpret the command line arguments. 196 197 participants = [] 198 args = [] 199 store_type = [] 200 store_dir = [] 201 publishing_dir = [] 202 journal_dir = [] 203 preferences_dir = [] 204 ignored = [] 205 206 # Collect user details first, switching to other arguments when encountering 207 # switches. 208 209 l = participants 210 211 for arg in sys.argv[1:]: 212 if arg in ("-n", "-s", "-v", "-r", "-q"): 213 args.append(arg) 214 l = ignored 215 elif arg == "-T": 216 l = store_type 217 elif arg == "-S": 218 l = store_dir 219 elif arg == "-P": 220 l = publishing_dir 221 elif arg == "-j": 222 l = journal_dir 223 elif arg == "-p": 224 l = preferences_dir 225 else: 226 l.append(arg) 227 228 try: 229 user = participants[0] 230 except IndexError: 231 print >>sys.stderr, """\ 232 Usage: %s <user> [ <other user> ... ] [ <options> ] 233 234 Need a user and optional participants (if different from the user), 235 along with the -s option if updating the store and the published details. 236 237 Specific options: 238 239 -q Access quotas in the journal instead of users in the store 240 -s Update the store and published details (write details to standard output 241 otherwise) 242 -n Include objects with PARTSTAT of NEEDS-ACTION 243 -r Inspect all objects, not just those expected to provide details 244 -v Show additional messages on standard error 245 246 General options: 247 248 -j Indicates the journal directory location 249 -p Indicates the preferences directory location 250 -P Indicates the publishing directory location 251 -S Indicates the store directory location 252 -T Indicates the store type (the configured value if omitted) 253 """ % split(sys.argv[0])[1] 254 sys.exit(1) 255 256 # Define any other participant of interest plus options. 257 258 participants = participants[1:] 259 using_journal = "-q" in args 260 store_and_publish = "-s" in args 261 include_needs_action = "-n" in args 262 reset_updated_list = "-r" in args 263 verbose = "-v" in args 264 265 # Override defaults if indicated. 266 267 getvalue = lambda value, default=None: value and value[0] or default 268 269 store_type = getvalue(store_type, settings["STORE_TYPE"]) 270 store_dir = getvalue(store_dir) 271 publishing_dir = getvalue(publishing_dir) 272 journal_dir = getvalue(journal_dir) 273 preferences_dir = getvalue(preferences_dir) 274 275 # Obtain store-related objects or delegate this to the Client initialiser. 276 277 store = get_store(store_type, store_dir) 278 publisher = get_publisher(publishing_dir) 279 journal = get_journal(store_type, journal_dir) 280 281 # Determine which kind of object will be accessed. 282 283 if using_journal: 284 storage = journal 285 else: 286 storage = store 287 288 # Obtain a list of users for processing. 289 290 if user in ("*", "all"): 291 users = storage.get_users() 292 else: 293 users = [user] 294 295 # Obtain a list of participants for processing. 296 297 if participants and participants[0] in ("*", "all"): 298 participants = storage.get_freebusy_others(user) 299 300 # Provide a participants list to iterate over even if no specific 301 # participant is involved. This updates a user's own records, but only for 302 # the general data store. 303 304 elif not participants: 305 if not using_journal: 306 participants = None 307 else: 308 print >>sys.stderr, "Participants must be indicated when updating quota records." 309 sys.exit(1) 310 311 # Process the given users. 312 313 for user in users: 314 if verbose: 315 print >>sys.stderr, user 316 make_freebusy( 317 Client(user, None, store, publisher, journal, preferences_dir), 318 participants, storage, store_and_publish, include_needs_action, 319 reset_updated_list, verbose) 320 321 # vim: tabstop=4 expandtab shiftwidth=4