imip-agent

Changeset

1028:7f4bd7d4236a
2016-01-29 Paul Boddie raw files shortlog changelog graph Added support for additional scheduling modules, moving the existing functionality into the freebusy module, providing initial functionality for an access-based module, and introducing a mechanism to update the registry of modules explicitly. Added a test of combining scheduling functions.
docs/wiki/Administration (file) imiptools/handlers/scheduling/__init__.py (file) imiptools/handlers/scheduling/access.py (file) imiptools/handlers/scheduling/freebusy.py (file) imiptools/handlers/scheduling/manifest.py (file) tests/templates/event-request-sauna-outsider.txt (file) tests/test_resource_invitation_constraints_multiple.sh (file) tools/install.sh (file) tools/update_scheduling_modules.py (file)
     1.1 --- a/docs/wiki/Administration	Fri Jan 29 22:17:47 2016 +0100
     1.2 +++ b/docs/wiki/Administration	Fri Jan 29 22:25:02 2016 +0100
     1.3 @@ -16,6 +16,8 @@
     1.4  In the background, imip-agent uses and updates information as described in the
     1.5  [[../FilesystemUsage|filesystem usage guide]].
     1.6  
     1.7 +<<TableOfContents(2,3)>>
     1.8 +
     1.9  == Initialising the System ==
    1.10  
    1.11  As described in the [[../GettingStarted|getting started guide]], the system is
    1.12 @@ -61,7 +63,7 @@
    1.13  be "handed over" at regular intervals.
    1.14  
    1.15  The `freebusy_offers` setting, together with the `scheduling_function` setting,
    1.16 -would allow different kinds of resources to "keep open" tentatively-suggested
    1.17 +allows different kinds of resources to "keep open" tentatively-suggested
    1.18  periods for different lengths of time, allowing frequently-requested resources
    1.19  to respond to scheduling requests in a timely fashion, whilst also allowing
    1.20  other resources to give more time to event organisers to respond to their
    1.21 @@ -108,3 +110,20 @@
    1.22  from being routed via imip-agent. This is as simple as either not listing the
    1.23  identity in [[../MailIntegration/Simple|lists of addresses]] or by adjusting
    1.24  [[../MailIntegration/LDAP|queries yielding calendar users]].
    1.25 +
    1.26 +== Adding Scheduling Functions ==
    1.27 +
    1.28 +The `scheduling_function` setting employs functions that reside within modules in
    1.29 +the `imiptools.handlers.scheduling` package. Extra modules can be installed in
    1.30 +this package by adding files to the `scheduling` directory within the software
    1.31 +installation.
    1.32 +
    1.33 +After adding modules, a tool must be run to register the new modules:
    1.34 +
    1.35 +{{{
    1.36 +tools/update_scheduling_modules.py
    1.37 +}}}
    1.38 +
    1.39 +It is envisaged that the installation of additional scheduling modules and the
    1.40 +use of this tool will be performed by the packaging system provided by an
    1.41 +operating system distribution.
     2.1 --- a/imiptools/handlers/scheduling/__init__.py	Fri Jan 29 22:17:47 2016 +0100
     2.2 +++ b/imiptools/handlers/scheduling/__init__.py	Fri Jan 29 22:25:02 2016 +0100
     2.3 @@ -19,11 +19,7 @@
     2.4  this program.  If not, see <http://www.gnu.org/licenses/>.
     2.5  """
     2.6  
     2.7 -from imiptools.data import uri_values
     2.8 -from imiptools.dates import ValidityError, to_timezone
     2.9 -from imiptools.period import coalesce_freebusy, invert_freebusy, \
    2.10 -                             periods_from, remove_event_periods, \
    2.11 -                             remove_periods
    2.12 +from imiptools.handlers.scheduling.manifest import scheduling_functions
    2.13  
    2.14  def apply_scheduling_functions(functions, handler):
    2.15  
    2.16 @@ -60,196 +56,4 @@
    2.17  
    2.18      return response
    2.19  
    2.20 -def schedule_in_freebusy(handler, freebusy=None):
    2.21 -
    2.22 -    """
    2.23 -    Attempt to schedule the current object of the given 'handler' in the
    2.24 -    free/busy schedule of a resource, returning an indication of the kind of
    2.25 -    response to be returned.
    2.26 -
    2.27 -    If 'freebusy' is specified, the given collection of busy periods will be
    2.28 -    used to determine whether any conflicts occur. Otherwise, the current user's
    2.29 -    free/busy records will be used.
    2.30 -    """
    2.31 -
    2.32 -    # If newer than any old version, discard old details from the
    2.33 -    # free/busy record and check for suitability.
    2.34 -
    2.35 -    periods = handler.get_periods(handler.obj)
    2.36 -
    2.37 -    freebusy = freebusy or handler.store.get_freebusy(handler.user)
    2.38 -    offers = handler.store.get_freebusy_offers(handler.user)
    2.39 -
    2.40 -    # Check the periods against any scheduled events and against
    2.41 -    # any outstanding offers.
    2.42 -
    2.43 -    scheduled = handler.can_schedule(freebusy, periods)
    2.44 -    scheduled = scheduled and handler.can_schedule(offers, periods)
    2.45 -
    2.46 -    return scheduled and "ACCEPTED" or "DECLINED"
    2.47 -
    2.48 -def schedule_corrected_in_freebusy(handler):
    2.49 -
    2.50 -    """
    2.51 -    Attempt to schedule the current object of the given 'handler', correcting
    2.52 -    specified datetimes according to the configuration of a resource,
    2.53 -    returning an indication of the kind of response to be returned.
    2.54 -    """
    2.55 -
    2.56 -    obj = handler.obj.copy()
    2.57 -
    2.58 -    # Check any constraints on the request.
    2.59 -
    2.60 -    try:
    2.61 -        corrected = handler.correct_object()
    2.62 -
    2.63 -    # Refuse to schedule obviously invalid requests.
    2.64 -
    2.65 -    except ValidityError:
    2.66 -        return None
    2.67 -
    2.68 -    # With a valid request, determine whether the event can be scheduled.
    2.69 -
    2.70 -    scheduled = schedule_in_freebusy(handler)
    2.71 -
    2.72 -    # Restore the original object if it was corrected but could not be
    2.73 -    # scheduled.
    2.74 -
    2.75 -    if scheduled == "DECLINED" and corrected:
    2.76 -        handler.set_object(obj)
    2.77 -    
    2.78 -    # Where the corrected object can be scheduled, issue a counter
    2.79 -    # request.
    2.80 -
    2.81 -    return scheduled == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED"
    2.82 -
    2.83 -def schedule_next_available_in_freebusy(handler):
    2.84 -
    2.85 -    """
    2.86 -    Attempt to schedule the current object of the given 'handler', correcting
    2.87 -    specified datetimes according to the configuration of a resource, then
    2.88 -    suggesting the next available period in the free/busy records if scheduling
    2.89 -    cannot occur for the requested period, returning an indication of the kind
    2.90 -    of response to be returned.
    2.91 -    """
    2.92 -
    2.93 -    scheduled = schedule_corrected_in_freebusy(handler)
    2.94 -
    2.95 -    if scheduled in ("ACCEPTED", "COUNTER"):
    2.96 -        return scheduled
    2.97 -
    2.98 -    # There should already be free/busy information for the user.
    2.99 -
   2.100 -    user_freebusy = handler.store.get_freebusy(handler.user)
   2.101 -    busy = user_freebusy
   2.102 -
   2.103 -    # Subtract any periods from this event from the free/busy collections.
   2.104 -
   2.105 -    event_periods = remove_event_periods(user_freebusy, handler.uid, handler.recurrenceid)
   2.106 -
   2.107 -    # Find busy periods for the other attendees.
   2.108 -
   2.109 -    for attendee in uri_values(handler.obj.get_values("ATTENDEE")):
   2.110 -        if attendee != handler.user:
   2.111 -            freebusy = handler.store.get_freebusy_for_other(handler.user, attendee)
   2.112 -            if freebusy:
   2.113 -                remove_periods(freebusy, event_periods)
   2.114 -                busy += freebusy
   2.115 -
   2.116 -    # Obtain the combined busy periods.
   2.117 -
   2.118 -    busy.sort()
   2.119 -    busy = coalesce_freebusy(busy)
   2.120 -
   2.121 -    # Obtain free periods.
   2.122 -
   2.123 -    free = invert_freebusy(busy)
   2.124 -    permitted_values = handler.get_permitted_values()
   2.125 -    periods = []
   2.126 -
   2.127 -    # Do not attempt to redefine rule-based periods.
   2.128 -
   2.129 -    last = None
   2.130 -
   2.131 -    for period in handler.get_periods(handler.obj, explicit_only=True):
   2.132 -        duration = period.get_duration()
   2.133 -
   2.134 -        # Try and schedule periods normally since some of them may be
   2.135 -        # compatible with the schedule.
   2.136 -
   2.137 -        if permitted_values:
   2.138 -            period = period.get_corrected(permitted_values)
   2.139 -
   2.140 -        scheduled = handler.can_schedule(freebusy, [period])
   2.141 -
   2.142 -        if scheduled == "ACCEPTED":
   2.143 -            periods.append(period)
   2.144 -            last = period.get_end()
   2.145 -            continue
   2.146 -
   2.147 -        # Get free periods from the time of each period.
   2.148 -
   2.149 -        for found in periods_from(free, period):
   2.150 -
   2.151 -            # Skip any periods before the last period.
   2.152 -
   2.153 -            if last:
   2.154 -                if last > found.get_end():
   2.155 -                    continue
   2.156 -
   2.157 -                # Adjust the start of the free period to exclude the last period.
   2.158 -
   2.159 -                found = found.make_corrected(max(found.get_start(), last), found.get_end())
   2.160 -
   2.161 -            # Only test free periods long enough to hold the requested period.
   2.162 -
   2.163 -            if found.get_duration() >= duration:
   2.164 -
   2.165 -                # Obtain a possible period, starting at the found point and
   2.166 -                # with the requested duration. Then, correct the period if
   2.167 -                # necessary.
   2.168 -
   2.169 -                start = to_timezone(found.get_start(), period.get_tzid())
   2.170 -                possible = period.make_corrected(start, start + period.get_duration())
   2.171 -                if permitted_values:
   2.172 -                    possible = possible.get_corrected(permitted_values)
   2.173 -
   2.174 -                # Only if the possible period is still within the free period
   2.175 -                # can it be used.
   2.176 -
   2.177 -                if possible.within(found):
   2.178 -                    periods.append(possible)
   2.179 -                    break
   2.180 -
   2.181 -        # Where no period can be found, decline the invitation.
   2.182 -
   2.183 -        else:
   2.184 -            return "DECLINED"
   2.185 -
   2.186 -        # Use the found period to set the start of the next window to search.
   2.187 -
   2.188 -        last = periods[-1].get_end()
   2.189 -
   2.190 -    # Replace the periods in the object.
   2.191 -
   2.192 -    obj = handler.obj.copy()
   2.193 -    changed = handler.obj.set_periods(periods)
   2.194 -
   2.195 -    # Check one last time, reverting the change if not scheduled.
   2.196 -
   2.197 -    scheduled = schedule_in_freebusy(handler, busy)
   2.198 -
   2.199 -    if scheduled == "DECLINED":
   2.200 -        handler.set_object(obj)
   2.201 -
   2.202 -    return scheduled == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED"
   2.203 -
   2.204 -# Registry of scheduling functions.
   2.205 -
   2.206 -scheduling_functions = {
   2.207 -    "schedule_in_freebusy" : schedule_in_freebusy,
   2.208 -    "schedule_corrected_in_freebusy" : schedule_corrected_in_freebusy,
   2.209 -    "schedule_next_available_in_freebusy" : schedule_next_available_in_freebusy,
   2.210 -    }
   2.211 -
   2.212  # vim: tabstop=4 expandtab shiftwidth=4
     3.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     3.2 +++ b/imiptools/handlers/scheduling/access.py	Fri Jan 29 22:25:02 2016 +0100
     3.3 @@ -0,0 +1,45 @@
     3.4 +#!/usr/bin/env python
     3.5 +
     3.6 +"""
     3.7 +Access-control-related scheduling functionality.
     3.8 +
     3.9 +Copyright (C) 2016 Paul Boddie <paul@boddie.org.uk>
    3.10 +
    3.11 +This program is free software; you can redistribute it and/or modify it under
    3.12 +the terms of the GNU General Public License as published by the Free Software
    3.13 +Foundation; either version 3 of the License, or (at your option) any later
    3.14 +version.
    3.15 +
    3.16 +This program is distributed in the hope that it will be useful, but WITHOUT
    3.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    3.18 +FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
    3.19 +details.
    3.20 +
    3.21 +You should have received a copy of the GNU General Public License along with
    3.22 +this program.  If not, see <http://www.gnu.org/licenses/>.
    3.23 +"""
    3.24 +
    3.25 +from imiptools.data import get_address
    3.26 +
    3.27 +def same_domain_only(handler):
    3.28 +
    3.29 +    """
    3.30 +    Attempt to schedule the current object of the given 'handler' if the
    3.31 +    organiser employs an address in the same domain as the resource.
    3.32 +    """
    3.33 +
    3.34 +    organiser = get_address(handler.obj.get_value("ORGANIZER"))
    3.35 +    user = get_address(handler.user)
    3.36 +
    3.37 +    organiser_domain = organiser.rsplit("@", 1)[-1]
    3.38 +    user_domain = user.rsplit("@", 1)[-1]
    3.39 +    
    3.40 +    return organiser_domain == user_domain and "ACCEPTED" or "DECLINED"
    3.41 +
    3.42 +# Registry of scheduling functions.
    3.43 +
    3.44 +scheduling_functions = {
    3.45 +    "same_domain_only" : same_domain_only,
    3.46 +    }
    3.47 +
    3.48 +# vim: tabstop=4 expandtab shiftwidth=4
     4.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.2 +++ b/imiptools/handlers/scheduling/freebusy.py	Fri Jan 29 22:25:02 2016 +0100
     4.3 @@ -0,0 +1,220 @@
     4.4 +#!/usr/bin/env python
     4.5 +
     4.6 +"""
     4.7 +Free/busy-related scheduling functionality.
     4.8 +
     4.9 +Copyright (C) 2015, 2016 Paul Boddie <paul@boddie.org.uk>
    4.10 +
    4.11 +This program is free software; you can redistribute it and/or modify it under
    4.12 +the terms of the GNU General Public License as published by the Free Software
    4.13 +Foundation; either version 3 of the License, or (at your option) any later
    4.14 +version.
    4.15 +
    4.16 +This program is distributed in the hope that it will be useful, but WITHOUT
    4.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    4.18 +FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
    4.19 +details.
    4.20 +
    4.21 +You should have received a copy of the GNU General Public License along with
    4.22 +this program.  If not, see <http://www.gnu.org/licenses/>.
    4.23 +"""
    4.24 +
    4.25 +from imiptools.data import uri_values
    4.26 +from imiptools.dates import ValidityError, to_timezone
    4.27 +from imiptools.period import coalesce_freebusy, invert_freebusy, \
    4.28 +                             periods_from, remove_event_periods, \
    4.29 +                             remove_periods
    4.30 +
    4.31 +def schedule_in_freebusy(handler, freebusy=None):
    4.32 +
    4.33 +    """
    4.34 +    Attempt to schedule the current object of the given 'handler' in the
    4.35 +    free/busy schedule of a resource, returning an indication of the kind of
    4.36 +    response to be returned.
    4.37 +
    4.38 +    If 'freebusy' is specified, the given collection of busy periods will be
    4.39 +    used to determine whether any conflicts occur. Otherwise, the current user's
    4.40 +    free/busy records will be used.
    4.41 +    """
    4.42 +
    4.43 +    # If newer than any old version, discard old details from the
    4.44 +    # free/busy record and check for suitability.
    4.45 +
    4.46 +    periods = handler.get_periods(handler.obj)
    4.47 +
    4.48 +    freebusy = freebusy or handler.store.get_freebusy(handler.user)
    4.49 +    offers = handler.store.get_freebusy_offers(handler.user)
    4.50 +
    4.51 +    # Check the periods against any scheduled events and against
    4.52 +    # any outstanding offers.
    4.53 +
    4.54 +    scheduled = handler.can_schedule(freebusy, periods)
    4.55 +    scheduled = scheduled and handler.can_schedule(offers, periods)
    4.56 +
    4.57 +    return scheduled and "ACCEPTED" or "DECLINED"
    4.58 +
    4.59 +def schedule_corrected_in_freebusy(handler):
    4.60 +
    4.61 +    """
    4.62 +    Attempt to schedule the current object of the given 'handler', correcting
    4.63 +    specified datetimes according to the configuration of a resource,
    4.64 +    returning an indication of the kind of response to be returned.
    4.65 +    """
    4.66 +
    4.67 +    obj = handler.obj.copy()
    4.68 +
    4.69 +    # Check any constraints on the request.
    4.70 +
    4.71 +    try:
    4.72 +        corrected = handler.correct_object()
    4.73 +
    4.74 +    # Refuse to schedule obviously invalid requests.
    4.75 +
    4.76 +    except ValidityError:
    4.77 +        return None
    4.78 +
    4.79 +    # With a valid request, determine whether the event can be scheduled.
    4.80 +
    4.81 +    scheduled = schedule_in_freebusy(handler)
    4.82 +
    4.83 +    # Restore the original object if it was corrected but could not be
    4.84 +    # scheduled.
    4.85 +
    4.86 +    if scheduled == "DECLINED" and corrected:
    4.87 +        handler.set_object(obj)
    4.88 +    
    4.89 +    # Where the corrected object can be scheduled, issue a counter
    4.90 +    # request.
    4.91 +
    4.92 +    return scheduled == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED"
    4.93 +
    4.94 +def schedule_next_available_in_freebusy(handler):
    4.95 +
    4.96 +    """
    4.97 +    Attempt to schedule the current object of the given 'handler', correcting
    4.98 +    specified datetimes according to the configuration of a resource, then
    4.99 +    suggesting the next available period in the free/busy records if scheduling
   4.100 +    cannot occur for the requested period, returning an indication of the kind
   4.101 +    of response to be returned.
   4.102 +    """
   4.103 +
   4.104 +    scheduled = schedule_corrected_in_freebusy(handler)
   4.105 +
   4.106 +    if scheduled in ("ACCEPTED", "COUNTER"):
   4.107 +        return scheduled
   4.108 +
   4.109 +    # There should already be free/busy information for the user.
   4.110 +
   4.111 +    user_freebusy = handler.store.get_freebusy(handler.user)
   4.112 +    busy = user_freebusy
   4.113 +
   4.114 +    # Subtract any periods from this event from the free/busy collections.
   4.115 +
   4.116 +    event_periods = remove_event_periods(user_freebusy, handler.uid, handler.recurrenceid)
   4.117 +
   4.118 +    # Find busy periods for the other attendees.
   4.119 +
   4.120 +    for attendee in uri_values(handler.obj.get_values("ATTENDEE")):
   4.121 +        if attendee != handler.user:
   4.122 +            freebusy = handler.store.get_freebusy_for_other(handler.user, attendee)
   4.123 +            if freebusy:
   4.124 +                remove_periods(freebusy, event_periods)
   4.125 +                busy += freebusy
   4.126 +
   4.127 +    # Obtain the combined busy periods.
   4.128 +
   4.129 +    busy.sort()
   4.130 +    busy = coalesce_freebusy(busy)
   4.131 +
   4.132 +    # Obtain free periods.
   4.133 +
   4.134 +    free = invert_freebusy(busy)
   4.135 +    permitted_values = handler.get_permitted_values()
   4.136 +    periods = []
   4.137 +
   4.138 +    # Do not attempt to redefine rule-based periods.
   4.139 +
   4.140 +    last = None
   4.141 +
   4.142 +    for period in handler.get_periods(handler.obj, explicit_only=True):
   4.143 +        duration = period.get_duration()
   4.144 +
   4.145 +        # Try and schedule periods normally since some of them may be
   4.146 +        # compatible with the schedule.
   4.147 +
   4.148 +        if permitted_values:
   4.149 +            period = period.get_corrected(permitted_values)
   4.150 +
   4.151 +        scheduled = handler.can_schedule(freebusy, [period])
   4.152 +
   4.153 +        if scheduled == "ACCEPTED":
   4.154 +            periods.append(period)
   4.155 +            last = period.get_end()
   4.156 +            continue
   4.157 +
   4.158 +        # Get free periods from the time of each period.
   4.159 +
   4.160 +        for found in periods_from(free, period):
   4.161 +
   4.162 +            # Skip any periods before the last period.
   4.163 +
   4.164 +            if last:
   4.165 +                if last > found.get_end():
   4.166 +                    continue
   4.167 +
   4.168 +                # Adjust the start of the free period to exclude the last period.
   4.169 +
   4.170 +                found = found.make_corrected(max(found.get_start(), last), found.get_end())
   4.171 +
   4.172 +            # Only test free periods long enough to hold the requested period.
   4.173 +
   4.174 +            if found.get_duration() >= duration:
   4.175 +
   4.176 +                # Obtain a possible period, starting at the found point and
   4.177 +                # with the requested duration. Then, correct the period if
   4.178 +                # necessary.
   4.179 +
   4.180 +                start = to_timezone(found.get_start(), period.get_tzid())
   4.181 +                possible = period.make_corrected(start, start + period.get_duration())
   4.182 +                if permitted_values:
   4.183 +                    possible = possible.get_corrected(permitted_values)
   4.184 +
   4.185 +                # Only if the possible period is still within the free period
   4.186 +                # can it be used.
   4.187 +
   4.188 +                if possible.within(found):
   4.189 +                    periods.append(possible)
   4.190 +                    break
   4.191 +
   4.192 +        # Where no period can be found, decline the invitation.
   4.193 +
   4.194 +        else:
   4.195 +            return "DECLINED"
   4.196 +
   4.197 +        # Use the found period to set the start of the next window to search.
   4.198 +
   4.199 +        last = periods[-1].get_end()
   4.200 +
   4.201 +    # Replace the periods in the object.
   4.202 +
   4.203 +    obj = handler.obj.copy()
   4.204 +    changed = handler.obj.set_periods(periods)
   4.205 +
   4.206 +    # Check one last time, reverting the change if not scheduled.
   4.207 +
   4.208 +    scheduled = schedule_in_freebusy(handler, busy)
   4.209 +
   4.210 +    if scheduled == "DECLINED":
   4.211 +        handler.set_object(obj)
   4.212 +
   4.213 +    return scheduled == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED"
   4.214 +
   4.215 +# Registry of scheduling functions.
   4.216 +
   4.217 +scheduling_functions = {
   4.218 +    "schedule_in_freebusy" : schedule_in_freebusy,
   4.219 +    "schedule_corrected_in_freebusy" : schedule_corrected_in_freebusy,
   4.220 +    "schedule_next_available_in_freebusy" : schedule_next_available_in_freebusy,
   4.221 +    }
   4.222 +
   4.223 +# vim: tabstop=4 expandtab shiftwidth=4
     5.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.2 +++ b/imiptools/handlers/scheduling/manifest.py	Fri Jan 29 22:25:02 2016 +0100
     5.3 @@ -0,0 +1,5 @@
     5.4 +scheduling_functions = {}
     5.5 +from imiptools.handlers.scheduling.freebusy import scheduling_functions as l
     5.6 +scheduling_functions.update(l)
     5.7 +from imiptools.handlers.scheduling.access import scheduling_functions as l
     5.8 +scheduling_functions.update(l)
     6.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     6.2 +++ b/tests/templates/event-request-sauna-outsider.txt	Fri Jan 29 22:25:02 2016 +0100
     6.3 @@ -0,0 +1,34 @@
     6.4 +Content-Type: multipart/alternative; boundary="===============0047278175=="
     6.5 +MIME-Version: 1.0
     6.6 +From: paul.boddie@example.net
     6.7 +To: resource-room-sauna@example.com
     6.8 +Subject: Wanting to book the sauna!
     6.9 +
    6.10 +--===============0047278175==
    6.11 +Content-Type: text/plain; charset="us-ascii"
    6.12 +MIME-Version: 1.0
    6.13 +Content-Transfer-Encoding: 7bit
    6.14 +
    6.15 +This message contains an event.
    6.16 +--===============0047278175==
    6.17 +MIME-Version: 1.0
    6.18 +Content-Transfer-Encoding: 7bit
    6.19 +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST"
    6.20 +
    6.21 +BEGIN:VCALENDAR
    6.22 +PRODID:-//imip-agent/test//EN
    6.23 +METHOD:REQUEST
    6.24 +VERSION:2.0
    6.25 +BEGIN:VEVENT
    6.26 +ORGANIZER:mailto:paul.boddie@example.net
    6.27 +ATTENDEE;ROLE=CHAIR:mailto:paul.boddie@example.net
    6.28 +ATTENDEE;RSVP=TRUE:mailto:resource-room-sauna@example.com
    6.29 +DTSTAMP:20141125T004600Z
    6.30 +DTSTART;TZID=Europe/Oslo:20141126T160000
    6.31 +DTEND;TZID=Europe/Oslo:20141126T164500
    6.32 +SUMMARY:Meeting at 4pm
    6.33 +UID:event1@example.net
    6.34 +END:VEVENT
    6.35 +END:VCALENDAR
    6.36 +
    6.37 +--===============0047278175==--
     7.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.2 +++ b/tests/test_resource_invitation_constraints_multiple.sh	Fri Jan 29 22:25:02 2016 +0100
     7.3 @@ -0,0 +1,89 @@
     7.4 +#!/bin/sh
     7.5 +
     7.6 +. "`dirname \"$0\"`/common.sh"
     7.7 +
     7.8 +USER="mailto:resource-room-sauna@example.com"
     7.9 +SENDER="mailto:paul.boddie@example.net"
    7.10 +FBFILE="$STORE/$USER/freebusy"
    7.11 +FBOFFERFILE="$STORE/$USER/freebusy-offers"
    7.12 +FBSENDERFILE="$STORE/$SENDER/freebusy"
    7.13 +FBSENDEROTHERFILE="$STORE/$SENDER/freebusy-other/$USER"
    7.14 +FBSENDERREQUESTS="$STORE/$SENDER/requests"
    7.15 +
    7.16 +mkdir -p "$PREFS/$USER"
    7.17 +echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
    7.18 +echo 'share' > "$PREFS/$USER/freebusy_sharing"
    7.19 +cat > "$PREFS/$USER/scheduling_function" <<EOF
    7.20 +schedule_in_freebusy
    7.21 +same_domain_only
    7.22 +EOF
    7.23 +echo '10,12,14,16,18:0,15,30,45' > "$PREFS/$USER/permitted_times"
    7.24 +echo 'PT60S' > "$PREFS/$USER/freebusy_offers"
    7.25 +
    7.26 +  "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-sauna-all.txt" 2>> $ERROR \
    7.27 +| "$SHOWMAIL" \
    7.28 +> out0.tmp
    7.29 +
    7.30 +   grep -q 'METHOD:REPLY' out0.tmp \
    7.31 +&& ! grep -q '^FREEBUSY' out0.tmp \
    7.32 +&& echo "Success" \
    7.33 +|| echo "Failed"
    7.34 +
    7.35 +# Attempt to schedule an event.
    7.36 +
    7.37 +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-outsider.txt" 2>> $ERROR
    7.38 +
    7.39 +   grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
    7.40 +&& echo "Success" \
    7.41 +|| echo "Failed"
    7.42 +
    7.43 +# Present the request to the resource.
    7.44 +
    7.45 +  "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-outsider.txt" 2>> $ERROR \
    7.46 +| tee out1r.tmp \
    7.47 +| "$SHOWMAIL" \
    7.48 +> out1.tmp
    7.49 +
    7.50 +   grep -q 'METHOD:REPLY' out1.tmp \
    7.51 +&& grep -q 'ATTENDEE.*;PARTSTAT=DECLINED' out1.tmp \
    7.52 +&& echo "Success" \
    7.53 +|| echo "Failed"
    7.54 +
    7.55 +   ! [ -e "$FBFILE" ] \
    7.56 +|| ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBFILE" \
    7.57 +&& echo "Success" \
    7.58 +|| echo "Failed"
    7.59 +
    7.60 +# Relax the scheduling function.
    7.61 +
    7.62 +cat > "$PREFS/$USER/scheduling_function" <<EOF
    7.63 +schedule_in_freebusy
    7.64 +EOF
    7.65 +
    7.66 +# Present the request to the resource.
    7.67 +
    7.68 +  "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-outsider.txt" 2>> $ERROR \
    7.69 +| tee out2r.tmp \
    7.70 +| "$SHOWMAIL" \
    7.71 +> out2.tmp
    7.72 +
    7.73 +   grep -q 'METHOD:REPLY' out2.tmp \
    7.74 +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out2.tmp \
    7.75 +&& echo "Success" \
    7.76 +|| echo "Failed"
    7.77 +
    7.78 +   [ -e "$FBFILE" ] \
    7.79 +&& grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBFILE" \
    7.80 +&& echo "Success" \
    7.81 +|| echo "Failed"
    7.82 +
    7.83 +# Check the free/busy state of the resource again.
    7.84 +
    7.85 +  "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-sauna-all.txt" 2>> $ERROR \
    7.86 +| "$SHOWMAIL" \
    7.87 +> out3.tmp
    7.88 +
    7.89 +   grep -q 'METHOD:REPLY' out3.tmp \
    7.90 +&& grep -q 'FREEBUSY;FBTYPE=BUSY:20141126T150000Z/20141126T154500Z' out3.tmp \
    7.91 +&& echo "Success" \
    7.92 +|| echo "Failed"
     8.1 --- a/tools/install.sh	Fri Jan 29 22:17:47 2016 +0100
     8.2 +++ b/tools/install.sh	Fri Jan 29 22:25:02 2016 +0100
     8.3 @@ -26,12 +26,13 @@
     8.4  cp $AGENTS "$INSTALL_DIR"
     8.5  cp $MODULES "$INSTALL_DIR"
     8.6  
     8.7 -if [ ! -e "$INSTALL_DIR/imiptools" ]; then
     8.8 -    mkdir "$INSTALL_DIR/imiptools"
     8.9 -fi
    8.10 -if [ ! -e "$INSTALL_DIR/imiptools/handlers" ]; then
    8.11 -    mkdir "$INSTALL_DIR/imiptools/handlers"
    8.12 -fi
    8.13 +for DIR in "$INSTALL_DIR/imiptools" \
    8.14 +           "$INSTALL_DIR/imiptools/handlers" \
    8.15 +           "$INSTALL_DIR/imiptools/handlers/scheduling" ; do
    8.16 +    if [ ! -e "$DIR" ]; then
    8.17 +        mkdir "$DIR"
    8.18 +    fi
    8.19 +done
    8.20  
    8.21  # Remove any symbolic link to the config module.
    8.22  
    8.23 @@ -43,6 +44,13 @@
    8.24  
    8.25  cp imiptools/*.py "$INSTALL_DIR/imiptools/"
    8.26  cp imiptools/handlers/*.py "$INSTALL_DIR/imiptools/handlers/"
    8.27 +cp imiptools/handlers/scheduling/*.py "$INSTALL_DIR/imiptools/handlers/scheduling/"
    8.28 +
    8.29 +# Remove migrated modules.
    8.30 +
    8.31 +if [ -e "$INSTALL_DIR/imiptools/handlers/scheduling.py" ]; then
    8.32 +    rm "$INSTALL_DIR/imiptools/handlers/scheduling.py"*
    8.33 +fi
    8.34  
    8.35  # Install the config module in a more appropriate location.
    8.36  
     9.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     9.2 +++ b/tools/update_scheduling_modules.py	Fri Jan 29 22:25:02 2016 +0100
     9.3 @@ -0,0 +1,55 @@
     9.4 +#!/usr/bin/env python
     9.5 +
     9.6 +"""
     9.7 +Update the scheduling modules import manifest.
     9.8 +
     9.9 +Copyright (C) 2016 Paul Boddie <paul@boddie.org.uk>
    9.10 +
    9.11 +This program is free software; you can redistribute it and/or modify it under
    9.12 +the terms of the GNU General Public License as published by the Free Software
    9.13 +Foundation; either version 3 of the License, or (at your option) any later
    9.14 +version.
    9.15 +
    9.16 +This program is distributed in the hope that it will be useful, but WITHOUT
    9.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    9.18 +FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
    9.19 +details.
    9.20 +
    9.21 +You should have received a copy of the GNU General Public License along with
    9.22 +this program.  If not, see <http://www.gnu.org/licenses/>.
    9.23 +"""
    9.24 +
    9.25 +from glob import glob
    9.26 +from os.path import join, split, splitext
    9.27 +import imiptools.handlers
    9.28 +
    9.29 +reserved = ["__init__.py", "manifest.py"]
    9.30 +
    9.31 +# The main program generating a new version of the manifest module.
    9.32 +
    9.33 +if __name__ == "__main__":
    9.34 +    dirname = join(split(imiptools.handlers.__file__)[0], "scheduling")
    9.35 +
    9.36 +    # Get all Python files in the scheduling directory, filtering out the
    9.37 +    # reserved files that do not provide scheduling functions.
    9.38 +
    9.39 +    filenames = []
    9.40 +    for filename in glob(join(dirname, "*.py")):
    9.41 +        filename = split(filename)[-1]
    9.42 +        if filename not in reserved:
    9.43 +            filenames.append(filename)
    9.44 +
    9.45 +    # Open the manifest module and write code to import and combine the
    9.46 +    # functions from each module.
    9.47 +
    9.48 +    f = open(join(dirname, "manifest.py"), "w")
    9.49 +    try:
    9.50 +        print >>f, "scheduling_functions = {}"
    9.51 +        for filename in filenames:
    9.52 +            module = splitext(filename)[0]
    9.53 +            print >>f, "from imiptools.handlers.scheduling.%s import scheduling_functions as l" % module
    9.54 +            print >>f, "scheduling_functions.update(l)"
    9.55 +    finally:
    9.56 +        f.close()
    9.57 +
    9.58 +# vim: tabstop=4 expandtab shiftwidth=4