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