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