imip-agent

imiptools/handlers/scheduling/quota.py

1048:a55598b6f053
2016-02-08 Paul Boddie Added expiry times to recorded events so that quotas can be updated.
     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