# HG changeset patch # User Paul Boddie # Date 1301789161 -7200 # Node ID 5704afcbc54af705cfb7705838f018f8fadabc98 # Parent ffa908a396010e4b2d749652e66ccebf415152e9 Split the monolithic getEvents function into separate functions that find event pages in categories, find events in event pages, retain events within a given period, and define such periods at a particular resolution. Moved timespan and date conversion into a special class hierarchy used by date- and timespan-related classes. Simplified the getCoverage function which now takes events that can be provided by requesting items within a particular range from collections of timespans/events. Changed the EventAggregatorSupport imports to reduce the amount of repetition when using common functions and classes. diff -r ffa908a39601 -r 5704afcbc54a EventAggregatorSupport.py --- a/EventAggregatorSupport.py Sat Apr 02 19:07:50 2011 +0200 +++ b/EventAggregatorSupport.py Sun Apr 03 02:06:01 2011 +0200 @@ -374,6 +374,49 @@ pages.append(page) return pages +def getAllCategoryPages(category_names, request): + + """ + Return all pages belonging to the categories having the given + 'category_names', using the given 'request'. + """ + + pages = [] + pagenames = set() + + for category_name in category_names: + + # Get the pages and page names in the category. + + pages_in_category = getCategoryPages(category_name, request) + + # Visit each page in the category. + + for page_in_category in pages_in_category: + pagename = page_in_category.page_name + + # Only process each page once. + + if pagename in pagenames: + continue + else: + pagenames.add(pagename) + + pages.append(page_in_category) + + return pages + +def getPagesFromResults(result_pages, request): + + "Return genuine pages for the given 'result_pages' using the 'request'." + + return [Page(request, page.page_name) for page in result_pages] + +# Interfaces. + +class ActsAsTimespan: + pass + # The main activity functions. class EventPage: @@ -628,9 +671,6 @@ return linkToPage(request, self.page, text, query_string) -class ActsAsTimespan: - pass - class Event(ActsAsTimespan): "A description of an event." @@ -715,112 +755,49 @@ def as_limits(self): return self.as_timespan().as_limits() -def getEvents(request, category_names, calendar_start=None, calendar_end=None, resolution="month"): - - """ - Using the 'request', generate a list of events found on pages belonging to - the specified 'category_names', using the optional 'calendar_start' and - 'calendar_end' values to indicate a window of interest. - - The optional 'resolution' determines the unit of time used in providing the - results: - - * a list of events - * a dictionary mapping time units to event lists (within the window of - interest), usable as a kind of index to groups of events - * a list of all events within the window of interest - * the earliest time value of an event within the window of interest - * the latest time value of an event within the window of interest. - """ - - # Dates need to comply with the requested resolution. - # Here, None values need to be preserved when converting. - - if resolution == "month": - convert = lambda x: x and x.as_month() - get_values = lambda x, y: x.months_until(y) - else: - convert = lambda x: x and x.as_date() - get_values = lambda x, y: x.days_until(y) - - # Re-order the window, if appropriate. - - if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: - calendar_start, calendar_end = map(convert, (calendar_end, calendar_start)) - - # Otherwise, just convert the calendar limits. - - else: - calendar_start, calendar_end = map(convert, (calendar_start, calendar_end)) - - calendar_period = Timespan(calendar_start, calendar_end) +def getEventsFromPages(pages): + + "Return a list of events found on the given 'pages'." events = [] - shown_events = {} - all_shown_events = [] - processed_pages = set() - - earliest = None - latest = None - - for category_name in category_names: - - # Get the pages and page names in the category. - - pages_in_category = getCategoryPages(category_name, request) - - # Visit each page in the category. - - for page_in_category in pages_in_category: - pagename = page_in_category.page_name - - # Only process each page once. - - if pagename in processed_pages: - continue - else: - processed_pages.add(pagename) - - # Get a real page, not a result page. - - event_page = EventPage(Page(request, pagename)) - - # Get all events described in the page. - - for event in event_page.getEvents(): - event_details = event.getDetails() - - # Remember the event. - - events.append(event) - - # Test for the suitability of the event. - - if event.as_timespan() is not None: - start, end = map(convert, event.as_timespan().as_limits()) - - # Compare the dates to the requested calendar window, if any. - - if event in calendar_period: - - all_shown_events.append(event) - - if earliest is None or start < earliest: - earliest = start - if latest is None or end > latest: - latest = end - - # Store the event in the time-specific dictionary. - - first = max(start, calendar_start or start) - last = min(end, calendar_end or end) - - for event_time_value in get_values(first, last): - if not shown_events.has_key(event_time_value): - shown_events[event_time_value] = [] - shown_events[event_time_value].append(event) - - return events, shown_events, all_shown_events, earliest, latest + + for page in pages: + + # Get a real page, not a result page. + + event_page = EventPage(page) + + # Get all events described in the page. + + for event in event_page.getEvents(): + + # Remember the event. + + events.append(event) + + return events + +def getEventsInPeriod(events, calendar_period, resolution): + + """ + Return a collection containing those of the given 'events' which occur + within the given 'calendar_period' at the given 'resolution'. + """ + + all_shown_events = TimespanCollection(resolution) + + for event in events: + + # Test for the suitability of the event. + + if event.as_timespan() is not None: + + # Compare the dates to the requested calendar window, if any. + + if event in calendar_period: + all_shown_events.insert_in_order(event) + + return all_shown_events def setEventTimestamps(request, events): @@ -867,6 +844,23 @@ ordered_events.sort() return ordered_events +def getCalendarPeriod(calendar_start, calendar_end, resolution): + + """ + Return a calendar period for the given 'calendar_start' and 'calendar_end', + employing the given 'resolution'. The 'calendar_start' and 'calendar_end' + parameters can be given as None. + """ + + # Re-order the window, if appropriate. + + if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: + calendar_start, calendar_end = calendar_end, calendar_start + + # Return a timespan at the given resolution. + + return Timespan(calendar_start, calendar_end).convert(resolution) + def getConcretePeriod(calendar_start, calendar_end, earliest, latest): """ @@ -903,70 +897,63 @@ return min(first, last), last -def getCoverage(start, end, events, resolution="date"): +def getCoverage(events, resolution="date"): """ - Within the period defined by the 'start' and 'end' dates, determine the - coverage of the days in the period by the given 'events', returning a - collection of timespans, along with a dictionary mapping locations to - collections of slots, where each slot contains a tuple of the form - (timespans, events). + Determine the coverage of the given 'events', returning a collection of + timespans, along with a dictionary mapping locations to collections of + slots, where each slot contains a tuple of the form (timespans, events). """ all_events = {} full_coverage = TimespanCollection(resolution) - coverage_period = full_coverage.convert(Timespan(start, end)) # Get event details. for event in events: event_details = event.getDetails() - # Test for the event in the period. - - if event in coverage_period: - - # Find the coverage of this period for the event. - - # For day views, each location has its own slot, but for month - # views, all locations are pooled together since having separate - # slots for each location can lead to poor usage of vertical space. - - if resolution == "datetime": - event_location = event_details.get("location") - else: - event_location = None - - # Update the overall coverage. - - full_coverage.insert_in_order(event) - - # Add a new events list for a new location. - # Locations can be unspecified, thus None refers to all unlocalised - # events. - - if not all_events.has_key(event_location): - all_events[event_location] = [TimespanCollection(resolution, [event])] - - # Try and fit the event into an events list. + # Find the coverage of this period for the event. + + # For day views, each location has its own slot, but for month + # views, all locations are pooled together since having separate + # slots for each location can lead to poor usage of vertical space. + + if resolution == "datetime": + event_location = event_details.get("location") + else: + event_location = None + + # Update the overall coverage. + + full_coverage.insert_in_order(event) + + # Add a new events list for a new location. + # Locations can be unspecified, thus None refers to all unlocalised + # events. + + if not all_events.has_key(event_location): + all_events[event_location] = [TimespanCollection(resolution, [event])] + + # Try and fit the event into an events list. + + else: + slot = all_events[event_location] + + for slot_events in slot: + + # Where the event does not overlap with the events in the + # current collection, add it alongside these events. + + if not event in slot_events: + slot_events.insert_in_order(event) + break + + # Make a new element in the list if the event cannot be + # marked alongside existing events. else: - slot = all_events[event_location] - - for slot_events in slot: - - # Where the event does not overlap with the events in the - # current collection, add it alongside these events. - - if not event in slot_events: - slot_events.insert_in_order(event) - break - - # Make a new element in the list if the event cannot be - # marked alongside existing events. - - else: - slot.append(TimespanCollection(resolution, [event])) + slot.append(TimespanCollection(resolution, [event])) return full_coverage, all_events @@ -1041,7 +1028,21 @@ def months(self): return self.data[0] * 12 + self.data[1] -class Temporal: +class Convertible: + + "Support for converting temporal objects." + + def _get_converter(self, resolution): + if resolution == "month": + return lambda x: x and x.as_month() + elif resolution == "date": + return lambda x: x and x.as_date() + elif resolution == "datetime": + return lambda x: x and x.as_datetime_or_date() + else: + return lambda x: x + +class Temporal(Convertible): "A simple temporal representation, common to dates and times." @@ -1057,6 +1058,9 @@ def as_tuple(self): return tuple(self.data) + def convert(self, resolution): + return self._get_converter(resolution)(self) + def __cmp__(self, other): """ @@ -1167,6 +1171,8 @@ return self.until(self.as_month(), end.as_month(), Month.next_month, Month.previous_month) + step_to = months_until + class Date(Month): "A simple year-month-day representation." @@ -1241,6 +1247,8 @@ return self.until(self.as_date(), end.as_date(), Date.next_day, Date.previous_day) + step_to = days_until + class DateTime(Date): "A simple date plus time representation." @@ -1522,7 +1530,7 @@ return 0 -class Timespan(ActsAsTimespan): +class Timespan(ActsAsTimespan, Convertible): """ A period of time which can be compared against others to check for overlaps. @@ -1544,6 +1552,9 @@ def as_limits(self): return self.start, self.end + def convert(self, resolution): + return Timespan(*map(self._get_converter(resolution), self.as_limits())) + def is_before(self, a, b): """ @@ -1607,25 +1618,31 @@ """ def __init__(self, resolution, values=None): - - # Timespans need to be given converted start and end dates/times. - - if resolution == "date": - self.convert_time = lambda x: x.as_date() - elif resolution == "datetime": - self.convert_time = lambda x: x.as_datetime_or_date() + self.resolution = resolution + self.values = values or [] + + def as_timespan(self): + return Timespan(*self.as_limits()) + + def as_limits(self): + + "Return the earliest and latest points in time for this collection." + + if not self.values: + return None, None else: - self.convert_time = lambda x: x - - self.values = values or [] + first, last = self.values[0], self.values[-1] + if isinstance(first, ActsAsTimespan): + first = first.as_timespan().start + if isinstance(last, ActsAsTimespan): + last = last.as_timespan().end + return first, last def convert(self, value): if isinstance(value, ActsAsTimespan): - value = value.as_timespan() - start, end = map(self.convert_time, value.as_limits()) - return Timespan(start, end) + return value.as_timespan().convert(self.resolution) else: - return self.convert_time(value) + return value.convert(self.resolution) def __iter__(self): return iter(self.values) @@ -1655,6 +1672,11 @@ def insert_in_order(self, value): bisect.insort_left(self, value) + def items_in_range(self, start, end): + slice_start = bisect.bisect_left(self, start) + slice_end = bisect.bisect_right(self, end, slice_start) + return self.values[slice_start:slice_end] + def getCountry(s): "Find a country code in the given string 's'." diff -r ffa908a39601 -r 5704afcbc54a actions/EventAggregatorNewEvent.py --- a/actions/EventAggregatorNewEvent.py Sat Apr 02 19:07:50 2011 +0200 +++ b/actions/EventAggregatorNewEvent.py Sun Apr 03 02:06:01 2011 +0200 @@ -13,10 +13,7 @@ from MoinMoin.action import ActionBase from MoinMoin.Page import Page from MoinMoin.PageEditor import PageEditor -import EventAggregatorSupport - -escape = EventAggregatorSupport.escape -escattr = EventAggregatorSupport.escattr +from EventAggregatorSupport import * try: import pytz @@ -27,7 +24,7 @@ # Action class and supporting functions. -class EventAggregatorNewEvent(ActionBase, EventAggregatorSupport.ActionSupport): +class EventAggregatorNewEvent(ActionBase, ActionSupport): "An event creation dialogue requesting various parameters." @@ -54,10 +51,7 @@ category_list = [] category_pagenames = form.get("category", []) - for category_name, category_pagename in \ - EventAggregatorSupport.getCategoryMapping( - EventAggregatorSupport.getCategories(request), - request): + for category_name, category_pagename in getCategoryMapping(getCategories(request), request): selected = self._get_selected_for_list(category_pagename, category_pagenames) @@ -525,12 +519,12 @@ except (TypeError, ValueError): return 0, _("Hours, minutes and seconds must be numbers yielding a valid time!") - start_date = EventAggregatorSupport.DateTime( + start_date = DateTime( (start_year, start_month, start_day, start_hour, start_minute, start_second, start_zone) ) start_date.constrain() - end_date = EventAggregatorSupport.DateTime( + end_date = DateTime( (end_year, end_month, end_day, end_hour, end_minute, end_second, end_zone) ) end_date.constrain() @@ -556,7 +550,7 @@ # Use any parent page information. - full_title = EventAggregatorSupport.getFullPageName(parent, title) + full_title = getFullPageName(parent, title) # Load the new page and replace the event details in the body. @@ -567,8 +561,7 @@ # Complete the new page. - EventAggregatorSupport.fillEventPageFromTemplate(template_page, - new_page, event_details, category_pagenames) + fillEventPageFromTemplate(template_page, new_page, event_details, category_pagenames) # Redirect and return success. diff -r ffa908a39601 -r 5704afcbc54a actions/EventAggregatorSummary.py --- a/actions/EventAggregatorSummary.py Sat Apr 02 19:07:50 2011 +0200 +++ b/actions/EventAggregatorSummary.py Sun Apr 03 02:06:01 2011 +0200 @@ -15,16 +15,13 @@ from MoinMoin import config from MoinMoin.Page import Page from MoinMoin import wikiutil -import EventAggregatorSupport - -escape = EventAggregatorSupport.escape -escattr = EventAggregatorSupport.escattr +from EventAggregatorSupport import * Dependencies = ['pages'] # Action class and supporting functions. -class EventAggregatorSummary(ActionBase, EventAggregatorSupport.ActionSupport): +class EventAggregatorSummary(ActionBase, ActionSupport): "A summary dialogue requesting various parameters." @@ -38,10 +35,7 @@ category_list = [] category_pagenames = form.get("category", []) - for category_name, category_pagename in \ - EventAggregatorSupport.getCategoryMapping( - EventAggregatorSupport.getCategories(request), - request): + for category_name, category_pagename in getCategoryMapping(getCategories(request), request): selected = self._get_selected_for_list(category_pagename, category_pagenames) @@ -60,11 +54,11 @@ end_criteria_default = form.get("end", [""])[0] if resolution == "date": - get_parameter = EventAggregatorSupport.getParameterDate - get_label = EventAggregatorSupport.getFullDateLabel + get_parameter = getParameterDate + get_label = getFullDateLabel else: - get_parameter = EventAggregatorSupport.getParameterMonth - get_label = EventAggregatorSupport.getFullMonthLabel + get_parameter = getParameterMonth + get_label = getFullMonthLabel start_criteria_evaluated = get_parameter(start_criteria_default) end_criteria_evaluated = get_parameter(end_criteria_default) @@ -242,7 +236,7 @@ can be specified. """ - form = EventAggregatorSupport.get_form(request) + form = get_form(request) category_names = form.get("category", []) format = form.get("format", ["iCalendar"])[0] @@ -255,30 +249,31 @@ # dates or for years and months. if resolution == "date": - calendar_start = EventAggregatorSupport.getFormDate(request, None, "start") - calendar_end = EventAggregatorSupport.getFormDate(request, None, "end") + calendar_start = getFormDate(request, None, "start") + calendar_end = getFormDate(request, None, "end") if calendar_start is None: - calendar_start = EventAggregatorSupport.getFormDateTriple(request, "start-year", "start-month", "start-day") + calendar_start = getFormDateTriple(request, "start-year", "start-month", "start-day") if calendar_end is None: - calendar_end = EventAggregatorSupport.getFormDateTriple(request, "end-year", "end-month", "end-day") + calendar_end = getFormDateTriple(request, "end-year", "end-month", "end-day") elif resolution == "month": - calendar_start = EventAggregatorSupport.getFormMonth(request, None, "start") - calendar_end = EventAggregatorSupport.getFormMonth(request, None, "end") + calendar_start = getFormMonth(request, None, "start") + calendar_end = getFormMonth(request, None, "end") if calendar_start is None: - calendar_start = EventAggregatorSupport.getFormMonthPair(request, "start-year", "start-month") + calendar_start = getFormMonthPair(request, "start-year", "start-month") if calendar_end is None: - calendar_end = EventAggregatorSupport.getFormMonthPair(request, "end-year", "end-month") + calendar_end = getFormMonthPair(request, "end-year", "end-month") # Determine the period and get the events involved. - events, shown_events, all_shown_events, earliest, latest = \ - EventAggregatorSupport.getEvents(request, category_names, calendar_start, calendar_end, - resolution) + event_pages = getPagesFromResults(getAllCategoryPages(category_names, request), request) + events = getEventsFromPages(event_pages) + calendar_period = getCalendarPeriod(calendar_start, calendar_end, resolution) + all_shown_events = getEventsInPeriod(events, calendar_period, resolution) - latest_timestamp = EventAggregatorSupport.setEventTimestamps(request, all_shown_events) + latest_timestamp = setEventTimestamps(request, all_shown_events) # Output summary data... @@ -287,7 +282,7 @@ elif hasattr(request, "emit_http_headers"): send_headers = request.emit_http_headers else: - send_headers = EventAggregatorSupport.send_headers(request) + send_headers = send_headers(request) # Define headers. @@ -299,7 +294,7 @@ # Define the last modified time. if latest_timestamp is not None: - headers.append("Last-Modified: %s" % EventAggregatorSupport.getHTTPTimeString(latest_timestamp)) + headers.append("Last-Modified: %s" % getHTTPTimeString(latest_timestamp)) send_headers(headers) @@ -331,13 +326,13 @@ start = event_details["start"] end = event_details["end"] - if isinstance(start, EventAggregatorSupport.DateTime): + if isinstance(start, DateTime): request.write("DTSTART") write_calendar_datetime(request, start) else: request.write("DTSTART;VALUE=DATE:%04d%02d%02d\r\n" % start.as_date().as_tuple()) - if isinstance(end, EventAggregatorSupport.DateTime): + if isinstance(end, DateTime): request.write("DTEND") write_calendar_datetime(request, end) else: @@ -377,7 +372,7 @@ request.write('Events published on %s%s\r\n' % (request.getBaseURL(), path_info)) if latest_timestamp is not None: - request.write('%s\r\n' % EventAggregatorSupport.getHTTPTimeString(latest_timestamp)) + request.write('%s\r\n' % getHTTPTimeString(latest_timestamp)) # Sort all_shown_events by start date, reversed: # @@ -386,7 +381,7 @@ # # So we use as sorting key the "start" key from the event details. - ordered_events = EventAggregatorSupport.getOrderedEvents(all_shown_events) + ordered_events = getOrderedEvents(all_shown_events) ordered_events.reverse() for event in ordered_events: @@ -415,7 +410,7 @@ for topic in event_details.get("topics") or event_details.get("categories") or []: request.write('%s\r\n' % topic) - request.write('%s\r\n' % EventAggregatorSupport.getHTTPTimeString(event_details["created"])) + request.write('%s\r\n' % getHTTPTimeString(event_details["created"])) request.write('%s#%s\r\n' % (link, event_details["sequence"])) request.write('\r\n') diff -r ffa908a39601 -r 5704afcbc54a macros/EventAggregator.py --- a/macros/EventAggregator.py Sat Apr 02 19:07:50 2011 +0200 +++ b/macros/EventAggregator.py Sun Apr 03 02:06:01 2011 +0200 @@ -9,11 +9,9 @@ """ from MoinMoin import wikiutil -import EventAggregatorSupport +from EventAggregatorSupport import * import calendar -linkToPage = EventAggregatorSupport.linkToPage - Dependencies = ['pages'] # Abstractions. @@ -79,7 +77,7 @@ "Return the 'argname' qualified using the calendar name." - return EventAggregatorSupport.getQualifiedParameterName(self.calendar_name, argname) + return getQualifiedParameterName(self.calendar_name, argname) def getDateQueryString(self, argname, date, prefix=1): @@ -142,12 +140,12 @@ def getFullDateLabel(self, date): page = self.page request = page.request - return EventAggregatorSupport.getFullDateLabel(request, date) + return getFullDateLabel(request, date) def getFullMonthLabel(self, year_month): page = self.page request = page.request - return EventAggregatorSupport.getFullMonthLabel(request, year_month) + return getFullMonthLabel(request, year_month) def writeDownloadControls(self): @@ -537,7 +535,7 @@ for weekday in range(0, 7): output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : "3"})) - output.append(fmt.text(_(EventAggregatorSupport.getDayLabel(weekday)))) + output.append(fmt.text(_(getDayLabel(weekday)))) output.append(fmt.table_cell(on=0)) output.append(fmt.table_row(on=0)) @@ -604,7 +602,7 @@ output = [] locations = week_slots.keys() - locations.sort(EventAggregatorSupport.sort_none_first) + locations.sort(sort_none_first) # Visit each slot corresponding to a location (or no location). @@ -859,12 +857,12 @@ output = [] locations = day_slots.keys() - locations.sort(EventAggregatorSupport.sort_none_first) + locations.sort(sort_none_first) # Traverse the time scale of the full coverage, visiting each slot to # determine whether it provides content for each period. - scale = EventAggregatorSupport.getCoverageScale(full_coverage) + scale = getCoverageScale(full_coverage) # Define a mapping of events to rowspans. @@ -1125,16 +1123,16 @@ # Find request parameters to override settings. - mode = EventAggregatorSupport.getQualifiedParameter(request, calendar_name, "mode", mode or "calendar") + mode = getQualifiedParameter(request, calendar_name, "mode", mode or "calendar") # Different modes require different levels of precision. if mode == "day": - get_date = EventAggregatorSupport.getParameterDate - get_form_date = EventAggregatorSupport.getFormDate + get_date = getParameterDate + get_form_date = getFormDate else: - get_date = EventAggregatorSupport.getParameterMonth - get_form_date = EventAggregatorSupport.getFormMonth + get_date = getParameterMonth + get_form_date = getFormMonth # Determine the limits of the calendar. @@ -1146,13 +1144,18 @@ # Get the events according to the resolution of the calendar. - events, shown_events, all_shown_events, earliest, latest = \ - EventAggregatorSupport.getEvents(request, category_names, calendar_start, calendar_end, - mode == "day" and "date" or "month") + resolution = mode == "day" and "date" or "month" + + event_pages = getPagesFromResults(getAllCategoryPages(category_names, request), request) + events = getEventsFromPages(event_pages) + calendar_period = getCalendarPeriod(calendar_start, calendar_end, resolution) + all_shown_events = getEventsInPeriod(events, calendar_period, resolution) + + earliest, latest = all_shown_events.as_limits() # Get a concrete period of time. - first, last = EventAggregatorSupport.getConcretePeriod(calendar_start, calendar_end, earliest, latest) + first, last = getConcretePeriod(calendar_start, calendar_end, earliest, latest) # Define a view of the calendar, retaining useful navigational information. @@ -1194,7 +1197,7 @@ # Get the events in order. - ordered_events = EventAggregatorSupport.getOrderedEvents(all_shown_events) + ordered_events = getOrderedEvents(all_shown_events) # Show the events in order. @@ -1304,8 +1307,8 @@ week_start = month.as_date(max(first_day, 1)) week_end = month.as_date(min(first_day + 6, number_of_days)) - full_coverage, week_slots = EventAggregatorSupport.getCoverage( - week_start, week_end, shown_events.get(month, [])) + full_coverage, week_slots = getCoverage( + all_shown_events.items_in_range(week_start, week_end.next_day())) # Output a week, starting with the day numbers. @@ -1349,7 +1352,8 @@ # Get the events in order. - ordered_events = EventAggregatorSupport.getOrderedEvents(shown_events.get(month, [])) + ordered_events = getOrderedEvents( + all_shown_events.items_in_range(month, month.next_month())) # Show the events in order. @@ -1418,11 +1422,11 @@ output.append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day"})) - full_coverage, day_slots = EventAggregatorSupport.getCoverage( - date, date, shown_events.get(date, []), "datetime") + full_coverage, day_slots = getCoverage( + all_shown_events.items_in_range(date, date.next_day()), "datetime") # Work out how many columns the day title will need. - # Includer spacers before each event column. + # Include spacers before each event column. colspan = sum(map(len, day_slots.values())) * 2 + 1