imip-agent

Annotated imiptools/handlers/scheduling/quota.py

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