1.1 --- a/imiptools/client.py Sun Oct 15 23:30:38 2017 +0200
1.2 +++ b/imiptools/client.py Mon Oct 16 18:37:33 2017 +0200
1.3 @@ -264,73 +264,31 @@
1.4 start=(future_only and self.get_window_start() or None),
1.5 end=(not explicit_only and self.get_window_end() or None))
1.6
1.7 - def get_updated_periods(self, obj):
1.8 + def get_updated_periods(self, obj, explicit_only=False, future_only=False):
1.9
1.10 """
1.11 Return the periods provided by 'obj' and associated recurrence
1.12 instances. Each original period is returned in a tuple with a
1.13 corresponding updated period which may be the same or which may be None
1.14 if the period is cancelled. A list of these tuples is returned.
1.15 +
1.16 + If 'explicit_only' is set to a true value, only explicit periods will be
1.17 + returned, not rule-based periods.
1.18 +
1.19 + If 'future_only' is set to a true value, only future periods will be
1.20 + returned, not all periods defined by an event starting in the past.
1.21 """
1.22
1.23 uid = obj.get_uid()
1.24 - recurrenceid = obj.get_recurrenceid()
1.25 -
1.26 - updated = []
1.27 -
1.28 - # Consider separate recurrences in isolation from the parent if
1.29 - # specified.
1.30 -
1.31 - if recurrenceid:
1.32 - for period in self.get_periods(obj):
1.33 - updated.append((period, period))
1.34 - return updated
1.35 -
1.36 - # For parent events, identify retained and replaced periods.
1.37 -
1.38 - recurrenceids = self.get_recurrences(uid)
1.39 -
1.40 - for period in self.get_periods(obj):
1.41 - recurrenceid = period.is_replaced(recurrenceids)
1.42 -
1.43 - # For parent event periods, obtain any replacement instead of the
1.44 - # replaced period.
1.45 -
1.46 - if recurrenceid:
1.47 - recurrence = self.get_stored_object(uid, recurrenceid)
1.48 - periods = recurrence and self.get_periods(recurrence)
1.49 -
1.50 - # Active periods are obtained.
1.51
1.52 - if periods:
1.53 -
1.54 - # Recurrence instances are assumed to provide only one
1.55 - # period.
1.56 -
1.57 - replacement = periods[0]
1.58 -
1.59 - # Redefine the origin of periods replacing recurrences and
1.60 - # not the main period, leaving DTSTART as the means of
1.61 - # identifying the main period.
1.62 -
1.63 - if replacement.origin == "DTSTART" and \
1.64 - period.origin != "DTSTART":
1.65 + if not obj.modifying:
1.66 + obj.set_modifying(self.store.get_active_recurrences(self.user, uid))
1.67 + if not obj.cancelling:
1.68 + obj.set_cancelling(self.store.get_cancelled_recurrences(self.user, uid))
1.69
1.70 - replacement.origin = "DTSTART-RECUR"
1.71 -
1.72 - updated.append((period, replacement))
1.73 -
1.74 - # Cancelled periods yield None.
1.75 -
1.76 - else:
1.77 - updated.append((period, None))
1.78 -
1.79 - # Otherwise, retain the known period.
1.80 -
1.81 - else:
1.82 - updated.append((period, period))
1.83 -
1.84 - return updated
1.85 + return obj.get_updated_periods(
1.86 + start=(future_only and self.get_window_start() or None),
1.87 + end=(not explicit_only and self.get_window_end() or None))
1.88
1.89 def get_main_period(self, obj):
1.90
2.1 --- a/imiptools/data.py Sun Oct 15 23:30:38 2017 +0200
2.2 +++ b/imiptools/data.py Mon Oct 16 18:37:33 2017 +0200
2.3 @@ -78,6 +78,11 @@
2.4 self.objtype, (self.details, self.attr) = fragment.items()[0]
2.5 self.set_tzid(tzid)
2.6
2.7 + # Modify the object with separate recurrences.
2.8 +
2.9 + self.modifying = []
2.10 + self.cancelling = []
2.11 +
2.12 def set_tzid(self, tzid):
2.13
2.14 """
2.15 @@ -87,7 +92,34 @@
2.16
2.17 self.tzid = tzid
2.18
2.19 + def set_modifying(self, modifying):
2.20 +
2.21 + """
2.22 + Set the 'modifying' objects affecting the periods provided by this
2.23 + object. Such modifications can only be performed on a parent object, not
2.24 + a specific recurrence object.
2.25 + """
2.26 +
2.27 + if not self.get_recurrenceid():
2.28 + self.modifying = modifying
2.29 +
2.30 + def set_cancelling(self, cancelling):
2.31 +
2.32 + """
2.33 + Set the 'cancelling' objects affecting the periods provided by this
2.34 + object. Such cancellations can only be performed on a parent object, not
2.35 + a specific recurrence object.
2.36 + """
2.37 +
2.38 + if not self.get_recurrenceid():
2.39 + self.cancelling = cancelling
2.40 +
2.41 + # Basic object identification.
2.42 +
2.43 def get_uid(self):
2.44 +
2.45 + "Return the universal identifier."
2.46 +
2.47 return self.get_value("UID")
2.48
2.49 def get_recurrenceid(self):
2.50 @@ -106,6 +138,7 @@
2.51
2.52 if not self.has_key("RECURRENCE-ID"):
2.53 return None
2.54 +
2.55 dt, attr = self.get_datetime_item("RECURRENCE-ID")
2.56
2.57 # Coerce any date to a UTC datetime if TZID was specified.
2.58 @@ -113,6 +146,7 @@
2.59 tzid = attr.get("TZID")
2.60 if tzid:
2.61 dt = to_timezone(to_datetime(dt, tzid), "UTC")
2.62 +
2.63 return format_datetime(dt)
2.64
2.65 def get_recurrence_start_point(self, recurrenceid):
2.66 @@ -327,35 +361,87 @@
2.67
2.68 return False
2.69
2.70 - def get_active_periods(self, recurrenceids, start=None, end=None):
2.71 + def get_updated_periods(self, start=None, end=None):
2.72
2.73 """
2.74 - Return all periods specified by this object that are not replaced by
2.75 - those defined by 'recurrenceids', using the fallback time zone to
2.76 - convert floating dates and datetimes, and using 'start' and 'end' to
2.77 - respectively indicate the start and end of the time window within which
2.78 - periods are considered.
2.79 + Return pairs of periods specified by this object and any modifying or
2.80 + cancelling objects, providing correspondences between the original
2.81 + period definitions and those applying after modifications and
2.82 + cancellations have been considered.
2.83 +
2.84 + The fallback time zone is used to convert floating dates and datetimes,
2.85 + and 'start' and 'end' respectively indicate the start and end of any
2.86 + time window within which periods are considered.
2.87 """
2.88
2.89 # Specific recurrences yield all specified periods.
2.90
2.91 - periods = self.get_periods(start, end)
2.92 + original = self.get_periods(start, end)
2.93
2.94 if self.get_recurrenceid():
2.95 - return periods
2.96 + return original
2.97
2.98 # Parent objects need to have their periods tested against redefined
2.99 # recurrences.
2.100
2.101 + modified = {}
2.102 +
2.103 + for obj in self.modifying:
2.104 + periods = obj.get_periods(start, end)
2.105 + if periods:
2.106 + modified[obj.get_recurrenceid()] = periods[0]
2.107 +
2.108 + cancelled = set()
2.109 +
2.110 + for obj in self.cancelling:
2.111 + cancelled.add(obj.get_recurrenceid())
2.112 +
2.113 + updated = []
2.114 +
2.115 + for p in original:
2.116 + recurrenceid = p.is_replaced(modified.keys())
2.117 +
2.118 + # Produce an original-to-modified correspondence, setting the origin
2.119 + # to distinguish the period from the main period.
2.120 +
2.121 + if recurrenceid:
2.122 + mp = modified.get(recurrenceid)
2.123 + if mp.origin == "DTSTART" and p.origin != "DTSTART":
2.124 + mp.origin = "DTSTART-RECUR"
2.125 + updated.append((p, mp))
2.126 + break
2.127 +
2.128 + # Produce an original-to-null correspondence where cancellation has
2.129 + # occurred.
2.130 +
2.131 + recurrenceid = p.is_replaced(cancelled)
2.132 +
2.133 + if recurrenceid:
2.134 + updated.append((p, None))
2.135 + break
2.136 +
2.137 + # Produce an identity correspondence where no modification or
2.138 + # cancellation has occurred.
2.139 +
2.140 + updated.append((p, p))
2.141 +
2.142 + return updated
2.143 +
2.144 + def get_active_periods(self, start=None, end=None):
2.145 +
2.146 + """
2.147 + Return all periods specified by this object that are not replaced by
2.148 + those defined by modifying or cancelling objects, using the fallback
2.149 + time zone to convert floating dates and datetimes, and using 'start' and
2.150 + 'end' to respectively indicate the start and end of the time window
2.151 + within which periods are considered.
2.152 + """
2.153 +
2.154 active = []
2.155
2.156 - for p in periods:
2.157 -
2.158 - # Subtract any recurrences from the free/busy details of a
2.159 - # parent object.
2.160 -
2.161 - if not p.is_replaced(recurrenceids):
2.162 - active.append(p)
2.163 + for old, new in self.get_updated_periods(start, end):
2.164 + if new:
2.165 + active.append(new)
2.166
2.167 return active
2.168
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3.2 +++ b/tests/internal/objects.py Mon Oct 16 18:37:33 2017 +0200
3.3 @@ -0,0 +1,100 @@
3.4 +#!/usr/bin/env python
3.5 +
3.6 +"""
3.7 +Test objects.
3.8 +
3.9 +Copyright (C) 2017 Paul Boddie <paul@boddie.org.uk>
3.10 +
3.11 +This program is free software; you can redistribute it and/or modify it under
3.12 +the terms of the GNU General Public License as published by the Free Software
3.13 +Foundation; either version 3 of the License, or (at your option) any later
3.14 +version.
3.15 +
3.16 +This program is distributed in the hope that it will be useful, but WITHOUT
3.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
3.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
3.19 +details.
3.20 +
3.21 +You should have received a copy of the GNU General Public License along with
3.22 +this program. If not, see <http://www.gnu.org/licenses/>.
3.23 +"""
3.24 +
3.25 +from imiptools.data import parse_string, Object
3.26 +from imiptools.dates import get_datetime
3.27 +
3.28 +# Define a parent object plus recurrence instances.
3.29 +
3.30 +parent_str = """\
3.31 +BEGIN:VEVENT
3.32 +ORGANIZER:mailto:paul.boddie@example.com
3.33 +ATTENDEE;RSVP=TRUE:mailto:resource-room-confroom@example.com
3.34 +DTSTAMP:20141009T182400Z
3.35 +DTSTART;TZID=Europe/Oslo:20141010
3.36 +DTEND;TZID=Europe/Oslo:20141011
3.37 +RRULE:FREQ=MONTHLY;BYDAY=2FR;COUNT=3
3.38 +SUMMARY:Recurring event
3.39 +UID:event4@example.com
3.40 +END:VEVENT
3.41 +"""
3.42 +
3.43 +modification_str = """\
3.44 +BEGIN:VEVENT
3.45 +ORGANIZER:mailto:paul.boddie@example.com
3.46 +ATTENDEE;RSVP=TRUE:mailto:resource-room-confroom@example.com
3.47 +DTSTAMP:20141009T182500Z
3.48 +DTSTART;TZID=Europe/Oslo:20141115
3.49 +DTEND;TZID=Europe/Oslo:20141116
3.50 +SUMMARY:Recurring event
3.51 +UID:event4@example.com
3.52 +RECURRENCE-ID;TZID=Europe/Oslo:20141114
3.53 +SEQUENCE:2
3.54 +END:VEVENT
3.55 +"""
3.56 +
3.57 +cancellation_str = """\
3.58 +BEGIN:VEVENT
3.59 +ORGANIZER:mailto:paul.boddie@example.com
3.60 +ATTENDEE;RSVP=TRUE:mailto:resource-room-confroom@example.com
3.61 +DTSTAMP:20141009T182500Z
3.62 +DTSTART;TZID=Europe/Oslo:20141213
3.63 +DTEND;TZID=Europe/Oslo:20141214
3.64 +SUMMARY:Recurring event
3.65 +UID:event4@example.com
3.66 +RECURRENCE-ID;TZID=Europe/Oslo:20141213
3.67 +SEQUENCE:2
3.68 +END:VEVENT
3.69 +"""
3.70 +
3.71 +# Parse the objects.
3.72 +
3.73 +parent = Object(parse_string(parent_str, "utf-8"))
3.74 +modification = Object(parse_string(modification_str, "utf-8"))
3.75 +cancellation = Object(parse_string(cancellation_str, "utf-8"))
3.76 +
3.77 +# Set the recurrence objects in the parent.
3.78 +
3.79 +parent.set_modifying([modification])
3.80 +parent.set_cancelling([cancellation])
3.81 +
3.82 +# Obtain periods within a window of time.
3.83 +
3.84 +tzid = parent.get_tzid()
3.85 +start = get_datetime("20141001T000000", {"TZID" : tzid})
3.86 +end = get_datetime("20151001", {"TZID" : tzid})
3.87 +
3.88 +for p in parent.get_periods(start, end):
3.89 + print p
3.90 +
3.91 +print "----"
3.92 +
3.93 +for old, new in parent.get_updated_periods(start, end):
3.94 + print old
3.95 + print new
3.96 + print
3.97 +
3.98 +print "----"
3.99 +
3.100 +for p in parent.get_active_periods(start, end):
3.101 + print p
3.102 +
3.103 +# vim: tabstop=4 expandtab shiftwidth=4