1 #!/usr/bin/env python 2 3 """ 4 Quota-related scheduling functionality. 5 6 Copyright (C) 2016, 2017 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from imiptools.dates import get_duration, to_utc_datetime 23 from imiptools.handlers.scheduling.common import get_scheduling_conflicts, \ 24 standard_responses 25 from imiptools.period import Endless 26 from datetime import timedelta 27 28 # Quota maintenance. 29 30 def check_quota(handler, args): 31 32 """ 33 Check the current object of the given 'handler' against the applicable 34 quota. 35 """ 36 37 _ = handler.get_translator() 38 39 # Obtain the journal entries and check the balance. 40 41 journal = handler.get_journal() 42 if not journal: 43 return "DECLINED", _("You have no quota allocation for the recipient.") 44 45 quota, group = _get_quota_and_group(handler, args) 46 47 entries = journal.get_entries(quota, group) 48 limits = journal.get_limits(quota) 49 50 # Obtain a limit for the group or any general limit. 51 # Decline invitations if no limit has been set. 52 53 limit = limits.get(group) or limits.get("*") 54 if not limit: 55 return "DECLINED", _("You have no quota allocation for the recipient.") 56 57 # Where the quota is unlimited, accept the invitation. 58 59 if limit == "*": 60 return "ACCEPTED", _("The recipient has scheduled the requested period.") 61 62 # Decline endless events for limited quotas. 63 64 total = _get_duration(handler) 65 66 if total == Endless(): 67 return "DECLINED", _("The event period exceeds your quota allocation for the recipient.") 68 69 # Decline events whose durations exceed the balance. 70 71 balance = get_duration(limit) - _get_usage(entries) 72 73 if total > balance: 74 return "DECLINED", _("The event period exceeds your quota allocation for the recipient.") 75 else: 76 return "ACCEPTED", _("The recipient has scheduled the requested period.") 77 78 def add_to_quota(handler, args): 79 80 """ 81 Record details of the current object of the given 'handler' in the 82 applicable quota. 83 """ 84 85 if not handler.get_journal(): 86 return 87 88 quota, group = _get_quota_and_group(handler, args) 89 _add_to_quota(handler, quota, group, handler.user, False) 90 91 def remove_from_quota(handler, args): 92 93 """ 94 Remove details of the current object of the given 'handler' from the 95 applicable quota. 96 """ 97 98 if not handler.get_journal(): 99 return 100 101 quota, group = _get_quota_and_group(handler, args) 102 _remove_from_quota(handler, quota, group, handler.user) 103 104 def update_event(handler, args): 105 106 "Update a stored version of the current object of the given 'handler'." 107 108 journal = handler.get_journal() 109 if not journal: 110 return 111 112 quota, group = _get_quota_and_group(handler, args) 113 114 # Where an existing version of the object exists, merge the recipient's 115 # attendance information. 116 117 obj = journal.get_event(quota, handler.uid, handler.recurrenceid) 118 if not obj: 119 obj = handler.obj 120 121 # Set attendance. 122 123 attendee_map = obj.get_uri_map("ATTENDEE") 124 attendee_map[handler.user]["PARTSTAT"] = "ACCEPTED" 125 obj["ATTENDEE"] = attendee_map.items() 126 127 # Record the object so that recurrences can be generated. 128 129 journal.set_event(quota, handler.uid, handler.recurrenceid, obj.to_node()) 130 131 def remove_event(handler, args): 132 133 "Remove a stored version of the current object of the given 'handler'." 134 135 journal = handler.get_journal() 136 if not journal: 137 return 138 139 quota, group = _get_quota_and_group(handler, args) 140 141 # Where an existing version of the object exists, remove the recipient's 142 # attendance information. 143 144 obj = journal.get_event(quota, handler.uid, handler.recurrenceid) 145 if not obj: 146 return 147 148 attendee_map = obj.get_uri_map("ATTENDEE") 149 delegates = journal.get_delegates(quota) 150 151 # Determine whether any of the delegates are still involved. 152 153 attendees = set(delegates).intersection(attendee_map.keys()) 154 if handler.user in attendees: 155 attendees.remove(handler.user) 156 157 # Remove event details where no delegates will be involved. 158 159 if not attendees: 160 journal.remove_event(quota, handler.uid, handler.recurrenceid) 161 return 162 163 del attendee_map[handler.user] 164 obj["ATTENDEE"] = attendee_map.items() 165 166 # Record the object so that recurrences can be generated. 167 168 journal.set_event(quota, handler.uid, handler.recurrenceid, obj.to_node()) 169 170 def _get_quota_and_group(handler, args): 171 172 """ 173 Combine information about the current object from the 'handler' with the 174 given 'args' to return a tuple containing the quota group and the user 175 identity or group involved. 176 """ 177 178 journal = handler.get_journal() 179 assert journal 180 181 quota = args and args[0] or handler.user 182 183 # Obtain the identity to whom the quota will apply. 184 185 organiser = handler.obj.get_uri("ORGANIZER") 186 187 # Obtain any user group to which the quota will apply instead. 188 189 groups = journal.get_groups(quota) 190 191 return quota, groups.get(organiser) or groups.get("*") or organiser 192 193 def _get_duration(handler): 194 195 "Return the duration of the current object provided by the 'handler'." 196 197 # Reject indefinitely recurring events. 198 199 if handler.obj.possibly_recurring_indefinitely(): 200 return Endless() 201 202 # Otherwise, return a sum of the period durations. 203 204 total = timedelta(0) 205 206 for period in handler.get_periods(handler.obj, future_only=True): 207 duration = period.get_duration() 208 209 # Decline events whose period durations are endless. 210 211 if duration == Endless(): 212 return duration 213 else: 214 total += duration 215 216 return total 217 218 def _get_expiry_time(handler): 219 220 """ 221 Return the expiry time for quota purposes of the current object provided by 222 the 'handler'. 223 """ 224 225 # Reject indefinitely recurring events. 226 227 if handler.obj.possibly_recurring_indefinitely(): 228 return None 229 230 periods = handler.get_periods(handler.obj, future_only=True) 231 return periods and to_utc_datetime(periods[-1].get_end_point()) or None 232 233 def _get_usage(entries): 234 235 "Return the usage total according to the given 'entries'." 236 237 total = timedelta(0) 238 for period in entries: 239 total += period.get_duration() 240 return total 241 242 def _add_to_quota(handler, quota, user, participant, is_organiser): 243 244 """ 245 Record details of the current object of the 'handler' in the applicable 246 free/busy resource. 247 """ 248 249 journal = handler.get_journal() 250 assert journal 251 252 freebusy = journal.get_entries_for_update(quota, user) 253 handler.update_freebusy(freebusy, participant, is_organiser) 254 255 # Remove original recurrence details replaced by additional 256 # recurrences, as well as obsolete additional recurrences. 257 258 handler.remove_freebusy_for_recurrences(freebusy, journal.get_recurrences(quota, handler.uid)) 259 260 # Update free/busy provider information if the event may recur indefinitely. 261 262 if handler.possibly_recurring_indefinitely(): 263 journal.append_freebusy_provider(quota, handler.obj) 264 265 journal.set_entries(quota, user, freebusy) 266 267 def _remove_from_quota(handler, quota, user, participant): 268 269 """ 270 Remove details of the current object of the 'handler' from the applicable 271 free/busy resource. 272 """ 273 274 journal = handler.get_journal() 275 assert journal 276 277 freebusy = journal.get_entries_for_update(quota, user) 278 279 # Remove only the entries associated with this recipient. 280 281 handler.remove_from_freebusy(freebusy, participant) 282 283 # Update free/busy provider information if the event may recur indefinitely. 284 285 if handler.possibly_recurring_indefinitely(): 286 journal.remove_freebusy_provider(quota, handler.obj) 287 288 journal.set_entries(quota, user, freebusy) 289 290 # Collective free/busy maintenance. 291 292 def schedule_across_quota(handler, args): 293 294 """ 295 Check the current object of the given 'handler' against the individual 296 schedules managed by the quota. The consolidated schedules are not tested, 297 nor are the quotas themselves. 298 """ 299 300 if not handler.get_journal(): 301 return standard_responses(handler, "DECLINED") 302 303 quota, organiser = _get_quota_and_identity(handler, args) 304 305 # Check the event periods against the quota's consolidated record of the 306 # organiser's reservations. 307 308 periods = handler.get_periods(handler.obj, future_only=True) 309 freebusy = handler.get_journal().get_entries(quota, organiser) 310 scheduled = handler.can_schedule(freebusy, periods) 311 312 return standard_responses(handler, scheduled and "ACCEPTED" or "DECLINED") 313 314 def add_to_quota_freebusy(handler, args): 315 316 """ 317 Record details of the current object of the 'handler' in the applicable 318 free/busy resource. 319 """ 320 321 if not handler.get_journal(): 322 return 323 324 quota, organiser = _get_quota_and_identity(handler, args) 325 _add_to_quota(handler, quota, organiser, organiser, True) 326 327 def remove_from_quota_freebusy(handler, args): 328 329 """ 330 Remove details of the current object of the 'handler' from the applicable 331 free/busy resource. 332 """ 333 334 if not handler.get_journal(): 335 return 336 337 quota, organiser = _get_quota_and_identity(handler, args) 338 _remove_from_quota(handler, quota, organiser, organiser) 339 340 def _get_quota_and_identity(handler, args): 341 342 """ 343 Combine information about the current object from the 'handler' with the 344 given 'args' to return a tuple containing the quota group and the user 345 identity involved. 346 """ 347 348 assert handler.get_journal() 349 350 quota = args and args[0] or handler.user 351 352 # Obtain the identity for whom the scheduling will apply. 353 354 organiser = handler.obj.get_uri("ORGANIZER") 355 356 return quota, organiser 357 358 # Delegation of reservations. 359 360 def schedule_for_delegate(handler, args): 361 362 """ 363 Check the current object of the given 'handler' against the schedules 364 managed by the quota, delegating to a specific recipient according to the 365 given policies. 366 """ 367 368 # First check the quota and decline any request that would exceed the quota. 369 370 scheduled = check_quota(handler, args) 371 response, description = scheduled or ("DECLINED", None) 372 373 if response == "DECLINED": 374 return response, description 375 376 # Obtain the journal. Any absent journal should have caused a DECLINED 377 # response, so this is merely to document the dependency. 378 379 journal = handler.get_journal() 380 assert journal 381 382 # Obtain the quota and organiser group details to evaluate delegation. 383 384 quota, group = _get_quota_and_group(handler, args) 385 policies = args and args[1:] or ["available"] 386 387 # Determine the status of the recipient. 388 389 attendee_map = handler.obj.get_uri_map("ATTENDEE") 390 attendee_attr = attendee_map[handler.user] 391 392 # Prevent delegation by a delegate. 393 394 if attendee_attr.get("DELEGATED-FROM"): 395 delegates = set([handler.user]) 396 397 # Obtain the delegate pool for the quota. 398 399 else: 400 delegates = journal.get_delegates(quota) 401 402 # Obtain the remaining delegates not already involved in the event. 403 404 delegates = set(delegates).difference(attendee_map) 405 delegates.add(handler.user) 406 407 # Get the quota's schedule for the requested periods and identify 408 # unavailable delegates. 409 410 entries = journal.get_entries(quota, group) 411 conflicts = get_scheduling_conflicts(handler, entries, delegates, attendee=True) 412 413 # Get the delegates in order of increasing unavailability (or decreasing 414 # availability). 415 416 unavailability = conflicts.items() 417 418 # Apply the policies to choose a suitable delegate. 419 420 if "most-available" in policies: 421 unavailability.sort(key=lambda t: t[1]) 422 available = [delegate for (delegate, commitments) in unavailability] 423 delegate = available and available[0] 424 425 # The default is to select completely available delegates. 426 427 else: 428 available = [delegate for (delegate, commitments) in unavailability if not commitments] 429 delegate = available and (handler.user in available and handler.user or available[0]) 430 431 # Only accept or delegate if a suitably available delegate is found. 432 433 if delegate: 434 435 # Add attendee for delegate, obtaining the original attendee dictionary. 436 # Modify this user's status to refer to the delegate. 437 438 if delegate != handler.user: 439 attendee_map = handler.obj.get_value_map("ATTENDEE") 440 attendee_map[delegate] = {"DELEGATED-FROM" : [handler.user]} 441 attendee_attr["DELEGATED-TO"] = [delegate] 442 handler.obj["ATTENDEE"] = attendee_map.items() 443 444 response = "DELEGATED" 445 else: 446 response = "ACCEPTED" 447 else: 448 response = "DECLINED" 449 450 return standard_responses(handler, response) 451 452 # Locking and unlocking. 453 454 def lock_journal(handler, args): 455 456 "Using the 'handler' and 'args', lock the journal for the quota." 457 458 journal = handler.get_journal() 459 if journal: 460 journal.acquire_lock(_get_quota(handler, args)) 461 462 def unlock_journal(handler, args): 463 464 "Using the 'handler' and 'args', unlock the journal for the quota." 465 466 journal = handler.get_journal() 467 if journal: 468 journal.release_lock(_get_quota(handler, args)) 469 470 def _get_quota(handler, args): 471 472 "Return the quota using the 'handler' and 'args'." 473 474 return args and args[0] or handler.user 475 476 # Registry of scheduling functions. 477 478 scheduling_functions = { 479 "check_quota" : [check_quota], 480 "schedule_across_quota" : [schedule_across_quota], 481 "schedule_for_delegate" : [schedule_for_delegate], 482 } 483 484 # Registries of locking and unlocking functions. 485 486 locking_functions = { 487 "check_quota" : [lock_journal], 488 "schedule_across_quota" : [lock_journal], 489 "schedule_for_delegate" : [lock_journal], 490 } 491 492 unlocking_functions = { 493 "check_quota" : [unlock_journal], 494 "schedule_across_quota" : [unlock_journal], 495 "schedule_for_delegate" : [unlock_journal], 496 } 497 498 # Registries of listener functions. 499 500 confirmation_functions = { 501 "check_quota" : [add_to_quota, update_event], 502 "schedule_across_quota" : [add_to_quota_freebusy, update_event], 503 "schedule_for_delegate" : [add_to_quota, update_event], 504 } 505 506 retraction_functions = { 507 "check_quota" : [remove_from_quota, remove_event], 508 "schedule_across_quota" : [remove_from_quota_freebusy, remove_event], 509 "schedule_for_delegate" : [remove_from_quota, remove_event], 510 } 511 512 # vim: tabstop=4 expandtab shiftwidth=4