# HG changeset patch # User Paul Boddie # Date 1422053906 -3600 # Node ID ba7f19be84c0e1872e39cc4fc1bd4df4bf324d22 # Parent 3feba2b55cfcf838e76934238996764f20e2f890 Added initial support for presenting incoming requests alongside the free/busy information already available. This requires each group of periods to remain separated whilst exposing the same scale of time points in all of them. diff -r 3feba2b55cfc -r ba7f19be84c0 imip_manager.py --- a/imip_manager.py Thu Jan 22 20:13:06 2015 +0100 +++ b/imip_manager.py Fri Jan 23 23:58:26 2015 +0100 @@ -35,7 +35,9 @@ from imiptools.dates import format_datetime, get_datetime, get_start_of_day, \ to_timezone from imiptools.mail import Messenger -from imiptools.period import have_conflict, get_slots, get_spans, partition_slots +from imiptools.period import add_day_start_points, add_slots, convert_periods, \ + get_scale, have_conflict, get_slots, get_spans, \ + partition_by_day from imiptools.profile import Preferences from vCalendar import to_node import markup @@ -179,6 +181,7 @@ self.encoding = "utf-8" self.store = imip_store.FileStore() + self.objects = {} try: self.publisher = imip_store.FilePublisher() @@ -189,12 +192,15 @@ return path_info.lstrip("/").split("/", 1)[0] def _get_object(self, uid): + if self.objects.has_key(uid): + return self.objects[uid] + f = uid and self.store.get_event(self.user, uid) or None if not f: return None - obj = parse_object(f, "utf-8") + self.objects[uid] = obj = parse_object(f, "utf-8") if not obj: return None @@ -210,6 +216,19 @@ self.requests = self.store.get_requests(self.user) return self.requests + def _get_request_summary(self): + summary = [] + for uid in self._get_requests(): + obj = self._get_object(uid) + if obj: + details = self._get_details(obj) + summary.append(( + get_value(details, "DTSTART"), + get_value(details, "DTEND"), + uid + )) + return summary + # Preference methods. def get_user_locale(self): @@ -222,6 +241,8 @@ self.preferences = Preferences(self.user) return self.preferences + # Prettyprinting of dates and times. + def format_date(self, dt, format): return self._format_datetime(babel.dates.format_date, dt, format) @@ -466,10 +487,12 @@ "Show the calendar for the current user." self.new_page(title="Calendar") + page = self.page + self.show_requests_on_page() + request_summary = self._get_request_summary() freebusy = self.store.get_freebusy(self.user) - page = self.page if not freebusy: page.p("No events scheduled.") @@ -493,29 +516,126 @@ # Requests could be listed and linked to their tentative positions in # the calendar. - slots = get_slots(freebusy) - partitioned = partition_slots(slots, tzid) - columns = max(map(lambda i: len(i[1]), slots)) + 1 + groups = [] + group_columns = [] + all_points = set() + + # Obtain time point information for each group of periods. + + for periods in [request_summary, freebusy]: + periods = convert_periods(periods, tzid) + + # Get the time scale with start and end points. + + scale = get_scale(periods) + + # Get the time slots for the periods. + + slots = get_slots(scale) + + # Add start of day time points for multi-day periods. + + add_day_start_points(slots) + + # Record the slots and all time points employed. + + groups.append(slots) + all_points.update([point for point, slot in slots]) + + # Partition the groups into days. + + days = {} + partitioned_groups = [] + + for slots in groups: + + # Propagate time points to all groups of time slots. + + add_slots(slots, all_points) + + # Count the number of columns employed by the group. + + columns = 0 + + # Partition the time slots by day. + + partitioned = {} + + for day, day_slots in partition_by_day(slots).items(): + columns = max(columns, max(map(lambda i: len(i[1]), day_slots))) + + if not days.has_key(day): + days[day] = set() + + # Convert each partition to a mapping from points to active + # periods. + + day_slots = dict(day_slots) + partitioned[day] = day_slots + days[day].update(day_slots.keys()) + + if partitioned: + group_columns.append(columns + 1) + partitioned_groups.append(partitioned) page.table(border=1, cellspacing=0, cellpadding=5) + self.show_calendar_days(days, partitioned_groups, group_columns) + page.table.close() - for day, slots in partitioned: - spans = get_spans(slots) + def show_calendar_days(self, days, partitioned_groups, group_columns): + page = self.page + + # Determine the number of columns required, the days providing time + # slots. + all_columns = sum(group_columns) + all_days = days.items() + all_days.sort() + + # Produce a heading and time points for each day. + + for day, points in all_days: page.tr() - page.th(class_="dayheading", colspan=columns) + page.th(class_="dayheading", colspan=all_columns) page.add(self.format_date(day, "full")) page.th.close() page.tr.close() - for point, active in slots: - dt = to_timezone(get_datetime(point), tzid) - continuation = dt == get_start_of_day(dt) + groups_for_day = [partitioned.get(day) for partitioned in partitioned_groups] + + self.show_calendar_points(points, groups_for_day, group_columns) + + def show_calendar_points(self, points, groups, group_columns): + page = self.page + + # Produce a row for each time point. + + points = list(points) + points.sort() + + for point in points: + continuation = point == get_start_of_day(point) - page.tr() - page.th(class_="timeslot") - page.add(self.format_time(dt, "long")) - page.th.close() + page.tr() + page.th(class_="timeslot") + page.add(self.format_time(point, "long")) + page.th.close() + + # Obtain slots for the time point from each group. + + for columns, slots in zip(group_columns, groups): + active = slots and slots.get(point) + + if not active: + page.td(class_="empty", colspan=columns) + page.td.close() + continue + + slots = slots.items() + slots.sort() + spans = get_spans(slots) + + # Show a column for each active period. for t in active: if t: @@ -533,9 +653,7 @@ page.td(class_="empty") page.td.close() - page.tr.close() - - page.table.close() + page.tr.close() def select_action(self): diff -r 3feba2b55cfc -r ba7f19be84c0 imiptools/period.py --- a/imiptools/period.py Thu Jan 22 20:13:06 2015 +0100 +++ b/imiptools/period.py Fri Jan 23 23:58:26 2015 +0100 @@ -100,19 +100,36 @@ # Period layout. -def get_scale(l): +def convert_periods(periods, tzid): + + "Convert 'periods' to use datetime objects employing the given 'tzid'." + + l = [] + + for t in periods: + start, end = t[:2] + start = to_timezone(get_datetime(start), tzid) + end = to_timezone(get_datetime(end), tzid) + l.append((start, end) + tuple(t[2:])) + + return l + +def get_scale(periods): """ - Return an ordered time scale from the given list 'l' of tuples, with the - first two elements of each tuple being start and end times. + Return an ordered time scale from the given list 'periods', with the first + two elements of each tuple being start and end times. - The returned scale is a collection of (time, (starting, ending)) tuples, - where starting and ending are collections of tuples from 'l'. + The given 'tzid' is used to make sure that the times are defined according + to the chosen time zone. + + The returned scale is a mapping from time to (starting, ending) tuples, + where starting and ending are collections of tuples from 'periods'. """ scale = {} - for t in l: + for t in periods: start, end = t[:2] # Add a point and this event to the starting list. @@ -127,15 +144,12 @@ scale[end] = [], [] scale[end][1].append(t) - scale = scale.items() - scale.sort() return scale -def get_slots(l): +def get_slots(scale): """ - Return an ordered list of time slots from the given list 'l' of tuples, with - the first two elements of each tuple being start and end times. + Return an ordered list of time slots from the given 'scale'. Each slot is a tuple containing a point in time for the start of the slot, together with a list of parallel event tuples, each tuple containing the @@ -145,7 +159,10 @@ slots = [] active = [] - for point, (starting, ending) in get_scale(l): + points = scale.items() + points.sort() + + for point, (starting, ending) in points: # Discard all active events ending at or before this start time. # Free up the position in the active list. @@ -173,45 +190,84 @@ return slots -def partition_slots(slots, tzid): +def add_day_start_points(slots): """ - Partition the given 'slots' into separate collections having a date-level - resolution, using the given 'tzid' to make sure that the day boundaries are - defined according to the chosen time zone. - - Return a collection of (date, slots) tuples. + Introduce into the 'slots' any day start points required by multi-day + periods. """ - partitioned = {} - current = None + new_slots = [] current_date = None previously_active = None for point, active in slots: - dt = to_timezone(get_datetime(point), tzid) - start_of_day = get_start_of_day(dt) - this_date = dt.date() + start_of_day = get_start_of_day(point) + this_date = point.date() # For each new day, create a partition of the original collection. if this_date != current_date: current_date = this_date - partitioned[current_date] = current = [] # Add any continuing periods. - if dt != start_of_day and previously_active: - current.append((start_of_day, previously_active)) + if point != start_of_day and previously_active: + new_slots.append((start_of_day, previously_active)) # Add the currently active periods at this point in time. - current.append((point, active)) previously_active = active - partitioned = partitioned.items() - partitioned.sort() - return partitioned + for t in new_slots: + insort_left(slots, t) + +def add_slots(slots, points): + + """ + Introduce into the 'slots' entries for those in 'points' that are not + already present, propagating active periods from time points preceding and + succeeding those added. + """ + + new_slots = [] + + for point in points: + i = bisect_left(slots, (point, None)) + if i < len(slots) and slots[i][0] == point: + continue + + previously_active = i > 0 and slots[i-1] or [] + subsequently_active = i < len(slots) and slots[i] or [] + + active = [] + + for p, s in zip(previously_active, subsequently_active): + if p == s: + active.append(p) + else: + active.append(None) + + new_slots.append((point, active)) + + for t in new_slots: + insort_left(slots, t) + +def partition_by_day(slots): + + """ + Return a mapping from dates to time points provided by 'slots'. + """ + + d = {} + + for point, value in slots: + day = point.date() + if not d.has_key(day): + d[day] = [] + d[day].append((point, value)) + + return d def get_spans(slots):