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 format_datetime, format_duration, get_datetime, \ 23 get_duration, to_utc_datetime 24 from imiptools.data import get_uri 25 from imiptools.period import Endless 26 from datetime import timedelta 27 28 # Quota maintenance. 29 30 def check_quota(handler, args): 31 32 """ 33 Check the current object of the given 'handler' against the applicable 34 quota. 35 """ 36 37 quota, group = _get_quota_and_group(handler, args) 38 39 # Obtain the journal entries and check the balance. 40 41 journal = handler.get_journal() 42 entries = journal.get_entries(quota, group) 43 limits = journal.get_limits(quota) 44 45 # Obtain a limit for the group or any general limit. 46 # Decline invitations if no limit has been set. 47 48 limit = limits.get(group) or limits.get("*") 49 if not limit: 50 return "DECLINED" 51 52 # Decline events whose durations exceed the balance. 53 54 total = _get_duration(handler) 55 56 if total == Endless(): 57 return "DECLINED" 58 59 balance = get_duration(limit) - _get_usage(entries) 60 61 if total > balance: 62 return "DECLINED" 63 else: 64 return "ACCEPTED" 65 66 def add_to_quota(handler, args): 67 68 """ 69 Record details of the current object of the given 'handler' in the 70 applicable quota. 71 """ 72 73 quota, group = _get_quota_and_group(handler, args) 74 75 total = _get_duration(handler) 76 expiry = _get_expiry_time(handler) 77 78 # Reject indefinitely recurring events. 79 80 if total == Endless() or not expiry: 81 return 82 83 # Obtain the journal entries and limits. 84 85 journal = handler.get_journal() 86 entries = journal.get_entries(quota, group) 87 88 if _add_to_entries(entries, handler.uid, handler.recurrenceid, format_duration(total), format_datetime(expiry)): 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 # Obtain the journal entries and limits. 108 109 journal = handler.get_journal() 110 entries = journal.get_entries(quota, group) 111 112 if _remove_from_entries(entries, handler.uid, handler.recurrenceid, format_duration(total)): 113 journal.set_entries(quota, group, entries) 114 115 def _get_quota_and_group(handler, args): 116 117 """ 118 Combine information about the current object from the 'handler' with the 119 given 'args' to return a tuple containing the quota group and the user 120 identity or group involved. 121 """ 122 123 quota = args and args[0] or handler.user 124 125 # Obtain the identity to whom the quota will apply. 126 127 organiser = get_uri(handler.obj.get_value("ORGANIZER")) 128 129 # Obtain any user group to which the quota will apply instead. 130 131 journal = handler.get_journal() 132 groups = journal.get_groups(quota) 133 134 return quota, groups.get(organiser) or organiser 135 136 def _get_duration(handler): 137 138 "Return the duration of the current object provided by the 'handler'." 139 140 # Reject indefinitely recurring events. 141 142 if handler.obj.possibly_recurring_indefinitely(): 143 return Endless() 144 145 # Otherwise, return a sum of the period durations. 146 147 total = timedelta(0) 148 149 for period in handler.get_periods(handler.obj): 150 duration = period.get_duration() 151 152 # Decline events whose period durations are endless. 153 154 if duration == Endless(): 155 return duration 156 else: 157 total += duration 158 159 return total 160 161 def _get_expiry_time(handler): 162 163 """ 164 Return the expiry time for quota purposes of the current object provided by 165 the 'handler'. 166 """ 167 168 # Reject indefinitely recurring events. 169 170 if handler.obj.possibly_recurring_indefinitely(): 171 return None 172 173 periods = handler.get_periods(handler.obj) 174 return periods and to_utc_datetime(periods[-1].get_end_point()) or None 175 176 def _get_usage(entries): 177 178 "Return the usage total according to the given 'entries'." 179 180 total = timedelta(0) 181 182 for found_uid, found_recurrenceid, found_duration, found_expiry in entries: 183 retraction = found_duration.startswith("-") 184 multiplier = retraction and -1 or 1 185 total += multiplier * get_duration(found_duration[retraction and 1 or 0:]) 186 187 return total 188 189 def _add_to_entries(entries, uid, recurrenceid, duration, expiry): 190 191 """ 192 Add to 'entries' an entry for the event having the given 'uid' and 193 'recurrenceid' with the given 'duration' and 'expiry' time. 194 """ 195 196 confirmed = _find_applicable_entry(entries, uid, recurrenceid) 197 198 # Where a previous entry still applies, retract it if different. 199 200 if confirmed: 201 found_uid, found_recurrenceid, found_duration, found_expiry = confirmed 202 if found_duration != duration: 203 entries.append((found_uid, found_recurrenceid, "-%s" % found_duration, found_expiry)) 204 else: 205 return False 206 207 # Without an applicable previous entry, add a new entry. 208 209 entries.append((uid, recurrenceid, duration, expiry)) 210 return True 211 212 def _remove_from_entries(entries, uid, recurrenceid, duration): 213 214 """ 215 Remove from the given 'entries' any entry for the event having the given 216 'uid' and 'recurrenceid' with the given 'duration'. 217 """ 218 219 confirmed = _find_applicable_entry(entries, uid, recurrenceid) 220 221 # Where a previous entry still applies, retract it. 222 223 if confirmed: 224 found_uid, found_recurrenceid, found_duration, found_expiry = confirmed 225 entries.append((found_uid, found_recurrenceid, "-%s" % found_duration, found_expiry)) 226 return True 227 228 return False 229 230 def _find_applicable_entry(entries, uid, recurrenceid): 231 232 """ 233 Within 'entries', find any applicable previous entry for this event, 234 using the 'uid' and 'recurrenceid'. 235 """ 236 237 confirmed = None 238 239 for found_uid, found_recurrenceid, found_duration, found_expiry in entries: 240 if uid == found_uid and recurrenceid == found_recurrenceid: 241 if found_duration.startswith("-"): 242 confirmed = None 243 else: 244 confirmed = found_uid, found_recurrenceid, found_duration, found_expiry 245 246 return confirmed 247 248 # Collective free/busy maintenance. 249 250 def schedule_across_quota(handler, args): 251 252 """ 253 Check the current object of the given 'handler' against the schedules 254 managed by the quota. 255 """ 256 257 quota, organiser = _get_quota_and_identity(handler, args) 258 259 # If newer than any old version, discard old details from the 260 # free/busy record and check for suitability. 261 262 periods = handler.get_periods(handler.obj) 263 freebusy = handler.get_journal().get_freebusy(quota, organiser) 264 scheduled = handler.can_schedule(freebusy, periods) 265 266 return scheduled and "ACCEPTED" or "DECLINED" 267 268 def add_to_quota_freebusy(handler, args): 269 270 """ 271 Record details of the current object of the 'handler' in the applicable 272 free/busy resource. 273 """ 274 275 quota, organiser = _get_quota_and_identity(handler, args) 276 277 journal = handler.get_journal() 278 freebusy = journal.get_freebusy(quota, organiser) 279 handler.update_freebusy(freebusy, organiser, True) 280 journal.set_freebusy(quota, organiser, freebusy) 281 282 def remove_from_quota_freebusy(handler, args): 283 284 """ 285 Remove details of the current object of the 'handler' from the applicable 286 free/busy resource. 287 """ 288 289 quota, organiser = _get_quota_and_identity(handler, args) 290 291 journal = handler.get_journal() 292 freebusy = journal.get_freebusy(quota, organiser) 293 handler.remove_from_freebusy(freebusy) 294 journal.set_freebusy(quota, organiser, freebusy) 295 296 def _get_quota_and_identity(handler, args): 297 298 """ 299 Combine information about the current object from the 'handler' with the 300 given 'args' to return a tuple containing the quota group and the user 301 identity involved. 302 """ 303 304 quota = args and args[0] or handler.user 305 306 # Obtain the identity for whom the scheduling will apply. 307 308 organiser = get_uri(handler.obj.get_value("ORGANIZER")) 309 310 return quota, organiser 311 312 # Locking and unlocking. 313 314 def lock_journal(handler, args): 315 316 "Using the 'handler' and 'args', lock the journal for the quota." 317 318 handler.get_journal().acquire_lock(_get_quota(handler, args)) 319 320 def unlock_journal(handler, args): 321 322 "Using the 'handler' and 'args', unlock the journal for the quota." 323 324 handler.get_journal().release_lock(_get_quota(handler, args)) 325 326 def _get_quota(handler, args): 327 328 "Return the quota using the 'handler' and 'args'." 329 330 return args and args[0] or handler.user 331 332 # Registry of scheduling functions. 333 334 scheduling_functions = { 335 "check_quota" : check_quota, 336 "schedule_across_quota" : schedule_across_quota, 337 } 338 339 # Registries of locking and unlocking functions. 340 341 locking_functions = { 342 "check_quota" : lock_journal, 343 "schedule_across_quota" : lock_journal, 344 } 345 346 unlocking_functions = { 347 "check_quota" : unlock_journal, 348 "schedule_across_quota" : unlock_journal, 349 } 350 351 # Registries of listener functions. 352 353 confirmation_functions = { 354 "add_to_quota" : add_to_quota, 355 "add_to_quota_freebusy" : add_to_quota_freebusy, 356 } 357 358 retraction_functions = { 359 "remove_from_quota" : remove_from_quota, 360 "remove_from_quota_freebusy" : remove_from_quota_freebusy, 361 } 362 363 # vim: tabstop=4 expandtab shiftwidth=4