imip-agent

imiptools/handlers/scheduling/quota.py

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