1.1 --- a/imip_manager.py Thu Jan 22 20:13:06 2015 +0100
1.2 +++ b/imip_manager.py Fri Jan 23 23:58:26 2015 +0100
1.3 @@ -35,7 +35,9 @@
1.4 from imiptools.dates import format_datetime, get_datetime, get_start_of_day, \
1.5 to_timezone
1.6 from imiptools.mail import Messenger
1.7 -from imiptools.period import have_conflict, get_slots, get_spans, partition_slots
1.8 +from imiptools.period import add_day_start_points, add_slots, convert_periods, \
1.9 + get_scale, have_conflict, get_slots, get_spans, \
1.10 + partition_by_day
1.11 from imiptools.profile import Preferences
1.12 from vCalendar import to_node
1.13 import markup
1.14 @@ -179,6 +181,7 @@
1.15 self.encoding = "utf-8"
1.16
1.17 self.store = imip_store.FileStore()
1.18 + self.objects = {}
1.19
1.20 try:
1.21 self.publisher = imip_store.FilePublisher()
1.22 @@ -189,12 +192,15 @@
1.23 return path_info.lstrip("/").split("/", 1)[0]
1.24
1.25 def _get_object(self, uid):
1.26 + if self.objects.has_key(uid):
1.27 + return self.objects[uid]
1.28 +
1.29 f = uid and self.store.get_event(self.user, uid) or None
1.30
1.31 if not f:
1.32 return None
1.33
1.34 - obj = parse_object(f, "utf-8")
1.35 + self.objects[uid] = obj = parse_object(f, "utf-8")
1.36
1.37 if not obj:
1.38 return None
1.39 @@ -210,6 +216,19 @@
1.40 self.requests = self.store.get_requests(self.user)
1.41 return self.requests
1.42
1.43 + def _get_request_summary(self):
1.44 + summary = []
1.45 + for uid in self._get_requests():
1.46 + obj = self._get_object(uid)
1.47 + if obj:
1.48 + details = self._get_details(obj)
1.49 + summary.append((
1.50 + get_value(details, "DTSTART"),
1.51 + get_value(details, "DTEND"),
1.52 + uid
1.53 + ))
1.54 + return summary
1.55 +
1.56 # Preference methods.
1.57
1.58 def get_user_locale(self):
1.59 @@ -222,6 +241,8 @@
1.60 self.preferences = Preferences(self.user)
1.61 return self.preferences
1.62
1.63 + # Prettyprinting of dates and times.
1.64 +
1.65 def format_date(self, dt, format):
1.66 return self._format_datetime(babel.dates.format_date, dt, format)
1.67
1.68 @@ -466,10 +487,12 @@
1.69 "Show the calendar for the current user."
1.70
1.71 self.new_page(title="Calendar")
1.72 + page = self.page
1.73 +
1.74 self.show_requests_on_page()
1.75
1.76 + request_summary = self._get_request_summary()
1.77 freebusy = self.store.get_freebusy(self.user)
1.78 - page = self.page
1.79
1.80 if not freebusy:
1.81 page.p("No events scheduled.")
1.82 @@ -493,29 +516,126 @@
1.83 # Requests could be listed and linked to their tentative positions in
1.84 # the calendar.
1.85
1.86 - slots = get_slots(freebusy)
1.87 - partitioned = partition_slots(slots, tzid)
1.88 - columns = max(map(lambda i: len(i[1]), slots)) + 1
1.89 + groups = []
1.90 + group_columns = []
1.91 + all_points = set()
1.92 +
1.93 + # Obtain time point information for each group of periods.
1.94 +
1.95 + for periods in [request_summary, freebusy]:
1.96 + periods = convert_periods(periods, tzid)
1.97 +
1.98 + # Get the time scale with start and end points.
1.99 +
1.100 + scale = get_scale(periods)
1.101 +
1.102 + # Get the time slots for the periods.
1.103 +
1.104 + slots = get_slots(scale)
1.105 +
1.106 + # Add start of day time points for multi-day periods.
1.107 +
1.108 + add_day_start_points(slots)
1.109 +
1.110 + # Record the slots and all time points employed.
1.111 +
1.112 + groups.append(slots)
1.113 + all_points.update([point for point, slot in slots])
1.114 +
1.115 + # Partition the groups into days.
1.116 +
1.117 + days = {}
1.118 + partitioned_groups = []
1.119 +
1.120 + for slots in groups:
1.121 +
1.122 + # Propagate time points to all groups of time slots.
1.123 +
1.124 + add_slots(slots, all_points)
1.125 +
1.126 + # Count the number of columns employed by the group.
1.127 +
1.128 + columns = 0
1.129 +
1.130 + # Partition the time slots by day.
1.131 +
1.132 + partitioned = {}
1.133 +
1.134 + for day, day_slots in partition_by_day(slots).items():
1.135 + columns = max(columns, max(map(lambda i: len(i[1]), day_slots)))
1.136 +
1.137 + if not days.has_key(day):
1.138 + days[day] = set()
1.139 +
1.140 + # Convert each partition to a mapping from points to active
1.141 + # periods.
1.142 +
1.143 + day_slots = dict(day_slots)
1.144 + partitioned[day] = day_slots
1.145 + days[day].update(day_slots.keys())
1.146 +
1.147 + if partitioned:
1.148 + group_columns.append(columns + 1)
1.149 + partitioned_groups.append(partitioned)
1.150
1.151 page.table(border=1, cellspacing=0, cellpadding=5)
1.152 + self.show_calendar_days(days, partitioned_groups, group_columns)
1.153 + page.table.close()
1.154
1.155 - for day, slots in partitioned:
1.156 - spans = get_spans(slots)
1.157 + def show_calendar_days(self, days, partitioned_groups, group_columns):
1.158 + page = self.page
1.159 +
1.160 + # Determine the number of columns required, the days providing time
1.161 + # slots.
1.162
1.163 + all_columns = sum(group_columns)
1.164 + all_days = days.items()
1.165 + all_days.sort()
1.166 +
1.167 + # Produce a heading and time points for each day.
1.168 +
1.169 + for day, points in all_days:
1.170 page.tr()
1.171 - page.th(class_="dayheading", colspan=columns)
1.172 + page.th(class_="dayheading", colspan=all_columns)
1.173 page.add(self.format_date(day, "full"))
1.174 page.th.close()
1.175 page.tr.close()
1.176
1.177 - for point, active in slots:
1.178 - dt = to_timezone(get_datetime(point), tzid)
1.179 - continuation = dt == get_start_of_day(dt)
1.180 + groups_for_day = [partitioned.get(day) for partitioned in partitioned_groups]
1.181 +
1.182 + self.show_calendar_points(points, groups_for_day, group_columns)
1.183 +
1.184 + def show_calendar_points(self, points, groups, group_columns):
1.185 + page = self.page
1.186 +
1.187 + # Produce a row for each time point.
1.188 +
1.189 + points = list(points)
1.190 + points.sort()
1.191 +
1.192 + for point in points:
1.193 + continuation = point == get_start_of_day(point)
1.194
1.195 - page.tr()
1.196 - page.th(class_="timeslot")
1.197 - page.add(self.format_time(dt, "long"))
1.198 - page.th.close()
1.199 + page.tr()
1.200 + page.th(class_="timeslot")
1.201 + page.add(self.format_time(point, "long"))
1.202 + page.th.close()
1.203 +
1.204 + # Obtain slots for the time point from each group.
1.205 +
1.206 + for columns, slots in zip(group_columns, groups):
1.207 + active = slots and slots.get(point)
1.208 +
1.209 + if not active:
1.210 + page.td(class_="empty", colspan=columns)
1.211 + page.td.close()
1.212 + continue
1.213 +
1.214 + slots = slots.items()
1.215 + slots.sort()
1.216 + spans = get_spans(slots)
1.217 +
1.218 + # Show a column for each active period.
1.219
1.220 for t in active:
1.221 if t:
1.222 @@ -533,9 +653,7 @@
1.223 page.td(class_="empty")
1.224 page.td.close()
1.225
1.226 - page.tr.close()
1.227 -
1.228 - page.table.close()
1.229 + page.tr.close()
1.230
1.231 def select_action(self):
1.232
2.1 --- a/imiptools/period.py Thu Jan 22 20:13:06 2015 +0100
2.2 +++ b/imiptools/period.py Fri Jan 23 23:58:26 2015 +0100
2.3 @@ -100,19 +100,36 @@
2.4
2.5 # Period layout.
2.6
2.7 -def get_scale(l):
2.8 +def convert_periods(periods, tzid):
2.9 +
2.10 + "Convert 'periods' to use datetime objects employing the given 'tzid'."
2.11 +
2.12 + l = []
2.13 +
2.14 + for t in periods:
2.15 + start, end = t[:2]
2.16 + start = to_timezone(get_datetime(start), tzid)
2.17 + end = to_timezone(get_datetime(end), tzid)
2.18 + l.append((start, end) + tuple(t[2:]))
2.19 +
2.20 + return l
2.21 +
2.22 +def get_scale(periods):
2.23
2.24 """
2.25 - Return an ordered time scale from the given list 'l' of tuples, with the
2.26 - first two elements of each tuple being start and end times.
2.27 + Return an ordered time scale from the given list 'periods', with the first
2.28 + two elements of each tuple being start and end times.
2.29
2.30 - The returned scale is a collection of (time, (starting, ending)) tuples,
2.31 - where starting and ending are collections of tuples from 'l'.
2.32 + The given 'tzid' is used to make sure that the times are defined according
2.33 + to the chosen time zone.
2.34 +
2.35 + The returned scale is a mapping from time to (starting, ending) tuples,
2.36 + where starting and ending are collections of tuples from 'periods'.
2.37 """
2.38
2.39 scale = {}
2.40
2.41 - for t in l:
2.42 + for t in periods:
2.43 start, end = t[:2]
2.44
2.45 # Add a point and this event to the starting list.
2.46 @@ -127,15 +144,12 @@
2.47 scale[end] = [], []
2.48 scale[end][1].append(t)
2.49
2.50 - scale = scale.items()
2.51 - scale.sort()
2.52 return scale
2.53
2.54 -def get_slots(l):
2.55 +def get_slots(scale):
2.56
2.57 """
2.58 - Return an ordered list of time slots from the given list 'l' of tuples, with
2.59 - the first two elements of each tuple being start and end times.
2.60 + Return an ordered list of time slots from the given 'scale'.
2.61
2.62 Each slot is a tuple containing a point in time for the start of the slot,
2.63 together with a list of parallel event tuples, each tuple containing the
2.64 @@ -145,7 +159,10 @@
2.65 slots = []
2.66 active = []
2.67
2.68 - for point, (starting, ending) in get_scale(l):
2.69 + points = scale.items()
2.70 + points.sort()
2.71 +
2.72 + for point, (starting, ending) in points:
2.73
2.74 # Discard all active events ending at or before this start time.
2.75 # Free up the position in the active list.
2.76 @@ -173,45 +190,84 @@
2.77
2.78 return slots
2.79
2.80 -def partition_slots(slots, tzid):
2.81 +def add_day_start_points(slots):
2.82
2.83 """
2.84 - Partition the given 'slots' into separate collections having a date-level
2.85 - resolution, using the given 'tzid' to make sure that the day boundaries are
2.86 - defined according to the chosen time zone.
2.87 -
2.88 - Return a collection of (date, slots) tuples.
2.89 + Introduce into the 'slots' any day start points required by multi-day
2.90 + periods.
2.91 """
2.92
2.93 - partitioned = {}
2.94 - current = None
2.95 + new_slots = []
2.96 current_date = None
2.97 previously_active = None
2.98
2.99 for point, active in slots:
2.100 - dt = to_timezone(get_datetime(point), tzid)
2.101 - start_of_day = get_start_of_day(dt)
2.102 - this_date = dt.date()
2.103 + start_of_day = get_start_of_day(point)
2.104 + this_date = point.date()
2.105
2.106 # For each new day, create a partition of the original collection.
2.107
2.108 if this_date != current_date:
2.109 current_date = this_date
2.110 - partitioned[current_date] = current = []
2.111
2.112 # Add any continuing periods.
2.113
2.114 - if dt != start_of_day and previously_active:
2.115 - current.append((start_of_day, previously_active))
2.116 + if point != start_of_day and previously_active:
2.117 + new_slots.append((start_of_day, previously_active))
2.118
2.119 # Add the currently active periods at this point in time.
2.120
2.121 - current.append((point, active))
2.122 previously_active = active
2.123
2.124 - partitioned = partitioned.items()
2.125 - partitioned.sort()
2.126 - return partitioned
2.127 + for t in new_slots:
2.128 + insort_left(slots, t)
2.129 +
2.130 +def add_slots(slots, points):
2.131 +
2.132 + """
2.133 + Introduce into the 'slots' entries for those in 'points' that are not
2.134 + already present, propagating active periods from time points preceding and
2.135 + succeeding those added.
2.136 + """
2.137 +
2.138 + new_slots = []
2.139 +
2.140 + for point in points:
2.141 + i = bisect_left(slots, (point, None))
2.142 + if i < len(slots) and slots[i][0] == point:
2.143 + continue
2.144 +
2.145 + previously_active = i > 0 and slots[i-1] or []
2.146 + subsequently_active = i < len(slots) and slots[i] or []
2.147 +
2.148 + active = []
2.149 +
2.150 + for p, s in zip(previously_active, subsequently_active):
2.151 + if p == s:
2.152 + active.append(p)
2.153 + else:
2.154 + active.append(None)
2.155 +
2.156 + new_slots.append((point, active))
2.157 +
2.158 + for t in new_slots:
2.159 + insort_left(slots, t)
2.160 +
2.161 +def partition_by_day(slots):
2.162 +
2.163 + """
2.164 + Return a mapping from dates to time points provided by 'slots'.
2.165 + """
2.166 +
2.167 + d = {}
2.168 +
2.169 + for point, value in slots:
2.170 + day = point.date()
2.171 + if not d.has_key(day):
2.172 + d[day] = []
2.173 + d[day].append((point, value))
2.174 +
2.175 + return d
2.176
2.177 def get_spans(slots):
2.178