1.1 --- a/docs/preferences.txt Tue Oct 27 00:05:46 2015 +0100
1.2 +++ b/docs/preferences.txt Tue Oct 27 00:26:59 2015 +0100
1.3 @@ -206,16 +206,21 @@
1.4 period for an event. The imiptools.handlers.scheduling module contains the
1.5 built-in scheduling functions which include the following:
1.6
1.7 - schedule_in_freebusy accept an invitation if the event periods
1.8 - are free according to the free/busy
1.9 - records for the resource; decline
1.10 - otherwise
1.11 + schedule_in_freebusy accept an invitation if the event periods are free
1.12 + according to the free/busy records for the resource;
1.13 + decline otherwise
1.14
1.15 schedule_corrected_in_freebusy correct periods in an event according to
1.16 the permitted_times setting (see above),
1.17 - then attempt to schedule the event
1.18 - according to the free/busy records for the
1.19 - resource
1.20 + then attempt to schedule the event according to the
1.21 + free/busy records for the resource
1.22 +
1.23 + schedule_next_available_in_freebusy correct periods in an event according
1.24 + to the permitted_times setting (see
1.25 + above), if configured, and attempt to schedule the
1.26 + event according to the free/busy records for the
1.27 + resource, seeking the next available free period for
1.28 + each period that conflicts with an existing event
1.29
1.30 The scheduling mechanism can be extended by implementing additional scheduling
1.31 functions or by extending the handler framework directly.
2.1 --- a/imiptools/handlers/scheduling.py Tue Oct 27 00:05:46 2015 +0100
2.2 +++ b/imiptools/handlers/scheduling.py Tue Oct 27 00:26:59 2015 +0100
2.3 @@ -19,7 +19,8 @@
2.4 this program. If not, see <http://www.gnu.org/licenses/>.
2.5 """
2.6
2.7 -from imiptools.dates import ValidityError
2.8 +from imiptools.dates import ValidityError, to_timezone
2.9 +from imiptools.period import invert_freebusy, periods_from
2.10
2.11 def schedule_in_freebusy(handler):
2.12
2.13 @@ -80,11 +81,111 @@
2.14
2.15 return scheduled == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED"
2.16
2.17 +def schedule_next_available_in_freebusy(handler):
2.18 +
2.19 + """
2.20 + Attempt to schedule the current object of the given 'handler', correcting
2.21 + specified datetimes according to the configuration of a resource, then
2.22 + suggesting the next available period in the free/busy records if scheduling
2.23 + cannot occur for the requested period, returning an indication of the kind
2.24 + of response to be returned.
2.25 + """
2.26 +
2.27 + scheduled = schedule_corrected_in_freebusy(handler)
2.28 +
2.29 + if scheduled in ("ACCEPTED", "COUNTER"):
2.30 + return scheduled
2.31 +
2.32 + # Find free periods, update the object with the details.
2.33 +
2.34 + freebusy = handler.store.get_freebusy(handler.user)
2.35 + free = invert_freebusy(freebusy)
2.36 + permitted_values = handler.get_permitted_values()
2.37 + periods = []
2.38 +
2.39 + # Do not attempt to redefine rule-based periods.
2.40 +
2.41 + last = None
2.42 +
2.43 + for period in handler.get_periods(handler.obj, explicit_only=True):
2.44 + duration = period.get_duration()
2.45 +
2.46 + # Try and schedule periods normally since some of them may be
2.47 + # compatible with the schedule.
2.48 +
2.49 + if permitted_values:
2.50 + period = period.get_corrected(permitted_values)
2.51 +
2.52 + scheduled = handler.can_schedule(freebusy, [period])
2.53 +
2.54 + if scheduled == "ACCEPTED":
2.55 + periods.append(period)
2.56 + last = period.get_end()
2.57 + continue
2.58 +
2.59 + # Get free periods from the time of each period.
2.60 +
2.61 + for found in periods_from(free, period):
2.62 +
2.63 + # Skip any periods before the last period.
2.64 +
2.65 + if last:
2.66 + if last > found.get_end():
2.67 + continue
2.68 +
2.69 + # Adjust the start of the free period to exclude the last period.
2.70 +
2.71 + found = found.make_corrected(max(found.get_start(), last), found.get_end())
2.72 +
2.73 + # Only test free periods long enough to hold the requested period.
2.74 +
2.75 + if found.get_duration() >= duration:
2.76 +
2.77 + # Obtain a possible period, starting at the found point and
2.78 + # with the requested duration. Then, correct the period if
2.79 + # necessary.
2.80 +
2.81 + start = to_timezone(found.get_start(), period.get_tzid())
2.82 + possible = period.make_corrected(start, start + period.get_duration())
2.83 + if permitted_values:
2.84 + possible = possible.get_corrected(permitted_values)
2.85 +
2.86 + # Only if the possible period is still within the free period
2.87 + # can it be used.
2.88 +
2.89 + if possible.within(found):
2.90 + periods.append(possible)
2.91 + break
2.92 +
2.93 + # Where no period can be found, decline the invitation.
2.94 +
2.95 + else:
2.96 + return "DECLINED"
2.97 +
2.98 + # Use the found period to set the start of the next window to search.
2.99 +
2.100 + last = periods[-1].get_end()
2.101 +
2.102 + # Replace the periods in the object.
2.103 +
2.104 + obj = handler.obj.copy()
2.105 + changed = handler.obj.set_periods(periods)
2.106 +
2.107 + # Check one last time, reverting the change if not scheduled.
2.108 +
2.109 + scheduled = schedule_in_freebusy(handler)
2.110 +
2.111 + if scheduled == "DECLINED":
2.112 + handler.set_object(obj)
2.113 +
2.114 + return scheduled == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED"
2.115 +
2.116 # Registry of scheduling functions.
2.117
2.118 scheduling_functions = {
2.119 "schedule_in_freebusy" : schedule_in_freebusy,
2.120 "schedule_corrected_in_freebusy" : schedule_corrected_in_freebusy,
2.121 + "schedule_next_available_in_freebusy" : schedule_next_available_in_freebusy,
2.122 }
2.123
2.124 # vim: tabstop=4 expandtab shiftwidth=4
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3.2 +++ b/tests/templates/event-request-sauna-busy.txt Tue Oct 27 00:26:59 2015 +0100
3.3 @@ -0,0 +1,37 @@
3.4 +Content-Type: multipart/alternative; boundary="===============0047278175=="
3.5 +MIME-Version: 1.0
3.6 +From: vincent.vole@example.com
3.7 +To: resource-room-sauna@example.com
3.8 +Subject: Invitation!
3.9 +
3.10 +--===============0047278175==
3.11 +Content-Type: text/plain; charset="us-ascii"
3.12 +MIME-Version: 1.0
3.13 +Content-Transfer-Encoding: 7bit
3.14 +
3.15 +This message contains an event. This books the sauna every other hour for
3.16 +five occasions.
3.17 +
3.18 +--===============0047278175==
3.19 +MIME-Version: 1.0
3.20 +Content-Transfer-Encoding: 7bit
3.21 +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST"
3.22 +
3.23 +BEGIN:VCALENDAR
3.24 +PRODID:-//imip-agent/test//EN
3.25 +METHOD:REQUEST
3.26 +VERSION:2.0
3.27 +BEGIN:VEVENT
3.28 +ORGANIZER:mailto:vincent.vole@example.com
3.29 +ATTENDEE;ROLE=CHAIR:mailto:vincent.vole@example.com
3.30 +ATTENDEE;RSVP=TRUE:mailto:resource-room-sauna@example.com
3.31 +DTSTAMP:20141125T004600Z
3.32 +DTSTART;TZID=Europe/Oslo:20141126T160000
3.33 +DTEND;TZID=Europe/Oslo:20141126T170000
3.34 +RRULE:FREQ=HOURLY;COUNT=5;INTERVAL=2
3.35 +SUMMARY:Meetings from 4pm
3.36 +UID:event19@example.com
3.37 +END:VEVENT
3.38 +END:VCALENDAR
3.39 +
3.40 +--===============0047278175==--
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
4.2 +++ b/tests/test_resource_invitation_constraints_next_free.sh Tue Oct 27 00:26:59 2015 +0100
4.3 @@ -0,0 +1,168 @@
4.4 +#!/bin/sh
4.5 +
4.6 +THIS_DIR=`dirname $0`
4.7 +
4.8 +TEMPLATES="$THIS_DIR/templates"
4.9 +PERSON_SCRIPT="$THIS_DIR/../imip_person.py"
4.10 +RESOURCE_SCRIPT="$THIS_DIR/../imip_resource.py"
4.11 +SHOWMAIL="$THIS_DIR/../tools/showmail.py"
4.12 +STORE=/tmp/store
4.13 +STATIC=/tmp/static
4.14 +PREFS=/tmp/prefs
4.15 +ARGS="-S $STORE -P $STATIC -p $PREFS -d"
4.16 +USER="mailto:resource-room-sauna@example.com"
4.17 +SENDER="mailto:paul.boddie@example.com"
4.18 +RIVALSENDER="mailto:vincent.vole@example.com"
4.19 +FBFILE="$STORE/$USER/freebusy"
4.20 +FBOFFERFILE="$STORE/$USER/freebusy-offers"
4.21 +FBSENDERFILE="$STORE/$SENDER/freebusy"
4.22 +FBSENDEROTHERFILE="$STORE/$SENDER/freebusy-other/$USER"
4.23 +FBSENDERREQUESTS="$STORE/$SENDER/requests"
4.24 +FBRIVALSENDERFILE="$STORE/$RIVALSENDER/freebusy"
4.25 +TAB=`printf '\t'`
4.26 +
4.27 +OUTGOING_SCRIPT="$THIS_DIR/../imip_person_outgoing.py"
4.28 +
4.29 +ERROR=err.tmp
4.30 +
4.31 +rm -r $STORE
4.32 +rm -r $STATIC
4.33 +rm -r $PREFS
4.34 +rm $ERROR
4.35 +rm out*.tmp
4.36 +
4.37 +mkdir -p "$PREFS/$USER"
4.38 +echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
4.39 +echo 'share' > "$PREFS/$USER/freebusy_sharing"
4.40 +echo 'schedule_next_available_in_freebusy' > "$PREFS/$USER/scheduling_function"
4.41 +echo 'PT60S' > "$PREFS/$USER/freebusy_offers"
4.42 +
4.43 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-sauna-all.txt" 2>> $ERROR \
4.44 +| "$SHOWMAIL" \
4.45 +> out0.tmp
4.46 +
4.47 + grep -q 'METHOD:REPLY' out0.tmp \
4.48 +&& ! grep -q '^FREEBUSY' out0.tmp \
4.49 +&& echo "Success" \
4.50 +|| echo "Failed"
4.51 +
4.52 +# Let the rival sender book the resource.
4.53 +
4.54 +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-busy.txt" 2>> $ERROR
4.55 +
4.56 + [ `grep "event19@example.com" "$FBRIVALSENDERFILE" | wc -l` = '5' ] \
4.57 +&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBRIVALSENDERFILE" \
4.58 +&& echo "Success" \
4.59 +|| echo "Failed"
4.60 +
4.61 +# Present the request to the resource.
4.62 +
4.63 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-busy.txt" 2>> $ERROR \
4.64 +| tee out1r.tmp \
4.65 +| "$SHOWMAIL" \
4.66 +> out1.tmp
4.67 +
4.68 + grep -q 'METHOD:REPLY' out1.tmp \
4.69 +&& grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T160000' out1.tmp \
4.70 +&& echo "Success" \
4.71 +|| echo "Failed"
4.72 +
4.73 + [ `grep "event19@example.com" "$FBFILE" | wc -l` = '5' ] \
4.74 +&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE" \
4.75 +&& echo "Success" \
4.76 +|| echo "Failed"
4.77 +
4.78 +# Present the response to the rival sender.
4.79 +
4.80 + "$PERSON_SCRIPT" $ARGS < out1r.tmp 2>> $ERROR \
4.81 +| tee out2r.tmp \
4.82 +| "$SHOWMAIL" \
4.83 +> out2.tmp
4.84 +
4.85 +# Attempt to schedule an event with the resource.
4.86 +
4.87 +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-good.txt" 2>> $ERROR
4.88 +
4.89 + grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
4.90 +&& echo "Success" \
4.91 +|| echo "Failed"
4.92 +
4.93 +# Present the request to the resource. This should cause the event to be
4.94 +# proposed one hour later.
4.95 +
4.96 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-good.txt" 2>> $ERROR \
4.97 +| tee out6r.tmp \
4.98 +| "$SHOWMAIL" \
4.99 +> out6.tmp
4.100 +
4.101 + grep -q 'METHOD:COUNTER' out6.tmp \
4.102 +&& ! grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out6.tmp \
4.103 +&& echo "Success" \
4.104 +|| echo "Failed"
4.105 +
4.106 + ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBOFFERFILE" \
4.107 +&& grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBOFFERFILE" \
4.108 +&& echo "Success" \
4.109 +|| echo "Failed"
4.110 +
4.111 +# Present the response to the organiser.
4.112 +
4.113 + "$PERSON_SCRIPT" $ARGS < out6r.tmp 2>> $ERROR \
4.114 +| tee out7r.tmp \
4.115 +| "$SHOWMAIL" \
4.116 +> out7.tmp
4.117 +
4.118 + grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
4.119 +&& ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBSENDERFILE" \
4.120 +&& echo "Success" \
4.121 +|| echo "Failed"
4.122 +
4.123 + [ -e "$STORE/$SENDER/counters/objects/event13@example.com/$USER" ] \
4.124 +&& echo "Success" \
4.125 +|| echo "Failed"
4.126 +
4.127 + grep -q 'event13@example.com' "$FBSENDERREQUESTS" \
4.128 +&& echo "Success" \
4.129 +|| echo "Failed"
4.130 +
4.131 +# Reschedule the event by accepting the counter-proposal.
4.132 +
4.133 + sed 's/COUNTER/REQUEST/' < out6.tmp \
4.134 +| sed 's/^From: calendar/To: resource-room-sauna/' \
4.135 +| sed 's/^To: paul.boddie/From: paul.boddie/' \
4.136 +> out8.tmp
4.137 +
4.138 +"$OUTGOING_SCRIPT" $ARGS < out8.tmp 2>> $ERROR
4.139 +
4.140 + ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
4.141 +&& grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBSENDERFILE" \
4.142 +&& echo "Success" \
4.143 +|| echo "Failed"
4.144 +
4.145 + ! [ -e "$STORE/$SENDER/counters/objects/event13@example.com/$USER" ] \
4.146 +&& echo "Success" \
4.147 +|| echo "Failed"
4.148 +
4.149 + ! grep -q 'event13@example.com' "$FBSENDERREQUESTS" \
4.150 +&& echo "Success" \
4.151 +|| echo "Failed"
4.152 +
4.153 + "$RESOURCE_SCRIPT" $ARGS < out8.tmp 2>> $ERROR \
4.154 +| "$SHOWMAIL" \
4.155 +> out9.tmp
4.156 +
4.157 + grep -q 'METHOD:REPLY' out9.tmp \
4.158 +&& grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T170000' out9.tmp \
4.159 +&& echo "Success" \
4.160 +|| echo "Failed"
4.161 +
4.162 + ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBOFFERFILE" \
4.163 +&& ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBOFFERFILE" \
4.164 +&& echo "Success" \
4.165 +|| echo "Failed"
4.166 +
4.167 + [ `grep "event19@example.com" "$FBFILE" | wc -l` = '5' ] \
4.168 +&& [ `grep "event13@example.com" "$FBFILE" | wc -l` = '1' ] \
4.169 +&& grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBFILE" \
4.170 +&& echo "Success" \
4.171 +|| echo "Failed"