# HG changeset patch # User Paul Boddie # Date 1289259633 -3600 # Node ID 96ab99ea002ce4349d2c32a62d39959774078c4c # Parent 0dfd107465873657e12c8759e6bba8b3e4b64228 Introduced coverage lists that include the events themselves acting as wrappers around timespans, where such lists permit the comparison of events/timespans at a certain resolution, thus permitting two events at different times in the same day to appear to overlap at a per-day resolution but to appear separate at a date-level resolution. Fixed the coverage scale function. Added a getDateTime function to do the work previously done by getDate. Made __contains__ methods on Event and Timespan act as equality tests. Changed various interface methods to support time resolutions other than months. diff -r 0dfd10746587 -r 96ab99ea002c EventAggregatorSupport.py --- a/EventAggregatorSupport.py Sun Nov 07 23:56:44 2010 +0100 +++ b/EventAggregatorSupport.py Tue Nov 09 00:40:33 2010 +0100 @@ -446,7 +446,7 @@ # Dates. if term in ("start", "end"): - desc = getDate(desc) + desc = getDateTime(desc) # Lists (whose elements may be quoted). @@ -620,19 +620,6 @@ self.page = page self.details = details - def __cmp__(self, other): - - """ - Compare this object with 'other' using the event start and end details. - """ - - details1 = self.details - details2 = other.details - return cmp( - (details1["start"], details1["end"]), - (details2["start"], details2["end"]) - ) - def getPage(self): "Return the page describing this event." @@ -685,6 +672,27 @@ self.details = event_details + # Timespan-related methods. + + def __contains__(self, other): + return self == other + + def __cmp__(self, other): + if isinstance(other, Event): + return cmp(self.as_timespan(), other.as_timespan()) + else: + return cmp(self.as_timespan(), other) + + def as_timespan(self): + details = self.details + if details.has_key("start") and details.has_key("end"): + return Timespan(details["start"], details["end"]) + else: + return None + + def as_times(self): + return self.as_timespan().as_times() + def getEvents(request, category_names, calendar_start=None, calendar_end=None, resolution="month"): """ @@ -723,6 +731,8 @@ else: calendar_start, calendar_end = map(convert, (calendar_start, calendar_end)) + calendar_period = Timespan(calendar_start, calendar_end) + events = [] shown_events = {} all_shown_events = [] @@ -764,15 +774,12 @@ # Test for the suitability of the event. - if event_details.has_key("start") and event_details.has_key("end"): - - start = convert(event_details["start"]) - end = convert(event_details["end"]) + if event.as_timespan() is not None: + start, end = map(convert, event.as_timespan().as_times()) # Compare the dates to the requested calendar window, if any. - if (calendar_start is None or end >= calendar_start) and \ - (calendar_end is None or start <= calendar_end): + if event in calendar_period: all_shown_events.append(event) @@ -885,14 +892,8 @@ """ all_events = {} - full_coverage = [] - - # Timespans need to be given converted start and end dates/times. - - if resolution == "date": - convert = lambda x: x.as_date() - else: - convert = lambda x: x + full_coverage = TimespanCollection(resolution) + coverage_period = full_coverage.convert(Timespan(start, end)) # Get event details. @@ -901,46 +902,43 @@ # Test for the event in the period. - if event_details["start"] <= end and event_details["end"] >= start: + if event in coverage_period: # Find the coverage of this period for the event. - event_start = convert(max(event_details["start"], start)) - event_end = convert(min(event_details["end"], end)) - event_coverage = Timespan(event_start, event_end) event_location = event_details.get("location") # Update the overall coverage. - updateCoverage(full_coverage, event_coverage) + updateCoverage(full_coverage, 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] = [([event_coverage], [event])] + all_events[event_location] = [TimespanCollection(resolution, [event])] # Try and fit the event into an events list. else: slot = all_events[event_location] - for i, (coverage, covered_events) in enumerate(slot): + for slot_events in slot: # Where the event does not overlap with the current # element, add it alongside existing events. + # NOTE: Need to use the resolution when testing for overlaps. - if not event_coverage in coverage: - covered_events.append(event) - updateCoverage(coverage, event_coverage) + if not event in slot_events: + updateCoverage(slot_events, event) break # Make a new element in the list if the event cannot be # marked alongside existing events. else: - slot.append(([event_coverage], [event])) + slot.append(TimespanCollection(resolution, [event])) return full_coverage, all_events @@ -950,8 +948,9 @@ def getCoverageScale(coverage): times = set() for timespan in coverage: - times.add(timespan.start) - times.add(timespan.end) + start, end = timespan.as_times() + times.add(start) + times.add(end) times = list(times) times.sort() @@ -960,7 +959,7 @@ start = None for time in times: if not first: - scale.add(Timespan(start, time)) + scale.append(Timespan(start, time)) else: first = 0 start = time @@ -1395,6 +1394,9 @@ def __hash__(self): return hash((self.start, self.end)) + def as_times(self): + return self.start, self.end + def is_before(self, a, b): if isinstance(a, DateTime) and isinstance(b, DateTime): return a <= b @@ -1408,10 +1410,7 @@ return a >= b def __contains__(self, other): - if isinstance(other, Timespan): - return self.start <= other.start and self.end >= other.end - else: - return self.start <= other <= self.end + return self == other def __cmp__(self, other): @@ -1421,9 +1420,9 @@ """ if isinstance(other, Timespan): - if self.is_before(self.end, other.start): + if self.end is not None and other.start is not None and self.is_before(self.end, other.start): return -1 - elif self.is_before(other.end, self.start): + elif self.start is not None and other.end is not None and self.is_before(other.end, self.start): return 1 else: return 0 @@ -1432,13 +1431,63 @@ # non-inclusive timespan. else: - if self.is_before(self.end, other): + if self.end is not None and self.is_before(self.end, other): return -1 - elif self.start > other: + elif self.start is not None and self.start > other: return 1 else: return 0 +class TimespanCollection: + + "A collection of timespans with a particular resolution." + + 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() + else: + self.convert_time = lambda x: x + + self.values = values or [] + + def convert(self, value): + if isinstance(value, Event): + value = value.as_timespan() + + if isinstance(value, Timespan): + start, end = map(self.convert_time, value.as_times()) + return Timespan(start, end) + else: + return self.convert_time(value) + + def __iter__(self): + return iter(self.values) + + def __len__(self): + return len(self.values) + + def __getitem__(self, i): + return self.values[i] + + def __setitem__(self, i, value): + self.values[i] = value + + def __contains__(self, value): + test_value = self.convert(value) + return test_value in self.values + + def append(self, value): + self.values.append(value) + + def insert(self, i, value): + self.values.insert(i, value) + + def pop(self): + return self.values.pop() + def getCountry(s): "Find a country code in the given string 's'." @@ -1451,6 +1500,9 @@ return None def getDate(s): + return getDateTime(s).as_date() + +def getDateTime(s): "Parse the string 's', extracting and returning a datetime object." diff -r 0dfd10746587 -r 96ab99ea002c macros/EventAggregator.py --- a/macros/EventAggregator.py Sun Nov 07 23:56:44 2010 +0100 +++ b/macros/EventAggregator.py Tue Nov 09 00:40:33 2010 +0100 @@ -75,7 +75,7 @@ return EventAggregatorSupport.getQualifiedParameterName(self.calendar_name, argname) - def getMonthYearQueryString(self, argname, year_month, prefix=1): + def getDateQueryString(self, argname, date, prefix=1): """ Return a query string fragment for the given 'argname', referring to the @@ -87,22 +87,24 @@ summary action. """ - if year_month is not None: - year, month = year_month.as_tuple() - month_argname = "%s-month" % argname - year_argname = "%s-year" % argname - if prefix: - month_argname = self.getQualifiedParameterName(month_argname) - year_argname = self.getQualifiedParameterName(year_argname) - return "%s=%s&%s=%s" % (month_argname, month, year_argname, year) + suffixes = ["year", "month", "day"] + + if date is not None: + args = [] + for suffix, value in zip(suffixes, date.as_tuple()): + suffixed_argname = "%s-%s" % (argname, suffix) + if prefix: + suffixed_argname = self.getQualifiedParameterName(suffixed_argname) + args.append("%s=%s" % (suffixed_argname, value)) + return "&".join(args) else: return "" - def getMonthQueryString(self, argname, month, prefix=1): + def getRawDateQueryString(self, argname, date, prefix=1): """ Return a query string fragment for the given 'argname', referring to the - month given by the specified 'month' value, appropriate for this + date given by the specified 'date' value, appropriate for this calendar. If 'prefix' is specified and set to a false value, the parameters in the @@ -110,10 +112,10 @@ summary action. """ - if month is not None: + if date is not None: if prefix: argname = self.getQualifiedParameterName(argname) - return "%s=%s" % (argname, month) + return "%s=%s" % (argname, date) else: return "" @@ -126,8 +128,8 @@ """ return "%s&%s&%s=%s" % ( - self.getMonthQueryString("start", start), - self.getMonthQueryString("end", end), + self.getRawDateQueryString("start", start), + self.getRawDateQueryString("end", end), self.getQualifiedParameterName("mode"), mode or self.mode ) @@ -156,8 +158,8 @@ ) download_all_link = download_dialogue_link + "&doit=1" download_link = download_all_link + ("&%s&%s" % ( - self.getMonthYearQueryString("start", self.calendar_start, prefix=0), - self.getMonthYearQueryString("end", self.calendar_end, prefix=0) + self.getDateQueryString("start", self.calendar_start, prefix=0), + self.getDateQueryString("end", self.calendar_end, prefix=0) )) # Subscription links just explicitly select the RSS format. @@ -173,11 +175,11 @@ if self.raw_calendar_start: period_limits.append("&%s" % - self.getMonthQueryString("start", self.raw_calendar_start, prefix=0) + self.getRawDateQueryString("start", self.raw_calendar_start, prefix=0) ) if self.raw_calendar_end: period_limits.append("&%s" % - self.getMonthQueryString("end", self.raw_calendar_end, prefix=0) + self.getRawDateQueryString("end", self.raw_calendar_end, prefix=0) ) period_limits = "".join(period_limits) @@ -498,19 +500,19 @@ # Visit each coverage span, presenting the events in the span. - for coverage, events in week_slots[location]: + for events in week_slots[location]: # Output each set. - output.append(self.writeWeekSlot(first_day, number_of_days, month, week_end, coverage, events)) + output.append(self.writeWeekSlot(first_day, number_of_days, month, week_end, events)) # Add a spacer. - output.append(self.writeSpacer(first_day, number_of_days)) + output.append(self.writeWeekSpacer(first_day, number_of_days)) return "".join(output) - def writeWeekSlot(self, first_day, number_of_days, month, week_end, coverage, events): + def writeWeekSlot(self, first_day, number_of_days, month, week_end, events): page = self.page request = page.request fmt = page.formatter @@ -534,7 +536,7 @@ # Output the day. - if date not in coverage: + if date not in events: output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"})) @@ -544,7 +546,7 @@ event_page = event.getPage() event_details = event.getDetails() - if not (event_details["start"] <= date <= event_details["end"]): + if date not in event: continue # Get basic properties of the event. @@ -718,7 +720,7 @@ output.append(fmt.table_row(on=0)) return "".join(output) - def writeSpacer(self, first_day, number_of_days): + def writeWeekSpacer(self, first_day, number_of_days): page = self.page fmt = page.formatter @@ -759,6 +761,60 @@ output.append(fmt.table_row(on=0)) return "".join(output) + def writeEmptyDay(self, date): + page = self.page + fmt = page.formatter + + output = [] + output.append(fmt.table_row(on=1)) + + output.append(fmt.table_cell(on=1, + attrs={"class" : "event-day-content event-day-empty"})) + + output.append(fmt.table_row(on=0)) + return "".join(output) + + def writeDaySlots(self, date, full_coverage, day_slots): + page = self.page + fmt = page.formatter + + output = [] + + locations = day_slots.keys() + locations.sort(EventAggregatorSupport.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) + + for period in scale: + start = period.start + + output.append(fmt.table_row(on=1)) + output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading"})) + output.append(fmt.text(str(start))) + output.append(fmt.table_cell(on=0)) + + # Visit each slot corresponding to a location (or no location). + + #for location in locations: + + # # Visit each coverage span, presenting the events in the span. + + # for events in day_slots[location]: + + # # Output each set. + + # output.append(self.writeDaySlot(day, events)) + + output.append(fmt.table_row(on=0)) + + return "".join(output) + + def writeDaySlot(self, date, events): + pass + # HTML-related functions. def getColour(s): @@ -1159,7 +1215,17 @@ full_coverage, day_slots = EventAggregatorSupport.getCoverage( date, date, shown_events.get(date, [])) - output.append(self.writeDayHeading(date)) + output.append(view.writeDayHeading(date)) + + # Either generate empty days... + + if not day_slots: + output.append(view.writeEmptyDay(date)) + + # Or generate each set of scheduled events... + + else: + output.append(view.writeDaySlots(date, full_coverage, day_slots)) # End of day.