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 # Obtain the journal entries and limits. 79 80 journal = handler.get_journal() 81 entries = journal.get_entries(quota, group) 82 83 if _add_to_entries(entries, handler.uid, handler.recurrenceid, format_duration(total), format_datetime(expiry)): 84 journal.set_entries(quota, group, entries) 85 86 def remove_from_quota(handler, args): 87 88 """ 89 Remove details of the current object of the given 'handler' from the 90 applicable quota. 91 """ 92 93 quota, group = _get_quota_and_group(handler, args) 94 95 total = _get_duration(handler) 96 97 # Obtain the journal entries and limits. 98 99 journal = handler.get_journal() 100 entries = journal.get_entries(quota, group) 101 102 if _remove_from_entries(entries, handler.uid, handler.recurrenceid, format_duration(total)): 103 journal.set_entries(quota, group, entries) 104 105 def _get_quota_and_group(handler, args): 106 107 """ 108 Combine information about the current object from the 'handler' with the 109 given 'args' to return a tuple containing the quota group and the user 110 identity or group involved. 111 """ 112 113 quota = args and args[0] or handler.user 114 115 # Obtain the identity to whom the quota will apply. 116 117 organiser = get_uri(handler.obj.get_value("ORGANIZER")) 118 119 # Obtain any user group to which the quota will apply instead. 120 121 journal = handler.get_journal() 122 groups = journal.get_groups(quota) 123 124 return quota, groups.get(organiser) or organiser 125 126 def _get_duration(handler): 127 128 "Return the duration of the current object provided by the 'handler'." 129 130 # Count only explicit periods. 131 # NOTE: Should reject indefinitely recurring events. 132 133 total = timedelta(0) 134 135 for period in handler.get_periods(handler.obj, explicit_only=True): 136 duration = period.get_duration() 137 138 # Decline events whose period durations are endless. 139 140 if duration == Endless(): 141 return duration 142 else: 143 total += duration 144 145 return total 146 147 def _get_expiry_time(handler): 148 149 """ 150 Return the expiry time for quota purposes of the current object provided by 151 the 'handler'. 152 """ 153 154 # Count only explicit periods. 155 # NOTE: Should reject indefinitely recurring events. 156 157 periods = handler.get_periods(handler.obj, explicit_only=True) 158 return periods and to_utc_datetime(periods[-1].get_end_point()) or None 159 160 def _get_usage(entries): 161 162 "Return the usage total according to the given 'entries'." 163 164 total = timedelta(0) 165 166 for found_uid, found_recurrenceid, found_duration, found_expiry in entries: 167 retraction = found_duration.startswith("-") 168 multiplier = retraction and -1 or 1 169 total += multiplier * get_duration(found_duration[retraction and 1 or 0:]) 170 171 return total 172 173 def _add_to_entries(entries, uid, recurrenceid, duration, expiry): 174 175 """ 176 Add to 'entries' an entry for the event having the given 'uid' and 177 'recurrenceid' with the given 'duration' and 'expiry' time. 178 """ 179 180 confirmed = _find_applicable_entry(entries, uid, recurrenceid, duration) 181 182 # Where a previous entry still applies, retract it if different. 183 184 if confirmed: 185 found_uid, found_recurrenceid, found_duration, found_expiry = confirmed 186 if found_duration != duration: 187 entries.append((found_uid, found_recurrenceid, "-%s" % found_duration, found_expiry)) 188 else: 189 return False 190 191 # Without an applicable previous entry, add a new entry. 192 193 entries.append((uid, recurrenceid, duration, expiry)) 194 return True 195 196 def _remove_from_entries(entries, uid, recurrenceid, duration): 197 198 """ 199 Remove from the given 'entries' any entry for the event having the given 200 'uid' and 'recurrenceid' with the given 'duration'. 201 """ 202 203 confirmed = _find_applicable_entry(entries, uid, recurrenceid, duration) 204 205 # Where a previous entry still applies, retract it. 206 207 if confirmed: 208 found_uid, found_recurrenceid, found_duration, found_expiry = confirmed 209 entries.append((found_uid, found_recurrenceid, "-%s" % found_duration, found_expiry)) 210 return found_duration == duration 211 212 return False 213 214 def _find_applicable_entry(entries, uid, recurrenceid, duration): 215 216 """ 217 Within 'entries', find any applicable previous entry for this event, 218 using the 'uid', 'recurrenceid' and 'duration'. 219 """ 220 221 confirmed = None 222 223 for found_uid, found_recurrenceid, found_duration, found_expiry in entries: 224 if uid == found_uid and recurrenceid == found_recurrenceid: 225 if found_duration.startswith("-"): 226 confirmed = None 227 else: 228 confirmed = found_uid, found_recurrenceid, found_duration, found_expiry 229 230 return confirmed 231 232 # Collective free/busy maintenance. 233 234 def schedule_across_quota(handler, args): 235 236 """ 237 Check the current object of the given 'handler' against the schedules 238 managed by the quota. 239 """ 240 241 quota, organiser = _get_quota_and_identity(handler, args) 242 243 # If newer than any old version, discard old details from the 244 # free/busy record and check for suitability. 245 246 periods = handler.get_periods(handler.obj) 247 freebusy = handler.get_journal().get_freebusy(quota, organiser) 248 scheduled = handler.can_schedule(freebusy, periods) 249 250 return scheduled and "ACCEPTED" or "DECLINED" 251 252 def add_to_quota_freebusy(handler, args): 253 254 """ 255 Record details of the current object of the 'handler' in the applicable 256 free/busy resource. 257 """ 258 259 quota, organiser = _get_quota_and_identity(handler, args) 260 261 journal = handler.get_journal() 262 freebusy = journal.get_freebusy(quota, organiser) 263 handler.update_freebusy(freebusy, organiser, True) 264 journal.set_freebusy(quota, organiser, freebusy) 265 266 def remove_from_quota_freebusy(handler, args): 267 268 """ 269 Remove details of the current object of the 'handler' from the applicable 270 free/busy resource. 271 """ 272 273 quota, organiser = _get_quota_and_identity(handler, args) 274 275 journal = handler.get_journal() 276 freebusy = journal.get_freebusy(quota, organiser) 277 handler.remove_from_freebusy(freebusy) 278 journal.set_freebusy(quota, organiser, freebusy) 279 280 def _get_quota_and_identity(handler, args): 281 282 """ 283 Combine information about the current object from the 'handler' with the 284 given 'args' to return a tuple containing the quota group and the user 285 identity involved. 286 """ 287 288 quota = args and args[0] or handler.user 289 290 # Obtain the identity for whom the scheduling will apply. 291 292 organiser = get_uri(handler.obj.get_value("ORGANIZER")) 293 294 return quota, organiser 295 296 # Locking and unlocking. 297 298 def lock_journal(handler, args): 299 300 "Using the 'handler' and 'args', lock the journal for the quota." 301 302 handler.get_journal().acquire_lock(_get_quota(handler, args)) 303 304 def unlock_journal(handler, args): 305 306 "Using the 'handler' and 'args', unlock the journal for the quota." 307 308 handler.get_journal().release_lock(_get_quota(handler, args)) 309 310 def _get_quota(handler, args): 311 312 "Return the quota using the 'handler' and 'args'." 313 314 return args and args[0] or handler.user 315 316 # Registry of scheduling functions. 317 318 scheduling_functions = { 319 "check_quota" : check_quota, 320 "schedule_across_quota" : schedule_across_quota, 321 } 322 323 # Registries of locking and unlocking functions. 324 325 locking_functions = { 326 "check_quota" : lock_journal, 327 "schedule_across_quota" : lock_journal, 328 } 329 330 unlocking_functions = { 331 "check_quota" : unlock_journal, 332 "schedule_across_quota" : unlock_journal, 333 } 334 335 # Registries of listener functions. 336 337 confirmation_functions = { 338 "add_to_quota" : add_to_quota, 339 "add_to_quota_freebusy" : add_to_quota_freebusy, 340 } 341 342 retraction_functions = { 343 "remove_from_quota" : remove_from_quota, 344 "remove_from_quota_freebusy" : remove_from_quota_freebusy, 345 } 346 347 # vim: tabstop=4 expandtab shiftwidth=4