# HG changeset patch # User Paul Boddie # Date 1462975168 -7200 # Node ID 8fb731c8db48e80eff4b899de1c12cc8623b1c6c # Parent 6470334f8449ed52270f5a0bbedae8d82ccf1e67 Introduced special period and collection classes for different kinds of free/busy information. Added attendee details to quota-specific free/busy tables. diff -r 6470334f8449 -r 8fb731c8db48 conf/postgresql/schema.sql --- a/conf/postgresql/schema.sql Wed May 11 14:04:30 2016 +0200 +++ b/conf/postgresql/schema.sql Wed May 11 15:59:28 2016 +0200 @@ -44,8 +44,7 @@ transp varchar, object_recurrenceid varchar, summary varchar, - organiser varchar, - expires varchar + organiser varchar ); create index freebusy_start on freebusy(store_user, "start"); @@ -75,8 +74,7 @@ transp varchar, object_recurrenceid varchar, summary varchar, - organiser varchar, - expires varchar + organiser varchar ); create index freebusy_other_start on freebusy_other(store_user, other, "start"); @@ -124,7 +122,7 @@ object_recurrenceid varchar, summary varchar, organiser varchar, - expires varchar + attendee varchar ); create index quota_freebusy_start on quota_freebusy(quota, user_group, "start"); @@ -139,8 +137,7 @@ transp varchar, object_recurrenceid varchar, summary varchar, - organiser varchar, - expires varchar + organiser varchar ); create index user_freebusy_start on user_freebusy(quota, store_user, "start"); diff -r 6470334f8449 -r 8fb731c8db48 imiptools/client.py --- a/imiptools/client.py Wed May 11 14:04:30 2016 +0200 +++ b/imiptools/client.py Wed May 11 15:59:28 2016 +0200 @@ -27,6 +27,7 @@ from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \ get_duration, get_timestamp from imiptools.i18n import get_translator +from imiptools.period import SupportAttendee, SupportExpires from imiptools.profile import Preferences from imiptools.stores import get_store, get_publisher, get_journal @@ -285,7 +286,20 @@ offer. """ - freebusy.update_freebusy(periods, transp, uid, recurrenceid, summary, organiser, expires) + # Add specific attendee information for certain collections. + + if isinstance(freebusy, SupportAttendee): + freebusy.update_freebusy(periods, transp, uid, recurrenceid, summary, organiser, self.user) + + # Add expiry datetime for certain collections. + + elif isinstance(freebusy, SupportExpires): + freebusy.update_freebusy(periods, transp, uid, recurrenceid, summary, organiser, expires) + + # Provide only the essential attributes for other collections. + + else: + freebusy.update_freebusy(periods, transp, uid, recurrenceid, summary, organiser) # Preparation of messages communicating the state of events. diff -r 6470334f8449 -r 8fb731c8db48 imiptools/period.py --- a/imiptools/period.py Wed May 11 14:04:30 2016 +0200 +++ b/imiptools/period.py Wed May 11 15:59:28 2016 +0200 @@ -34,9 +34,6 @@ if x is None: return y else: return x -def from_strings(t, encoding): - return tuple([from_string(s, encoding) for s in t]) - def from_string(s, encoding): if s: return unicode(s, encoding) @@ -146,10 +143,17 @@ "A basic period abstraction." def __init__(self, start, end): + + """ + Define a period according to 'start' and 'end' which may be special + start/end of time values or iCalendar-format datetime strings. + """ + if isinstance(start, (date, PointInTime)): self.start = start else: self.start = get_datetime(start) or StartOfTime() + if isinstance(end, (date, PointInTime)): self.end = end else: @@ -358,16 +362,12 @@ "A free/busy record abstraction." def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, - summary=None, organiser=None, expires=None): + summary=None, organiser=None): """ Initialise a free/busy period with the given 'start' and 'end' points, plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' details. - - An additional 'expires' parameter can be used to indicate an expiry - datetime in conjunction with free/busy offers made when countering - event proposals. """ PeriodBase.__init__(self, start, end) @@ -376,7 +376,6 @@ self.recurrenceid = recurrenceid or None self.summary = summary or None self.organiser = organiser or None - self.expires = expires or None def as_tuple(self, strings_only=False, string_datetimes=False): @@ -395,8 +394,7 @@ self.transp or strings_only and "OPAQUE" or None, self.recurrenceid or null(self.recurrenceid), self.summary or null(self.summary), - self.organiser or null(self.organiser), - self.expires or null(self.expires) + self.organiser or null(self.organiser) ) def __cmp__(self, other): @@ -451,6 +449,79 @@ def make_corrected(self, start, end): return self.__class__(start, end) +class FreeBusyOfferPeriod(FreeBusyPeriod): + + "A free/busy record abstraction for an offer period." + + def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, + summary=None, organiser=None, expires=None): + + """ + Initialise a free/busy period with the given 'start' and 'end' points, + plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' + details. + + An additional 'expires' parameter can be used to indicate an expiry + datetime in conjunction with free/busy offers made when countering + event proposals. + """ + + FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid, + summary, organiser) + self.expires = expires or None + + def as_tuple(self, strings_only=False, string_datetimes=False): + + """ + Return the initialisation parameter tuple, converting datetimes and + false value parameters to strings if 'strings_only' is set to a true + value. Otherwise, if 'string_datetimes' is set to a true value, only the + datetime values are converted to strings. + """ + + null = lambda x: (strings_only and [""] or [x])[0] + return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + ( + self.expires or null(self.expires),) + + def __repr__(self): + return "FreeBusyOfferPeriod%r" % (self.as_tuple(),) + +class FreeBusyGroupPeriod(FreeBusyPeriod): + + "A free/busy record abstraction for a quota group period." + + def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, + summary=None, organiser=None, attendee=None): + + """ + Initialise a free/busy period with the given 'start' and 'end' points, + plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' + details. + + An additional 'attendee' parameter can be used to indicate the identity + of the attendee recording the period. + """ + + FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid, + summary, organiser) + self.attendee = attendee or None + + def as_tuple(self, strings_only=False, string_datetimes=False): + + """ + Return the initialisation parameter tuple, converting datetimes and + false value parameters to strings if 'strings_only' is set to a true + value. Otherwise, if 'string_datetimes' is set to a true value, only the + datetime values are converted to strings. + """ + + null = lambda x: (strings_only and [""] or [x])[0] + return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + ( + self.attendee or null(self.attendee),) + + def __repr__(self): + return "FreeBusyGroupPeriod%r" % (self.as_tuple(),) + class RecurringPeriod(Period): """ @@ -482,6 +553,13 @@ "Common operations on free/busy period collections." + period_columns = [ + "start", "end", "object_uid", "transp", "object_recurrenceid", + "summary", "organiser" + ] + + period_class = FreeBusyPeriod + def __init__(self, mutable=True): self.mutable = mutable @@ -495,6 +573,30 @@ return FreeBusyCollection(list(self), True) + def make_period(self, t): + + """ + Make a period using the given tuple of arguments and the collection's + column details. + """ + + args = [] + for arg, column in zip(t, self.period_columns): + args.append(from_string(arg, "utf-8")) + return self.period_class(*args) + + def make_tuple(self, t): + + """ + Return a tuple from the given tuple 't' conforming to the collection's + column details. + """ + + args = [] + for arg, column in zip(t, self.period_columns): + args.append(arg) + return tuple(args) + # List emulation methods. def __iadd__(self, periods): @@ -589,7 +691,7 @@ while True: period = it.next() if period.get_start_point() > end: - fb.append(FreeBusyPeriod(start, end)) + fb.append(self.period_class(start, end)) start = period.get_start_point() end = period.get_end_point() else: @@ -597,7 +699,7 @@ except StopIteration: pass - fb.append(FreeBusyPeriod(start, end)) + fb.append(self.period_class(start, end)) return FreeBusyCollection(fb) def invert_freebusy(self): @@ -605,7 +707,7 @@ "Return the free periods from the collection as a new collection." if not self: - return FreeBusyCollection([FreeBusyPeriod(None, None)]) + return FreeBusyCollection([self.period_class(None, None)]) # Coalesce periods that overlap or are adjacent. @@ -616,21 +718,83 @@ first = fb[0].get_start_point() if first: - free.append(FreeBusyPeriod(None, first)) + free.append(self.period_class(None, first)) start = fb[0].get_end_point() for period in fb[1:]: - free.append(FreeBusyPeriod(start, period.get_start_point())) + free.append(self.period_class(start, period.get_start_point())) start = period.get_end_point() # Add an end-of-time period if appropriate. if start: - free.append(FreeBusyPeriod(start, None)) + free.append(self.period_class(start, None)) return FreeBusyCollection(free) + def _update_freebusy(self, periods, uid, recurrenceid): + + """ + Update the free/busy details with the given 'periods', using the given + 'uid' plus 'recurrenceid' to remove existing periods. + """ + + self._check_mutable() + + self.remove_event_periods(uid, recurrenceid) + + for p in periods: + self.insert_period(p) + + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser): + + """ + Update the free/busy details with the given 'periods', 'transp' setting, + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details. + """ + + new_periods = [] + + for p in periods: + new_periods.append( + self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser) + ) + + self._update_freebusy(new_periods, uid, recurrenceid) + +class SupportAttendee: + + "A mix-in that supports the affected attendee in free/busy periods." + + period_columns = FreeBusyCollectionBase.period_columns + ["attendee"] + period_class = FreeBusyGroupPeriod + + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, attendee=None): + + """ + Update the free/busy details with the given 'periods', 'transp' setting, + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details. + + An optional 'attendee' indicates the attendee affected by the period. + """ + + new_periods = [] + + for p in periods: + new_periods.append( + self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, attendee) + ) + + self._update_freebusy(new_periods, uid, recurrenceid) + +class SupportExpires: + + "A mix-in that supports the expiry datetime in free/busy periods." + + period_columns = FreeBusyCollectionBase.period_columns + ["expires"] + period_class = FreeBusyOfferPeriod + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None): """ @@ -641,12 +805,14 @@ free/busy offer. """ - self._check_mutable() - - self.remove_event_periods(uid, recurrenceid) + new_periods = [] for p in periods: - self.insert_period(FreeBusyPeriod(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires)) + new_periods.append( + self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires) + ) + + self._update_freebusy(new_periods, uid, recurrenceid) class FreeBusyCollection(FreeBusyCollectionBase): @@ -851,6 +1017,18 @@ for fb in overlapping: self.periods.remove(fb) +class FreeBusyGroupCollection(SupportAttendee, FreeBusyCollection): + + "A collection of quota group free/busy objects." + + pass + +class FreeBusyOffersCollection(SupportExpires, FreeBusyCollection): + + "A collection of offered free/busy objects." + + pass + class FreeBusyDatabaseCollection(FreeBusyCollectionBase, DatabaseOperations): """ @@ -858,8 +1036,6 @@ system. """ - period_columns = ["start", "end", "object_uid", "transp", "object_recurrenceid", "summary", "organiser", "expires"] - def __init__(self, cursor, table_name, column_names=None, filter_values=None, mutable=True, paramstyle=None): @@ -875,9 +1051,6 @@ self.cursor = cursor self.table_name = table_name - def make_period(self, t): - return FreeBusyPeriod(*from_strings(t, "utf-8")) - # List emulation methods. def __nonzero__(self): @@ -1151,6 +1324,18 @@ return columns, values +class FreeBusyGroupDatabaseCollection(SupportAttendee, FreeBusyDatabaseCollection): + + "A collection of quota group free/busy objects." + + pass + +class FreeBusyOffersDatabaseCollection(SupportExpires, FreeBusyDatabaseCollection): + + "A collection of offered free/busy objects." + + pass + # Period layout. def get_scale(periods, tzid, view_period=None): diff -r 6470334f8449 -r 8fb731c8db48 imiptools/stores/database/common.py --- a/imiptools/stores/database/common.py Wed May 11 14:04:30 2016 +0200 +++ b/imiptools/stores/database/common.py Wed May 11 15:59:28 2016 +0200 @@ -24,7 +24,9 @@ from datetime import datetime from imiptools.data import parse_string, to_string from imiptools.dates import format_datetime, get_datetime, to_timezone -from imiptools.period import FreeBusyDatabaseCollection +from imiptools.period import FreeBusyDatabaseCollection, \ + FreeBusyGroupDatabaseCollection, \ + FreeBusyOffersDatabaseCollection from imiptools.sql import DatabaseOperations class DatabaseStoreBase(DatabaseOperations): @@ -429,12 +431,13 @@ # Free/busy period access. - def get_freebusy(self, user, name=None, mutable=False): + def get_freebusy(self, user, name=None, mutable=False, cls=None): "Get free/busy details for the given 'user'." table = name or "freebusy" - return FreeBusyDatabaseCollection(self.cursor, table, ["store_user"], [user], mutable, self.paramstyle) + cls = cls or FreeBusyDatabaseCollection + return cls(self.cursor, table, ["store_user"], [user], mutable, self.paramstyle) def get_freebusy_for_other(self, user, other, mutable=False): @@ -443,14 +446,15 @@ table = "freebusy_other" return FreeBusyDatabaseCollection(self.cursor, table, ["store_user", "other"], [user, other], mutable, self.paramstyle) - def set_freebusy(self, user, freebusy, name=None): + def set_freebusy(self, user, freebusy, name=None, cls=None): "For the given 'user', set 'freebusy' details." table = name or "freebusy" + cls = cls or FreeBusyDatabaseCollection - if not isinstance(freebusy, FreeBusyDatabaseCollection) or freebusy.table_name != table: - fbc = FreeBusyDatabaseCollection(self.cursor, table, ["store_user"], [user], True, self.paramstyle) + if not isinstance(freebusy, cls) or freebusy.table_name != table: + fbc = cls(self.cursor, table, ["store_user"], [user], True, self.paramstyle) fbc += freebusy return True @@ -502,13 +506,13 @@ self.cursor.execute(query, values) - return self.get_freebusy(user, "freebusy_offers", mutable) + return self.get_freebusy(user, "freebusy_offers", mutable, FreeBusyOffersDatabaseCollection) def set_freebusy_offers(self, user, freebusy): "For the given 'user', set 'freebusy' offers." - return self.set_freebusy(user, freebusy, "freebusy_offers") + return self.set_freebusy(user, freebusy, "freebusy_offers", cls=FreeBusyOffersDatabaseCollection) # Requests and counter-proposals. @@ -952,21 +956,23 @@ self.cursor.execute(query, values) return [r[0] for r in self.cursor.fetchall()] - def get_freebusy(self, quota, user, mutable=False): + def get_freebusy(self, quota, user, mutable=False, cls=None): "Get free/busy details for the given 'quota' and 'user'." table = "user_freebusy" - return FreeBusyDatabaseCollection(self.cursor, table, ["quota", "store_user"], [quota, user], mutable, self.paramstyle) + cls = cls or FreeBusyDatabaseCollection + return cls(self.cursor, table, ["quota", "store_user"], [quota, user], mutable, self.paramstyle) - def set_freebusy(self, quota, user, freebusy): + def set_freebusy(self, quota, user, freebusy, cls=None): "For the given 'quota' and 'user', set 'freebusy' details." table = "user_freebusy" + cls = cls or FreeBusyDatabaseCollection - if not isinstance(freebusy, FreeBusyDatabaseCollection) or freebusy.table_name != table: - fbc = FreeBusyDatabaseCollection(self.cursor, table, ["quota", "store_user"], [quota, user], True, self.paramstyle) + if not isinstance(freebusy, cls) or freebusy.table_name != table: + fbc = cls(self.cursor, table, ["quota", "store_user"], [quota, user], True, self.paramstyle) fbc += freebusy return True @@ -981,7 +987,7 @@ """ table = "quota_freebusy" - return FreeBusyDatabaseCollection(self.cursor, table, ["quota", "user_group"], [quota, group], mutable, self.paramstyle) + return FreeBusyGroupDatabaseCollection(self.cursor, table, ["quota", "user_group"], [quota, group], mutable, self.paramstyle) def set_entries(self, quota, group, entries): @@ -992,8 +998,8 @@ table = "quota_freebusy" - if not isinstance(entries, FreeBusyDatabaseCollection) or entries.table_name != table: - fbc = FreeBusyDatabaseCollection(self.cursor, table, ["quota", "user_group"], [quota, group], True, self.paramstyle) + if not isinstance(entries, FreeBusyGroupDatabaseCollection) or entries.table_name != table: + fbc = FreeBusyGroupDatabaseCollection(self.cursor, table, ["quota", "user_group"], [quota, group], True, self.paramstyle) fbc += entries return True diff -r 6470334f8449 -r 8fb731c8db48 imiptools/stores/file.py --- a/imiptools/stores/file.py Wed May 11 14:04:30 2016 +0200 +++ b/imiptools/stores/file.py Wed May 11 15:59:28 2016 +0200 @@ -26,7 +26,9 @@ from imiptools.data import make_calendar, parse_object, to_stream from imiptools.dates import format_datetime, get_datetime, to_timezone from imiptools.filesys import fix_permissions, FileBase -from imiptools.period import FreeBusyPeriod, FreeBusyCollection +from imiptools.period import FreeBusyPeriod, FreeBusyGroupPeriod, \ + FreeBusyOfferPeriod, FreeBusyCollection, \ + FreeBusyGroupCollection, FreeBusyOffersCollection from imiptools.text import parse_line from os.path import isdir, isfile, join from os import listdir, remove, rmdir @@ -148,6 +150,18 @@ finally: self.release_lock(user) + def _set_freebusy(self, user, freebusy, filename): + + """ + For the given 'user', convert the 'freebusy' details to a form suitable + for writing to 'filename'. + """ + + # Obtain tuples from the free/busy objects. + + self._set_table_atomic(user, filename, + map(lambda fb: freebusy.make_tuple(fb.as_tuple(strings_only=True)), list(freebusy))) + class Store(FileStoreBase, StoreBase): "A file store of tabular free/busy data and objects." @@ -462,7 +476,7 @@ # Free/busy period access. - def get_freebusy(self, user, name=None, mutable=False): + def get_freebusy(self, user, name=None, mutable=False, cls=None): "Get free/busy details for the given 'user'." @@ -471,7 +485,8 @@ if not filename or not isfile(filename): periods = [] else: - periods = map(lambda t: FreeBusyPeriod(*t), + cls = cls or FreeBusyPeriod + periods = map(lambda t: cls(*t), self._get_table_atomic(user, filename)) return FreeBusyCollection(periods, mutable) @@ -498,8 +513,7 @@ if not filename: return False - self._set_table_atomic(user, filename, - map(lambda fb: fb.as_tuple(strings_only=True), list(freebusy))) + self._set_freebusy(user, freebusy, filename) return True def set_freebusy_for_other(self, user, freebusy, other): @@ -510,8 +524,7 @@ if not filename: return False - self._set_table_atomic(user, filename, - map(lambda fb: fb.as_tuple(strings_only=True), list(freebusy))) + self._set_freebusy(user, freebusy, filename) return True def get_freebusy_others(self, user): @@ -542,7 +555,7 @@ self.acquire_lock(user) try: - l = self.get_freebusy(user, "freebusy-offers") + l = self.get_freebusy(user, "freebusy-offers", cls=FreeBusyOfferPeriod) for fb in l: if fb.expires and get_datetime(fb.expires) <= now: expired.append(fb) @@ -554,7 +567,7 @@ finally: self.release_lock(user) - return FreeBusyCollection(offers, mutable) + return FreeBusyOffersCollection(offers, mutable) # Requests and counter-proposals. @@ -884,7 +897,7 @@ return listdir(filename) - def get_freebusy(self, quota, user, mutable=False): + def get_freebusy(self, quota, user, mutable=False, cls=None): "Get free/busy details for the given 'quota' and 'user'." @@ -893,7 +906,8 @@ if not filename or not isfile(filename): periods = [] else: - periods = map(lambda t: FreeBusyPeriod(*t), + cls = cls or FreeBusyPeriod + periods = map(lambda t: cls(*t), self._get_table_atomic(quota, filename)) return FreeBusyCollection(periods, mutable) @@ -906,8 +920,7 @@ if not filename: return False - self._set_table_atomic(quota, filename, - map(lambda fb: fb.as_tuple(strings_only=True), list(freebusy))) + self._set_freebusy(quota, freebusy, filename) return True # Journal entry methods. @@ -924,10 +937,10 @@ if not filename or not isfile(filename): periods = [] else: - periods = map(lambda t: FreeBusyPeriod(*t), + periods = map(lambda t: FreeBusyGroupPeriod(*t), self._get_table_atomic(quota, filename)) - return FreeBusyCollection(periods, mutable) + return FreeBusyGroupCollection(periods, mutable) def set_entries(self, quota, group, entries): @@ -940,8 +953,7 @@ if not filename: return False - self._set_table_atomic(quota, filename, - map(lambda fb: fb.as_tuple(strings_only=True), list(entries))) + self._set_freebusy(quota, entries, filename) return True # vim: tabstop=4 expandtab shiftwidth=4