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