# HG changeset patch # User Paul Boddie # Date 1445902019 -3600 # Node ID eb7a1208adad82f557305a49ad4275dac6d5f266 # Parent 4d0a8c6e750c782eac184e88d3cf5ab874461290 Added support for scheduling event periods in the next available free period if they cannot be scheduled normally. diff -r 4d0a8c6e750c -r eb7a1208adad docs/preferences.txt --- a/docs/preferences.txt Tue Oct 27 00:05:46 2015 +0100 +++ b/docs/preferences.txt Tue Oct 27 00:26:59 2015 +0100 @@ -206,16 +206,21 @@ period for an event. The imiptools.handlers.scheduling module contains the built-in scheduling functions which include the following: - schedule_in_freebusy accept an invitation if the event periods - are free according to the free/busy - records for the resource; decline - otherwise + schedule_in_freebusy accept an invitation if the event periods are free + according to the free/busy records for the resource; + decline otherwise schedule_corrected_in_freebusy correct periods in an event according to the permitted_times setting (see above), - then attempt to schedule the event - according to the free/busy records for the - resource + then attempt to schedule the event according to the + free/busy records for the resource + + schedule_next_available_in_freebusy correct periods in an event according + to the permitted_times setting (see + above), if configured, and attempt to schedule the + event according to the free/busy records for the + resource, seeking the next available free period for + each period that conflicts with an existing event The scheduling mechanism can be extended by implementing additional scheduling functions or by extending the handler framework directly. diff -r 4d0a8c6e750c -r eb7a1208adad imiptools/handlers/scheduling.py --- a/imiptools/handlers/scheduling.py Tue Oct 27 00:05:46 2015 +0100 +++ b/imiptools/handlers/scheduling.py Tue Oct 27 00:26:59 2015 +0100 @@ -19,7 +19,8 @@ this program. If not, see . """ -from imiptools.dates import ValidityError +from imiptools.dates import ValidityError, to_timezone +from imiptools.period import invert_freebusy, periods_from def schedule_in_freebusy(handler): @@ -80,11 +81,111 @@ return scheduled == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED" +def schedule_next_available_in_freebusy(handler): + + """ + Attempt to schedule the current object of the given 'handler', correcting + specified datetimes according to the configuration of a resource, then + suggesting the next available period in the free/busy records if scheduling + cannot occur for the requested period, returning an indication of the kind + of response to be returned. + """ + + scheduled = schedule_corrected_in_freebusy(handler) + + if scheduled in ("ACCEPTED", "COUNTER"): + return scheduled + + # Find free periods, update the object with the details. + + freebusy = handler.store.get_freebusy(handler.user) + free = invert_freebusy(freebusy) + permitted_values = handler.get_permitted_values() + periods = [] + + # Do not attempt to redefine rule-based periods. + + last = None + + for period in handler.get_periods(handler.obj, explicit_only=True): + duration = period.get_duration() + + # Try and schedule periods normally since some of them may be + # compatible with the schedule. + + if permitted_values: + period = period.get_corrected(permitted_values) + + scheduled = handler.can_schedule(freebusy, [period]) + + if scheduled == "ACCEPTED": + periods.append(period) + last = period.get_end() + continue + + # Get free periods from the time of each period. + + for found in periods_from(free, period): + + # Skip any periods before the last period. + + if last: + if last > found.get_end(): + continue + + # Adjust the start of the free period to exclude the last period. + + found = found.make_corrected(max(found.get_start(), last), found.get_end()) + + # Only test free periods long enough to hold the requested period. + + if found.get_duration() >= duration: + + # Obtain a possible period, starting at the found point and + # with the requested duration. Then, correct the period if + # necessary. + + start = to_timezone(found.get_start(), period.get_tzid()) + possible = period.make_corrected(start, start + period.get_duration()) + if permitted_values: + possible = possible.get_corrected(permitted_values) + + # Only if the possible period is still within the free period + # can it be used. + + if possible.within(found): + periods.append(possible) + break + + # Where no period can be found, decline the invitation. + + else: + return "DECLINED" + + # Use the found period to set the start of the next window to search. + + last = periods[-1].get_end() + + # Replace the periods in the object. + + obj = handler.obj.copy() + changed = handler.obj.set_periods(periods) + + # Check one last time, reverting the change if not scheduled. + + scheduled = schedule_in_freebusy(handler) + + if scheduled == "DECLINED": + handler.set_object(obj) + + return scheduled == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED" + # Registry of scheduling functions. scheduling_functions = { "schedule_in_freebusy" : schedule_in_freebusy, "schedule_corrected_in_freebusy" : schedule_corrected_in_freebusy, + "schedule_next_available_in_freebusy" : schedule_next_available_in_freebusy, } # vim: tabstop=4 expandtab shiftwidth=4 diff -r 4d0a8c6e750c -r eb7a1208adad tests/templates/event-request-sauna-busy.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/templates/event-request-sauna-busy.txt Tue Oct 27 00:26:59 2015 +0100 @@ -0,0 +1,37 @@ +Content-Type: multipart/alternative; boundary="===============0047278175==" +MIME-Version: 1.0 +From: vincent.vole@example.com +To: resource-room-sauna@example.com +Subject: Invitation! + +--===============0047278175== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +This message contains an event. This books the sauna every other hour for +five occasions. + +--===============0047278175== +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST" + +BEGIN:VCALENDAR +PRODID:-//imip-agent/test//EN +METHOD:REQUEST +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:vincent.vole@example.com +ATTENDEE;ROLE=CHAIR:mailto:vincent.vole@example.com +ATTENDEE;RSVP=TRUE:mailto:resource-room-sauna@example.com +DTSTAMP:20141125T004600Z +DTSTART;TZID=Europe/Oslo:20141126T160000 +DTEND;TZID=Europe/Oslo:20141126T170000 +RRULE:FREQ=HOURLY;COUNT=5;INTERVAL=2 +SUMMARY:Meetings from 4pm +UID:event19@example.com +END:VEVENT +END:VCALENDAR + +--===============0047278175==-- diff -r 4d0a8c6e750c -r eb7a1208adad tests/test_resource_invitation_constraints_next_free.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_resource_invitation_constraints_next_free.sh Tue Oct 27 00:26:59 2015 +0100 @@ -0,0 +1,168 @@ +#!/bin/sh + +THIS_DIR=`dirname $0` + +TEMPLATES="$THIS_DIR/templates" +PERSON_SCRIPT="$THIS_DIR/../imip_person.py" +RESOURCE_SCRIPT="$THIS_DIR/../imip_resource.py" +SHOWMAIL="$THIS_DIR/../tools/showmail.py" +STORE=/tmp/store +STATIC=/tmp/static +PREFS=/tmp/prefs +ARGS="-S $STORE -P $STATIC -p $PREFS -d" +USER="mailto:resource-room-sauna@example.com" +SENDER="mailto:paul.boddie@example.com" +RIVALSENDER="mailto:vincent.vole@example.com" +FBFILE="$STORE/$USER/freebusy" +FBOFFERFILE="$STORE/$USER/freebusy-offers" +FBSENDERFILE="$STORE/$SENDER/freebusy" +FBSENDEROTHERFILE="$STORE/$SENDER/freebusy-other/$USER" +FBSENDERREQUESTS="$STORE/$SENDER/requests" +FBRIVALSENDERFILE="$STORE/$RIVALSENDER/freebusy" +TAB=`printf '\t'` + +OUTGOING_SCRIPT="$THIS_DIR/../imip_person_outgoing.py" + +ERROR=err.tmp + +rm -r $STORE +rm -r $STATIC +rm -r $PREFS +rm $ERROR +rm out*.tmp + +mkdir -p "$PREFS/$USER" +echo 'Europe/Oslo' > "$PREFS/$USER/TZID" +echo 'share' > "$PREFS/$USER/freebusy_sharing" +echo 'schedule_next_available_in_freebusy' > "$PREFS/$USER/scheduling_function" +echo 'PT60S' > "$PREFS/$USER/freebusy_offers" + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-sauna-all.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out0.tmp + + grep -q 'METHOD:REPLY' out0.tmp \ +&& ! grep -q '^FREEBUSY' out0.tmp \ +&& echo "Success" \ +|| echo "Failed" + +# Let the rival sender book the resource. + +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-busy.txt" 2>> $ERROR + + [ `grep "event19@example.com" "$FBRIVALSENDERFILE" | wc -l` = '5' ] \ +&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBRIVALSENDERFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the resource. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-busy.txt" 2>> $ERROR \ +| tee out1r.tmp \ +| "$SHOWMAIL" \ +> out1.tmp + + grep -q 'METHOD:REPLY' out1.tmp \ +&& grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T160000' out1.tmp \ +&& echo "Success" \ +|| echo "Failed" + + [ `grep "event19@example.com" "$FBFILE" | wc -l` = '5' ] \ +&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the response to the rival sender. + + "$PERSON_SCRIPT" $ARGS < out1r.tmp 2>> $ERROR \ +| tee out2r.tmp \ +| "$SHOWMAIL" \ +> out2.tmp + +# Attempt to schedule an event with the resource. + +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-good.txt" 2>> $ERROR + + grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the resource. This should cause the event to be +# proposed one hour later. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-good.txt" 2>> $ERROR \ +| tee out6r.tmp \ +| "$SHOWMAIL" \ +> out6.tmp + + grep -q 'METHOD:COUNTER' out6.tmp \ +&& ! grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out6.tmp \ +&& echo "Success" \ +|| echo "Failed" + + ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBOFFERFILE" \ +&& grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBOFFERFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the response to the organiser. + + "$PERSON_SCRIPT" $ARGS < out6r.tmp 2>> $ERROR \ +| tee out7r.tmp \ +| "$SHOWMAIL" \ +> out7.tmp + + grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \ +&& ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBSENDERFILE" \ +&& echo "Success" \ +|| echo "Failed" + + [ -e "$STORE/$SENDER/counters/objects/event13@example.com/$USER" ] \ +&& echo "Success" \ +|| echo "Failed" + + grep -q 'event13@example.com' "$FBSENDERREQUESTS" \ +&& echo "Success" \ +|| echo "Failed" + +# Reschedule the event by accepting the counter-proposal. + + sed 's/COUNTER/REQUEST/' < out6.tmp \ +| sed 's/^From: calendar/To: resource-room-sauna/' \ +| sed 's/^To: paul.boddie/From: paul.boddie/' \ +> out8.tmp + +"$OUTGOING_SCRIPT" $ARGS < out8.tmp 2>> $ERROR + + ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \ +&& grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBSENDERFILE" \ +&& echo "Success" \ +|| echo "Failed" + + ! [ -e "$STORE/$SENDER/counters/objects/event13@example.com/$USER" ] \ +&& echo "Success" \ +|| echo "Failed" + + ! grep -q 'event13@example.com' "$FBSENDERREQUESTS" \ +&& echo "Success" \ +|| echo "Failed" + + "$RESOURCE_SCRIPT" $ARGS < out8.tmp 2>> $ERROR \ +| "$SHOWMAIL" \ +> out9.tmp + + grep -q 'METHOD:REPLY' out9.tmp \ +&& grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T170000' out9.tmp \ +&& echo "Success" \ +|| echo "Failed" + + ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBOFFERFILE" \ +&& ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBOFFERFILE" \ +&& echo "Success" \ +|| echo "Failed" + + [ `grep "event19@example.com" "$FBFILE" | wc -l` = '5' ] \ +&& [ `grep "event13@example.com" "$FBFILE" | wc -l` = '1' ] \ +&& grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBFILE" \ +&& echo "Success" \ +|| echo "Failed"