# HG changeset patch # User Paul Boddie # Date 1311116020 -7200 # Node ID 18da3bd40274a58add990b4af772d0d32b572d5f # Parent 65a2a2bd4ea48507dcc0e82d8f9dba9b95e1c032 Added a general EventResource class along with a specific EventCalendar class so that non-Wiki events can be handled without Wiki pages - typically encapsulated by EventPage instances - needing to be defined to support such events. Added elementary iCalendar aggregation support. Changed the formatText method on classes supporting the EventPage interface so that a Wiki page need not exist for text to be appropriately formatted. This should make text formatting easier to perform in general. Added some notes about what needs to be done to make external aggregation function properly. diff -r 65a2a2bd4ea4 -r 18da3bd40274 EventAggregatorSupport.py --- a/EventAggregatorSupport.py Mon Jul 18 00:28:55 2011 +0200 +++ b/EventAggregatorSupport.py Wed Jul 20 00:53:40 2011 +0200 @@ -12,11 +12,13 @@ from MoinMoin import search, version from MoinMoin import wikiutil import calendar +import codecs import datetime import time import re import bisect import operator +import urllib try: set @@ -28,6 +30,11 @@ except ImportError: pytz = None +try: + import vCalendar +except ImportError: + vCalendar = None + escape = wikiutil.escape __version__ = "0.7" @@ -57,6 +64,8 @@ ur"(?:,(?:\s*[\w-]+)+)?" # country (optional) ur")$", re.UNICODE) +# Month, date, time and datetime parsing. + month_regexp_str = ur'(?P[0-9]{4})-(?P[0-9]{2})' date_regexp_str = ur'(?P[0-9]{4})-(?P[0-9]{2})-(?P[0-9]{2})' time_regexp_str = ur'(?P[0-2][0-9]):(?P[0-5][0-9])(?::(?P[0-6][0-9]))?' @@ -69,9 +78,23 @@ month_regexp = re.compile(month_regexp_str, re.UNICODE) date_regexp = re.compile(date_regexp_str, re.UNICODE) time_regexp = re.compile(time_regexp_str, re.UNICODE) -datetime_regexp = re.compile(datetime_regexp_str, re.UNICODE) timezone_olson_regexp = re.compile(timezone_olson_str, re.UNICODE) timezone_offset_regexp = re.compile(timezone_offset_str, re.UNICODE) +datetime_regexp = re.compile(datetime_regexp_str, re.UNICODE) + +# iCalendar date and datetime parsing. + +date_icalendar_regexp_str = ur'(?P[0-9]{4})(?P[0-9]{2})(?P[0-9]{2})' +datetime_icalendar_regexp_str = date_icalendar_regexp_str + \ + ur'(?:' \ + ur'T(?P[0-2][0-9])(?P[0-5][0-9])(?P[0-6][0-9])' \ + ur'(?PZ)?' \ + ur')?' + +date_icalendar_regexp = re.compile(date_icalendar_regexp_str, re.UNICODE) +datetime_icalendar_regexp = re.compile(datetime_icalendar_regexp_str, re.UNICODE) + +# Simple content parsing. verbatim_regexp = re.compile(ur'(?:' ur'<.*?)\)>>' @@ -476,7 +499,116 @@ class ActsAsTimespan: pass -# The main activity functions. +# Event resources providing collections of events. + +class EventResource: + + "A resource providing event information." + + def __init__(self, url): + self.url = url + + def getPageURL(self, request): + + "Using 'request', return the URL of this page." + + return self.url + + def getFormat(self): + + "Get the format used on this page." + + return "plain" + + def getEvents(self): + + "Return a list of events from this resource." + + return [] + + def linkToPage(self, request, text, query_string=None): + + """ + Using 'request', return a link to this page with the given link 'text' + and optional 'query_string'. + """ + + url = self.url + + if query_string: + query_string = wikiutil.makeQueryString(query_string) + url = "%s?%s" % (url, query_string) + + formatter = request.page and getattr(request.page, "formatter", None) or request.html_formatter + + output = [] + output.append(formatter.url(1, url)) + output.append(formatter.text(text)) + output.append(formatter.url(0)) + return "".join(output) + + # Formatting-related functions. + + def formatText(self, text, request, fmt): + + """ + Format the given 'text' using the specified 'request' and formatter + 'fmt'. + """ + + # Assume plain text which is then formatted appropriately. + + return fmt.text(text) + +class EventCalendar(EventResource): + + "An iCalendar resource." + + def __init__(self, url, calendar): + EventResource.__init__(self, url) + self.calendar = calendar + self.events = None + + def getEvents(self): + + "Return a list of events from this resource." + + if self.events is None: + self.events = [] + + _calendar, _empty, calendar = self.calendar + + for objtype, attrs, obj in calendar: + + # Read events. + + if objtype == "VEVENT": + details = {} + + for property, attrs, value in obj: + + # Convert dates. + + if property in ("DTSTART", "DTEND"): + property = property[2:].lower() + if attrs.get("VALUE") == "DATE": + value = getDateFromCalendar(value) + else: + value = getDateTimeFromCalendar(value) + + # Accept other textual data as it is. + + elif property in ("CATEGORIES", "LOCATION", "SUMMARY"): + property = property.lower() + + else: + continue + + details[property] = value + + self.events.append(Event(self, details)) + + return self.events class EventPage: @@ -730,6 +862,38 @@ return linkToPage(request, self.page, text, query_string) + # Formatting-related functions. + + def getParserClass(self, request, format): + + """ + Return a parser class using the 'request' for the given 'format', returning + a plain text parser if no parser can be found for the specified 'format'. + """ + + try: + return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain") + except wikiutil.PluginMissingError: + return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain") + + def formatText(self, text, request, fmt): + + """ + Format the given 'text' using the specified 'request' and formatter + 'fmt'. + """ + + # Suppress line anchors. + + parser_cls = self.getParserClass(request, self.getFormat()) + parser = parser_cls(text, request, line_anchors=False) + + # Fix lists by indicating that a paragraph is already started. + + return request.redirectedOutput(parser.format, fmt, inhibit_p=True) + +# Event details. + class Event(ActsAsTimespan): "A description of an event." @@ -738,6 +902,11 @@ self.page = page self.details = details + # Permit omission of the end of the event by duplicating the start. + + if self.details.has_key("start") and not self.details.has_key("end"): + self.details["end"] = self.details["start"] + def __repr__(self): return "" % (self.getSummary(), self.as_limits()) @@ -818,21 +987,95 @@ ts = self.as_timespan() return ts and ts.as_limits() -def getEventsFromPages(pages): +# Obtaining event containers and events from such containers. + +def getEventPages(pages): "Return a list of events found on the given 'pages'." + # Get real pages instead of result pages. + + return map(EventPage, pages) + +def getEventResources(sources, calendar_start, calendar_end, request): + + """ + Return resource objects for the given 'sources' using the given + 'calendar_start' and 'calendar_end' to parameterise requests to the sources, + and the 'request' to access configuration settings in the Wiki. + """ + + sources_page = getattr(request.cfg, "event_aggregator_sources_page", "EventSourcesDict") + + # Remote sources are accessed via dictionary page definitions. + + if request.user.may.read(sources_page): + sources_dict = request.dicts.dict(sources_page) + else: + return [] + + # Use dates for the calendar limits. + + if isinstance(calendar_start, Month): + calendar_start = calendar_start.as_date(1) + + if isinstance(calendar_end, Month): + calendar_end = calendar_end.as_date(-1) + + resources = [] + + for source in sources: + try: + url, format = sources_dict[source].split() + + # Prevent local file access. + + if url.startswith("file:"): + continue + + # Parameterise the URL. + + url = url.replace("{start}", calendar_start and str(calendar_start) or "") + url = url.replace("{end}", calendar_end and str(calendar_end) or "") + + # Get a parser. + + if format == "ical" and vCalendar is not None: + parser = vCalendar.parse + resource_cls = EventCalendar + else: + continue + + # Access the remote data source. + + f = urllib.urlopen(url) + + # NOTE: Should look at the metadata first. + + uf = codecs.getreader("utf-8")(f) + + try: + resources.append(resource_cls(url, parser(uf))) + finally: + f.close() + uf.close() + + except (KeyError, ValueError): + pass + + return resources + +def getEventsFromResources(resources): + + "Return a list of events supplied by the given event 'resources'." + events = [] - for page in pages: - - # Get a real page, not a result page. - - event_page = EventPage(page) - - # Get all events described in the page. - - for event in event_page.getEvents(): + for resource in resources: + + # Get all events described by the resource. + + for event in resource.getEvents(): # Remember the event. @@ -840,6 +1083,8 @@ return events +# Event filtering and limits. + def getEventsInPeriod(events, calendar_period): """ @@ -1207,6 +1452,9 @@ return DateTime(self.as_tuple() + (day, hour, minute, second, zone)) def as_date(self, day): + if day < 0: + weekday, ndays = self.month_properties() + day = ndays + 1 + day return Date(self.as_tuple() + (day,)) def as_month(self): @@ -1817,6 +2065,37 @@ else: return None +def getDateFromCalendar(s): + + """ + Parse the iCalendar format string 's', extracting and returning a date + object. + """ + + dt = getDateTimeFromCalendar(s) + if dt is not None: + return dt.as_date() + else: + return None + +def getDateTimeFromCalendar(s): + + """ + Parse the iCalendar format datetime string 's', extracting and returning a + datetime object where time information has been given or a date object where + time information is absent. + """ + + m = datetime_icalendar_regexp.search(s) + if m: + groups = list(m.groups()) + + # Convert date and time data to integer or None. + + return DateTime(map(int_or_none, groups[:6]) + [m.group("utc") and "UTC" or None]).as_datetime_or_date() + else: + return None + def getDateStrings(s): "Parse the string 's', extracting and returning all date strings." @@ -2172,48 +2451,4 @@ new_event_page.setCategoryMembership(category_pagenames) new_event_page.saveChanges() -# Formatting-related functions. - -def getParserClass(request, format): - - """ - Return a parser class using the 'request' for the given 'format', returning - a plain text parser if no parser can be found for the specified 'format'. - """ - - try: - return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain") - except wikiutil.PluginMissingError: - return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain") - -def getFormatter(request, mimetype, page): - - """ - Return a formatter using the given 'request' for the given 'mimetype' for - use on the indicated 'page'. - """ - - try: - cls = wikiutil.searchAndImportPlugin(request.cfg, "formatter", mimetype) - except wikiutil.PluginMissingError: - cls = wikiutil.searchAndImportPlugin(request.cfg, "formatter", "text/plain") - fmt = request.formatter = page.formatter = cls(request) - fmt.setPage(page) - return fmt - -def formatText(text, request, fmt, parser_cls): - - """ - Format the given 'text' using the specified 'request', formatter 'fmt' and - parser class 'parser_cls'. - """ - - # Suppress line anchors. - - parser = parser_cls(text, request, line_anchors=False) - - # Fix lists by indicating that a paragraph is already started. - - return request.redirectedOutput(parser.format, fmt, inhibit_p=True) - # vim: tabstop=4 expandtab shiftwidth=4 diff -r 65a2a2bd4ea4 -r 18da3bd40274 TO_DO.txt --- a/TO_DO.txt Mon Jul 18 00:28:55 2011 +0200 +++ b/TO_DO.txt Wed Jul 20 00:53:40 2011 +0200 @@ -1,3 +1,15 @@ +GriCal and External Aggregation +------------------------------- + +Support a linkToEvent method on Event instances, possibly just delegating to +linkToPage for Wiki events (although event sections could provide anchors for +events in Wiki pages). + +Support caching and proper encoding detection. + +Support navigation where the full extent of external events cannot be +detected. + Localised Keywords ------------------ diff -r 65a2a2bd4ea4 -r 18da3bd40274 macros/EventAggregator.py --- a/macros/EventAggregator.py Mon Jul 18 00:28:55 2011 +0200 +++ b/macros/EventAggregator.py Wed Jul 20 00:53:40 2011 +0200 @@ -1318,8 +1318,6 @@ page = fmt.page _ = request.getText - parser_cls = getParserClass(request, page.pi["format"]) - # Interpret the arguments. try: @@ -1332,6 +1330,7 @@ # Get special arguments. category_names = [] + remote_sources = [] raw_calendar_start = None raw_calendar_end = None calendar_start = None @@ -1368,6 +1367,9 @@ elif arg.startswith("map="): map_name = arg[4:] + elif arg.startswith("source="): + remote_sources.append(arg[7:]) + else: category_names.append(arg) @@ -1397,10 +1399,11 @@ # Get the events according to the resolution of the calendar. - event_pages = getPagesFromResults(getAllCategoryPages(category_names, request), request) - events = getEventsFromPages(event_pages) - all_shown_events = getEventsInPeriod(events, getCalendarPeriod(calendar_start, calendar_end)) - earliest, latest = getEventLimits(all_shown_events) + pages = getPagesFromResults(getAllCategoryPages(category_names, request), request) + events = getEventsFromResources(getEventPages(pages)) + events += getEventsFromResources(getEventResources(remote_sources, calendar_start, calendar_end, request)) + all_shown_events = getEventsInPeriod(events, getCalendarPeriod(calendar_start, calendar_end)) + earliest, latest = getEventLimits(all_shown_events) # Get a concrete period of time. @@ -1486,7 +1489,7 @@ output.append(fmt.table_cell(on=1, attrs=attrs)) if event_details.has_key("location"): - output.append(formatText(event_details["location"], request, fmt, parser_cls)) + output.append(event_page.formatText(event_details["location"], request, fmt)) output.append(fmt.table_cell(on=0)) @@ -1786,7 +1789,7 @@ if event_details.has_key("location"): output.append(fmt.paragraph(on=1)) - output.append(formatText(event_details["location"], request, fmt, parser_cls)) + output.append(event_page.formatText(event_details["location"], request, fmt)) output.append(fmt.paragraph(on=1)) # Topics. @@ -1796,7 +1799,7 @@ for topic in event_details.get("topics") or event_details.get("categories") or []: output.append(fmt.listitem(on=1)) - output.append(formatText(topic, request, fmt, parser_cls)) + output.append(event_page.formatText(topic, request, fmt)) output.append(fmt.listitem(on=0)) output.append(fmt.bullet_list(on=0))