1.1 --- a/imip_store.py Tue Feb 09 15:57:23 2016 +0100
1.2 +++ b/imip_store.py Wed Mar 02 21:17:11 2016 +0100
1.3 @@ -24,7 +24,7 @@
1.4 from imiptools.data import make_calendar, parse_object, to_stream
1.5 from imiptools.dates import format_datetime, get_datetime, to_timezone
1.6 from imiptools.filesys import fix_permissions, FileBase
1.7 -from imiptools.period import FreeBusyPeriod
1.8 +from imiptools.period import FreeBusyPeriod, FreeBusyCollection
1.9 from imiptools.text import parse_line
1.10 from os.path import isdir, isfile, join
1.11 from os import listdir, remove, rmdir
1.12 @@ -577,23 +577,29 @@
1.13 "Get free/busy details for the given 'user'."
1.14
1.15 filename = self.get_object_in_store(user, name or "freebusy")
1.16 +
1.17 if not filename or not isfile(filename):
1.18 - return []
1.19 + periods = []
1.20 else:
1.21 - return map(lambda t: FreeBusyPeriod(*t),
1.22 + periods = map(lambda t: FreeBusyPeriod(*t),
1.23 (get_table or self._get_table_atomic)(user, filename, [(4, None)]))
1.24
1.25 + return FreeBusyCollection(periods)
1.26 +
1.27 def get_freebusy_for_other(self, user, other, get_table=None):
1.28
1.29 "For the given 'user', get free/busy details for the 'other' user."
1.30
1.31 filename = self.get_object_in_store(user, "freebusy-other", other)
1.32 +
1.33 if not filename or not isfile(filename):
1.34 - return []
1.35 + periods = []
1.36 else:
1.37 - return map(lambda t: FreeBusyPeriod(*t),
1.38 + periods = map(lambda t: FreeBusyPeriod(*t),
1.39 (get_table or self._get_table_atomic)(user, filename, [(4, None)]))
1.40
1.41 + return FreeBusyCollection(periods)
1.42 +
1.43 def set_freebusy(self, user, freebusy, name=None, set_table=None):
1.44
1.45 "For the given 'user', set 'freebusy' details."
1.46 @@ -603,7 +609,7 @@
1.47 return False
1.48
1.49 (set_table or self._set_table_atomic)(user, filename,
1.50 - map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
1.51 + map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
1.52 return True
1.53
1.54 def set_freebusy_for_other(self, user, freebusy, other, set_table=None):
1.55 @@ -615,7 +621,7 @@
1.56 return False
1.57
1.58 (set_table or self._set_table_atomic)(user, filename,
1.59 - map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
1.60 + map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
1.61 return True
1.62
1.63 # Tentative free/busy periods related to countering.
1.64 @@ -644,7 +650,7 @@
1.65 finally:
1.66 self.release_lock(user)
1.67
1.68 - return offers
1.69 + return FreeBusyCollection(offers)
1.70
1.71 def set_freebusy_offers(self, user, freebusy):
1.72
1.73 @@ -1011,11 +1017,14 @@
1.74 "Get free/busy details for the given 'quota' and 'user'."
1.75
1.76 filename = self.get_object_in_store(quota, "freebusy", user)
1.77 - if not filename or not isfile(filename):
1.78 - return []
1.79
1.80 - return map(lambda t: FreeBusyPeriod(*t),
1.81 - (get_table or self._get_table_atomic)(quota, filename, [(4, None)]))
1.82 + if not filename or not isfile(filename):
1.83 + periods = []
1.84 + else:
1.85 + periods = map(lambda t: FreeBusyPeriod(*t),
1.86 + (get_table or self._get_table_atomic)(quota, filename, [(4, None)]))
1.87 +
1.88 + return FreeBusyCollection(periods)
1.89
1.90 def set_freebusy(self, quota, user, freebusy, set_table=None):
1.91
1.92 @@ -1026,7 +1035,7 @@
1.93 return False
1.94
1.95 (set_table or self._set_table_atomic)(quota, filename,
1.96 - map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
1.97 + map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
1.98 return True
1.99
1.100 # Journal entry methods.
1.101 @@ -1039,11 +1048,14 @@
1.102 """
1.103
1.104 filename = self.get_object_in_store(quota, "journal", group)
1.105 - if not filename or not isfile(filename):
1.106 - return []
1.107
1.108 - return map(lambda t: FreeBusyPeriod(*t),
1.109 - self._get_table_atomic(quota, filename, [(4, None)]))
1.110 + if not filename or not isfile(filename):
1.111 + periods = []
1.112 + else:
1.113 + periods = map(lambda t: FreeBusyPeriod(*t),
1.114 + self._get_table_atomic(quota, filename, [(4, None)]))
1.115 +
1.116 + return FreeBusyCollection(periods)
1.117
1.118 def set_entries(self, quota, group, entries):
1.119
1.120 @@ -1057,7 +1069,7 @@
1.121 return False
1.122
1.123 self._set_table_atomic(quota, filename,
1.124 - map(lambda fb: fb.as_tuple(strings_only=True), entries))
1.125 + map(lambda fb: fb.as_tuple(strings_only=True), entries.periods))
1.126 return True
1.127
1.128 # vim: tabstop=4 expandtab shiftwidth=4
2.1 --- a/imiptools/client.py Tue Feb 09 15:57:23 2016 +0100
2.2 +++ b/imiptools/client.py Wed Mar 02 21:17:11 2016 +0100
2.3 @@ -27,9 +27,6 @@
2.4 from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \
2.5 get_duration, get_timestamp
2.6 from imiptools.i18n import get_translator
2.7 -from imiptools.period import can_schedule, remove_event_periods, \
2.8 - remove_additional_periods, remove_affected_period, \
2.9 - update_freebusy
2.10 from imiptools.profile import Preferences
2.11 import imip_store
2.12
2.13 @@ -288,7 +285,7 @@
2.14 offer.
2.15 """
2.16
2.17 - update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires)
2.18 + freebusy.update_freebusy(periods, transp, uid, recurrenceid, summary, organiser, expires)
2.19
2.20 # Preparation of messages communicating the state of events.
2.21
2.22 @@ -862,7 +859,7 @@
2.23 Indicate whether within 'freebusy' the given 'periods' can be scheduled.
2.24 """
2.25
2.26 - return can_schedule(freebusy, periods, self.uid, self.recurrenceid)
2.27 + return freebusy.can_schedule(periods, self.uid, self.recurrenceid)
2.28
2.29 def have_new_object(self, strict=True):
2.30
2.31 @@ -993,9 +990,9 @@
2.32
2.33 "Remove this event from the given 'freebusy' collection."
2.34
2.35 - removed = remove_event_periods(freebusy, self.uid, self.recurrenceid)
2.36 + removed = freebusy.remove_event_periods(self.uid, self.recurrenceid)
2.37 if not removed and self.recurrenceid:
2.38 - return remove_affected_period(freebusy, self.uid, self.get_recurrence_start_point(self.recurrenceid))
2.39 + return freebusy.remove_affected_period(self.uid, self.get_recurrence_start_point(self.recurrenceid))
2.40 else:
2.41 return removed
2.42
2.43 @@ -1011,18 +1008,18 @@
2.44
2.45 if self.recurrenceid:
2.46 recurrenceid = self.get_recurrence_start_point(self.recurrenceid)
2.47 - remove_affected_period(freebusy, self.uid, recurrenceid)
2.48 + freebusy.remove_affected_period(self.uid, recurrenceid)
2.49 else:
2.50 # Remove obsolete recurrence periods.
2.51
2.52 - remove_additional_periods(freebusy, self.uid, recurrenceids)
2.53 + freebusy.remove_additional_periods(self.uid, recurrenceids)
2.54
2.55 # Remove original periods affected by additional recurrences.
2.56
2.57 if recurrenceids:
2.58 for recurrenceid in recurrenceids:
2.59 recurrenceid = self.get_recurrence_start_point(recurrenceid)
2.60 - remove_affected_period(freebusy, self.uid, recurrenceid)
2.61 + freebusy.remove_affected_period(self.uid, recurrenceid)
2.62
2.63 def update_freebusy(self, freebusy, user, as_organiser, offer=False):
2.64
3.1 --- a/imiptools/data.py Tue Feb 09 15:57:23 2016 +0100
3.2 +++ b/imiptools/data.py Wed Mar 02 21:17:11 2016 +0100
3.3 @@ -3,7 +3,7 @@
3.4 """
3.5 Interpretation of vCalendar content.
3.6
3.7 -Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
3.8 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
3.9
3.10 This program is free software; you can redistribute it and/or modify it under
3.11 the terms of the GNU General Public License as published by the Free Software
3.12 @@ -29,7 +29,7 @@
3.13 get_recurrence_start_point, \
3.14 get_time, get_tzid, to_datetime, to_timezone, \
3.15 to_utc_datetime
3.16 -from imiptools.period import FreeBusyPeriod, Period, RecurringPeriod, period_overlaps
3.17 +from imiptools.period import FreeBusyPeriod, Period, RecurringPeriod
3.18 from vCalendar import iterwrite, parse, ParseError, to_dict, to_node
3.19 from vRecurrence import get_parameters, get_rule
3.20 import email.utils
3.21 @@ -568,7 +568,7 @@
3.22 # Get a constrained view if start and end limits are specified.
3.23
3.24 if period:
3.25 - periods = period_overlaps(freebusy, period, True)
3.26 + periods = freebusy.period_overlaps(period, True)
3.27 else:
3.28 periods = freebusy
3.29
4.1 --- a/imiptools/handlers/common.py Tue Feb 09 15:57:23 2016 +0100
4.2 +++ b/imiptools/handlers/common.py Wed Mar 02 21:17:11 2016 +0100
4.3 @@ -3,7 +3,7 @@
4.4 """
4.5 Common handler functionality for different entities.
4.6
4.7 -Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
4.8 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
4.9
4.10 This program is free software; you can redistribute it and/or modify it under
4.11 the terms of the GNU General Public License as published by the Free Software
4.12 @@ -22,7 +22,7 @@
4.13 from imiptools.data import get_address, get_uri, make_freebusy, to_part, \
4.14 uri_dict
4.15 from imiptools.dates import format_datetime
4.16 -from imiptools.period import FreeBusyPeriod, Period, replace_overlapping
4.17 +from imiptools.period import FreeBusyPeriod, Period
4.18
4.19 class CommonFreebusy:
4.20
4.21 @@ -59,7 +59,7 @@
4.22
4.23 for sender, sender_attr in senders:
4.24 stored_freebusy = self.store.get_freebusy_for_other(self.user, sender)
4.25 - replace_overlapping(stored_freebusy, period, freebusy)
4.26 + stored_freebusy.replace_overlapping(period, freebusy)
4.27 self.store.set_freebusy_for_other(self.user, stored_freebusy, sender)
4.28
4.29 def request(self):
5.1 --- a/imiptools/handlers/scheduling/freebusy.py Tue Feb 09 15:57:23 2016 +0100
5.2 +++ b/imiptools/handlers/scheduling/freebusy.py Wed Mar 02 21:17:11 2016 +0100
5.3 @@ -21,8 +21,6 @@
5.4
5.5 from imiptools.data import uri_values
5.6 from imiptools.dates import ValidityError, to_timezone
5.7 -from imiptools.period import coalesce_freebusy, invert_freebusy, \
5.8 - periods_from, remove_periods
5.9
5.10 def schedule_in_freebusy(handler, args, freebusy=None):
5.11
5.12 @@ -117,17 +115,16 @@
5.13 if attendee != handler.user:
5.14 freebusy = handler.get_store().get_freebusy_for_other(handler.user, attendee)
5.15 if freebusy:
5.16 - remove_periods(freebusy, event_periods)
5.17 + freebusy.remove_periods(event_periods)
5.18 busy += freebusy
5.19
5.20 # Obtain the combined busy periods.
5.21
5.22 - busy.sort()
5.23 - busy = coalesce_freebusy(busy)
5.24 + busy = busy.coalesce_freebusy()
5.25
5.26 # Obtain free periods.
5.27
5.28 - free = invert_freebusy(busy)
5.29 + free = busy.invert_freebusy()
5.30 permitted_values = handler.get_permitted_values()
5.31 periods = []
5.32
5.33 @@ -153,7 +150,7 @@
5.34
5.35 # Get free periods from the time of each period.
5.36
5.37 - for found in periods_from(free, period):
5.38 + for found in free.periods_from(period):
5.39
5.40 # Skip any periods before the last period.
5.41
6.1 --- a/imiptools/period.py Tue Feb 09 15:57:23 2016 +0100
6.2 +++ b/imiptools/period.py Wed Mar 02 21:17:11 2016 +0100
6.3 @@ -454,280 +454,328 @@
6.4 def make_corrected(self, start, end):
6.5 return self.__class__(start, end, self.tzid, self.origin, self.get_start_attr(), self.get_end_attr())
6.6
6.7 -# Time and period management.
6.8 +class FreeBusyCollection:
6.9 +
6.10 + "An abstraction for a collection of free/busy periods."
6.11 +
6.12 + def __init__(self, periods=None):
6.13 +
6.14 + """
6.15 + Initialise the collection with the given list of 'periods', or start an
6.16 + empty collection if no list is given.
6.17 + """
6.18 +
6.19 + self.periods = periods or []
6.20 +
6.21 + # List emulation methods.
6.22
6.23 -def can_schedule(freebusy, periods, uid, recurrenceid):
6.24 + def __list__(self):
6.25 + return self.periods
6.26 +
6.27 + def __iter__(self):
6.28 + return iter(self.periods)
6.29 +
6.30 + def __len__(self):
6.31 + return len(self.periods)
6.32 +
6.33 + def __getitem__(self, i):
6.34 + return self.periods[i]
6.35 +
6.36 + def __iadd__(self, other):
6.37 + for period in other:
6.38 + self.insert_period(period)
6.39 + return self
6.40 +
6.41 + # Operations.
6.42
6.43 - """
6.44 - Return whether the 'freebusy' list can accommodate the given 'periods'
6.45 - employing the specified 'uid' and 'recurrenceid'.
6.46 - """
6.47 + def can_schedule(self, periods, uid, recurrenceid):
6.48 +
6.49 + """
6.50 + Return whether the collection can accommodate the given 'periods'
6.51 + employing the specified 'uid' and 'recurrenceid'.
6.52 + """
6.53 +
6.54 + for conflict in self.have_conflict(periods, True):
6.55 + if conflict.uid != uid or conflict.recurrenceid != recurrenceid:
6.56 + return False
6.57 +
6.58 + return True
6.59 +
6.60 + def have_conflict(self, periods, get_conflicts=False):
6.61
6.62 - for conflict in have_conflict(freebusy, periods, True):
6.63 - if conflict.uid != uid or conflict.recurrenceid != recurrenceid:
6.64 + """
6.65 + Return whether any period in the collection overlaps with the given
6.66 + 'periods', returning a collection of such overlapping periods if
6.67 + 'get_conflicts' is set to a true value.
6.68 + """
6.69 +
6.70 + conflicts = set()
6.71 + for p in periods:
6.72 + overlapping = self.period_overlaps(p, get_conflicts)
6.73 + if overlapping:
6.74 + if get_conflicts:
6.75 + conflicts.update(overlapping)
6.76 + else:
6.77 + return True
6.78 +
6.79 + if get_conflicts:
6.80 + return conflicts
6.81 + else:
6.82 return False
6.83
6.84 - return True
6.85 + def insert_period(self, period):
6.86
6.87 -def have_conflict(freebusy, periods, get_conflicts=False):
6.88 + "Insert the given 'period' into the collection."
6.89
6.90 - """
6.91 - Return whether any period in 'freebusy' overlaps with the given 'periods',
6.92 - returning a collection of such overlapping periods if 'get_conflicts' is
6.93 - set to a true value.
6.94 - """
6.95 + i = bisect_left(self.periods, period)
6.96 + if i == len(self.periods):
6.97 + self.periods.append(period)
6.98 + elif self.periods[i] != period:
6.99 + self.periods.insert(i, period)
6.100 +
6.101 + def remove_periods(self, periods):
6.102
6.103 - conflicts = set()
6.104 - for p in periods:
6.105 - overlapping = period_overlaps(freebusy, p, get_conflicts)
6.106 - if overlapping:
6.107 - if get_conflicts:
6.108 - conflicts.update(overlapping)
6.109 - else:
6.110 - return True
6.111 + "Remove the given 'periods' from the collection."
6.112 +
6.113 + for period in periods:
6.114 + i = bisect_left(self.periods, period)
6.115 + if i < len(self.periods) and self.periods[i] == period:
6.116 + del self.periods[i]
6.117
6.118 - if get_conflicts:
6.119 - return conflicts
6.120 - else:
6.121 - return False
6.122 + def remove_event_periods(self, uid, recurrenceid=None):
6.123
6.124 -def insert_period(freebusy, period):
6.125 -
6.126 - "Insert into 'freebusy' the given 'period'."
6.127 + """
6.128 + Remove from the collection all periods associated with 'uid' and
6.129 + 'recurrenceid' (which if omitted causes the "parent" object's periods to
6.130 + be referenced).
6.131
6.132 - i = bisect_left(freebusy, period)
6.133 - if i == len(freebusy):
6.134 - freebusy.append(period)
6.135 - elif freebusy[i] != period:
6.136 - freebusy.insert(i, period)
6.137 -
6.138 -def remove_periods(freebusy, periods):
6.139 + Return the removed periods.
6.140 + """
6.141
6.142 - "Remove from 'freebusy' the given 'periods'."
6.143 -
6.144 - for period in periods:
6.145 - i = bisect_left(freebusy, period)
6.146 - if i < len(freebusy) and freebusy[i] == period:
6.147 - del freebusy[i]
6.148 -
6.149 -def remove_event_periods(freebusy, uid, recurrenceid=None):
6.150 + removed = []
6.151 + i = 0
6.152 + while i < len(self.periods):
6.153 + fb = self.periods[i]
6.154 + if fb.uid == uid and fb.recurrenceid == recurrenceid:
6.155 + removed.append(self.periods[i])
6.156 + del self.periods[i]
6.157 + else:
6.158 + i += 1
6.159
6.160 - """
6.161 - Remove from 'freebusy' all periods associated with 'uid' and 'recurrenceid'
6.162 - (which if omitted causes the "parent" object's periods to be referenced).
6.163 + return removed
6.164
6.165 - Return the removed periods.
6.166 - """
6.167 + def remove_additional_periods(self, uid, recurrenceids=None):
6.168
6.169 - removed = []
6.170 - i = 0
6.171 - while i < len(freebusy):
6.172 - fb = freebusy[i]
6.173 - if fb.uid == uid and fb.recurrenceid == recurrenceid:
6.174 - removed.append(freebusy[i])
6.175 - del freebusy[i]
6.176 - else:
6.177 - i += 1
6.178 + """
6.179 + Remove from the collection all periods associated with 'uid' having a
6.180 + recurrence identifier indicating an additional or modified period.
6.181
6.182 - return removed
6.183 + If 'recurrenceids' is specified, remove all periods associated with
6.184 + 'uid' that do not have a recurrence identifier in the given list.
6.185 +
6.186 + Return the removed periods.
6.187 + """
6.188
6.189 -def remove_additional_periods(freebusy, uid, recurrenceids=None):
6.190 -
6.191 - """
6.192 - Remove from 'freebusy' all periods associated with 'uid' having a
6.193 - recurrence identifier indicating an additional or modified period.
6.194 + removed = []
6.195 + i = 0
6.196 + while i < len(self.periods):
6.197 + fb = self.periods[i]
6.198 + if fb.uid == uid and fb.recurrenceid and (
6.199 + recurrenceids is None or
6.200 + recurrenceids is not None and fb.recurrenceid not in recurrenceids
6.201 + ):
6.202 + removed.append(self.periods[i])
6.203 + del self.periods[i]
6.204 + else:
6.205 + i += 1
6.206
6.207 - If 'recurrenceids' is specified, remove all periods associated with 'uid'
6.208 - that do not have a recurrence identifier in the given list.
6.209 + return removed
6.210
6.211 - Return the removed periods.
6.212 - """
6.213 + def remove_affected_period(self, uid, start):
6.214
6.215 - removed = []
6.216 - i = 0
6.217 - while i < len(freebusy):
6.218 - fb = freebusy[i]
6.219 - if fb.uid == uid and fb.recurrenceid and (
6.220 - recurrenceids is None or
6.221 - recurrenceids is not None and fb.recurrenceid not in recurrenceids
6.222 - ):
6.223 - removed.append(freebusy[i])
6.224 - del freebusy[i]
6.225 - else:
6.226 - i += 1
6.227 + """
6.228 + Remove from the collection the period associated with 'uid' that
6.229 + provides an occurrence starting at the given 'start' (provided by a
6.230 + recurrence identifier, converted to a datetime). A recurrence identifier
6.231 + is used to provide an alternative time period whilst also acting as a
6.232 + reference to the originally-defined occurrence.
6.233 +
6.234 + Return any removed period in a list.
6.235 + """
6.236
6.237 - return removed
6.238 + removed = []
6.239 +
6.240 + search = Period(start, start)
6.241 + found = bisect_left(self.periods, search)
6.242
6.243 -def remove_affected_period(freebusy, uid, start):
6.244 + while found < len(self.periods):
6.245 + fb = self.periods[found]
6.246 +
6.247 + # Stop looking if the start no longer matches the recurrence identifier.
6.248
6.249 - """
6.250 - Remove from 'freebusy' a period associated with 'uid' that provides an
6.251 - occurrence starting at the given 'start' (provided by a recurrence
6.252 - identifier, converted to a datetime). A recurrence identifier is used to
6.253 - provide an alternative time period whilst also acting as a reference to the
6.254 - originally-defined occurrence.
6.255 + if fb.get_start_point() != search.get_start_point():
6.256 + break
6.257 +
6.258 + # If the period belongs to the parent object, remove it and return.
6.259
6.260 - Return any removed period in a list.
6.261 - """
6.262 -
6.263 - removed = []
6.264 + if not fb.recurrenceid and uid == fb.uid:
6.265 + removed.append(self.periods[found])
6.266 + del self.periods[found]
6.267 + break
6.268
6.269 - search = Period(start, start)
6.270 - found = bisect_left(freebusy, search)
6.271 + # Otherwise, keep looking for a matching period.
6.272 +
6.273 + found += 1
6.274
6.275 - while found < len(freebusy):
6.276 - fb = freebusy[found]
6.277 + return removed
6.278 +
6.279 + def periods_from(self, period):
6.280
6.281 - # Stop looking if the start no longer matches the recurrence identifier.
6.282 + "Return the entries in the collection at or after 'period'."
6.283
6.284 - if fb.get_start_point() != search.get_start_point():
6.285 - break
6.286 + first = bisect_left(self.periods, period)
6.287 + return self.periods[first:]
6.288
6.289 - # If the period belongs to the parent object, remove it and return.
6.290 + def periods_until(self, period):
6.291 +
6.292 + "Return the entries in the collection before 'period'."
6.293
6.294 - if not fb.recurrenceid and uid == fb.uid:
6.295 - removed.append(freebusy[found])
6.296 - del freebusy[found]
6.297 - break
6.298 + last = bisect_right(self.periods, Period(period.get_end(), period.get_end(), period.get_tzid()))
6.299 + return self.periods[:last]
6.300 +
6.301 + def get_overlapping(self, period):
6.302
6.303 - # Otherwise, keep looking for a matching period.
6.304 -
6.305 - found += 1
6.306 -
6.307 - return removed
6.308 -
6.309 -def periods_from(freebusy, period):
6.310 + """
6.311 + Return the entries in the collection providing periods overlapping with
6.312 + 'period'.
6.313 + """
6.314
6.315 - "Return the entries in 'freebusy' at or after 'period'."
6.316 + # Find the range of periods potentially overlapping the period in the
6.317 + # free/busy collection.
6.318
6.319 - first = bisect_left(freebusy, period)
6.320 - return freebusy[first:]
6.321 + startpoints = self.periods_until(period)
6.322
6.323 -def periods_until(freebusy, period):
6.324 + # Find the range of periods potentially overlapping the period in a version
6.325 + # of the free/busy collection sorted according to end datetimes.
6.326
6.327 - "Return the entries in 'freebusy' before 'period'."
6.328 + endpoints = [(Period(fb.get_end_point(), fb.get_end_point()), fb) for fb in startpoints]
6.329 + endpoints.sort()
6.330 + first = bisect_left(endpoints, (Period(period.get_start_point(), period.get_start_point()),))
6.331 + endpoints = endpoints[first:]
6.332
6.333 - last = bisect_right(freebusy, Period(period.get_end(), period.get_end(), period.get_tzid()))
6.334 - return freebusy[:last]
6.335 + overlapping = set()
6.336
6.337 -def get_overlapping(freebusy, period):
6.338 + for p, fb in endpoints:
6.339 + if fb.overlaps(period):
6.340 + overlapping.add(fb)
6.341
6.342 - """
6.343 - Return the entries in 'freebusy' providing periods overlapping with
6.344 - 'period'.
6.345 - """
6.346 + overlapping = list(overlapping)
6.347 + overlapping.sort()
6.348 + return overlapping
6.349
6.350 - # Find the range of periods potentially overlapping the period in the
6.351 - # free/busy collection.
6.352 + def period_overlaps(self, period, get_periods=False):
6.353
6.354 - startpoints = periods_until(freebusy, period)
6.355 -
6.356 - # Find the range of periods potentially overlapping the period in a version
6.357 - # of the free/busy collection sorted according to end datetimes.
6.358 + """
6.359 + Return whether any period in the collection overlaps with the given
6.360 + 'period', returning a collection of overlapping periods if 'get_periods'
6.361 + is set to a true value.
6.362 + """
6.363
6.364 - endpoints = [(Period(fb.get_end_point(), fb.get_end_point()), fb) for fb in startpoints]
6.365 - endpoints.sort()
6.366 - first = bisect_left(endpoints, (Period(period.get_start_point(), period.get_start_point()),))
6.367 - endpoints = endpoints[first:]
6.368 -
6.369 - overlapping = set()
6.370 + overlapping = self.get_overlapping(period)
6.371
6.372 - for p, fb in endpoints:
6.373 - if fb.overlaps(period):
6.374 - overlapping.add(fb)
6.375 + if get_periods:
6.376 + return overlapping
6.377 + else:
6.378 + return len(overlapping) != 0
6.379
6.380 - overlapping = list(overlapping)
6.381 - overlapping.sort()
6.382 - return overlapping
6.383 + def remove_overlapping(self, period):
6.384
6.385 -def period_overlaps(freebusy, period, get_periods=False):
6.386 + "Remove all periods overlapping with 'period' from the collection."
6.387 +
6.388 + overlapping = self.get_overlapping(period)
6.389
6.390 - """
6.391 - Return whether any period in 'freebusy' overlaps with the given 'period',
6.392 - returning a collection of overlapping periods if 'get_periods' is set to a
6.393 - true value.
6.394 - """
6.395 + if overlapping:
6.396 + for fb in overlapping:
6.397 + self.periods.remove(fb)
6.398
6.399 - overlapping = get_overlapping(freebusy, period)
6.400 + def replace_overlapping(self, period, replacements):
6.401
6.402 - if get_periods:
6.403 - return overlapping
6.404 - else:
6.405 - return len(overlapping) != 0
6.406 + """
6.407 + Replace existing periods in the collection within the given 'period',
6.408 + using the given 'replacements'.
6.409 + """
6.410
6.411 -def remove_overlapping(freebusy, period):
6.412 + self.remove_overlapping(period)
6.413 + for replacement in replacements:
6.414 + self.insert_period(replacement)
6.415
6.416 - "Remove from 'freebusy' all periods overlapping with 'period'."
6.417 + def coalesce_freebusy(self):
6.418
6.419 - overlapping = get_overlapping(freebusy, period)
6.420 + "Coalesce the periods in the collection, returning a new collection."
6.421
6.422 - if overlapping:
6.423 - for fb in overlapping:
6.424 - freebusy.remove(fb)
6.425 + if not self.periods:
6.426 + return FreeBusyCollection(self.periods)
6.427
6.428 -def replace_overlapping(freebusy, period, replacements):
6.429 + fb = []
6.430 + start = self.periods[0].get_start_point()
6.431 + end = self.periods[0].get_end_point()
6.432
6.433 - """
6.434 - Replace existing periods in 'freebusy' within the given 'period', using the
6.435 - given 'replacements'.
6.436 - """
6.437 -
6.438 - remove_overlapping(freebusy, period)
6.439 - for replacement in replacements:
6.440 - insert_period(freebusy, replacement)
6.441 -
6.442 -def coalesce_freebusy(freebusy):
6.443 + for period in self.periods[1:]:
6.444 + if period.get_start_point() > end:
6.445 + fb.append(FreeBusyPeriod(start, end))
6.446 + start = period.get_start_point()
6.447 + end = period.get_end_point()
6.448 + else:
6.449 + end = max(end, period.get_end_point())
6.450
6.451 - "Coalesce the periods in 'freebusy'."
6.452 + fb.append(FreeBusyPeriod(start, end))
6.453 + return FreeBusyCollection(fb)
6.454
6.455 - if not freebusy:
6.456 - return freebusy
6.457 + def invert_freebusy(self):
6.458 +
6.459 + "Return the free periods from the collection as a new collection."
6.460
6.461 - fb = []
6.462 - start = freebusy[0].get_start_point()
6.463 - end = freebusy[0].get_end_point()
6.464 + if not self.periods:
6.465 + return FreeBusyCollection([FreeBusyPeriod(None, None)])
6.466 +
6.467 + # Coalesce periods that overlap or are adjacent.
6.468
6.469 - for period in freebusy[1:]:
6.470 - if period.get_start_point() > end:
6.471 - fb.append(FreeBusyPeriod(start, end))
6.472 - start = period.get_start_point()
6.473 - end = period.get_end_point()
6.474 - else:
6.475 - end = max(end, period.get_end_point())
6.476 + fb = self.coalesce_freebusy()
6.477 + free = []
6.478 +
6.479 + # Add a start-of-time period if appropriate.
6.480
6.481 - fb.append(FreeBusyPeriod(start, end))
6.482 - return fb
6.483 + first = fb[0].get_start_point()
6.484 + if first:
6.485 + free.append(FreeBusyPeriod(None, first))
6.486
6.487 -def invert_freebusy(freebusy):
6.488 -
6.489 - "Return the free periods from 'freebusy'."
6.490 + start = fb[0].get_end_point()
6.491
6.492 - if not freebusy:
6.493 - return [FreeBusyPeriod(None, None)]
6.494 -
6.495 - # Coalesce periods that overlap or are adjacent.
6.496 + for period in fb[1:]:
6.497 + free.append(FreeBusyPeriod(start, period.get_start_point()))
6.498 + start = period.get_end_point()
6.499
6.500 - fb = coalesce_freebusy(freebusy)
6.501 - free = []
6.502 + # Add an end-of-time period if appropriate.
6.503
6.504 - # Add a start-of-time period if appropriate.
6.505 + if start:
6.506 + free.append(FreeBusyPeriod(start, None))
6.507
6.508 - first = fb[0].get_start_point()
6.509 - if first:
6.510 - free.append(FreeBusyPeriod(None, first))
6.511 + return FreeBusyCollection(free)
6.512 +
6.513 + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
6.514
6.515 - start = fb[0].get_end_point()
6.516 + """
6.517 + Update the free/busy details with the given 'periods', 'transp' setting,
6.518 + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
6.519
6.520 - for period in fb[1:]:
6.521 - free.append(FreeBusyPeriod(start, period.get_start_point()))
6.522 - start = period.get_end_point()
6.523 + An optional 'expires' datetime string indicates the expiry time of any
6.524 + free/busy offer.
6.525 + """
6.526
6.527 - # Add an end-of-time period if appropriate.
6.528 + self.remove_event_periods(uid, recurrenceid)
6.529
6.530 - if start:
6.531 - free.append(FreeBusyPeriod(start, None))
6.532 -
6.533 - return free
6.534 + for p in periods:
6.535 + self.insert_period(FreeBusyPeriod(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires))
6.536
6.537 # Period layout.
6.538
6.539 @@ -1014,19 +1062,4 @@
6.540
6.541 return spans
6.542
6.543 -def update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
6.544 -
6.545 - """
6.546 - Update the free/busy details with the given 'periods', 'transp' setting,
6.547 - 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
6.548 -
6.549 - An optional 'expires' datetime string indicates the expiry time of any
6.550 - free/busy offer.
6.551 - """
6.552 -
6.553 - remove_event_periods(freebusy, uid, recurrenceid)
6.554 -
6.555 - for p in periods:
6.556 - insert_period(freebusy, FreeBusyPeriod(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires))
6.557 -
6.558 # vim: tabstop=4 expandtab shiftwidth=4
7.1 --- a/imipweb/calendar.py Tue Feb 09 15:57:23 2016 +0100
7.2 +++ b/imipweb/calendar.py Wed Mar 02 21:17:11 2016 +0100
7.3 @@ -3,7 +3,7 @@
7.4 """
7.5 A Web interface to an event calendar.
7.6
7.7 -Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
7.8 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
7.9
7.10 This program is free software; you can redistribute it and/or modify it under
7.11 the terms of the GNU General Public License as published by the Free Software
7.12 @@ -26,7 +26,6 @@
7.13 get_start_of_next_day, get_timestamp, ends_on_same_day, \
7.14 to_date, to_timezone
7.15 from imiptools.period import add_day_start_points, add_empty_days, add_slots, \
7.16 - get_overlapping, \
7.17 get_scale, get_slots, get_spans, partition_by_day, \
7.18 remove_end_slot, Period, Point
7.19 from imipweb.resource import FormUtilities, ResourceClient
7.20 @@ -323,8 +322,8 @@
7.21 view_end = view_period.get_end()
7.22 duration = view_period.get_duration()
7.23
7.24 - preceding_events = view_start and get_overlapping(freebusy, Period(None, view_start, self.get_tzid())) or []
7.25 - following_events = view_end and get_overlapping(freebusy, Period(view_end, None, self.get_tzid())) or []
7.26 + preceding_events = view_start and freebusy.get_overlapping(Period(None, view_start, self.get_tzid())) or []
7.27 + following_events = view_end and freebusy.get_overlapping(Period(view_end, None, self.get_tzid())) or []
7.28
7.29 last_preceding = preceding_events and to_date(preceding_events[-1].get_end()) + timedelta(1) or None
7.30 first_following = following_events and to_date(following_events[0].get_start()) or None
8.1 --- a/imipweb/event.py Tue Feb 09 15:57:23 2016 +0100
8.2 +++ b/imipweb/event.py Wed Mar 02 21:17:11 2016 +0100
8.3 @@ -3,7 +3,7 @@
8.4 """
8.5 A Web interface to a calendar event.
8.6
8.7 -Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
8.8 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
8.9
8.10 This program is free software; you can redistribute it and/or modify it under
8.11 the terms of the GNU General Public License as published by the Free Software
8.12 @@ -23,7 +23,6 @@
8.13 uri_parts, uri_values
8.14 from imiptools.dates import format_datetime, to_timezone
8.15 from imiptools.mail import Messenger
8.16 -from imiptools.period import have_conflict
8.17 from imipweb.data import EventPeriod, event_period_from_period, FormPeriod, PeriodError
8.18 from imipweb.resource import DateTimeFormUtilities, FormUtilities, ResourceClientForObject
8.19
8.20 @@ -713,7 +712,7 @@
8.21 partstat = participant_attr and participant_attr.get("PARTSTAT")
8.22 recurrences = self.obj.get_recurrence_start_points(recurrenceids, tzid)
8.23
8.24 - for p in have_conflict(freebusy, periods, True):
8.25 + for p in freebusy.have_conflict(periods, True):
8.26 if not self.recurrenceid and p.is_replaced(recurrences):
8.27 continue
8.28
9.1 --- a/tools/make_freebusy.py Tue Feb 09 15:57:23 2016 +0100
9.2 +++ b/tools/make_freebusy.py Wed Mar 02 21:17:11 2016 +0100
9.3 @@ -37,7 +37,7 @@
9.4 from imiptools.client import Client
9.5 from imiptools.data import get_window_end, Object
9.6 from imiptools.dates import get_default_timezone, to_utc_datetime
9.7 -from imiptools.period import insert_period
9.8 +from imiptools.period import FreeBusyCollection
9.9 from imip_store import FileStore, FilePublisher, FileJournal
9.10
9.11 def make_freebusy(client, participant, store_and_publish, include_needs_action,
9.12 @@ -86,7 +86,7 @@
9.13
9.14 if not all_events:
9.15 all_events = store.get_all_events(user)
9.16 - fb = []
9.17 + fb = FreeBusyCollection()
9.18
9.19 # With providers of additional periods, append to the existing collection.
9.20
9.21 @@ -115,7 +115,7 @@
9.22 if obj.get_participation(partstat, include_needs_action):
9.23 for p in obj.get_active_periods(recurrenceids, tzid, window_end):
9.24 fbp = obj.get_freebusy_period(p, partstat == "ORG")
9.25 - insert_period(fb, fbp)
9.26 + fb.insert_period(fbp)
9.27
9.28 # Store and publish the free/busy collection.
9.29