imip-agent

imiptools/handlers/scheduling/quota.py

1339:b839e8ad5f77
2017-10-17 Paul Boddie Added notes about local Unix mailbox message storage.
     1 #!/usr/bin/env python     2      3 """     4 Quota-related scheduling functionality.     5      6 Copyright (C) 2016, 2017 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.handlers.scheduling.common import get_scheduling_conflicts, \    24                                                  standard_responses    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     _ = handler.get_translator()    38     39     # Obtain the journal entries and check the balance.    40     41     journal = handler.get_journal()    42     if not journal:    43         return "DECLINED", _("You have no quota allocation for the recipient.")    44     45     quota, group = _get_quota_and_group(handler, args)    46     47     entries = journal.get_entries(quota, group)    48     limits = journal.get_limits(quota)    49     50     # Obtain a limit for the group or any general limit.    51     # Decline invitations if no limit has been set.    52     53     limit = limits.get(group) or limits.get("*")    54     if not limit:    55         return "DECLINED", _("You have no quota allocation for the recipient.")    56     57     # Where the quota is unlimited, accept the invitation.    58     59     if limit == "*":    60         return "ACCEPTED", _("The recipient has scheduled the requested period.")    61     62     # Decline endless events for limited quotas.    63     64     total = _get_duration(handler)    65     66     if total == Endless():    67         return "DECLINED", _("The event period exceeds your quota allocation for the recipient.")    68     69     # Decline events whose durations exceed the balance.    70     71     balance = get_duration(limit) - _get_usage(entries)    72     73     if total > balance:    74         return "DECLINED", _("The event period exceeds your quota allocation for the recipient.")    75     else:    76         return "ACCEPTED", _("The recipient has scheduled the requested period.")    77     78 def add_to_quota(handler, args):    79     80     """    81     Record details of the current object of the given 'handler' in the    82     applicable quota.    83     """    84     85     if not handler.get_journal():    86         return    87     88     quota, group = _get_quota_and_group(handler, args)    89     _add_to_quota(handler, quota, group, handler.user, False)    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     if not handler.get_journal():    99         return   100    101     quota, group = _get_quota_and_group(handler, args)   102     _remove_from_quota(handler, quota, group, handler.user)   103    104 def update_event(handler, args):   105    106     "Update a stored version of the current object of the given 'handler'."   107    108     journal = handler.get_journal()   109     if not journal:   110         return   111    112     quota, group = _get_quota_and_group(handler, args)   113    114     # Where an existing version of the object exists, merge the recipient's   115     # attendance information.   116    117     obj = journal.get_event(quota, handler.uid, handler.recurrenceid)   118     if not obj:   119         obj = handler.obj   120    121     # Set attendance.   122    123     attendee_map = obj.get_uri_map("ATTENDEE")   124     attendee_map[handler.user]["PARTSTAT"] = "ACCEPTED"   125     obj["ATTENDEE"] = attendee_map.items()   126    127     # Record the object so that recurrences can be generated.   128    129     journal.set_event(quota, handler.uid, handler.recurrenceid, obj.to_node())   130    131 def remove_event(handler, args):   132    133     "Remove a stored version of the current object of the given 'handler'."   134    135     journal = handler.get_journal()   136     if not journal:   137         return   138    139     quota, group = _get_quota_and_group(handler, args)   140    141     # Where an existing version of the object exists, remove the recipient's   142     # attendance information.   143    144     obj = journal.get_event(quota, handler.uid, handler.recurrenceid)   145     if not obj:   146         return   147    148     attendee_map = obj.get_uri_map("ATTENDEE")   149     delegates = journal.get_delegates(quota)   150    151     # Determine whether any of the delegates are still involved.   152    153     attendees = set(delegates).intersection(attendee_map.keys())   154     if handler.user in attendees:   155         attendees.remove(handler.user)   156    157     # Remove event details where no delegates will be involved.   158    159     if not attendees:   160         journal.remove_event(quota, handler.uid, handler.recurrenceid)   161         return   162    163     del attendee_map[handler.user]   164     obj["ATTENDEE"] = attendee_map.items()   165    166     # Record the object so that recurrences can be generated.   167    168     journal.set_event(quota, handler.uid, handler.recurrenceid, obj.to_node())   169    170 def _get_quota_and_group(handler, args):   171    172     """   173     Combine information about the current object from the 'handler' with the   174     given 'args' to return a tuple containing the quota group and the user   175     identity or group involved.   176     """   177    178     journal = handler.get_journal()   179     assert journal   180    181     quota = args and args[0] or handler.user   182    183     # Obtain the identity to whom the quota will apply.   184    185     organiser = handler.obj.get_uri("ORGANIZER")   186    187     # Obtain any user group to which the quota will apply instead.   188    189     groups = journal.get_groups(quota)   190    191     return quota, groups.get(organiser) or groups.get("*") or organiser   192    193 def _get_duration(handler):   194    195     "Return the duration of the current object provided by the 'handler'."   196    197     # Reject indefinitely recurring events.   198    199     if handler.obj.possibly_recurring_indefinitely():   200         return Endless()   201    202     # Otherwise, return a sum of the period durations.   203    204     total = timedelta(0)   205    206     for period in handler.get_periods(handler.obj, future_only=True):   207         duration = period.get_duration()   208    209         # Decline events whose period durations are endless.   210    211         if duration == Endless():   212             return duration   213         else:   214             total += duration   215    216     return total   217    218 def _get_expiry_time(handler):   219    220     """   221     Return the expiry time for quota purposes of the current object provided by   222     the 'handler'.   223     """   224    225     # Reject indefinitely recurring events.   226    227     if handler.obj.possibly_recurring_indefinitely():   228         return None   229    230     periods = handler.get_periods(handler.obj, future_only=True)   231     return periods and to_utc_datetime(periods[-1].get_end_point()) or None   232    233 def _get_usage(entries):   234    235     "Return the usage total according to the given 'entries'."   236    237     total = timedelta(0)   238     for period in entries:   239         total += period.get_duration()   240     return total   241    242 def _add_to_quota(handler, quota, user, participant, is_organiser):   243    244     """   245     Record details of the current object of the 'handler' in the applicable   246     free/busy resource.   247     """   248    249     journal = handler.get_journal()   250     assert journal   251    252     freebusy = journal.get_entries_for_update(quota, user)   253     handler.update_freebusy(freebusy, participant, is_organiser)   254    255     # Remove original recurrence details replaced by additional   256     # recurrences, as well as obsolete additional recurrences.   257    258     handler.remove_freebusy_for_recurrences(freebusy, journal.get_recurrences(quota, handler.uid))   259    260     # Update free/busy provider information if the event may recur indefinitely.   261    262     if handler.possibly_recurring_indefinitely():   263         journal.append_freebusy_provider(quota, handler.obj)   264    265     journal.set_entries(quota, user, freebusy)   266    267 def _remove_from_quota(handler, quota, user, participant):   268    269     """   270     Remove details of the current object of the 'handler' from the applicable   271     free/busy resource.   272     """   273    274     journal = handler.get_journal()   275     assert journal   276    277     freebusy = journal.get_entries_for_update(quota, user)   278    279     # Remove only the entries associated with this recipient.   280    281     handler.remove_from_freebusy(freebusy, participant)   282    283     # Update free/busy provider information if the event may recur indefinitely.   284    285     if handler.possibly_recurring_indefinitely():   286         journal.remove_freebusy_provider(quota, handler.obj)   287    288     journal.set_entries(quota, user, freebusy)   289    290 # Collective free/busy maintenance.   291    292 def schedule_across_quota(handler, args):   293    294     """   295     Check the current object of the given 'handler' against the individual   296     schedules managed by the quota. The consolidated schedules are not tested,   297     nor are the quotas themselves.   298     """   299    300     if not handler.get_journal():   301         return standard_responses(handler, "DECLINED")   302    303     quota, organiser = _get_quota_and_identity(handler, args)   304    305     # Check the event periods against the quota's consolidated record of the   306     # organiser's reservations.   307    308     periods = handler.get_periods(handler.obj, future_only=True)   309     freebusy = handler.get_journal().get_entries(quota, organiser)   310     scheduled = handler.can_schedule(freebusy, periods)   311    312     return standard_responses(handler, scheduled and "ACCEPTED" or "DECLINED")   313    314 def add_to_quota_freebusy(handler, args):   315    316     """   317     Record details of the current object of the 'handler' in the applicable   318     free/busy resource.   319     """   320    321     if not handler.get_journal():   322         return   323    324     quota, organiser = _get_quota_and_identity(handler, args)   325     _add_to_quota(handler, quota, organiser, organiser, True)   326    327 def remove_from_quota_freebusy(handler, args):   328    329     """   330     Remove details of the current object of the 'handler' from the applicable   331     free/busy resource.   332     """   333    334     if not handler.get_journal():   335         return   336    337     quota, organiser = _get_quota_and_identity(handler, args)   338     _remove_from_quota(handler, quota, organiser, organiser)   339    340 def _get_quota_and_identity(handler, args):   341    342     """   343     Combine information about the current object from the 'handler' with the   344     given 'args' to return a tuple containing the quota group and the user   345     identity involved.   346     """   347    348     assert handler.get_journal()   349    350     quota = args and args[0] or handler.user   351    352     # Obtain the identity for whom the scheduling will apply.   353    354     organiser = handler.obj.get_uri("ORGANIZER")   355    356     return quota, organiser   357    358 # Delegation of reservations.   359    360 def schedule_for_delegate(handler, args):   361    362     """   363     Check the current object of the given 'handler' against the schedules   364     managed by the quota, delegating to a specific recipient according to the   365     given policies.   366     """   367    368     # First check the quota and decline any request that would exceed the quota.   369    370     scheduled = check_quota(handler, args)   371     response, description = scheduled or ("DECLINED", None)   372    373     if response == "DECLINED":   374         return response, description   375    376     # Obtain the journal. Any absent journal should have caused a DECLINED   377     # response, so this is merely to document the dependency.   378    379     journal = handler.get_journal()   380     assert journal   381    382     # Obtain the quota and organiser group details to evaluate delegation.   383    384     quota, group = _get_quota_and_group(handler, args)   385     policies = args and args[1:] or ["available"]   386    387     # Determine the status of the recipient.   388    389     attendee_map = handler.obj.get_uri_map("ATTENDEE")   390     attendee_attr = attendee_map[handler.user]   391    392     # Prevent delegation by a delegate.   393    394     if attendee_attr.get("DELEGATED-FROM"):   395         delegates = set([handler.user])   396    397     # Obtain the delegate pool for the quota.   398    399     else:   400         delegates = journal.get_delegates(quota)   401    402         # Obtain the remaining delegates not already involved in the event.   403    404         delegates = set(delegates).difference(attendee_map)   405         delegates.add(handler.user)   406    407     # Get the quota's schedule for the requested periods and identify   408     # unavailable delegates.   409    410     entries = journal.get_entries(quota, group)   411     conflicts = get_scheduling_conflicts(handler, entries, delegates, attendee=True)   412    413     # Get the delegates in order of increasing unavailability (or decreasing   414     # availability).   415    416     unavailability = conflicts.items()   417    418     # Apply the policies to choose a suitable delegate.   419    420     if "most-available" in policies:   421         unavailability.sort(key=lambda t: t[1])   422         available = [delegate for (delegate, commitments) in unavailability]   423         delegate = available and available[0]   424    425     # The default is to select completely available delegates.   426    427     else:   428         available = [delegate for (delegate, commitments) in unavailability if not commitments]   429         delegate = available and (handler.user in available and handler.user or available[0])   430    431     # Only accept or delegate if a suitably available delegate is found.   432    433     if delegate:   434    435         # Add attendee for delegate, obtaining the original attendee dictionary.   436         # Modify this user's status to refer to the delegate.   437    438         if delegate != handler.user:   439             attendee_map = handler.obj.get_value_map("ATTENDEE")   440             attendee_map[delegate] = {"DELEGATED-FROM" : [handler.user]}   441             attendee_attr["DELEGATED-TO"] = [delegate]   442             handler.obj["ATTENDEE"] = attendee_map.items()   443    444             response = "DELEGATED"   445         else:   446             response = "ACCEPTED"   447     else:   448         response = "DECLINED"   449    450     return standard_responses(handler, response)   451    452 # Locking and unlocking.   453    454 def lock_journal(handler, args):   455    456     "Using the 'handler' and 'args', lock the journal for the quota."   457    458     journal = handler.get_journal()   459     if journal:   460         journal.acquire_lock(_get_quota(handler, args))   461    462 def unlock_journal(handler, args):   463    464     "Using the 'handler' and 'args', unlock the journal for the quota."   465    466     journal = handler.get_journal()   467     if journal:   468         journal.release_lock(_get_quota(handler, args))   469    470 def _get_quota(handler, args):   471    472     "Return the quota using the 'handler' and 'args'."   473    474     return args and args[0] or handler.user   475    476 # Registry of scheduling functions.   477    478 scheduling_functions = {   479     "check_quota" : [check_quota],   480     "schedule_across_quota" : [schedule_across_quota],   481     "schedule_for_delegate" : [schedule_for_delegate],   482     }   483    484 # Registries of locking and unlocking functions.   485    486 locking_functions = {   487     "check_quota" : [lock_journal],   488     "schedule_across_quota" : [lock_journal],   489     "schedule_for_delegate" : [lock_journal],   490     }   491    492 unlocking_functions = {   493     "check_quota" : [unlock_journal],   494     "schedule_across_quota" : [unlock_journal],   495     "schedule_for_delegate" : [unlock_journal],   496     }   497    498 # Registries of listener functions.   499    500 confirmation_functions = {   501     "check_quota" : [add_to_quota, update_event],   502     "schedule_across_quota" : [add_to_quota_freebusy, update_event],   503     "schedule_for_delegate" : [add_to_quota, update_event],   504     }   505    506 retraction_functions = {   507     "check_quota" : [remove_from_quota, remove_event],   508     "schedule_across_quota" : [remove_from_quota_freebusy, remove_event],   509     "schedule_for_delegate" : [remove_from_quota, remove_event],   510     }   511    512 # vim: tabstop=4 expandtab shiftwidth=4