# HG changeset patch # User Paul Boddie # Date 1295227577 -3600 # Node ID 2c3decb19e692108ef92395012351552804f4137 # Parent 0c87ac42fed20e7cb62883c6268ff7fd5c30231a Introduced an as_datetime_or_date method to the DateTime class in order to support conversion of incomplete datetimes to dates and comparisons of dates and datetimes. Introduced time_string and time methods to the DateTime class. Introduced usage of has_time where merely testing for DateTime compliance is not sufficient. Converted end dates from events in the scale used for the day view, incrementing such dates in order to provide a usable endpoint for whole day events. Renamed the as_times method to as_limits, since the limits may not convey time information. Made Event instances hashable. Changed the macro and summary action to use a resolution parameter for the action. Added the "original" calendar limits to View instances so that subscriptions and downloads of a calendar are advertised using the appropriate calculated period. Changed the macro to employ rowspans and to emit events only for the first period on a given day that they occur, with the rowspan causing the event to fill the remaining periods. Tidied the time scale used for the day view, padding each cell and employing only time information. diff -r 0c87ac42fed2 -r 2c3decb19e69 EventAggregatorSupport.py --- a/EventAggregatorSupport.py Sun Jan 16 19:30:01 2011 +0100 +++ b/EventAggregatorSupport.py Mon Jan 17 02:26:17 2011 +0100 @@ -631,6 +631,9 @@ self.page = page self.details = details + def __hash__(self): + return hash(self.getSummary()) + def getPage(self): "Return the page describing this event." @@ -701,8 +704,8 @@ else: return None - def as_times(self): - return self.as_timespan().as_times() + def as_limits(self): + return self.as_timespan().as_limits() def getEvents(request, category_names, calendar_start=None, calendar_end=None, resolution="month"): @@ -786,7 +789,7 @@ # Test for the suitability of the event. if event.as_timespan() is not None: - start, end = map(convert, event.as_timespan().as_times()) + start, end = map(convert, event.as_timespan().as_limits()) # Compare the dates to the requested calendar window, if any. @@ -963,9 +966,19 @@ times = set() for timespan in coverage: - start, end = timespan.as_times() - times.add(start) - times.add(end) + start, end = timespan.as_limits() + + # Add either genuine times or dates converted to times. + + if isinstance(start, DateTime): + times.add(start) + + if isinstance(end, DateTime): + if end.has_time(): + times.add(end) + else: + times.add(end.as_date().next_day()) + times = list(times) times.sort() @@ -1194,6 +1207,9 @@ self.data[3:6] = hour, minute, second def __str__(self): + return Date.__str__(self) + self.time_string() + + def time_string(self): if self.has_time(): data = self.as_tuple() time_str = " %02d:%02d" % data[3:5] @@ -1201,10 +1217,9 @@ time_str += ":%02d" % data[5] if data[6] is not None: time_str += " %s" % data[6] + return time_str else: - time_str = "" - - return Date.__str__(self) + time_str + return "" def as_datetime(self): return self @@ -1212,17 +1227,37 @@ def as_date(self): return Date(self.data[:3]) + def as_datetime_or_date(self): + + """ + Return a date for this datetime if fields are missing. Otherwise, return + this datetime itself. + """ + + if not self.has_time(): + return self.as_date() + else: + return self + def __cmp__(self, other): - if isinstance(other, DateTime): - self_utc = self.to_utc() - other_utc = other.to_utc() - if self_utc is not None and other_utc is not None: - return cmp(self_utc.as_tuple(), other_utc.as_tuple()) - return Month.__cmp__(self, other) + this = self.as_datetime_or_date() + + if isinstance(this, DateTime) and isinstance(other, DateTime): + other = other.as_datetime_or_date() + if isinstance(other, DateTime): + this_utc = this.to_utc() + other_utc = other.to_utc() + if this_utc is not None and other_utc is not None: + return cmp(this_utc.as_tuple(), other_utc.as_tuple()) + + return Date.__cmp__(this, other) def has_time(self): return self.data[3] is not None and self.data[4] is not None + def time(self): + return self.data[3:] + def seconds(self): return self.data[5] @@ -1246,6 +1281,9 @@ defined. """ + if not self.has_time(): + return None + offset = self.utc_offset() if offset: hours, minutes = offset @@ -1256,7 +1294,7 @@ # Get the components. - hour, minute, second, zone = self.as_tuple()[3:] + hour, minute, second, zone = self.time() date = self.as_date() # Add the minutes and hours. @@ -1418,17 +1456,17 @@ def __hash__(self): return hash((self.start, self.end)) - def as_times(self): + def as_limits(self): return self.start, self.end def is_before(self, a, b): - if isinstance(a, DateTime) and isinstance(b, DateTime): + if isinstance(a, DateTime) and a.has_time() and isinstance(b, DateTime) and b.has_time(): return a <= b else: return a < b def is_after_or_during(self, a, b): - if isinstance(a, DateTime) and isinstance(b, DateTime): + if isinstance(a, DateTime) and a.has_time() and isinstance(b, DateTime) and b.has_time(): return a > b else: return a >= b @@ -1482,7 +1520,7 @@ value = value.as_timespan() if isinstance(value, Timespan): - start, end = map(self.convert_time, value.as_times()) + start, end = map(self.convert_time, value.as_limits()) return Timespan(start, end) else: return self.convert_time(value) @@ -1634,7 +1672,10 @@ n = None - if arg.startswith("current"): + if arg is None: + return None + + elif arg.startswith("current"): date = getCurrentDate() if len(arg) > 8: n = int(arg[7:]) @@ -1663,7 +1704,10 @@ n = None - if arg.startswith("current"): + if arg is None: + return None + + elif arg.startswith("current"): date = getCurrentMonth() if len(arg) > 8: n = int(arg[7:]) @@ -1694,10 +1738,7 @@ """ arg = getQualifiedParameter(request, calendar_name, argname) - if arg is not None: - return getParameterDate(arg) - else: - return None + return getParameterDate(arg) def getFormMonth(request, calendar_name, argname): @@ -1707,10 +1748,7 @@ """ arg = getQualifiedParameter(request, calendar_name, argname) - if arg is not None: - return getParameterMonth(arg) - else: - return None + return getParameterMonth(arg) def getFormDateTriple(request, yeararg, montharg, dayarg): @@ -1750,6 +1788,9 @@ 'year_month'. """ + if not date: + return "" + _ = request.getText year, month, day = date.as_tuple()[:3] start_weekday, number_of_days = date.month_properties() @@ -1765,6 +1806,9 @@ 'year_month'. """ + if not year_month: + return "" + _ = request.getText year, month = year_month.as_tuple()[:2] month_label = _(getMonthLabel(month)) diff -r 0c87ac42fed2 -r 2c3decb19e69 actions/EventAggregatorSummary.py --- a/actions/EventAggregatorSummary.py Sun Jan 16 19:30:01 2011 +0100 +++ b/actions/EventAggregatorSummary.py Mon Jan 17 02:26:17 2011 +0100 @@ -25,22 +25,13 @@ "A summary dialogue requesting various parameters." - def get_evaluated_label(self, evaluated): - _ = self._ - request = self.request - - if isinstance(evaluated, EventAggregatorSupport.Date): - return EventAggregatorSupport.getFullDateLabel(request, evaluated) - elif isinstance(evaluated, EventAggregatorSupport.Month): - return EventAggregatorSupport.getFullMonthLabel(request, evaluated) - else: - return "" - def get_form_html(self, buttons_html): _ = self._ request = self.request form = self.get_form() + resolution = form.get("resolution", ["month"])[0] + category_list = [] category_pagenames = form.get("category", []) @@ -64,13 +55,18 @@ start_criteria_default = form.get("start", [""])[0] end_criteria_default = form.get("end", [""])[0] - start_criteria_evaluated = EventAggregatorSupport.getParameterDate(start_criteria_default) or \ - EventAggregatorSupport.getParameterMonth(start_criteria_default) - end_criteria_evaluated = EventAggregatorSupport.getParameterDate(end_criteria_default) or \ - EventAggregatorSupport.getParameterMonth(end_criteria_default) + if resolution == "date": + get_parameter = EventAggregatorSupport.getParameterDate + get_label = EventAggregatorSupport.getFullDateLabel + else: + get_parameter = EventAggregatorSupport.getParameterMonth + get_label = EventAggregatorSupport.getFullMonthLabel - start_criteria_evaluated = self.get_evaluated_label(start_criteria_evaluated) - end_criteria_evaluated = self.get_evaluated_label(end_criteria_evaluated) + start_criteria_evaluated = get_parameter(start_criteria_default) + end_criteria_evaluated = get_parameter(end_criteria_default) + + start_criteria_evaluated = get_label(request, start_criteria_evaluated) + end_criteria_evaluated = get_label(request, end_criteria_evaluated) # Descriptions. @@ -267,18 +263,18 @@ if isinstance(calendar_start, EventAggregatorSupport.Date): if isinstance(calendar_end, EventAggregatorSupport.Date): - mode = "day" + resolution = "date" else: calendar_start = calendar_start.as_month() - mode = "month" + resolution = "month" else: - mode = "month" + resolution = "month" if isinstance(calendar_end, EventAggregatorSupport.Date): calendar_end = calendar_end.as_month() events, shown_events, all_shown_events, earliest, latest = \ EventAggregatorSupport.getEvents(request, category_names, calendar_start, calendar_end, - mode == "day" and "day" or "month") + resolution) latest_timestamp = EventAggregatorSupport.setEventTimestamps(request, all_shown_events) diff -r 0c87ac42fed2 -r 2c3decb19e69 css/event-aggregator.css --- a/css/event-aggregator.css Sun Jan 16 19:30:01 2011 +0100 +++ b/css/event-aggregator.css Mon Jan 17 02:26:17 2011 +0100 @@ -267,14 +267,13 @@ border-bottom: 1px solid #dddddd; border-left: 0; border-right: 0; + padding-bottom: 2em; } .event-timespan-content { + vertical-align: top; border-left: 0; border-right: 0; -} - -.event-timespan-empty { border-top: 1px solid #dddddd; border-bottom: 1px solid #dddddd; } diff -r 0c87ac42fed2 -r 2c3decb19e69 macros/EventAggregator.py --- a/macros/EventAggregator.py Sun Jan 16 19:30:01 2011 +0100 +++ b/macros/EventAggregator.py Mon Jan 17 02:26:17 2011 +0100 @@ -23,15 +23,19 @@ "A view of the event calendar." def __init__(self, page, calendar_name, raw_calendar_start, raw_calendar_end, - calendar_start, calendar_end, first, last, category_names, template_name, - parent_name, mode, name_usage): + original_calendar_start, original_calendar_end, calendar_start, calendar_end, + first, last, category_names, template_name, parent_name, mode, name_usage): """ Initialise the view with the current 'page', a 'calendar_name' (which may be None), the 'raw_calendar_start' and 'raw_calendar_end' (which are the actual start and end values provided by the request), the - requested, calculated 'calendar_start' and 'calendar_end', and the - 'first' and 'last' months of event coverage. + calculated 'original_calendar_start' and 'original_calendar_end' (which + are the result of calculating the calendar's limits from the raw start + and end values), and the requested, calculated 'calendar_start' and + 'calendar_end' (which may involve different start and end values due to + navigation in the user interface), along with the 'first' and 'last' + months of event coverage. The additional 'category_names', 'template_name', 'parent_name' and 'mode' parameters are used to configure the links employed by the view. @@ -44,6 +48,8 @@ self.calendar_name = calendar_name self.raw_calendar_start = raw_calendar_start self.raw_calendar_end = raw_calendar_end + self.original_calendar_start = original_calendar_start + self.original_calendar_end = original_calendar_end self.calendar_start = calendar_start self.calendar_end = calendar_end self.template_name = template_name @@ -153,8 +159,10 @@ # Generate the links. - download_dialogue_link = "action=EventAggregatorSummary&parent=%s&%s" % ( - self.parent_name or "", self.category_name_parameters + download_dialogue_link = "action=EventAggregatorSummary&parent=%s&resolution=%s&%s" % ( + self.parent_name or "", + self.mode == "day" and "date" or "month", + self.category_name_parameters ) download_all_link = download_dialogue_link + "&doit=1" download_link = download_all_link + ("&%s&%s" % ( @@ -197,6 +205,10 @@ get_label(self.calendar_start), get_label(self.calendar_end) ) + original_calendar_period = "%s - %s" % ( + get_label(self.original_calendar_start), + get_label(self.original_calendar_end) + ) raw_calendar_period = "%s - %s" % (self.raw_calendar_start, self.raw_calendar_end) # Write the controls. @@ -215,7 +227,7 @@ output.append(linkToPage(request, page, _("Download this calendar"), download_all_link)) output.append(fmt.span(on=1, css_class="event-download-popup")) output.append(fmt.span(on=1, css_class="event-download-period")) - output.append(fmt.text(calendar_period)) + output.append(fmt.text(original_calendar_period)) output.append(fmt.span(on=0)) output.append(fmt.span(on=1, css_class="event-download-period-raw")) output.append(fmt.text(raw_calendar_period)) @@ -243,7 +255,7 @@ output.append(linkToPage(request, page, _("Subscribe to this calendar"), subscribe_all_link)) output.append(fmt.span(on=1, css_class="event-download-popup")) output.append(fmt.span(on=1, css_class="event-download-period")) - output.append(fmt.text(calendar_period)) + output.append(fmt.text(original_calendar_period)) output.append(fmt.span(on=0)) output.append(fmt.span(on=1, css_class="event-download-period-raw")) output.append(fmt.text(raw_calendar_period)) @@ -808,15 +820,49 @@ # determine whether it provides content for each period. scale = EventAggregatorSupport.getCoverageScale(full_coverage) - period = None + + # Define a mapping of events to rowspans. + + rowspans = {} + + # Populate each period with event details, recording how many periods + # each event populates. + + day_rows = [] for period in scale: # Ignore timespans before this day. - if not date in period: + if period != date: continue + # Visit each slot corresponding to a location (or no location). + + day_row = [] + + for location in locations: + + # Visit each coverage span, presenting the events in the span. + + for events in day_slots[location]: + event = self.getActiveEvent(period, events) + if event is not None: + if not rowspans.has_key(event): + rowspans[event] = 1 + else: + rowspans[event] += 1 + day_row.append((location, event)) + + day_rows.append((period, day_row)) + + # Output the periods with event details. + + period = None + events_written = set() + + for period, day_row in day_rows: + # Write an empty heading for the start of the day where the first # applicable timespan starts before this day. @@ -828,23 +874,22 @@ else: output.append(fmt.table_row(on=1)) - output.append(self.writeDayScaleHeading(str(period.start))) + output.append(self.writeDayScaleHeading(period.start.time_string())) # Visit each slot corresponding to a location (or no location). - for location in locations: + for location, event in day_row: - # Visit each coverage span, presenting the events in the span. + # Add a spacer. - for events in day_slots[location]: + output.append(self.writeDaySpacer()) - # Add a spacer. - - output.append(self.writeDaySpacer()) + # Output each location slot's contribution. - # Output each set's contribution to this period. - - output.append(self.writeDaySlot(period, events)) + if event is None or event not in events_written: + output.append(self.writeDaySlot(period, event, event is None and 1 or rowspans[event])) + if event is not None: + events_written.add(event) output.append(fmt.table_row(on=0)) @@ -853,12 +898,11 @@ if period is not None: if period.end == date: output.append(fmt.table_row(on=1)) - output.append(self.writeDayScaleHeading(str(period.end))) + output.append(self.writeDayScaleHeading(period.end.time_string())) - for location in locations: - for events in day_slots[location]: - output.append(self.writeDaySpacer()) - output.append(self.writeEmptyDaySlot()) + for slot in day_row: + output.append(self.writeDaySpacer()) + output.append(self.writeEmptyDaySlot()) output.append(fmt.table_row(on=0)) @@ -875,26 +919,31 @@ return "".join(output) - def writeDaySlot(self, period, events): + def getActiveEvent(self, period, events): + for event in events: + if period not in event: + continue + return event + else: + return None + + def writeDaySlot(self, period, event, rowspan): page = self.page fmt = page.formatter output = [] - for event in events: - if period not in event: - continue - + if event is not None: event_summary = event.getSummary(self.parent_name) style = self.getEventStyle(event_summary) output.append(fmt.table_cell(on=1, attrs={ "class" : "event-timespan-content event-timespan-busy", - "style" : style})) + "style" : style, + "rowspan" : str(rowspan) + })) output.append(self.writeEventSummaryBox(event)) output.append(fmt.table_cell(on=0)) - break - else: output.append(self.writeEmptyDaySlot()) @@ -1042,8 +1091,8 @@ # Determine the limits of the calendar. - calendar_start = get_date(raw_calendar_start) - calendar_end = get_date(raw_calendar_end) + original_calendar_start = calendar_start = get_date(raw_calendar_start) + original_calendar_end = calendar_end = get_date(raw_calendar_end) if calendar_name is not None: calendar_start = get_form_date(request, calendar_name, "start") or calendar_start @@ -1061,7 +1110,8 @@ # Define a view of the calendar, retaining useful navigational information. - view = View(page, calendar_name, raw_calendar_start, raw_calendar_end, calendar_start, calendar_end, + view = View(page, calendar_name, raw_calendar_start, raw_calendar_end, + original_calendar_start, original_calendar_end, calendar_start, calendar_end, first, last, category_names, template_name, parent_name, mode, name_usage) # Make a calendar.