1.1 --- a/imiptools/client.py Wed Aug 12 22:40:50 2015 +0200
1.2 +++ b/imiptools/client.py Fri Aug 14 00:44:53 2015 +0200
1.3 @@ -81,6 +81,39 @@
1.4 prefs = self.get_preferences()
1.5 return prefs and prefs.get("freebusy_messages") == "notify" or False
1.6
1.7 + def get_scheduling_resolution(self):
1.8 +
1.9 + """
1.10 + Decode a specification of one of the following forms...
1.11 +
1.12 + <minute values>
1.13 + <hour values>:<minute values>
1.14 + <hour values>:<minute values>:<second values>
1.15 +
1.16 + ...with each list of values being comma-separated.
1.17 + """
1.18 +
1.19 + prefs = self.get_preferences()
1.20 + resolution = prefs and prefs.get("scheduling_resolution")
1.21 + if resolution:
1.22 + try:
1.23 + l = []
1.24 + for component in resolution.split(":")[:3]:
1.25 + if component:
1.26 + l.append(map(int, component.split(",")))
1.27 + else:
1.28 + l.append(None)
1.29 +
1.30 + # NOTE: Should probably report an error somehow.
1.31 +
1.32 + except ValueError:
1.33 + return None
1.34 + else:
1.35 + l = (len(l) < 2 and [None] or []) + l + (len(l) < 3 and [None] or [])
1.36 + return l
1.37 + else:
1.38 + return None
1.39 +
1.40 # Common operations on calendar data.
1.41
1.42 def update_attendees(self, obj, attendees, removed):
1.43 @@ -377,6 +410,50 @@
1.44
1.45 return False
1.46
1.47 + # Constraint application on event periods.
1.48 +
1.49 + class ValidityError(Exception):
1.50 + pass
1.51 +
1.52 + def check_object(self):
1.53 +
1.54 + "Check the object against any scheduling constraints."
1.55 +
1.56 + resolution = self.get_scheduling_resolution()
1.57 + if not resolution:
1.58 + return None
1.59 +
1.60 + tzid = self.get_tzid()
1.61 + invalid = []
1.62 +
1.63 + for period in self.obj.get_periods(tzid):
1.64 + start = period.get_start()
1.65 + end = period.get_end()
1.66 + start_result = self.check_resolution(start, resolution)
1.67 + end_result = self.check_resolution(end, resolution)
1.68 + if start_result or end_result:
1.69 + invalid.append((period.origin, start_result, end_result))
1.70 +
1.71 + return invalid
1.72 +
1.73 + def check_resolution(self, dt, resolution):
1.74 +
1.75 + "Check the datetime 'dt' against the 'resolution' list."
1.76 +
1.77 + if not isinstance(dt, datetime):
1.78 + raise ValidityError
1.79 +
1.80 + hours, minutes, seconds = resolution
1.81 +
1.82 + if hours and dt.hour not in hours:
1.83 + return "hour"
1.84 + if minutes and dt.minute not in minutes:
1.85 + return "minute"
1.86 + if seconds and dt.second not in seconds:
1.87 + return "second"
1.88 +
1.89 + return None
1.90 +
1.91 # Object retrieval.
1.92
1.93 def get_stored_object_version(self):
2.1 --- a/imiptools/handlers/resource.py Wed Aug 12 22:40:50 2015 +0200
2.2 +++ b/imiptools/handlers/resource.py Fri Aug 14 00:44:53 2015 +0200
2.3 @@ -61,19 +61,41 @@
2.4 def _schedule_for_attendee(self):
2.5
2.6 """
2.7 - Schedule the current object for the current user.
2.8 + Attempt to schedule the current object for the current user.
2.9 """
2.10
2.11 - # Interpretation of periods can depend on the time zone.
2.12 + # Check any constraints on the request.
2.13 +
2.14 + try:
2.15 + check = self.check_object()
2.16 +
2.17 + # NOTE: Support countering by correcting any invalid values and
2.18 + # NOTE: attempting to schedule using the corrected values.
2.19
2.20 - tzid = self.get_tzid()
2.21 + if check:
2.22 + raise self.ValidityError
2.23 +
2.24 + # Refuse to schedule obviously invalid requests.
2.25 +
2.26 + except self.ValidityError:
2.27 + scheduled = False
2.28
2.29 - # If newer than any old version, discard old details from the
2.30 - # free/busy record and check for suitability.
2.31 + # With a valid request, determine whether the event can be scheduled.
2.32 +
2.33 + else:
2.34 + # Interpretation of periods can depend on the time zone.
2.35 +
2.36 + tzid = self.get_tzid()
2.37
2.38 - periods = self.obj.get_periods(tzid, self.get_window_end())
2.39 - freebusy = self.store.get_freebusy(self.user)
2.40 - scheduled = self.can_schedule(freebusy, periods)
2.41 + # If newer than any old version, discard old details from the
2.42 + # free/busy record and check for suitability.
2.43 +
2.44 + periods = self.obj.get_periods(tzid, self.get_window_end())
2.45 + freebusy = self.store.get_freebusy(self.user)
2.46 + scheduled = self.can_schedule(freebusy, periods)
2.47 +
2.48 + # NOTE: Support countering by finding the next available slot if
2.49 + # NOTE: the event cannot be scheduled.
2.50
2.51 # Update the participation of the resource in the object.
2.52
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3.2 +++ b/tests/templates/event-request-sauna-bad.txt Fri Aug 14 00:44:53 2015 +0200
3.3 @@ -0,0 +1,34 @@
3.4 +Content-Type: multipart/alternative; boundary="===============0047278175=="
3.5 +MIME-Version: 1.0
3.6 +From: paul.boddie@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.
3.16 +--===============0047278175==
3.17 +MIME-Version: 1.0
3.18 +Content-Transfer-Encoding: 7bit
3.19 +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST"
3.20 +
3.21 +BEGIN:VCALENDAR
3.22 +PRODID:-//imip-agent/test//EN
3.23 +METHOD:REQUEST
3.24 +VERSION:2.0
3.25 +BEGIN:VEVENT
3.26 +ORGANIZER:mailto:paul.boddie@example.com
3.27 +ATTENDEE;ROLE=CHAIR:mailto:paul.boddie@example.com
3.28 +ATTENDEE;RSVP=TRUE:mailto:resource-room-sauna@example.com
3.29 +DTSTAMP:20141125T004600Z
3.30 +DTSTART;TZID=Europe/Oslo:20141126T161000
3.31 +DTEND;TZID=Europe/Oslo:20141126T170000
3.32 +SUMMARY:Meeting at 4:10pm
3.33 +UID:event13@example.com
3.34 +END:VEVENT
3.35 +END:VCALENDAR
3.36 +
3.37 +--===============0047278175==--
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
4.2 +++ b/tests/templates/event-request-sauna-good.txt Fri Aug 14 00:44:53 2015 +0200
4.3 @@ -0,0 +1,34 @@
4.4 +Content-Type: multipart/alternative; boundary="===============0047278175=="
4.5 +MIME-Version: 1.0
4.6 +From: paul.boddie@example.com
4.7 +To: resource-room-sauna@example.com
4.8 +Subject: Invitation!
4.9 +
4.10 +--===============0047278175==
4.11 +Content-Type: text/plain; charset="us-ascii"
4.12 +MIME-Version: 1.0
4.13 +Content-Transfer-Encoding: 7bit
4.14 +
4.15 +This message contains an event.
4.16 +--===============0047278175==
4.17 +MIME-Version: 1.0
4.18 +Content-Transfer-Encoding: 7bit
4.19 +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST"
4.20 +
4.21 +BEGIN:VCALENDAR
4.22 +PRODID:-//imip-agent/test//EN
4.23 +METHOD:REQUEST
4.24 +VERSION:2.0
4.25 +BEGIN:VEVENT
4.26 +ORGANIZER:mailto:paul.boddie@example.com
4.27 +ATTENDEE;ROLE=CHAIR:mailto:paul.boddie@example.com
4.28 +ATTENDEE;RSVP=TRUE:mailto:resource-room-sauna@example.com
4.29 +DTSTAMP:20141125T004600Z
4.30 +DTSTART;TZID=Europe/Oslo:20141126T160000
4.31 +DTEND;TZID=Europe/Oslo:20141126T161500
4.32 +SUMMARY:Meeting at 4pm
4.33 +UID:event13@example.com
4.34 +END:VEVENT
4.35 +END:VCALENDAR
4.36 +
4.37 +--===============0047278175==--
5.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
5.2 +++ b/tests/templates/fb-request-sauna-all.txt Fri Aug 14 00:44:53 2015 +0200
5.3 @@ -0,0 +1,29 @@
5.4 +Content-Type: multipart/alternative; boundary="===============0945993647=="
5.5 +MIME-Version: 1.0
5.6 +From: paul.boddie@example.com
5.7 +To: resource-room-sauna@example.com
5.8 +
5.9 +--===============0945993647==
5.10 +Content-Type: text/plain; charset="us-ascii"
5.11 +MIME-Version: 1.0
5.12 +Content-Transfer-Encoding: 7bit
5.13 +
5.14 +This message contains a free/busy request.
5.15 +--===============0945993647==
5.16 +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST"
5.17 +MIME-Version: 1.0
5.18 +Content-Transfer-Encoding: 7bit
5.19 +
5.20 +BEGIN:VCALENDAR
5.21 +PRODID:-//imip-agent/test//EN
5.22 +METHOD:REQUEST
5.23 +VERSION:2.0
5.24 +BEGIN:VFREEBUSY
5.25 +ORGANIZER:mailto:paul.boddie@example.com
5.26 +ATTENDEE:mailto:resource-room-sauna@example.com
5.27 +DTSTAMP:20141125T164400Z
5.28 +UID:fb2@example.com
5.29 +END:VFREEBUSY
5.30 +END:VCALENDAR
5.31 +
5.32 +--===============0945993647==--
6.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
6.2 +++ b/tests/test_resource_invitation_constraints.sh Fri Aug 14 00:44:53 2015 +0200
6.3 @@ -0,0 +1,69 @@
6.4 +#!/bin/sh
6.5 +
6.6 +THIS_DIR=`dirname $0`
6.7 +
6.8 +TEMPLATES="$THIS_DIR/templates"
6.9 +RESOURCE_SCRIPT="$THIS_DIR/../imip_resource.py"
6.10 +SHOWMAIL="$THIS_DIR/../tools/showmail.py"
6.11 +STORE=/tmp/store
6.12 +STATIC=/tmp/static
6.13 +PREFS=/tmp/prefs
6.14 +ARGS="-S $STORE -P $STATIC -p $PREFS -d"
6.15 +USER="mailto:resource-room-sauna@example.com"
6.16 +ERROR=err.tmp
6.17 +
6.18 +rm -r $STORE
6.19 +rm -r $STATIC
6.20 +rm -r $PREFS
6.21 +rm $ERROR
6.22 +rm out*.tmp
6.23 +
6.24 +mkdir -p "$PREFS/$USER"
6.25 +echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
6.26 +echo 'share' > "$PREFS/$USER/freebusy_sharing"
6.27 +echo '10,12,14,16,18:0,15,30,45' > "$PREFS/$USER/scheduling_resolution"
6.28 +
6.29 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-sauna-all.txt" 2>> $ERROR \
6.30 +| "$SHOWMAIL" \
6.31 +> out0.tmp
6.32 +
6.33 + grep -q 'METHOD:REPLY' out0.tmp \
6.34 +&& ! grep -q '^FREEBUSY' out0.tmp \
6.35 +&& echo "Success" \
6.36 +|| echo "Failed"
6.37 +
6.38 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-bad.txt" 2>> $ERROR \
6.39 +| "$SHOWMAIL" \
6.40 +> out2.tmp
6.41 +
6.42 + grep -q 'METHOD:REPLY' out2.tmp \
6.43 +&& grep -q 'ATTENDEE;PARTSTAT=DECLINED' out2.tmp \
6.44 +&& echo "Success" \
6.45 +|| echo "Failed"
6.46 +
6.47 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-sauna-all.txt" 2>> $ERROR \
6.48 +| "$SHOWMAIL" \
6.49 +> out3.tmp
6.50 +
6.51 + grep -q 'METHOD:REPLY' out3.tmp \
6.52 +&& ! grep -q 'FREEBUSY;FBTYPE=BUSY:20141126T151000Z/20141126T160000Z' out3.tmp \
6.53 +&& echo "Success" \
6.54 +|| echo "Failed"
6.55 +
6.56 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-good.txt" 2>> $ERROR \
6.57 +| "$SHOWMAIL" \
6.58 +> out4.tmp
6.59 +
6.60 + grep -q 'METHOD:REPLY' out4.tmp \
6.61 +&& grep -q 'ATTENDEE;PARTSTAT=ACCEPTED' out4.tmp \
6.62 +&& echo "Success" \
6.63 +|| echo "Failed"
6.64 +
6.65 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-sauna-all.txt" 2>> $ERROR \
6.66 +| "$SHOWMAIL" \
6.67 +> out6.tmp
6.68 +
6.69 + grep -q 'METHOD:REPLY' out6.tmp \
6.70 +&& grep -q 'FREEBUSY;FBTYPE=BUSY:20141126T150000Z/20141126T151500Z' out6.tmp \
6.71 +&& echo "Success" \
6.72 +|| echo "Failed"