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