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