1.1 --- a/EventAggregatorSupport.py Sun Nov 07 23:56:44 2010 +0100
1.2 +++ b/EventAggregatorSupport.py Tue Nov 09 00:40:33 2010 +0100
1.3 @@ -446,7 +446,7 @@
1.4 # Dates.
1.5
1.6 if term in ("start", "end"):
1.7 - desc = getDate(desc)
1.8 + desc = getDateTime(desc)
1.9
1.10 # Lists (whose elements may be quoted).
1.11
1.12 @@ -620,19 +620,6 @@
1.13 self.page = page
1.14 self.details = details
1.15
1.16 - def __cmp__(self, other):
1.17 -
1.18 - """
1.19 - Compare this object with 'other' using the event start and end details.
1.20 - """
1.21 -
1.22 - details1 = self.details
1.23 - details2 = other.details
1.24 - return cmp(
1.25 - (details1["start"], details1["end"]),
1.26 - (details2["start"], details2["end"])
1.27 - )
1.28 -
1.29 def getPage(self):
1.30
1.31 "Return the page describing this event."
1.32 @@ -685,6 +672,27 @@
1.33
1.34 self.details = event_details
1.35
1.36 + # Timespan-related methods.
1.37 +
1.38 + def __contains__(self, other):
1.39 + return self == other
1.40 +
1.41 + def __cmp__(self, other):
1.42 + if isinstance(other, Event):
1.43 + return cmp(self.as_timespan(), other.as_timespan())
1.44 + else:
1.45 + return cmp(self.as_timespan(), other)
1.46 +
1.47 + def as_timespan(self):
1.48 + details = self.details
1.49 + if details.has_key("start") and details.has_key("end"):
1.50 + return Timespan(details["start"], details["end"])
1.51 + else:
1.52 + return None
1.53 +
1.54 + def as_times(self):
1.55 + return self.as_timespan().as_times()
1.56 +
1.57 def getEvents(request, category_names, calendar_start=None, calendar_end=None, resolution="month"):
1.58
1.59 """
1.60 @@ -723,6 +731,8 @@
1.61 else:
1.62 calendar_start, calendar_end = map(convert, (calendar_start, calendar_end))
1.63
1.64 + calendar_period = Timespan(calendar_start, calendar_end)
1.65 +
1.66 events = []
1.67 shown_events = {}
1.68 all_shown_events = []
1.69 @@ -764,15 +774,12 @@
1.70
1.71 # Test for the suitability of the event.
1.72
1.73 - if event_details.has_key("start") and event_details.has_key("end"):
1.74 -
1.75 - start = convert(event_details["start"])
1.76 - end = convert(event_details["end"])
1.77 + if event.as_timespan() is not None:
1.78 + start, end = map(convert, event.as_timespan().as_times())
1.79
1.80 # Compare the dates to the requested calendar window, if any.
1.81
1.82 - if (calendar_start is None or end >= calendar_start) and \
1.83 - (calendar_end is None or start <= calendar_end):
1.84 + if event in calendar_period:
1.85
1.86 all_shown_events.append(event)
1.87
1.88 @@ -885,14 +892,8 @@
1.89 """
1.90
1.91 all_events = {}
1.92 - full_coverage = []
1.93 -
1.94 - # Timespans need to be given converted start and end dates/times.
1.95 -
1.96 - if resolution == "date":
1.97 - convert = lambda x: x.as_date()
1.98 - else:
1.99 - convert = lambda x: x
1.100 + full_coverage = TimespanCollection(resolution)
1.101 + coverage_period = full_coverage.convert(Timespan(start, end))
1.102
1.103 # Get event details.
1.104
1.105 @@ -901,46 +902,43 @@
1.106
1.107 # Test for the event in the period.
1.108
1.109 - if event_details["start"] <= end and event_details["end"] >= start:
1.110 + if event in coverage_period:
1.111
1.112 # Find the coverage of this period for the event.
1.113
1.114 - event_start = convert(max(event_details["start"], start))
1.115 - event_end = convert(min(event_details["end"], end))
1.116 - event_coverage = Timespan(event_start, event_end)
1.117 event_location = event_details.get("location")
1.118
1.119 # Update the overall coverage.
1.120
1.121 - updateCoverage(full_coverage, event_coverage)
1.122 + updateCoverage(full_coverage, event)
1.123
1.124 # Add a new events list for a new location.
1.125 # Locations can be unspecified, thus None refers to all unlocalised
1.126 # events.
1.127
1.128 if not all_events.has_key(event_location):
1.129 - all_events[event_location] = [([event_coverage], [event])]
1.130 + all_events[event_location] = [TimespanCollection(resolution, [event])]
1.131
1.132 # Try and fit the event into an events list.
1.133
1.134 else:
1.135 slot = all_events[event_location]
1.136
1.137 - for i, (coverage, covered_events) in enumerate(slot):
1.138 + for slot_events in slot:
1.139
1.140 # Where the event does not overlap with the current
1.141 # element, add it alongside existing events.
1.142 + # NOTE: Need to use the resolution when testing for overlaps.
1.143
1.144 - if not event_coverage in coverage:
1.145 - covered_events.append(event)
1.146 - updateCoverage(coverage, event_coverage)
1.147 + if not event in slot_events:
1.148 + updateCoverage(slot_events, event)
1.149 break
1.150
1.151 # Make a new element in the list if the event cannot be
1.152 # marked alongside existing events.
1.153
1.154 else:
1.155 - slot.append(([event_coverage], [event]))
1.156 + slot.append(TimespanCollection(resolution, [event]))
1.157
1.158 return full_coverage, all_events
1.159
1.160 @@ -950,8 +948,9 @@
1.161 def getCoverageScale(coverage):
1.162 times = set()
1.163 for timespan in coverage:
1.164 - times.add(timespan.start)
1.165 - times.add(timespan.end)
1.166 + start, end = timespan.as_times()
1.167 + times.add(start)
1.168 + times.add(end)
1.169 times = list(times)
1.170 times.sort()
1.171
1.172 @@ -960,7 +959,7 @@
1.173 start = None
1.174 for time in times:
1.175 if not first:
1.176 - scale.add(Timespan(start, time))
1.177 + scale.append(Timespan(start, time))
1.178 else:
1.179 first = 0
1.180 start = time
1.181 @@ -1395,6 +1394,9 @@
1.182 def __hash__(self):
1.183 return hash((self.start, self.end))
1.184
1.185 + def as_times(self):
1.186 + return self.start, self.end
1.187 +
1.188 def is_before(self, a, b):
1.189 if isinstance(a, DateTime) and isinstance(b, DateTime):
1.190 return a <= b
1.191 @@ -1408,10 +1410,7 @@
1.192 return a >= b
1.193
1.194 def __contains__(self, other):
1.195 - if isinstance(other, Timespan):
1.196 - return self.start <= other.start and self.end >= other.end
1.197 - else:
1.198 - return self.start <= other <= self.end
1.199 + return self == other
1.200
1.201 def __cmp__(self, other):
1.202
1.203 @@ -1421,9 +1420,9 @@
1.204 """
1.205
1.206 if isinstance(other, Timespan):
1.207 - if self.is_before(self.end, other.start):
1.208 + if self.end is not None and other.start is not None and self.is_before(self.end, other.start):
1.209 return -1
1.210 - elif self.is_before(other.end, self.start):
1.211 + elif self.start is not None and other.end is not None and self.is_before(other.end, self.start):
1.212 return 1
1.213 else:
1.214 return 0
1.215 @@ -1432,13 +1431,63 @@
1.216 # non-inclusive timespan.
1.217
1.218 else:
1.219 - if self.is_before(self.end, other):
1.220 + if self.end is not None and self.is_before(self.end, other):
1.221 return -1
1.222 - elif self.start > other:
1.223 + elif self.start is not None and self.start > other:
1.224 return 1
1.225 else:
1.226 return 0
1.227
1.228 +class TimespanCollection:
1.229 +
1.230 + "A collection of timespans with a particular resolution."
1.231 +
1.232 + def __init__(self, resolution, values=None):
1.233 +
1.234 + # Timespans need to be given converted start and end dates/times.
1.235 +
1.236 + if resolution == "date":
1.237 + self.convert_time = lambda x: x.as_date()
1.238 + else:
1.239 + self.convert_time = lambda x: x
1.240 +
1.241 + self.values = values or []
1.242 +
1.243 + def convert(self, value):
1.244 + if isinstance(value, Event):
1.245 + value = value.as_timespan()
1.246 +
1.247 + if isinstance(value, Timespan):
1.248 + start, end = map(self.convert_time, value.as_times())
1.249 + return Timespan(start, end)
1.250 + else:
1.251 + return self.convert_time(value)
1.252 +
1.253 + def __iter__(self):
1.254 + return iter(self.values)
1.255 +
1.256 + def __len__(self):
1.257 + return len(self.values)
1.258 +
1.259 + def __getitem__(self, i):
1.260 + return self.values[i]
1.261 +
1.262 + def __setitem__(self, i, value):
1.263 + self.values[i] = value
1.264 +
1.265 + def __contains__(self, value):
1.266 + test_value = self.convert(value)
1.267 + return test_value in self.values
1.268 +
1.269 + def append(self, value):
1.270 + self.values.append(value)
1.271 +
1.272 + def insert(self, i, value):
1.273 + self.values.insert(i, value)
1.274 +
1.275 + def pop(self):
1.276 + return self.values.pop()
1.277 +
1.278 def getCountry(s):
1.279
1.280 "Find a country code in the given string 's'."
1.281 @@ -1451,6 +1500,9 @@
1.282 return None
1.283
1.284 def getDate(s):
1.285 + return getDateTime(s).as_date()
1.286 +
1.287 +def getDateTime(s):
1.288
1.289 "Parse the string 's', extracting and returning a datetime object."
1.290
2.1 --- a/macros/EventAggregator.py Sun Nov 07 23:56:44 2010 +0100
2.2 +++ b/macros/EventAggregator.py Tue Nov 09 00:40:33 2010 +0100
2.3 @@ -75,7 +75,7 @@
2.4
2.5 return EventAggregatorSupport.getQualifiedParameterName(self.calendar_name, argname)
2.6
2.7 - def getMonthYearQueryString(self, argname, year_month, prefix=1):
2.8 + def getDateQueryString(self, argname, date, prefix=1):
2.9
2.10 """
2.11 Return a query string fragment for the given 'argname', referring to the
2.12 @@ -87,22 +87,24 @@
2.13 summary action.
2.14 """
2.15
2.16 - if year_month is not None:
2.17 - year, month = year_month.as_tuple()
2.18 - month_argname = "%s-month" % argname
2.19 - year_argname = "%s-year" % argname
2.20 - if prefix:
2.21 - month_argname = self.getQualifiedParameterName(month_argname)
2.22 - year_argname = self.getQualifiedParameterName(year_argname)
2.23 - return "%s=%s&%s=%s" % (month_argname, month, year_argname, year)
2.24 + suffixes = ["year", "month", "day"]
2.25 +
2.26 + if date is not None:
2.27 + args = []
2.28 + for suffix, value in zip(suffixes, date.as_tuple()):
2.29 + suffixed_argname = "%s-%s" % (argname, suffix)
2.30 + if prefix:
2.31 + suffixed_argname = self.getQualifiedParameterName(suffixed_argname)
2.32 + args.append("%s=%s" % (suffixed_argname, value))
2.33 + return "&".join(args)
2.34 else:
2.35 return ""
2.36
2.37 - def getMonthQueryString(self, argname, month, prefix=1):
2.38 + def getRawDateQueryString(self, argname, date, prefix=1):
2.39
2.40 """
2.41 Return a query string fragment for the given 'argname', referring to the
2.42 - month given by the specified 'month' value, appropriate for this
2.43 + date given by the specified 'date' value, appropriate for this
2.44 calendar.
2.45
2.46 If 'prefix' is specified and set to a false value, the parameters in the
2.47 @@ -110,10 +112,10 @@
2.48 summary action.
2.49 """
2.50
2.51 - if month is not None:
2.52 + if date is not None:
2.53 if prefix:
2.54 argname = self.getQualifiedParameterName(argname)
2.55 - return "%s=%s" % (argname, month)
2.56 + return "%s=%s" % (argname, date)
2.57 else:
2.58 return ""
2.59
2.60 @@ -126,8 +128,8 @@
2.61 """
2.62
2.63 return "%s&%s&%s=%s" % (
2.64 - self.getMonthQueryString("start", start),
2.65 - self.getMonthQueryString("end", end),
2.66 + self.getRawDateQueryString("start", start),
2.67 + self.getRawDateQueryString("end", end),
2.68 self.getQualifiedParameterName("mode"), mode or self.mode
2.69 )
2.70
2.71 @@ -156,8 +158,8 @@
2.72 )
2.73 download_all_link = download_dialogue_link + "&doit=1"
2.74 download_link = download_all_link + ("&%s&%s" % (
2.75 - self.getMonthYearQueryString("start", self.calendar_start, prefix=0),
2.76 - self.getMonthYearQueryString("end", self.calendar_end, prefix=0)
2.77 + self.getDateQueryString("start", self.calendar_start, prefix=0),
2.78 + self.getDateQueryString("end", self.calendar_end, prefix=0)
2.79 ))
2.80
2.81 # Subscription links just explicitly select the RSS format.
2.82 @@ -173,11 +175,11 @@
2.83
2.84 if self.raw_calendar_start:
2.85 period_limits.append("&%s" %
2.86 - self.getMonthQueryString("start", self.raw_calendar_start, prefix=0)
2.87 + self.getRawDateQueryString("start", self.raw_calendar_start, prefix=0)
2.88 )
2.89 if self.raw_calendar_end:
2.90 period_limits.append("&%s" %
2.91 - self.getMonthQueryString("end", self.raw_calendar_end, prefix=0)
2.92 + self.getRawDateQueryString("end", self.raw_calendar_end, prefix=0)
2.93 )
2.94
2.95 period_limits = "".join(period_limits)
2.96 @@ -498,19 +500,19 @@
2.97
2.98 # Visit each coverage span, presenting the events in the span.
2.99
2.100 - for coverage, events in week_slots[location]:
2.101 + for events in week_slots[location]:
2.102
2.103 # Output each set.
2.104
2.105 - output.append(self.writeWeekSlot(first_day, number_of_days, month, week_end, coverage, events))
2.106 + output.append(self.writeWeekSlot(first_day, number_of_days, month, week_end, events))
2.107
2.108 # Add a spacer.
2.109
2.110 - output.append(self.writeSpacer(first_day, number_of_days))
2.111 + output.append(self.writeWeekSpacer(first_day, number_of_days))
2.112
2.113 return "".join(output)
2.114
2.115 - def writeWeekSlot(self, first_day, number_of_days, month, week_end, coverage, events):
2.116 + def writeWeekSlot(self, first_day, number_of_days, month, week_end, events):
2.117 page = self.page
2.118 request = page.request
2.119 fmt = page.formatter
2.120 @@ -534,7 +536,7 @@
2.121
2.122 # Output the day.
2.123
2.124 - if date not in coverage:
2.125 + if date not in events:
2.126 output.append(fmt.table_cell(on=1,
2.127 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"}))
2.128
2.129 @@ -544,7 +546,7 @@
2.130 event_page = event.getPage()
2.131 event_details = event.getDetails()
2.132
2.133 - if not (event_details["start"] <= date <= event_details["end"]):
2.134 + if date not in event:
2.135 continue
2.136
2.137 # Get basic properties of the event.
2.138 @@ -718,7 +720,7 @@
2.139 output.append(fmt.table_row(on=0))
2.140 return "".join(output)
2.141
2.142 - def writeSpacer(self, first_day, number_of_days):
2.143 + def writeWeekSpacer(self, first_day, number_of_days):
2.144 page = self.page
2.145 fmt = page.formatter
2.146
2.147 @@ -759,6 +761,60 @@
2.148 output.append(fmt.table_row(on=0))
2.149 return "".join(output)
2.150
2.151 + def writeEmptyDay(self, date):
2.152 + page = self.page
2.153 + fmt = page.formatter
2.154 +
2.155 + output = []
2.156 + output.append(fmt.table_row(on=1))
2.157 +
2.158 + output.append(fmt.table_cell(on=1,
2.159 + attrs={"class" : "event-day-content event-day-empty"}))
2.160 +
2.161 + output.append(fmt.table_row(on=0))
2.162 + return "".join(output)
2.163 +
2.164 + def writeDaySlots(self, date, full_coverage, day_slots):
2.165 + page = self.page
2.166 + fmt = page.formatter
2.167 +
2.168 + output = []
2.169 +
2.170 + locations = day_slots.keys()
2.171 + locations.sort(EventAggregatorSupport.sort_none_first)
2.172 +
2.173 + # Traverse the time scale of the full coverage, visiting each slot to
2.174 + # determine whether it provides content for each period.
2.175 +
2.176 + scale = EventAggregatorSupport.getCoverageScale(full_coverage)
2.177 +
2.178 + for period in scale:
2.179 + start = period.start
2.180 +
2.181 + output.append(fmt.table_row(on=1))
2.182 + output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading"}))
2.183 + output.append(fmt.text(str(start)))
2.184 + output.append(fmt.table_cell(on=0))
2.185 +
2.186 + # Visit each slot corresponding to a location (or no location).
2.187 +
2.188 + #for location in locations:
2.189 +
2.190 + # # Visit each coverage span, presenting the events in the span.
2.191 +
2.192 + # for events in day_slots[location]:
2.193 +
2.194 + # # Output each set.
2.195 +
2.196 + # output.append(self.writeDaySlot(day, events))
2.197 +
2.198 + output.append(fmt.table_row(on=0))
2.199 +
2.200 + return "".join(output)
2.201 +
2.202 + def writeDaySlot(self, date, events):
2.203 + pass
2.204 +
2.205 # HTML-related functions.
2.206
2.207 def getColour(s):
2.208 @@ -1159,7 +1215,17 @@
2.209 full_coverage, day_slots = EventAggregatorSupport.getCoverage(
2.210 date, date, shown_events.get(date, []))
2.211
2.212 - output.append(self.writeDayHeading(date))
2.213 + output.append(view.writeDayHeading(date))
2.214 +
2.215 + # Either generate empty days...
2.216 +
2.217 + if not day_slots:
2.218 + output.append(view.writeEmptyDay(date))
2.219 +
2.220 + # Or generate each set of scheduled events...
2.221 +
2.222 + else:
2.223 + output.append(view.writeDaySlots(date, full_coverage, day_slots))
2.224
2.225 # End of day.
2.226