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.period import Endless 25 from datetime import timedelta 26 27 # Quota maintenance. 28 29 def check_quota(handler, args): 30 31 """ 32 Check the current object of the given 'handler' against the applicable 33 quota. 34 """ 35 36 _ = handler.get_translator() 37 38 quota, group = _get_quota_and_group(handler, args) 39 40 # Obtain the journal entries and check the balance. 41 42 journal = handler.get_journal() 43 entries = journal.get_entries(quota, group) 44 limits = journal.get_limits(quota) 45 46 # Obtain a limit for the group or any general limit. 47 # Decline invitations if no limit has been set. 48 49 limit = limits.get(group) or limits.get("*") 50 if not limit: 51 return "DECLINED", _("You have no quota allocation for the recipient.") 52 53 # Where the quota is unlimited, accept the invitation. 54 55 if limit == "*": 56 return "ACCEPTED", _("The recipient has scheduled the requested period.") 57 58 # Decline events whose durations exceed the balance. 59 60 total = _get_duration(handler) 61 62 if total == Endless(): 63 return "DECLINED", _("The event period exceeds your quota allocation for the recipient.") 64 65 balance = get_duration(limit) - _get_usage(entries) 66 67 if total > balance: 68 return "DECLINED", _("The event period exceeds your quota allocation for the recipient.") 69 else: 70 return "ACCEPTED", _("The recipient has scheduled the requested period.") 71 72 def add_to_quota(handler, args): 73 74 """ 75 Record details of the current object of the given 'handler' in the 76 applicable quota. 77 """ 78 79 quota, group = _get_quota_and_group(handler, args) 80 81 total = _get_duration(handler) 82 expiry = _get_expiry_time(handler) 83 84 # Reject indefinitely recurring events. 85 86 if total == Endless() or not expiry: 87 return 88 89 # Update the journal entries. 90 91 journal = handler.get_journal() 92 entries = journal.get_entries_for_update(quota, group) 93 handler.update_freebusy(entries, handler.user, False) 94 journal.set_entries(quota, group, entries) 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 total = _get_duration(handler) 106 107 # Allow indefinitely recurring events. 108 109 if total == Endless(): 110 total = None 111 112 # Update the journal entries. 113 114 journal = handler.get_journal() 115 entries = journal.get_entries_for_update(quota, group) 116 handler.remove_from_freebusy(entries) 117 journal.set_entries(quota, group, entries) 118 119 def _get_quota_and_group(handler, args): 120 121 """ 122 Combine information about the current object from the 'handler' with the 123 given 'args' to return a tuple containing the quota group and the user 124 identity or group involved. 125 """ 126 127 quota = args and args[0] or handler.user 128 129 # Obtain the identity to whom the quota will apply. 130 131 organiser = get_uri(handler.obj.get_value("ORGANIZER")) 132 133 # Obtain any user group to which the quota will apply instead. 134 135 journal = handler.get_journal() 136 groups = journal.get_groups(quota) 137 138 return quota, groups.get(organiser) or groups.get("*") or organiser 139 140 def _get_duration(handler): 141 142 "Return the duration of the current object provided by the 'handler'." 143 144 # Reject indefinitely recurring events. 145 146 if handler.obj.possibly_recurring_indefinitely(): 147 return Endless() 148 149 # Otherwise, return a sum of the period durations. 150 151 total = timedelta(0) 152 153 for period in handler.get_periods(handler.obj): 154 duration = period.get_duration() 155 156 # Decline events whose period durations are endless. 157 158 if duration == Endless(): 159 return duration 160 else: 161 total += duration 162 163 return total 164 165 def _get_expiry_time(handler): 166 167 """ 168 Return the expiry time for quota purposes of the current object provided by 169 the 'handler'. 170 """ 171 172 # Reject indefinitely recurring events. 173 174 if handler.obj.possibly_recurring_indefinitely(): 175 return None 176 177 periods = handler.get_periods(handler.obj) 178 return periods and to_utc_datetime(periods[-1].get_end_point()) or None 179 180 def _get_usage(entries): 181 182 "Return the usage total according to the given 'entries'." 183 184 total = timedelta(0) 185 for period in entries: 186 total += period.get_duration() 187 return total 188 189 # Collective free/busy maintenance. 190 191 def schedule_across_quota(handler, args): 192 193 """ 194 Check the current object of the given 'handler' against the individual 195 schedules managed by the quota. The consolidated schedules are not tested, 196 nor are the quotas themselves. 197 """ 198 199 _ = handler.get_translator() 200 201 quota, organiser = _get_quota_and_identity(handler, args) 202 203 # Check the event periods against the quota's consolidated record of the 204 # organiser's reservations. 205 206 periods = handler.get_periods(handler.obj) 207 freebusy = handler.get_journal().get_freebusy(quota, organiser) 208 scheduled = handler.can_schedule(freebusy, periods) 209 210 if scheduled: 211 return "ACCEPTED", _("The recipient has scheduled the requested period.") 212 else: 213 return "DECLINED", _("The requested period cannot be scheduled.") 214 215 def add_to_quota_freebusy(handler, args): 216 217 """ 218 Record details of the current object of the 'handler' in the applicable 219 free/busy resource. 220 """ 221 222 quota, organiser = _get_quota_and_identity(handler, args) 223 224 journal = handler.get_journal() 225 freebusy = journal.get_freebusy_for_update(quota, organiser) 226 handler.update_freebusy(freebusy, organiser, True) 227 journal.set_freebusy(quota, organiser, freebusy) 228 229 def remove_from_quota_freebusy(handler, args): 230 231 """ 232 Remove details of the current object of the 'handler' from the applicable 233 free/busy resource. 234 """ 235 236 quota, organiser = _get_quota_and_identity(handler, args) 237 238 journal = handler.get_journal() 239 freebusy = journal.get_freebusy_for_update(quota, organiser) 240 handler.remove_from_freebusy(freebusy) 241 journal.set_freebusy(quota, organiser, freebusy) 242 243 def _get_quota_and_identity(handler, args): 244 245 """ 246 Combine information about the current object from the 'handler' with the 247 given 'args' to return a tuple containing the quota group and the user 248 identity involved. 249 """ 250 251 quota = args and args[0] or handler.user 252 253 # Obtain the identity for whom the scheduling will apply. 254 255 organiser = get_uri(handler.obj.get_value("ORGANIZER")) 256 257 return quota, organiser 258 259 # Delegation of reservations. 260 261 def schedule_for_delegate(handler, args): 262 263 """ 264 Check the current object of the given 'handler' against the schedules 265 managed by the quota, delegating to a specific recipient according to the 266 given policy. 267 """ 268 269 _ = handler.get_translator() 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 policy = args and (args[1:] or ["arbitrary"])[0] 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 unavailable = set() 309 310 for period in handler.get_periods(handler.obj): 311 overlapping = entries.get_overlapping(period) 312 313 # Where scheduling cannot occur, find the busy potential delegates. 314 315 if overlapping: 316 for p in overlapping: 317 unavailable.add(p.attendee) 318 319 # Get the remaining, available delegates. 320 321 available = delegates.difference(unavailable) 322 323 # Apply the policy to choose an available delegate. 324 # NOTE: Currently an arbitrary delegate is chosen if not the recipient. 325 326 if available: 327 delegate = handler.user in available and handler.user or list(available)[0] 328 329 # Add attendee for delegate, obtaining the original attendee dictionary. 330 # Modify this user's status to refer to the delegate. 331 332 if delegate != handler.user: 333 attendee_map = handler.obj.get_value_map("ATTENDEE") 334 attendee_map[delegate] = {"DELEGATED-FROM" : [handler.user]} 335 attendee_attr["DELEGATED-TO"] = [delegate] 336 handler.obj["ATTENDEE"] = attendee_map.items() 337 338 return "DELEGATED", _("The recipient has delegated the requested period.") 339 else: 340 return "ACCEPTED", _("The recipient has scheduled the requested period.") 341 else: 342 return "DECLINED", _("The requested period cannot be scheduled.") 343 344 # Locking and unlocking. 345 346 def lock_journal(handler, args): 347 348 "Using the 'handler' and 'args', lock the journal for the quota." 349 350 handler.get_journal().acquire_lock(_get_quota(handler, args)) 351 352 def unlock_journal(handler, args): 353 354 "Using the 'handler' and 'args', unlock the journal for the quota." 355 356 handler.get_journal().release_lock(_get_quota(handler, args)) 357 358 def _get_quota(handler, args): 359 360 "Return the quota using the 'handler' and 'args'." 361 362 return args and args[0] or handler.user 363 364 # Registry of scheduling functions. 365 366 scheduling_functions = { 367 "check_quota" : check_quota, 368 "schedule_across_quota" : schedule_across_quota, 369 "schedule_for_delegate" : schedule_for_delegate, 370 } 371 372 # Registries of locking and unlocking functions. 373 374 locking_functions = { 375 "check_quota" : lock_journal, 376 "schedule_across_quota" : lock_journal, 377 "schedule_for_delegate" : lock_journal, 378 } 379 380 unlocking_functions = { 381 "check_quota" : unlock_journal, 382 "schedule_across_quota" : unlock_journal, 383 "schedule_for_delegate" : unlock_journal, 384 } 385 386 # Registries of listener functions. 387 388 confirmation_functions = { 389 "check_quota" : add_to_quota, 390 "schedule_across_quota" : add_to_quota_freebusy, 391 "schedule_for_delegate" : add_to_quota, 392 } 393 394 retraction_functions = { 395 "check_quota" : remove_from_quota, 396 "schedule_across_quota" : remove_from_quota_freebusy, 397 "schedule_for_delegate" : remove_from_quota, 398 } 399 400 # vim: tabstop=4 expandtab shiftwidth=4