1.1 --- a/EventAggregatorSupport.py Mon Jul 18 00:28:55 2011 +0200
1.2 +++ b/EventAggregatorSupport.py Wed Jul 20 00:53:40 2011 +0200
1.3 @@ -12,11 +12,13 @@
1.4 from MoinMoin import search, version
1.5 from MoinMoin import wikiutil
1.6 import calendar
1.7 +import codecs
1.8 import datetime
1.9 import time
1.10 import re
1.11 import bisect
1.12 import operator
1.13 +import urllib
1.14
1.15 try:
1.16 set
1.17 @@ -28,6 +30,11 @@
1.18 except ImportError:
1.19 pytz = None
1.20
1.21 +try:
1.22 + import vCalendar
1.23 +except ImportError:
1.24 + vCalendar = None
1.25 +
1.26 escape = wikiutil.escape
1.27
1.28 __version__ = "0.7"
1.29 @@ -57,6 +64,8 @@
1.30 ur"(?:,(?:\s*[\w-]+)+)?" # country (optional)
1.31 ur")$", re.UNICODE)
1.32
1.33 +# Month, date, time and datetime parsing.
1.34 +
1.35 month_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})'
1.36 date_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})'
1.37 time_regexp_str = ur'(?P<hour>[0-2][0-9]):(?P<minute>[0-5][0-9])(?::(?P<second>[0-6][0-9]))?'
1.38 @@ -69,9 +78,23 @@
1.39 month_regexp = re.compile(month_regexp_str, re.UNICODE)
1.40 date_regexp = re.compile(date_regexp_str, re.UNICODE)
1.41 time_regexp = re.compile(time_regexp_str, re.UNICODE)
1.42 -datetime_regexp = re.compile(datetime_regexp_str, re.UNICODE)
1.43 timezone_olson_regexp = re.compile(timezone_olson_str, re.UNICODE)
1.44 timezone_offset_regexp = re.compile(timezone_offset_str, re.UNICODE)
1.45 +datetime_regexp = re.compile(datetime_regexp_str, re.UNICODE)
1.46 +
1.47 +# iCalendar date and datetime parsing.
1.48 +
1.49 +date_icalendar_regexp_str = ur'(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})'
1.50 +datetime_icalendar_regexp_str = date_icalendar_regexp_str + \
1.51 + ur'(?:' \
1.52 + ur'T(?P<hour>[0-2][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-6][0-9])' \
1.53 + ur'(?P<utc>Z)?' \
1.54 + ur')?'
1.55 +
1.56 +date_icalendar_regexp = re.compile(date_icalendar_regexp_str, re.UNICODE)
1.57 +datetime_icalendar_regexp = re.compile(datetime_icalendar_regexp_str, re.UNICODE)
1.58 +
1.59 +# Simple content parsing.
1.60
1.61 verbatim_regexp = re.compile(ur'(?:'
1.62 ur'<<Verbatim\((?P<verbatim>.*?)\)>>'
1.63 @@ -476,7 +499,116 @@
1.64 class ActsAsTimespan:
1.65 pass
1.66
1.67 -# The main activity functions.
1.68 +# Event resources providing collections of events.
1.69 +
1.70 +class EventResource:
1.71 +
1.72 + "A resource providing event information."
1.73 +
1.74 + def __init__(self, url):
1.75 + self.url = url
1.76 +
1.77 + def getPageURL(self, request):
1.78 +
1.79 + "Using 'request', return the URL of this page."
1.80 +
1.81 + return self.url
1.82 +
1.83 + def getFormat(self):
1.84 +
1.85 + "Get the format used on this page."
1.86 +
1.87 + return "plain"
1.88 +
1.89 + def getEvents(self):
1.90 +
1.91 + "Return a list of events from this resource."
1.92 +
1.93 + return []
1.94 +
1.95 + def linkToPage(self, request, text, query_string=None):
1.96 +
1.97 + """
1.98 + Using 'request', return a link to this page with the given link 'text'
1.99 + and optional 'query_string'.
1.100 + """
1.101 +
1.102 + url = self.url
1.103 +
1.104 + if query_string:
1.105 + query_string = wikiutil.makeQueryString(query_string)
1.106 + url = "%s?%s" % (url, query_string)
1.107 +
1.108 + formatter = request.page and getattr(request.page, "formatter", None) or request.html_formatter
1.109 +
1.110 + output = []
1.111 + output.append(formatter.url(1, url))
1.112 + output.append(formatter.text(text))
1.113 + output.append(formatter.url(0))
1.114 + return "".join(output)
1.115 +
1.116 + # Formatting-related functions.
1.117 +
1.118 + def formatText(self, text, request, fmt):
1.119 +
1.120 + """
1.121 + Format the given 'text' using the specified 'request' and formatter
1.122 + 'fmt'.
1.123 + """
1.124 +
1.125 + # Assume plain text which is then formatted appropriately.
1.126 +
1.127 + return fmt.text(text)
1.128 +
1.129 +class EventCalendar(EventResource):
1.130 +
1.131 + "An iCalendar resource."
1.132 +
1.133 + def __init__(self, url, calendar):
1.134 + EventResource.__init__(self, url)
1.135 + self.calendar = calendar
1.136 + self.events = None
1.137 +
1.138 + def getEvents(self):
1.139 +
1.140 + "Return a list of events from this resource."
1.141 +
1.142 + if self.events is None:
1.143 + self.events = []
1.144 +
1.145 + _calendar, _empty, calendar = self.calendar
1.146 +
1.147 + for objtype, attrs, obj in calendar:
1.148 +
1.149 + # Read events.
1.150 +
1.151 + if objtype == "VEVENT":
1.152 + details = {}
1.153 +
1.154 + for property, attrs, value in obj:
1.155 +
1.156 + # Convert dates.
1.157 +
1.158 + if property in ("DTSTART", "DTEND"):
1.159 + property = property[2:].lower()
1.160 + if attrs.get("VALUE") == "DATE":
1.161 + value = getDateFromCalendar(value)
1.162 + else:
1.163 + value = getDateTimeFromCalendar(value)
1.164 +
1.165 + # Accept other textual data as it is.
1.166 +
1.167 + elif property in ("CATEGORIES", "LOCATION", "SUMMARY"):
1.168 + property = property.lower()
1.169 +
1.170 + else:
1.171 + continue
1.172 +
1.173 + details[property] = value
1.174 +
1.175 + self.events.append(Event(self, details))
1.176 +
1.177 + return self.events
1.178
1.179 class EventPage:
1.180
1.181 @@ -730,6 +862,38 @@
1.182
1.183 return linkToPage(request, self.page, text, query_string)
1.184
1.185 + # Formatting-related functions.
1.186 +
1.187 + def getParserClass(self, request, format):
1.188 +
1.189 + """
1.190 + Return a parser class using the 'request' for the given 'format', returning
1.191 + a plain text parser if no parser can be found for the specified 'format'.
1.192 + """
1.193 +
1.194 + try:
1.195 + return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain")
1.196 + except wikiutil.PluginMissingError:
1.197 + return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain")
1.198 +
1.199 + def formatText(self, text, request, fmt):
1.200 +
1.201 + """
1.202 + Format the given 'text' using the specified 'request' and formatter
1.203 + 'fmt'.
1.204 + """
1.205 +
1.206 + # Suppress line anchors.
1.207 +
1.208 + parser_cls = self.getParserClass(request, self.getFormat())
1.209 + parser = parser_cls(text, request, line_anchors=False)
1.210 +
1.211 + # Fix lists by indicating that a paragraph is already started.
1.212 +
1.213 + return request.redirectedOutput(parser.format, fmt, inhibit_p=True)
1.214 +
1.215 +# Event details.
1.216 +
1.217 class Event(ActsAsTimespan):
1.218
1.219 "A description of an event."
1.220 @@ -738,6 +902,11 @@
1.221 self.page = page
1.222 self.details = details
1.223
1.224 + # Permit omission of the end of the event by duplicating the start.
1.225 +
1.226 + if self.details.has_key("start") and not self.details.has_key("end"):
1.227 + self.details["end"] = self.details["start"]
1.228 +
1.229 def __repr__(self):
1.230 return "<Event %r %r>" % (self.getSummary(), self.as_limits())
1.231
1.232 @@ -818,21 +987,95 @@
1.233 ts = self.as_timespan()
1.234 return ts and ts.as_limits()
1.235
1.236 -def getEventsFromPages(pages):
1.237 +# Obtaining event containers and events from such containers.
1.238 +
1.239 +def getEventPages(pages):
1.240
1.241 "Return a list of events found on the given 'pages'."
1.242
1.243 + # Get real pages instead of result pages.
1.244 +
1.245 + return map(EventPage, pages)
1.246 +
1.247 +def getEventResources(sources, calendar_start, calendar_end, request):
1.248 +
1.249 + """
1.250 + Return resource objects for the given 'sources' using the given
1.251 + 'calendar_start' and 'calendar_end' to parameterise requests to the sources,
1.252 + and the 'request' to access configuration settings in the Wiki.
1.253 + """
1.254 +
1.255 + sources_page = getattr(request.cfg, "event_aggregator_sources_page", "EventSourcesDict")
1.256 +
1.257 + # Remote sources are accessed via dictionary page definitions.
1.258 +
1.259 + if request.user.may.read(sources_page):
1.260 + sources_dict = request.dicts.dict(sources_page)
1.261 + else:
1.262 + return []
1.263 +
1.264 + # Use dates for the calendar limits.
1.265 +
1.266 + if isinstance(calendar_start, Month):
1.267 + calendar_start = calendar_start.as_date(1)
1.268 +
1.269 + if isinstance(calendar_end, Month):
1.270 + calendar_end = calendar_end.as_date(-1)
1.271 +
1.272 + resources = []
1.273 +
1.274 + for source in sources:
1.275 + try:
1.276 + url, format = sources_dict[source].split()
1.277 +
1.278 + # Prevent local file access.
1.279 +
1.280 + if url.startswith("file:"):
1.281 + continue
1.282 +
1.283 + # Parameterise the URL.
1.284 +
1.285 + url = url.replace("{start}", calendar_start and str(calendar_start) or "")
1.286 + url = url.replace("{end}", calendar_end and str(calendar_end) or "")
1.287 +
1.288 + # Get a parser.
1.289 +
1.290 + if format == "ical" and vCalendar is not None:
1.291 + parser = vCalendar.parse
1.292 + resource_cls = EventCalendar
1.293 + else:
1.294 + continue
1.295 +
1.296 + # Access the remote data source.
1.297 +
1.298 + f = urllib.urlopen(url)
1.299 +
1.300 + # NOTE: Should look at the metadata first.
1.301 +
1.302 + uf = codecs.getreader("utf-8")(f)
1.303 +
1.304 + try:
1.305 + resources.append(resource_cls(url, parser(uf)))
1.306 + finally:
1.307 + f.close()
1.308 + uf.close()
1.309 +
1.310 + except (KeyError, ValueError):
1.311 + pass
1.312 +
1.313 + return resources
1.314 +
1.315 +def getEventsFromResources(resources):
1.316 +
1.317 + "Return a list of events supplied by the given event 'resources'."
1.318 +
1.319 events = []
1.320
1.321 - for page in pages:
1.322 -
1.323 - # Get a real page, not a result page.
1.324 -
1.325 - event_page = EventPage(page)
1.326 -
1.327 - # Get all events described in the page.
1.328 -
1.329 - for event in event_page.getEvents():
1.330 + for resource in resources:
1.331 +
1.332 + # Get all events described by the resource.
1.333 +
1.334 + for event in resource.getEvents():
1.335
1.336 # Remember the event.
1.337
1.338 @@ -840,6 +1083,8 @@
1.339
1.340 return events
1.341
1.342 +# Event filtering and limits.
1.343 +
1.344 def getEventsInPeriod(events, calendar_period):
1.345
1.346 """
1.347 @@ -1207,6 +1452,9 @@
1.348 return DateTime(self.as_tuple() + (day, hour, minute, second, zone))
1.349
1.350 def as_date(self, day):
1.351 + if day < 0:
1.352 + weekday, ndays = self.month_properties()
1.353 + day = ndays + 1 + day
1.354 return Date(self.as_tuple() + (day,))
1.355
1.356 def as_month(self):
1.357 @@ -1817,6 +2065,37 @@
1.358 else:
1.359 return None
1.360
1.361 +def getDateFromCalendar(s):
1.362 +
1.363 + """
1.364 + Parse the iCalendar format string 's', extracting and returning a date
1.365 + object.
1.366 + """
1.367 +
1.368 + dt = getDateTimeFromCalendar(s)
1.369 + if dt is not None:
1.370 + return dt.as_date()
1.371 + else:
1.372 + return None
1.373 +
1.374 +def getDateTimeFromCalendar(s):
1.375 +
1.376 + """
1.377 + Parse the iCalendar format datetime string 's', extracting and returning a
1.378 + datetime object where time information has been given or a date object where
1.379 + time information is absent.
1.380 + """
1.381 +
1.382 + m = datetime_icalendar_regexp.search(s)
1.383 + if m:
1.384 + groups = list(m.groups())
1.385 +
1.386 + # Convert date and time data to integer or None.
1.387 +
1.388 + return DateTime(map(int_or_none, groups[:6]) + [m.group("utc") and "UTC" or None]).as_datetime_or_date()
1.389 + else:
1.390 + return None
1.391 +
1.392 def getDateStrings(s):
1.393
1.394 "Parse the string 's', extracting and returning all date strings."
1.395 @@ -2172,48 +2451,4 @@
1.396 new_event_page.setCategoryMembership(category_pagenames)
1.397 new_event_page.saveChanges()
1.398
1.399 -# Formatting-related functions.
1.400 -
1.401 -def getParserClass(request, format):
1.402 -
1.403 - """
1.404 - Return a parser class using the 'request' for the given 'format', returning
1.405 - a plain text parser if no parser can be found for the specified 'format'.
1.406 - """
1.407 -
1.408 - try:
1.409 - return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain")
1.410 - except wikiutil.PluginMissingError:
1.411 - return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain")
1.412 -
1.413 -def getFormatter(request, mimetype, page):
1.414 -
1.415 - """
1.416 - Return a formatter using the given 'request' for the given 'mimetype' for
1.417 - use on the indicated 'page'.
1.418 - """
1.419 -
1.420 - try:
1.421 - cls = wikiutil.searchAndImportPlugin(request.cfg, "formatter", mimetype)
1.422 - except wikiutil.PluginMissingError:
1.423 - cls = wikiutil.searchAndImportPlugin(request.cfg, "formatter", "text/plain")
1.424 - fmt = request.formatter = page.formatter = cls(request)
1.425 - fmt.setPage(page)
1.426 - return fmt
1.427 -
1.428 -def formatText(text, request, fmt, parser_cls):
1.429 -
1.430 - """
1.431 - Format the given 'text' using the specified 'request', formatter 'fmt' and
1.432 - parser class 'parser_cls'.
1.433 - """
1.434 -
1.435 - # Suppress line anchors.
1.436 -
1.437 - parser = parser_cls(text, request, line_anchors=False)
1.438 -
1.439 - # Fix lists by indicating that a paragraph is already started.
1.440 -
1.441 - return request.redirectedOutput(parser.format, fmt, inhibit_p=True)
1.442 -
1.443 # vim: tabstop=4 expandtab shiftwidth=4
3.1 --- a/macros/EventAggregator.py Mon Jul 18 00:28:55 2011 +0200
3.2 +++ b/macros/EventAggregator.py Wed Jul 20 00:53:40 2011 +0200
3.3 @@ -1318,8 +1318,6 @@
3.4 page = fmt.page
3.5 _ = request.getText
3.6
3.7 - parser_cls = getParserClass(request, page.pi["format"])
3.8 -
3.9 # Interpret the arguments.
3.10
3.11 try:
3.12 @@ -1332,6 +1330,7 @@
3.13 # Get special arguments.
3.14
3.15 category_names = []
3.16 + remote_sources = []
3.17 raw_calendar_start = None
3.18 raw_calendar_end = None
3.19 calendar_start = None
3.20 @@ -1368,6 +1367,9 @@
3.21 elif arg.startswith("map="):
3.22 map_name = arg[4:]
3.23
3.24 + elif arg.startswith("source="):
3.25 + remote_sources.append(arg[7:])
3.26 +
3.27 else:
3.28 category_names.append(arg)
3.29
3.30 @@ -1397,10 +1399,11 @@
3.31
3.32 # Get the events according to the resolution of the calendar.
3.33
3.34 - event_pages = getPagesFromResults(getAllCategoryPages(category_names, request), request)
3.35 - events = getEventsFromPages(event_pages)
3.36 - all_shown_events = getEventsInPeriod(events, getCalendarPeriod(calendar_start, calendar_end))
3.37 - earliest, latest = getEventLimits(all_shown_events)
3.38 + pages = getPagesFromResults(getAllCategoryPages(category_names, request), request)
3.39 + events = getEventsFromResources(getEventPages(pages))
3.40 + events += getEventsFromResources(getEventResources(remote_sources, calendar_start, calendar_end, request))
3.41 + all_shown_events = getEventsInPeriod(events, getCalendarPeriod(calendar_start, calendar_end))
3.42 + earliest, latest = getEventLimits(all_shown_events)
3.43
3.44 # Get a concrete period of time.
3.45
3.46 @@ -1486,7 +1489,7 @@
3.47 output.append(fmt.table_cell(on=1, attrs=attrs))
3.48
3.49 if event_details.has_key("location"):
3.50 - output.append(formatText(event_details["location"], request, fmt, parser_cls))
3.51 + output.append(event_page.formatText(event_details["location"], request, fmt))
3.52
3.53 output.append(fmt.table_cell(on=0))
3.54
3.55 @@ -1786,7 +1789,7 @@
3.56
3.57 if event_details.has_key("location"):
3.58 output.append(fmt.paragraph(on=1))
3.59 - output.append(formatText(event_details["location"], request, fmt, parser_cls))
3.60 + output.append(event_page.formatText(event_details["location"], request, fmt))
3.61 output.append(fmt.paragraph(on=1))
3.62
3.63 # Topics.
3.64 @@ -1796,7 +1799,7 @@
3.65
3.66 for topic in event_details.get("topics") or event_details.get("categories") or []:
3.67 output.append(fmt.listitem(on=1))
3.68 - output.append(formatText(topic, request, fmt, parser_cls))
3.69 + output.append(event_page.formatText(topic, request, fmt))
3.70 output.append(fmt.listitem(on=0))
3.71
3.72 output.append(fmt.bullet_list(on=0))