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 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 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 # Locking and unlocking. 254 255 def lock_journal(handler, args): 256 257 "Using the 'handler' and 'args', lock the journal for the quota." 258 259 handler.get_journal().acquire_lock(_get_quota(handler, args)) 260 261 def unlock_journal(handler, args): 262 263 "Using the 'handler' and 'args', unlock the journal for the quota." 264 265 handler.get_journal().release_lock(_get_quota(handler, args)) 266 267 def _get_quota(handler, args): 268 269 "Return the quota using the 'handler' and 'args'." 270 271 return args and args[0] or handler.user 272 273 # Registry of scheduling functions. 274 275 scheduling_functions = { 276 "check_quota" : check_quota, 277 "schedule_across_quota" : schedule_across_quota, 278 } 279 280 # Registries of locking and unlocking functions. 281 282 locking_functions = { 283 "check_quota" : lock_journal, 284 "schedule_across_quota" : lock_journal, 285 } 286 287 unlocking_functions = { 288 "check_quota" : unlock_journal, 289 "schedule_across_quota" : unlock_journal, 290 } 291 292 # Registries of listener functions. 293 294 confirmation_functions = { 295 "check_quota" : add_to_quota, 296 "schedule_across_quota" : add_to_quota_freebusy, 297 } 298 299 retraction_functions = { 300 "check_quota" : remove_from_quota, 301 "schedule_across_quota" : remove_from_quota_freebusy, 302 } 303 304 # vim: tabstop=4 expandtab shiftwidth=4