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