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