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 handler.remove_from_freebusy(freebusy, participant) 264 265 # Update free/busy provider information if the event may recur indefinitely. 266 267 if handler.possibly_recurring_indefinitely(): 268 journal.remove_freebusy_provider(quota, handler.obj) 269 270 journal.set_entries(quota, user, freebusy) 271 272 # Collective free/busy maintenance. 273 274 def schedule_across_quota(handler, args): 275 276 """ 277 Check the current object of the given 'handler' against the individual 278 schedules managed by the quota. The consolidated schedules are not tested, 279 nor are the quotas themselves. 280 """ 281 282 quota, organiser = _get_quota_and_identity(handler, args) 283 284 # Check the event periods against the quota's consolidated record of the 285 # organiser's reservations. 286 287 periods = handler.get_periods(handler.obj) 288 freebusy = handler.get_journal().get_entries(quota, organiser) 289 scheduled = handler.can_schedule(freebusy, periods) 290 291 return standard_responses(handler, scheduled and "ACCEPTED" or "DECLINED") 292 293 def add_to_quota_freebusy(handler, args): 294 295 """ 296 Record details of the current object of the 'handler' in the applicable 297 free/busy resource. 298 """ 299 300 quota, organiser = _get_quota_and_identity(handler, args) 301 _add_to_quota(handler, quota, organiser, organiser, True) 302 303 def remove_from_quota_freebusy(handler, args): 304 305 """ 306 Remove details of the current object of the 'handler' from the applicable 307 free/busy resource. 308 """ 309 310 quota, organiser = _get_quota_and_identity(handler, args) 311 _remove_from_quota(handler, quota, organiser, organiser) 312 313 def _get_quota_and_identity(handler, args): 314 315 """ 316 Combine information about the current object from the 'handler' with the 317 given 'args' to return a tuple containing the quota group and the user 318 identity involved. 319 """ 320 321 quota = args and args[0] or handler.user 322 323 # Obtain the identity for whom the scheduling will apply. 324 325 organiser = get_uri(handler.obj.get_value("ORGANIZER")) 326 327 return quota, organiser 328 329 # Delegation of reservations. 330 331 def schedule_for_delegate(handler, args): 332 333 """ 334 Check the current object of the given 'handler' against the schedules 335 managed by the quota, delegating to a specific recipient according to the 336 given policies. 337 """ 338 339 # First check the quota and decline any request that would exceed the quota. 340 341 scheduled = check_quota(handler, args) 342 response, description = scheduled or ("DECLINED", None) 343 344 if response == "DECLINED": 345 return response, description 346 347 # Obtain the quota and organiser group details to evaluate delegation. 348 349 quota, group = _get_quota_and_group(handler, args) 350 policies = args and args[1:] or ["available"] 351 352 # Determine the status of the recipient. 353 354 attendee_map = uri_dict(handler.obj.get_value_map("ATTENDEE")) 355 attendee_attr = attendee_map[handler.user] 356 357 # Prevent delegation by a delegate. 358 359 if attendee_attr.get("DELEGATED-FROM"): 360 delegates = set([handler.user]) 361 362 # Obtain the delegate pool for the quota. 363 364 else: 365 delegates = handler.get_journal().get_delegates(quota) 366 367 # Obtain the remaining delegates not already involved in the event. 368 369 delegates = set(delegates).difference(attendee_map) 370 delegates.add(handler.user) 371 372 # Get the quota's schedule for the requested periods and identify 373 # unavailable delegates. 374 375 entries = handler.get_journal().get_entries(quota, group) 376 conflicts = get_scheduling_conflicts(handler, entries, delegates, attendee=True) 377 378 # Get the delegates in order of increasing unavailability (or decreasing 379 # availability). 380 381 unavailability = conflicts.items() 382 383 # Apply the policies to choose a suitable delegate. 384 385 if "most-available" in policies: 386 unavailability.sort(key=lambda t: t[1]) 387 available = [delegate for (delegate, commitments) in unavailability] 388 delegate = available and available[0] 389 390 # The default is to select completely available delegates. 391 392 else: 393 available = [delegate for (delegate, commitments) in unavailability if not commitments] 394 delegate = available and (handler.user in available and handler.user or available[0]) 395 396 # Only accept or delegate if a suitably available delegate is found. 397 398 if delegate: 399 400 # Add attendee for delegate, obtaining the original attendee dictionary. 401 # Modify this user's status to refer to the delegate. 402 403 if delegate != handler.user: 404 attendee_map = handler.obj.get_value_map("ATTENDEE") 405 attendee_map[delegate] = {"DELEGATED-FROM" : [handler.user]} 406 attendee_attr["DELEGATED-TO"] = [delegate] 407 handler.obj["ATTENDEE"] = attendee_map.items() 408 409 response = "DELEGATED" 410 else: 411 response = "ACCEPTED" 412 else: 413 response = "DECLINED" 414 415 return standard_responses(handler, response) 416 417 # Locking and unlocking. 418 419 def lock_journal(handler, args): 420 421 "Using the 'handler' and 'args', lock the journal for the quota." 422 423 handler.get_journal().acquire_lock(_get_quota(handler, args)) 424 425 def unlock_journal(handler, args): 426 427 "Using the 'handler' and 'args', unlock the journal for the quota." 428 429 handler.get_journal().release_lock(_get_quota(handler, args)) 430 431 def _get_quota(handler, args): 432 433 "Return the quota using the 'handler' and 'args'." 434 435 return args and args[0] or handler.user 436 437 # Registry of scheduling functions. 438 439 scheduling_functions = { 440 "check_quota" : [check_quota], 441 "schedule_across_quota" : [schedule_across_quota], 442 "schedule_for_delegate" : [schedule_for_delegate], 443 } 444 445 # Registries of locking and unlocking functions. 446 447 locking_functions = { 448 "check_quota" : [lock_journal], 449 "schedule_across_quota" : [lock_journal], 450 "schedule_for_delegate" : [lock_journal], 451 } 452 453 unlocking_functions = { 454 "check_quota" : [unlock_journal], 455 "schedule_across_quota" : [unlock_journal], 456 "schedule_for_delegate" : [unlock_journal], 457 } 458 459 # Registries of listener functions. 460 461 confirmation_functions = { 462 "check_quota" : [add_to_quota, update_event], 463 "schedule_across_quota" : [add_to_quota_freebusy, update_event], 464 "schedule_for_delegate" : [add_to_quota, update_event], 465 } 466 467 retraction_functions = { 468 "check_quota" : [remove_from_quota, remove_event], 469 "schedule_across_quota" : [remove_from_quota_freebusy, remove_event], 470 "schedule_for_delegate" : [remove_from_quota, remove_event], 471 } 472 473 # vim: tabstop=4 expandtab shiftwidth=4