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