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