1.1 --- a/EventAggregatorSupport.py Sun Oct 31 22:08:00 2010 +0100
1.2 +++ b/EventAggregatorSupport.py Sun Nov 07 23:56:44 2010 +0100
1.3 @@ -15,6 +15,8 @@
1.4 import datetime
1.5 import time
1.6 import re
1.7 +import bisect
1.8 +import operator
1.9
1.10 try:
1.11 set
1.12 @@ -683,24 +685,43 @@
1.13
1.14 self.details = event_details
1.15
1.16 -def getEvents(request, category_names, calendar_start=None, calendar_end=None):
1.17 +def getEvents(request, category_names, calendar_start=None, calendar_end=None, resolution="month"):
1.18
1.19 """
1.20 Using the 'request', generate a list of events found on pages belonging to
1.21 the specified 'category_names', using the optional 'calendar_start' and
1.22 - 'calendar_end' month tuples of the form (year, month) to indicate a window
1.23 - of interest.
1.24 + 'calendar_end' values to indicate a window of interest.
1.25 +
1.26 + The optional 'resolution' determines the unit of time used in providing the
1.27 + results:
1.28
1.29 - Return a list of events, a dictionary mapping months to event lists (within
1.30 - the window of interest), a list of all events within the window of interest,
1.31 - the earliest month of an event within the window of interest, and the latest
1.32 - month of an event within the window of interest.
1.33 + * a list of events
1.34 + * a dictionary mapping time units to event lists (within the window of
1.35 + interest)
1.36 + * a list of all events within the window of interest
1.37 + * the earliest time value of an event within the window of interest
1.38 + * the latest time value of an event within the window of interest.
1.39 """
1.40
1.41 + # Dates need to comply with the requested resolution.
1.42 + # Here, None values need to be preserved when converting.
1.43 +
1.44 + if resolution == "month":
1.45 + convert = lambda x: x and x.as_month()
1.46 + get_values = lambda x, y: x.months_until(y)
1.47 + else:
1.48 + convert = lambda x: x and x.as_date()
1.49 + get_values = lambda x, y: x.days_until(y)
1.50 +
1.51 # Re-order the window, if appropriate.
1.52
1.53 if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end:
1.54 - calendar_start, calendar_end = calendar_end, calendar_start
1.55 + calendar_start, calendar_end = map(convert, (calendar_end, calendar_start))
1.56 +
1.57 + # Otherwise, just convert the calendar limits.
1.58 +
1.59 + else:
1.60 + calendar_start, calendar_end = map(convert, (calendar_start, calendar_end))
1.61
1.62 events = []
1.63 shown_events = {}
1.64 @@ -745,31 +766,30 @@
1.65
1.66 if event_details.has_key("start") and event_details.has_key("end"):
1.67
1.68 - start_month = event_details["start"].as_month()
1.69 - end_month = event_details["end"].as_month()
1.70 + start = convert(event_details["start"])
1.71 + end = convert(event_details["end"])
1.72
1.73 - # Compare the months of the dates to the requested calendar
1.74 - # window, if any.
1.75 + # Compare the dates to the requested calendar window, if any.
1.76
1.77 - if (calendar_start is None or end_month >= calendar_start) and \
1.78 - (calendar_end is None or start_month <= calendar_end):
1.79 + if (calendar_start is None or end >= calendar_start) and \
1.80 + (calendar_end is None or start <= calendar_end):
1.81
1.82 all_shown_events.append(event)
1.83
1.84 - if earliest is None or start_month < earliest:
1.85 - earliest = start_month
1.86 - if latest is None or end_month > latest:
1.87 - latest = end_month
1.88 + if earliest is None or start < earliest:
1.89 + earliest = start
1.90 + if latest is None or end > latest:
1.91 + latest = end
1.92
1.93 - # Store the event in the month-specific dictionary.
1.94 + # Store the event in the time-specific dictionary.
1.95
1.96 - first = max(start_month, calendar_start or start_month)
1.97 - last = min(end_month, calendar_end or end_month)
1.98 + first = max(start, calendar_start or start)
1.99 + last = min(end, calendar_end or end)
1.100
1.101 - for event_month in first.months_until(last):
1.102 - if not shown_events.has_key(event_month):
1.103 - shown_events[event_month] = []
1.104 - shown_events[event_month].append(event)
1.105 + for event_time_value in get_values(first, last):
1.106 + if not shown_events.has_key(event_time_value):
1.107 + shown_events[event_time_value] = []
1.108 + shown_events[event_time_value].append(event)
1.109
1.110 return events, shown_events, all_shown_events, earliest, latest
1.111
1.112 @@ -840,30 +860,39 @@
1.113 # requested period. If all events were to be shown but none were found show
1.114 # the current month.
1.115
1.116 + if isinstance(first, Date):
1.117 + get_current = getCurrentDate
1.118 + else:
1.119 + get_current = getCurrentMonth
1.120 +
1.121 if first is None:
1.122 - first = last or getCurrentMonth()
1.123 + first = last or get_current()
1.124 if last is None:
1.125 - last = first or getCurrentMonth()
1.126 + last = first or get_current()
1.127
1.128 # Permit "expiring" periods (where the start date approaches the end date).
1.129
1.130 return min(first, last), last
1.131
1.132 -# NOTE: Support coverage using times within days. This will involve timespan
1.133 -# NOTE: objects which can be compared in such a way that set operations will be
1.134 -# NOTE: able to detect overlapping periods.
1.135 -
1.136 -def getCoverage(start, end, events):
1.137 +def getCoverage(start, end, events, resolution="date"):
1.138
1.139 """
1.140 Within the period defined by the 'start' and 'end' dates, determine the
1.141 - coverage of the days in the period by the given 'events', returning a set of
1.142 - covered days, along with a list of slots, where each slot contains a tuple
1.143 - of the form (set of covered days, events).
1.144 + coverage of the days in the period by the given 'events', returning a
1.145 + collection of timespans, along with a dictionary mapping locations to
1.146 + collections of slots, where each slot contains a tuple of the form
1.147 + (timespans, events).
1.148 """
1.149
1.150 all_events = {}
1.151 - full_coverage = set()
1.152 + full_coverage = []
1.153 +
1.154 + # Timespans need to be given converted start and end dates/times.
1.155 +
1.156 + if resolution == "date":
1.157 + convert = lambda x: x.as_date()
1.158 + else:
1.159 + convert = lambda x: x
1.160
1.161 # Get event details.
1.162
1.163 @@ -876,21 +905,21 @@
1.164
1.165 # Find the coverage of this period for the event.
1.166
1.167 - event_start = max(event_details["start"], start)
1.168 - event_end = min(event_details["end"], end)
1.169 - event_coverage = set(event_start.days_until(event_end))
1.170 + event_start = convert(max(event_details["start"], start))
1.171 + event_end = convert(min(event_details["end"], end))
1.172 + event_coverage = Timespan(event_start, event_end)
1.173 event_location = event_details.get("location")
1.174
1.175 # Update the overall coverage.
1.176
1.177 - full_coverage.update(event_coverage)
1.178 + updateCoverage(full_coverage, event_coverage)
1.179
1.180 # Add a new events list for a new location.
1.181 # Locations can be unspecified, thus None refers to all unlocalised
1.182 # events.
1.183
1.184 if not all_events.has_key(event_location):
1.185 - all_events[event_location] = [(event_coverage, [event])]
1.186 + all_events[event_location] = [([event_coverage], [event])]
1.187
1.188 # Try and fit the event into an events list.
1.189
1.190 @@ -902,19 +931,41 @@
1.191 # Where the event does not overlap with the current
1.192 # element, add it alongside existing events.
1.193
1.194 - if not coverage.intersection(event_coverage):
1.195 + if not event_coverage in coverage:
1.196 covered_events.append(event)
1.197 - slot[i] = coverage.union(event_coverage), covered_events
1.198 + updateCoverage(coverage, event_coverage)
1.199 break
1.200
1.201 # Make a new element in the list if the event cannot be
1.202 # marked alongside existing events.
1.203
1.204 else:
1.205 - slot.append((event_coverage, [event]))
1.206 + slot.append(([event_coverage], [event]))
1.207
1.208 return full_coverage, all_events
1.209
1.210 +def updateCoverage(coverage, event_coverage):
1.211 + bisect.insort_left(coverage, event_coverage)
1.212 +
1.213 +def getCoverageScale(coverage):
1.214 + times = set()
1.215 + for timespan in coverage:
1.216 + times.add(timespan.start)
1.217 + times.add(timespan.end)
1.218 + times = list(times)
1.219 + times.sort()
1.220 +
1.221 + scale = []
1.222 + first = 1
1.223 + start = None
1.224 + for time in times:
1.225 + if not first:
1.226 + scale.add(Timespan(start, time))
1.227 + else:
1.228 + first = 0
1.229 + start = time
1.230 + return scale
1.231 +
1.232 # Date-related functions.
1.233
1.234 class Period:
1.235 @@ -944,10 +995,13 @@
1.236 return tuple(self.data)
1.237
1.238 def __cmp__(self, other):
1.239 - data = self.as_tuple()
1.240 - other_data = other.as_tuple()
1.241 - length = min(len(data), len(other_data))
1.242 - return cmp(self.data[:length], other.data[:length])
1.243 + if not isinstance(other, Temporal):
1.244 + return NotImplemented
1.245 + else:
1.246 + data = self.as_tuple()
1.247 + other_data = other.as_tuple()
1.248 + length = min(len(data), len(other_data))
1.249 + return cmp(data[:length], other_data[:length])
1.250
1.251 def until(self, start, end, nextfn, prevfn):
1.252
1.253 @@ -1063,6 +1117,15 @@
1.254 def day(self):
1.255 return self.data[2]
1.256
1.257 + def day_update(self, n=1):
1.258 +
1.259 + "Return the month updated by 'n' months."
1.260 +
1.261 + delta = datetime.timedelta(n)
1.262 + dt = datetime.date(*self.as_tuple()[:3])
1.263 + dt_new = dt + delta
1.264 + return Date((dt_new.year, dt_new.month, dt_new.day))
1.265 +
1.266 def next_day(self):
1.267
1.268 "Return the date following this one."
1.269 @@ -1316,6 +1379,66 @@
1.270
1.271 return 0
1.272
1.273 +class Timespan:
1.274 +
1.275 + """
1.276 + A period of time which can be compared against others to check for overlaps.
1.277 + """
1.278 +
1.279 + def __init__(self, start, end):
1.280 + self.start = start
1.281 + self.end = end
1.282 +
1.283 + def __repr__(self):
1.284 + return "%s(%r, %r)" % (self.__class__.__name__, self.start, self.end)
1.285 +
1.286 + def __hash__(self):
1.287 + return hash((self.start, self.end))
1.288 +
1.289 + def is_before(self, a, b):
1.290 + if isinstance(a, DateTime) and isinstance(b, DateTime):
1.291 + return a <= b
1.292 + else:
1.293 + return a < b
1.294 +
1.295 + def is_after_or_during(self, a, b):
1.296 + if isinstance(a, DateTime) and isinstance(b, DateTime):
1.297 + return a > b
1.298 + else:
1.299 + return a >= b
1.300 +
1.301 + def __contains__(self, other):
1.302 + if isinstance(other, Timespan):
1.303 + return self.start <= other.start and self.end >= other.end
1.304 + else:
1.305 + return self.start <= other <= self.end
1.306 +
1.307 + def __cmp__(self, other):
1.308 +
1.309 + """
1.310 + Return whether this timespan occupies the same period of time as the
1.311 + 'other'.
1.312 + """
1.313 +
1.314 + if isinstance(other, Timespan):
1.315 + if self.is_before(self.end, other.start):
1.316 + return -1
1.317 + elif self.is_before(other.end, self.start):
1.318 + return 1
1.319 + else:
1.320 + return 0
1.321 +
1.322 + # Points in time are not considered to represent an upper bound on a
1.323 + # non-inclusive timespan.
1.324 +
1.325 + else:
1.326 + if self.is_before(self.end, other):
1.327 + return -1
1.328 + elif self.start > other:
1.329 + return 1
1.330 + else:
1.331 + return 0
1.332 +
1.333 def getCountry(s):
1.334
1.335 "Find a country code in the given string 's'."
1.336 @@ -1363,6 +1486,13 @@
1.337 else:
1.338 return None
1.339
1.340 +def getCurrentDate():
1.341 +
1.342 + "Return the current date as a (year, month, day) tuple."
1.343 +
1.344 + today = datetime.date.today()
1.345 + return Date((today.year, today.month, today.day))
1.346 +
1.347 def getCurrentMonth():
1.348
1.349 "Return the current month as a (year, month) tuple."
1.350 @@ -1412,6 +1542,35 @@
1.351 else:
1.352 return "%s-%s" % (calendar_name, argname)
1.353
1.354 +def getParameterDate(arg):
1.355 +
1.356 + "Interpret 'arg', recognising keywords and simple arithmetic operations."
1.357 +
1.358 + n = None
1.359 +
1.360 + if arg.startswith("current"):
1.361 + date = getCurrentDate()
1.362 + if len(arg) > 8:
1.363 + n = int(arg[7:])
1.364 +
1.365 + elif arg.startswith("yearstart"):
1.366 + date = Date((getCurrentYear(), 1, 1))
1.367 + if len(arg) > 10:
1.368 + n = int(arg[9:])
1.369 +
1.370 + elif arg.startswith("yearend"):
1.371 + date = Date((getCurrentYear(), 12, 31))
1.372 + if len(arg) > 8:
1.373 + n = int(arg[7:])
1.374 +
1.375 + else:
1.376 + date = getDate(arg)
1.377 +
1.378 + if n is not None:
1.379 + date = date.day_update(n)
1.380 +
1.381 + return date
1.382 +
1.383 def getParameterMonth(arg):
1.384
1.385 "Interpret 'arg', recognising keywords and simple arithmetic operations."
1.386 @@ -1441,6 +1600,19 @@
1.387
1.388 return date
1.389
1.390 +def getFormDate(request, calendar_name, argname):
1.391 +
1.392 + """
1.393 + Return the date from the 'request' for the calendar with the given
1.394 + 'calendar_name' using the parameter having the given 'argname'.
1.395 + """
1.396 +
1.397 + arg = getQualifiedParameter(request, calendar_name, argname)
1.398 + if arg is not None:
1.399 + return getParameterDate(arg)
1.400 + else:
1.401 + return None
1.402 +
1.403 def getFormMonth(request, calendar_name, argname):
1.404
1.405 """
1.406 @@ -1469,6 +1641,19 @@
1.407 else:
1.408 return None
1.409
1.410 +def getFullDateLabel(request, date):
1.411 +
1.412 + """
1.413 + Return the full month plus year label using the given 'request' and
1.414 + 'year_month'.
1.415 + """
1.416 +
1.417 + _ = request.getText
1.418 + year, month, day = date.as_tuple()[:3]
1.419 + day_label = _(getDayLabel(day))
1.420 + month_label = _(getMonthLabel(month))
1.421 + return "%s %s %s %s" % (day_label, day, month_label, year)
1.422 +
1.423 def getFullMonthLabel(request, year_month):
1.424
1.425 """
1.426 @@ -1477,7 +1662,7 @@
1.427 """
1.428
1.429 _ = request.getText
1.430 - year, month = year_month.as_tuple()
1.431 + year, month = year_month.as_tuple()[:2]
1.432 month_label = _(getMonthLabel(month))
1.433 return "%s %s" % (month_label, year)
1.434
3.1 --- a/macros/EventAggregator.py Sun Oct 31 22:08:00 2010 +0100
3.2 +++ b/macros/EventAggregator.py Sun Nov 07 23:56:44 2010 +0100
3.3 @@ -131,6 +131,11 @@
3.4 self.getQualifiedParameterName("mode"), mode or self.mode
3.5 )
3.6
3.7 + def getFullDateLabel(self, date):
3.8 + page = self.page
3.9 + request = page.request
3.10 + return EventAggregatorSupport.getFullDateLabel(request, date)
3.11 +
3.12 def getFullMonthLabel(self, year_month):
3.13 page = self.page
3.14 request = page.request
3.15 @@ -381,7 +386,39 @@
3.16
3.17 # Calendar layout methods.
3.18
3.19 - def writeDayNumbers(self, first_day, number_of_days, month, busy_dates):
3.20 + def writeMonthTableHeading(self, year_month):
3.21 + page = self.page
3.22 + fmt = page.formatter
3.23 +
3.24 + output = []
3.25 + output.append(fmt.table_row(on=1))
3.26 + output.append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "21"}))
3.27 +
3.28 + output.append(self.writeMonthHeading(year_month))
3.29 +
3.30 + output.append(fmt.table_cell(on=0))
3.31 + output.append(fmt.table_row(on=0))
3.32 +
3.33 + return "".join(output)
3.34 +
3.35 + def writeWeekdayHeadings(self):
3.36 + page = self.page
3.37 + request = page.request
3.38 + fmt = page.formatter
3.39 + _ = request.getText
3.40 +
3.41 + output = []
3.42 + output.append(fmt.table_row(on=1))
3.43 +
3.44 + for weekday in range(0, 7):
3.45 + output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : "3"}))
3.46 + output.append(fmt.text(_(EventAggregatorSupport.getDayLabel(weekday))))
3.47 + output.append(fmt.table_cell(on=0))
3.48 +
3.49 + output.append(fmt.table_row(on=0))
3.50 + return "".join(output)
3.51 +
3.52 + def writeDayNumbers(self, first_day, number_of_days, month, coverage):
3.53 page = self.page
3.54 fmt = page.formatter
3.55
3.56 @@ -402,7 +439,7 @@
3.57 # Output normal days.
3.58
3.59 else:
3.60 - if date in busy_dates:
3.61 + if date in coverage:
3.62 output.append(fmt.table_cell(on=1,
3.63 attrs={"class" : "event-day-heading event-day-busy", "colspan" : "3"}))
3.64 else:
3.65 @@ -703,6 +740,25 @@
3.66 output.append(fmt.table_row(on=0))
3.67 return "".join(output)
3.68
3.69 + # Day layout methods.
3.70 +
3.71 + def writeDayHeading(self, date):
3.72 + page = self.page
3.73 + request = page.request
3.74 + fmt = page.formatter
3.75 + _ = request.getText
3.76 + full_date_label = self.getFullDateLabel(date)
3.77 +
3.78 + output = []
3.79 + output.append(fmt.table_row(on=1))
3.80 +
3.81 + output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading"}))
3.82 + output.append(fmt.text(full_date_label))
3.83 + output.append(fmt.table_cell(on=0))
3.84 +
3.85 + output.append(fmt.table_row(on=0))
3.86 + return "".join(output)
3.87 +
3.88 # HTML-related functions.
3.89
3.90 def getColour(s):
3.91 @@ -731,11 +787,16 @@
3.92 optional named arguments of the following forms:
3.93
3.94 start=YYYY-MM shows event details starting from the specified month
3.95 + start=YYYY-MM-DD shows event details starting from the specified day
3.96 start=current-N shows event details relative to the current month
3.97 + (or relative to the current day in "day" mode)
3.98 end=YYYY-MM shows event details ending at the specified month
3.99 + end=YYYY-MM-DD shows event details ending on the specified day
3.100 end=current+N shows event details relative to the current month
3.101 + (or relative to the current day in "day" mode)
3.102
3.103 mode=calendar shows a calendar view of events
3.104 + mode=day shows a calendar day view of events
3.105 mode=list shows a list of events by month
3.106 mode=table shows a table of events
3.107
3.108 @@ -782,11 +843,9 @@
3.109 for arg in parsed_args:
3.110 if arg.startswith("start="):
3.111 raw_calendar_start = arg[6:]
3.112 - calendar_start = EventAggregatorSupport.getParameterMonth(raw_calendar_start)
3.113
3.114 elif arg.startswith("end="):
3.115 raw_calendar_end = arg[4:]
3.116 - calendar_end = EventAggregatorSupport.getParameterMonth(raw_calendar_end)
3.117
3.118 elif arg.startswith("mode="):
3.119 mode = arg[5:]
3.120 @@ -808,16 +867,29 @@
3.121
3.122 # Find request parameters to override settings.
3.123
3.124 - if calendar_name is not None:
3.125 - calendar_start = EventAggregatorSupport.getFormMonth(request, calendar_name, "start") or calendar_start
3.126 - calendar_end = EventAggregatorSupport.getFormMonth(request, calendar_name, "end") or calendar_end
3.127 -
3.128 mode = EventAggregatorSupport.getQualifiedParameter(request, calendar_name, "mode", mode or "calendar")
3.129
3.130 - # Get the events.
3.131 + if mode == "day":
3.132 + get_date = EventAggregatorSupport.getParameterDate
3.133 + get_form_date = EventAggregatorSupport.getFormDate
3.134 + else:
3.135 + get_date = EventAggregatorSupport.getParameterMonth
3.136 + get_form_date = EventAggregatorSupport.getFormMonth
3.137 +
3.138 + # Determine the limits of the calendar.
3.139 +
3.140 + calendar_start = get_date(raw_calendar_start)
3.141 + calendar_end = get_date(raw_calendar_end)
3.142 +
3.143 + if calendar_name is not None:
3.144 + calendar_start = get_form_date(request, calendar_name, "start") or calendar_start
3.145 + calendar_end = get_form_date(request, calendar_name, "end") or calendar_end
3.146 +
3.147 + # Get the events according to the resolution of the calendar.
3.148
3.149 events, shown_events, all_shown_events, earliest, latest = \
3.150 - EventAggregatorSupport.getEvents(request, category_names, calendar_start, calendar_end)
3.151 + EventAggregatorSupport.getEvents(request, category_names, calendar_start, calendar_end,
3.152 + mode == "day" and "date" or "month")
3.153
3.154 # Get a concrete period of time.
3.155
3.156 @@ -919,7 +991,7 @@
3.157
3.158 output.append(fmt.table(on=0))
3.159
3.160 - # Output a list or calendar.
3.161 + # Output a list or month calendar.
3.162
3.163 elif mode in ("list", "calendar"):
3.164
3.165 @@ -942,27 +1014,14 @@
3.166
3.167 output.append(fmt.table(on=1, attrs={"tableclass" : "event-month"}))
3.168
3.169 - output.append(fmt.table_row(on=1))
3.170 - output.append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "21"}))
3.171 -
3.172 # Either write a month heading or produce links for navigable
3.173 # calendars.
3.174
3.175 - output.append(view.writeMonthHeading(month))
3.176 -
3.177 - output.append(fmt.table_cell(on=0))
3.178 - output.append(fmt.table_row(on=0))
3.179 + output.append(view.writeMonthTableHeading(month))
3.180
3.181 # Weekday headings.
3.182
3.183 - output.append(fmt.table_row(on=1))
3.184 -
3.185 - for weekday in range(0, 7):
3.186 - output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : "3"}))
3.187 - output.append(fmt.text(_(EventAggregatorSupport.getDayLabel(weekday))))
3.188 - output.append(fmt.table_cell(on=0))
3.189 -
3.190 - output.append(fmt.table_row(on=0))
3.191 + output.append(view.writeWeekdayHeadings())
3.192
3.193 # Process the days of the month.
3.194
3.195 @@ -1087,6 +1146,25 @@
3.196 if mode == "list":
3.197 output.append(fmt.bullet_list(on=0))
3.198
3.199 + # Output a day view.
3.200 +
3.201 + elif mode == "day":
3.202 +
3.203 + # Visit all days in the requested range, or across known events.
3.204 +
3.205 + for date in first.days_until(last):
3.206 +
3.207 + output.append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day"}))
3.208 +
3.209 + full_coverage, day_slots = EventAggregatorSupport.getCoverage(
3.210 + date, date, shown_events.get(date, []))
3.211 +
3.212 + output.append(self.writeDayHeading(date))
3.213 +
3.214 + # End of day.
3.215 +
3.216 + output.append(fmt.table(on=0))
3.217 +
3.218 # Output view controls.
3.219
3.220 output.append(fmt.div(on=1, css_class="event-controls"))