# HG changeset patch # User Paul Boddie # Date 1439505893 -7200 # Node ID 7e307171a752ac856c9ffdf50862fc81e93aa5b0 # Parent e5f8a8e266c1609da850a3129e484e4767b81bfb Added tentative support for datetime resolution constraints. diff -r e5f8a8e266c1 -r 7e307171a752 imiptools/client.py --- a/imiptools/client.py Wed Aug 12 22:40:50 2015 +0200 +++ b/imiptools/client.py Fri Aug 14 00:44:53 2015 +0200 @@ -81,6 +81,39 @@ prefs = self.get_preferences() return prefs and prefs.get("freebusy_messages") == "notify" or False + def get_scheduling_resolution(self): + + """ + Decode a specification of one of the following forms... + + + : + :: + + ...with each list of values being comma-separated. + """ + + prefs = self.get_preferences() + resolution = prefs and prefs.get("scheduling_resolution") + if resolution: + try: + l = [] + for component in resolution.split(":")[:3]: + if component: + l.append(map(int, component.split(","))) + else: + l.append(None) + + # NOTE: Should probably report an error somehow. + + except ValueError: + return None + else: + l = (len(l) < 2 and [None] or []) + l + (len(l) < 3 and [None] or []) + return l + else: + return None + # Common operations on calendar data. def update_attendees(self, obj, attendees, removed): @@ -377,6 +410,50 @@ return False + # Constraint application on event periods. + + class ValidityError(Exception): + pass + + def check_object(self): + + "Check the object against any scheduling constraints." + + resolution = self.get_scheduling_resolution() + if not resolution: + return None + + tzid = self.get_tzid() + invalid = [] + + for period in self.obj.get_periods(tzid): + start = period.get_start() + end = period.get_end() + start_result = self.check_resolution(start, resolution) + end_result = self.check_resolution(end, resolution) + if start_result or end_result: + invalid.append((period.origin, start_result, end_result)) + + return invalid + + def check_resolution(self, dt, resolution): + + "Check the datetime 'dt' against the 'resolution' list." + + if not isinstance(dt, datetime): + raise ValidityError + + hours, minutes, seconds = resolution + + if hours and dt.hour not in hours: + return "hour" + if minutes and dt.minute not in minutes: + return "minute" + if seconds and dt.second not in seconds: + return "second" + + return None + # Object retrieval. def get_stored_object_version(self): diff -r e5f8a8e266c1 -r 7e307171a752 imiptools/handlers/resource.py --- a/imiptools/handlers/resource.py Wed Aug 12 22:40:50 2015 +0200 +++ b/imiptools/handlers/resource.py Fri Aug 14 00:44:53 2015 +0200 @@ -61,19 +61,41 @@ def _schedule_for_attendee(self): """ - Schedule the current object for the current user. + Attempt to schedule the current object for the current user. """ - # Interpretation of periods can depend on the time zone. + # Check any constraints on the request. + + try: + check = self.check_object() + + # NOTE: Support countering by correcting any invalid values and + # NOTE: attempting to schedule using the corrected values. - tzid = self.get_tzid() + if check: + raise self.ValidityError + + # Refuse to schedule obviously invalid requests. + + except self.ValidityError: + scheduled = False - # If newer than any old version, discard old details from the - # free/busy record and check for suitability. + # With a valid request, determine whether the event can be scheduled. + + else: + # Interpretation of periods can depend on the time zone. + + tzid = self.get_tzid() - periods = self.obj.get_periods(tzid, self.get_window_end()) - freebusy = self.store.get_freebusy(self.user) - scheduled = self.can_schedule(freebusy, periods) + # If newer than any old version, discard old details from the + # free/busy record and check for suitability. + + periods = self.obj.get_periods(tzid, self.get_window_end()) + freebusy = self.store.get_freebusy(self.user) + scheduled = self.can_schedule(freebusy, periods) + + # NOTE: Support countering by finding the next available slot if + # NOTE: the event cannot be scheduled. # Update the participation of the resource in the object. diff -r e5f8a8e266c1 -r 7e307171a752 tests/templates/event-request-sauna-bad.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/templates/event-request-sauna-bad.txt Fri Aug 14 00:44:53 2015 +0200 @@ -0,0 +1,34 @@ +Content-Type: multipart/alternative; boundary="===============0047278175==" +MIME-Version: 1.0 +From: paul.boddie@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. +--===============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:paul.boddie@example.com +ATTENDEE;ROLE=CHAIR:mailto:paul.boddie@example.com +ATTENDEE;RSVP=TRUE:mailto:resource-room-sauna@example.com +DTSTAMP:20141125T004600Z +DTSTART;TZID=Europe/Oslo:20141126T161000 +DTEND;TZID=Europe/Oslo:20141126T170000 +SUMMARY:Meeting at 4:10pm +UID:event13@example.com +END:VEVENT +END:VCALENDAR + +--===============0047278175==-- diff -r e5f8a8e266c1 -r 7e307171a752 tests/templates/event-request-sauna-good.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/templates/event-request-sauna-good.txt Fri Aug 14 00:44:53 2015 +0200 @@ -0,0 +1,34 @@ +Content-Type: multipart/alternative; boundary="===============0047278175==" +MIME-Version: 1.0 +From: paul.boddie@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. +--===============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:paul.boddie@example.com +ATTENDEE;ROLE=CHAIR:mailto:paul.boddie@example.com +ATTENDEE;RSVP=TRUE:mailto:resource-room-sauna@example.com +DTSTAMP:20141125T004600Z +DTSTART;TZID=Europe/Oslo:20141126T160000 +DTEND;TZID=Europe/Oslo:20141126T161500 +SUMMARY:Meeting at 4pm +UID:event13@example.com +END:VEVENT +END:VCALENDAR + +--===============0047278175==-- diff -r e5f8a8e266c1 -r 7e307171a752 tests/templates/fb-request-sauna-all.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/templates/fb-request-sauna-all.txt Fri Aug 14 00:44:53 2015 +0200 @@ -0,0 +1,29 @@ +Content-Type: multipart/alternative; boundary="===============0945993647==" +MIME-Version: 1.0 +From: paul.boddie@example.com +To: resource-room-sauna@example.com + +--===============0945993647== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +This message contains a free/busy request. +--===============0945993647== +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +BEGIN:VCALENDAR +PRODID:-//imip-agent/test//EN +METHOD:REQUEST +VERSION:2.0 +BEGIN:VFREEBUSY +ORGANIZER:mailto:paul.boddie@example.com +ATTENDEE:mailto:resource-room-sauna@example.com +DTSTAMP:20141125T164400Z +UID:fb2@example.com +END:VFREEBUSY +END:VCALENDAR + +--===============0945993647==-- diff -r e5f8a8e266c1 -r 7e307171a752 tests/test_resource_invitation_constraints.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_resource_invitation_constraints.sh Fri Aug 14 00:44:53 2015 +0200 @@ -0,0 +1,69 @@ +#!/bin/sh + +THIS_DIR=`dirname $0` + +TEMPLATES="$THIS_DIR/templates" +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" +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 '10,12,14,16,18:0,15,30,45' > "$PREFS/$USER/scheduling_resolution" + + "$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" + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-bad.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out2.tmp + + grep -q 'METHOD:REPLY' out2.tmp \ +&& grep -q 'ATTENDEE;PARTSTAT=DECLINED' out2.tmp \ +&& echo "Success" \ +|| echo "Failed" + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-sauna-all.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out3.tmp + + grep -q 'METHOD:REPLY' out3.tmp \ +&& ! grep -q 'FREEBUSY;FBTYPE=BUSY:20141126T151000Z/20141126T160000Z' out3.tmp \ +&& echo "Success" \ +|| echo "Failed" + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-good.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out4.tmp + + grep -q 'METHOD:REPLY' out4.tmp \ +&& grep -q 'ATTENDEE;PARTSTAT=ACCEPTED' out4.tmp \ +&& echo "Success" \ +|| echo "Failed" + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-sauna-all.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out6.tmp + + grep -q 'METHOD:REPLY' out6.tmp \ +&& grep -q 'FREEBUSY;FBTYPE=BUSY:20141126T150000Z/20141126T151500Z' out6.tmp \ +&& echo "Success" \ +|| echo "Failed"