1.1 --- a/imiptools/client.py Sat Aug 01 01:25:21 2015 +0200
1.2 +++ b/imiptools/client.py Sat Aug 01 01:35:03 2015 +0200
1.3 @@ -24,8 +24,7 @@
1.4 is_new_object, make_freebusy, to_part, \
1.5 uri_dict, uri_items, uri_values
1.6 from imiptools.dates import format_datetime, get_default_timezone, \
1.7 - get_recurrence_start_point, get_timestamp, \
1.8 - to_timezone
1.9 + get_timestamp, to_timezone
1.10 from imiptools.period import can_schedule, remove_period, \
1.11 remove_additional_periods, remove_affected_period, \
1.12 update_freebusy
1.13 @@ -413,8 +412,7 @@
1.14
1.15 "Get 'recurrenceid' in a form suitable for matching free/busy entries."
1.16
1.17 - tzid = self.obj.get_tzid() or self.get_tzid()
1.18 - return get_recurrence_start_point(recurrenceid, tzid)
1.19 + return self.obj.get_recurrence_start_point(recurrenceid, self.get_tzid())
1.20
1.21 def remove_from_freebusy(self, freebusy):
1.22
2.1 --- a/imiptools/data.py Sat Aug 01 01:25:21 2015 +0200
2.2 +++ b/imiptools/data.py Sat Aug 01 01:35:03 2015 +0200
2.3 @@ -26,7 +26,8 @@
2.4 get_datetime_item as get_item_from_datetime, \
2.5 get_datetime_tzid, \
2.6 get_duration, get_period, \
2.7 - get_tzid, to_timezone, to_utc_datetime
2.8 + get_recurrence_start_point, \
2.9 + get_tzid, to_datetime, to_timezone, to_utc_datetime
2.10 from imiptools.period import Period, RecurringPeriod, period_overlaps
2.11 from vCalendar import iterwrite, parse, ParseError, to_dict, to_node
2.12 from vRecurrence import get_parameters, get_rule
2.13 @@ -51,12 +52,44 @@
2.14
2.15 """
2.16 Return the recurrence identifier, normalised to a UTC datetime if
2.17 - specified as a datetime, converted to a date object otherwise. If no
2.18 - recurrence identifier is present, None is returned.
2.19 + specified as a datetime or date with accompanying time zone information,
2.20 + maintained as a date or floating datetime otherwise. If no recurrence
2.21 + identifier is present, None is returned.
2.22 +
2.23 + Note that this normalised form of the identifier may well not be the
2.24 + same as the originally-specified identifier because that could have been
2.25 + specified using an accompanying TZID attribute, whereas the normalised
2.26 + form is effectively a converted datetime value.
2.27 """
2.28
2.29 - recurrenceid = self.get_utc_datetime("RECURRENCE-ID")
2.30 - return recurrenceid and format_datetime(recurrenceid)
2.31 + if not self.has_key("RECURRENCE-ID"):
2.32 + return None
2.33 + dt, attr = self.get_datetime_item("RECURRENCE-ID")
2.34 + tzid = attr.get("TZID")
2.35 + if tzid:
2.36 + dt = to_timezone(to_datetime(dt, tzid), "UTC")
2.37 + return format_datetime(dt)
2.38 +
2.39 + def get_recurrence_start_point(self, recurrenceid, tzid):
2.40 +
2.41 + """
2.42 + Return the start point corresponding to the given 'recurrenceid', using
2.43 + the fallback 'tzid' to define the specific point in time referenced by
2.44 + the recurrence identifier if the identifier has a date representation.
2.45 +
2.46 + If 'recurrenceid' is given as None, this object's recurrence identifier
2.47 + is used to obtain a start point, but if this object does not provide a
2.48 + recurrence, None is returned.
2.49 +
2.50 + A start point is typically used to match free/busy periods which are
2.51 + themselves defined in terms of UTC datetimes.
2.52 + """
2.53 +
2.54 + recurrenceid = recurrenceid or self.get_recurrenceid()
2.55 + if recurrenceid:
2.56 + return get_recurrence_start_point(recurrenceid, tzid)
2.57 + else:
2.58 + return None
2.59
2.60 # Structure access.
2.61
2.62 @@ -94,20 +127,6 @@
2.63 dt, attr = t
2.64 return dt
2.65
2.66 - def set_datetime(self, name, dt, tzid):
2.67 -
2.68 - """
2.69 - Set a datetime for property 'name' using 'dt' and 'tzid', returning
2.70 - whether an update has occurred.
2.71 - """
2.72 -
2.73 - if dt:
2.74 - old_value = self.get_value(name)
2.75 - self[name] = [get_item_from_datetime(dt, tzid)]
2.76 - return format_datetime(dt) != old_value
2.77 -
2.78 - return False
2.79 -
2.80 def get_datetime_item(self, name):
2.81 return get_datetime_item(self.details, name)
2.82
2.83 @@ -159,14 +178,6 @@
2.84
2.85 return get_periods(self, tzid, end)
2.86
2.87 - def set_period(self, period):
2.88 -
2.89 - "Set the given 'period' as the main start and end."
2.90 -
2.91 - result = self.set_datetime("DTSTART", period.get_start(), period.start_attr().get("TZID"))
2.92 - result = self.set_datetime("DTEND", period.get_end(), period.end_attr().get("TZID")) or result
2.93 - return result
2.94 -
2.95 def get_tzid(self):
2.96
2.97 """
2.98 @@ -189,6 +200,54 @@
2.99
2.100 return self.get_value("SEQUENCE") is not None
2.101
2.102 + # Modification methods.
2.103 +
2.104 + def set_datetime(self, name, dt, tzid=None):
2.105 +
2.106 + """
2.107 + Set a datetime for property 'name' using 'dt' and the optional fallback
2.108 + 'tzid', returning whether an update has occurred.
2.109 + """
2.110 +
2.111 + if dt:
2.112 + old_value = self.get_value(name)
2.113 + self[name] = [get_item_from_datetime(dt, tzid)]
2.114 + return format_datetime(dt) != old_value
2.115 +
2.116 + return False
2.117 +
2.118 + def set_period(self, period):
2.119 +
2.120 + "Set the given 'period' as the main start and end."
2.121 +
2.122 + result = self.set_datetime("DTSTART", period.get_start())
2.123 + result = self.set_datetime("DTEND", period.get_end()) or result
2.124 + return result
2.125 +
2.126 + def set_periods(self, periods):
2.127 +
2.128 + """
2.129 + Set the given 'periods' as recurrence date properties, replacing the
2.130 + previous RDATE properties and ignoring any RRULE properties.
2.131 + """
2.132 +
2.133 + update = False
2.134 +
2.135 + old_values = self.get_values("RDATE")
2.136 + new_rdates = []
2.137 +
2.138 + if self.has_key("RDATE"):
2.139 + del self["RDATE"]
2.140 +
2.141 + for p in periods:
2.142 + if p.origin != "RRULE":
2.143 + new_rdates.append(get_period_item(p.get_start(), p.get_end()))
2.144 +
2.145 + self["RDATE"] = new_rdates
2.146 +
2.147 + # NOTE: To do: calculate the update status.
2.148 + return update
2.149 +
2.150 # Construction and serialisation.
2.151
2.152 def make_calendar(nodes, method=None):
3.1 --- a/imiptools/dates.py Sat Aug 01 01:25:21 2015 +0200
3.2 +++ b/imiptools/dates.py Sat Aug 01 01:35:03 2015 +0200
3.3 @@ -273,6 +273,15 @@
3.4 else:
3.5 return None
3.6
3.7 +def get_period_tzid(start, end):
3.8 +
3.9 + "Return the time zone identifier for 'start' and 'end' or None if unknown."
3.10 +
3.11 + if isinstance(start, datetime) or isinstance(end, datetime):
3.12 + return get_datetime_tzid(start) or get_datetime_tzid(end)
3.13 + else:
3.14 + return None
3.15 +
3.16 def to_date(dt):
3.17
3.18 "Return the date of 'dt'."
3.19 @@ -375,8 +384,6 @@
3.20 else:
3.21 return {"VALUE" : "DATE"}
3.22
3.23 - return {}
3.24 -
3.25 def get_datetime_item(dt, tzid=None):
3.26
3.27 """
3.28 @@ -392,11 +399,15 @@
3.29 attr = get_datetime_attributes(dt, tzid)
3.30 return value, attr
3.31
3.32 -def get_period_attributes(tzid=None):
3.33 +def get_period_attributes(start, end, tzid=None):
3.34
3.35 - "Return attributes for 'tzid'."
3.36 + """
3.37 + Return attributes for the 'start' and 'end' datetime objects with 'tzid'
3.38 + indicating the time zone if not otherwise defined.
3.39 + """
3.40
3.41 attr = {"VALUE" : "PERIOD"}
3.42 + tzid = get_period_tzid(start, end) or tzid
3.43 if tzid:
3.44 attr["TZID"] = tzid
3.45 return attr
3.46 @@ -408,17 +419,14 @@
3.47 'tzid'.
3.48 """
3.49
3.50 - start = start and to_timezone(start, tzid)
3.51 - end = end and to_timezone(end, tzid)
3.52 -
3.53 - start_value = start and format_datetime(start) or None
3.54 - end_value = end and format_datetime(end) or None
3.55 -
3.56 if start and end:
3.57 - attr = get_period_attributes(tzid)
3.58 + attr = get_period_attributes(start, end, tzid)
3.59 + start_value = format_datetime(to_timezone(start, attr.get("TZID")))
3.60 + end_value = format_datetime(to_timezone(end, attr.get("TZID")))
3.61 return "%s/%s" % (start_value, end_value), attr
3.62 elif start:
3.63 attr = get_datetime_attributes(start, tzid)
3.64 + start_value = format_datetime(to_timezone(start, attr.get("TZID")))
3.65 return start_value, attr
3.66 else:
3.67 return None, None
3.68 @@ -442,7 +450,9 @@
3.69
3.70 """
3.71 Return 'recurrenceid' in a form suitable for comparison with period start
3.72 - dates or datetimes.
3.73 + dates or datetimes. The 'recurrenceid' should be an identifier normalised to
3.74 + a UTC datetime or employing a date or floating datetime representation where
3.75 + no time zone information was originally provided.
3.76 """
3.77
3.78 return get_datetime(recurrenceid)
3.79 @@ -452,26 +462,11 @@
3.80 """
3.81 Return 'recurrenceid' in a form suitable for comparison with free/busy start
3.82 datetimes, using 'tzid' to convert recurrence identifiers that are dates.
3.83 + The 'recurrenceid' should be an identifier normalised to a UTC datetime or
3.84 + employing a date or floating datetime representation where no time zone
3.85 + information was originally provided.
3.86 """
3.87
3.88 return to_utc_datetime(get_datetime(recurrenceid), tzid)
3.89
3.90 -def to_recurrence_start(recurrenceid):
3.91 -
3.92 - """
3.93 - Return 'recurrenceid' in a form suitable for use as an unambiguous
3.94 - identifier.
3.95 - """
3.96 -
3.97 - return format_datetime(get_recurrence_start(recurrenceid))
3.98 -
3.99 -def to_recurrence_start_point(recurrenceid, tzid):
3.100 -
3.101 - """
3.102 - Return 'recurrenceid' in a form suitable for use as an unambiguous
3.103 - identifier, using 'tzid' to convert recurrence identifiers that are dates.
3.104 - """
3.105 -
3.106 - return format_datetime(get_recurrence_start_point(recurrenceid, tzid))
3.107 -
3.108 # vim: tabstop=4 expandtab shiftwidth=4
4.1 --- a/imiptools/period.py Sat Aug 01 01:25:21 2015 +0200
4.2 +++ b/imiptools/period.py Sat Aug 01 01:35:03 2015 +0200
4.3 @@ -189,7 +189,8 @@
4.4
4.5 """
4.6 Return whether 'period' refers to one of the 'recurrenceids', interpreted
4.7 - using 'tzid' if necessary.
4.8 + using 'tzid' if necessary. The 'recurrenceids' should be normalised to UTC
4.9 + datetimes according to time zone information provided by their objects.
4.10 """
4.11
4.12 for s in recurrenceids:
4.13 @@ -200,8 +201,11 @@
4.14 def is_affected(period, recurrenceid, tzid):
4.15
4.16 """
4.17 - Return whether 'period' refer to 'recurrenceid', interpreted using 'tzid' if
4.18 - necessary.
4.19 + Return whether 'period' refers to 'recurrenceid', interpreted using 'tzid'
4.20 + if necessary. The 'recurrenceid' should be normalised to UTC datetimes
4.21 + according to time zone information provided by their objects. Otherwise,
4.22 + 'tzid' is used to convert any date or floating datetime representation to a
4.23 + point in time.
4.24 """
4.25
4.26 if not recurrenceid:
5.1 --- a/imipweb/client.py Sat Aug 01 01:25:21 2015 +0200
5.2 +++ b/imipweb/client.py Sat Aug 01 01:35:03 2015 +0200
5.3 @@ -127,7 +127,7 @@
5.4 for p in to_unschedule:
5.5 if not p.origin:
5.6 continue
5.7 - obj["RECURRENCE-ID"] = [(format_datetime(p.get_start()), {})]
5.8 + obj["RECURRENCE-ID"] = [p.get_start_item()]
5.9 parts.append(obj.to_part("CANCEL"))
5.10
5.11 # Send the updated event, along with a cancellation for each of the
6.1 --- a/imipweb/event.py Sat Aug 01 01:25:21 2015 +0200
6.2 +++ b/imipweb/event.py Sat Aug 01 01:35:03 2015 +0200
6.3 @@ -22,7 +22,7 @@
6.4 from datetime import date, timedelta
6.5 from imiptools.data import get_uri, uri_dict, uri_values
6.6 from imiptools.dates import format_datetime, get_datetime_item, \
6.7 - get_period_item, to_date, to_timezone
6.8 + to_date, to_timezone
6.9 from imiptools.mail import Messenger
6.10 from imiptools.period import have_conflict
6.11 from imipweb.data import EventPeriod, \
6.12 @@ -166,7 +166,7 @@
6.13 to_unschedule = self.get_removed_periods()
6.14
6.15 obj.set_period(period)
6.16 - self.set_periods_in_object(obj, periods)
6.17 + obj.set_periods(periods)
6.18
6.19 # Update summary.
6.20
6.21 @@ -234,27 +234,6 @@
6.22
6.23 return None
6.24
6.25 - def set_periods_in_object(self, obj, periods):
6.26 -
6.27 - "Set in the given 'obj' the given 'periods'."
6.28 -
6.29 - update = False
6.30 -
6.31 - old_values = obj.get_values("RDATE")
6.32 - new_rdates = []
6.33 -
6.34 - if obj.has_key("RDATE"):
6.35 - del obj["RDATE"]
6.36 -
6.37 - for p in periods:
6.38 - if p.origin != "RRULE":
6.39 - new_rdates.append(get_period_item(p.get_start(), p.get_end(), p.get_tzid()))
6.40 -
6.41 - obj["RDATE"] = new_rdates
6.42 -
6.43 - # NOTE: To do: calculate the update status.
6.44 - return update
6.45 -
6.46 def handle_main_period(self):
6.47
6.48 "Return period details for the main start/end period in an event."
7.1 --- a/imipweb/resource.py Sat Aug 01 01:25:21 2015 +0200
7.2 +++ b/imipweb/resource.py Sat Aug 01 01:35:03 2015 +0200
7.3 @@ -214,10 +214,9 @@
7.4
7.5 # Subtract any recurrences from the free/busy details of a parent
7.6 # object.
7.7 - # NOTE: The time zone may need obtaining using the object details.
7.8
7.9 for recurrenceid in self._get_recurrences(uid):
7.10 - remove_affected_period(freebusy, uid, get_recurrence_start_point(recurrenceid, self.get_tzid()))
7.11 + remove_affected_period(freebusy, uid, obj.get_recurrence_start_point(recurrenceid, self.get_tzid()))
7.12
7.13 self.store.set_freebusy(self.user, freebusy)
7.14 self.publish_freebusy(freebusy)
8.1 --- a/tests/templates/event-request-recurring-reschedule-instance.txt Sat Aug 01 01:25:21 2015 +0200
8.2 +++ b/tests/templates/event-request-recurring-reschedule-instance.txt Sat Aug 01 01:35:03 2015 +0200
8.3 @@ -2,7 +2,7 @@
8.4 MIME-Version: 1.0
8.5 From: paul.boddie@example.com
8.6 To: resource-room-confroom@example.com
8.7 -Subject: Invitation!
8.8 +Subject: Rescheduling!
8.9
8.10 --===============0047278175==
8.11 Content-Type: text/plain; charset="us-ascii"
9.1 --- a/tests/test_resource_invitation_recurring_day.sh Sat Aug 01 01:25:21 2015 +0200
9.2 +++ b/tests/test_resource_invitation_recurring_day.sh Sat Aug 01 01:35:03 2015 +0200
9.3 @@ -43,3 +43,22 @@
9.4 && grep -q 'FREEBUSY;FBTYPE=BUSY:20141211T230000Z/20141212T230000Z' out2.tmp \
9.5 && echo "Success" \
9.6 || echo "Failed"
9.7 +
9.8 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-recurring-day-reschedule-instance.txt" 2>> $ERROR \
9.9 +| "$SHOWMAIL" \
9.10 +> out3.tmp
9.11 +
9.12 + grep -q 'METHOD:REPLY' out3.tmp \
9.13 +&& grep -q 'ATTENDEE;PARTSTAT=ACCEPTED' out3.tmp \
9.14 +&& echo "Success" \
9.15 +|| echo "Failed"
9.16 +
9.17 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-all.txt" 2>> $ERROR \
9.18 +| "$SHOWMAIL" \
9.19 +> out4.tmp
9.20 +
9.21 + grep -q 'METHOD:REPLY' out4.tmp \
9.22 +&& grep -q 'FREEBUSY;FBTYPE=BUSY:20141114T230000Z/20141115T230000Z' out4.tmp \
9.23 +&& ! grep -q 'FREEBUSY;FBTYPE=BUSY:20141113T230000Z/20141114T230000Z' out4.tmp \
9.24 +&& echo "Success" \
9.25 +|| echo "Failed"
10.1 --- a/tools/make_freebusy.py Sat Aug 01 01:25:21 2015 +0200
10.2 +++ b/tools/make_freebusy.py Sat Aug 01 01:35:03 2015 +0200
10.3 @@ -22,7 +22,7 @@
10.4 """
10.5
10.6 from imiptools.data import get_window_end, Object
10.7 -from imiptools.dates import format_datetime, get_default_timezone, to_recurrence_start
10.8 +from imiptools.dates import get_default_timezone
10.9 from imiptools.period import FreeBusyPeriod, is_replaced
10.10 from imiptools.profile import Preferences
10.11 from imip_store import FileStore, FilePublisher
10.12 @@ -36,7 +36,6 @@
10.13 """
10.14
10.15 recurrenceid = obj.get_recurrenceid()
10.16 - recurrenceids = [to_recurrence_start(r) for r in recurrenceids]
10.17
10.18 for p in obj.get_periods(tzid, window_end):
10.19 if recurrenceid or not is_replaced(p, recurrenceids, tzid):