# HG changeset patch # User Paul Boddie # Date 1396204452 -7200 # Node ID ae498249090e9835b18d4d4caffe2e80db32d09a # Parent dbabdfec25c8adac5eb0cdcf8750a5e8d011fb7d# Parent a0e4dad42a1ebfdf6f017d146f24d071331d33da Merged changes from the default branch. diff -r dbabdfec25c8 -r ae498249090e EventAggregatorSupport/Resources.py --- a/EventAggregatorSupport/Resources.py Sat Mar 29 19:42:14 2014 +0100 +++ b/EventAggregatorSupport/Resources.py Sun Mar 30 20:34:12 2014 +0200 @@ -12,6 +12,9 @@ from DateSupport import Date, Month from MoinSupport import * from MoinRemoteSupport import getCachedResource, getCachedResourceMetadata, imapreader +from ItemSupport import ItemStore + +from MoinMoin.Page import Page import urllib @@ -26,9 +29,9 @@ "Return a list of events found on the given 'pages'." - # Get real pages instead of result pages. + # Get event pages instead of result pages. - return map(EventPage, pages) + return EventResourceCollection(map(EventPage, pages)) def getAllEventSources(request): @@ -64,24 +67,51 @@ elif isinstance(calendar_end, Month): calendar_end = calendar_end.as_date(-1) - resources = [] + resources = EventResourceCollection() for source in sources: - try: - details = sources_dict[source].split() - details.extend([None] * (3 - len(details))) - url = details[0] - format = details[1] or "ical" - expected_content_type = details[2] - except (KeyError, ValueError): - pass + details = sources_dict[source].split() + type = details[0] + resource = None + + # Support non-URL sources. + + if type == "store": + details = details[1:] + + details.extend([None] * 3) + page_name, store_name, lockdir_name = details[:3] + + resource = getEventResourcesFromStore(page_name, store_name, lockdir_name, request) + + # Unrecognised types are treated as URLs. + else: + if type == "url": + details = details[1:] + + details.extend([None] * 3) + url, format, expected_content_type = details[:3] + format = format or "ical" + resource = getEventResourcesFromSource(url, format, expected_content_type, calendar_start, calendar_end, request) - if resource: - resources.append(resource) + + if resource: + resources.append(resource) return resources +def getEventResourcesFromStore(page_name, store_name, lockdir_name, request): + + """ + Return a resource object for the store indicated by the given 'page_name' + and 'store_name' (with the accompanying 'lockdir_name', specified as None + for a computed name to be used). + """ + + store = ItemStore(Page(request, page_name), store_name, lockdir_name) + return parseEventsInStore(store, request) + def getEventResourcesFromSource(url, format, expected_content_type, calendar_start, calendar_end, request): """ @@ -117,14 +147,8 @@ # NOTE: This could be done reactively by choosing a parser based on # NOTE: the content type provided by the URL. - if format == "ical": - parser = parseEventsInCalendarFromResource - required_content_type = expected_content_type or "text/calendar" - elif format == "xcal": - parser = parseEventsInXMLCalendarsFromResource - required_content_type = expected_content_type or "multipart/mixed" - else: - return None + parser, default_content_type = getParserForFormat(format) + required_content_type = expected_content_type or default_content_type # Obtain the resource, using a cached version if appropriate. @@ -152,23 +176,18 @@ finally: f.close() -def getEventsFromResources(resources): - - "Return a list of events supplied by the given event 'resources'." +def getParserForFormat(format): - events = [] - - for resource in resources: + "Return a parser and default content type for the given 'format'." - # Get all events described by the resource. - - for event in resource.getEvents(): - - # Remember the event. - - events.append(event) - - return events + if format == "ical": + return parseEventsInCalendarFromResource, "text/calendar" + elif format == "xcal": + return parseEventsInXMLCalendarFromResource, "application/calendar+xml" + elif format == "mbox": + return parseEventsInMailboxFromResource, "multipart/mixed" + else: + return None, None # Page-related functions. @@ -207,8 +226,8 @@ results += getAllCategoryPages(category_names, request) pages = getPagesFromResults(results, request) - events = getEventsFromResources(getEventPages(pages)) - events += getEventsFromResources(getEventResources(remote_sources, calendar_start, calendar_end, request)) + events = getEventPages(pages).getEvents() + events += getEventResources(remote_sources, calendar_start, calendar_end, request).getEvents() all_shown_events = getEventsInPeriod(events, getCalendarPeriod(calendar_start, calendar_end)) earliest, latest = getEventLimits(all_shown_events) diff -r dbabdfec25c8 -r ae498249090e EventAggregatorSupport/Types.py --- a/EventAggregatorSupport/Types.py Sat Mar 29 19:42:14 2014 +0100 +++ b/EventAggregatorSupport/Types.py Sun Mar 30 20:34:12 2014 +0200 @@ -36,6 +36,15 @@ except ImportError: libxml2dom = None +# Import MoinMessage for decryption. + +try: + import MoinMessageSupport + import MoinMessage +except ImportError: + MoinMessageSupport = None + MoinMessage = None + # Page parsing. definition_list_regexp = re.compile(ur'(?P^(?P#*)\s+(?P.*?):: )(?P.*?)$', re.UNICODE | re.MULTILINE) @@ -52,12 +61,12 @@ # Calendar-format pages are parsed directly by the iCalendar parser. if page.getFormat() == "calendar": - return parseEventsInCalendar(text) + return parseEventsInCalendar(text, page.getPageURL()) # xCalendar-format pages are parsed directly by the iCalendar parser. elif page.getFormat() == "xcalendar": - return parseEventsInXMLCalendar(text) + return parseEventsInXMLCalendar(text, page.getPageURL()) # Wiki-format pages are parsed region-by-region using the special markup. @@ -66,20 +75,20 @@ # Where a page contains events, potentially in regions, identify the page # regions and obtain the events within them. - events = [] + events = EventResourceCollection() for format, attributes, region in getFragments(text, True): if format == "calendar": - events += parseEventsInCalendar(region) + events.append(parseEventsInCalendar(region, page.getPageURL())) else: - events += parseEvents(region, page, attributes.get("fragment") or fragment) + events.append(parseEvents(region, page, attributes.get("fragment") or fragment)) return events # Unsupported format pages return no events. else: - return [] + return EventResourceCollection() -def parseEventsInCalendar(text): +def parseEventsInCalendar(text, url=None): """ Parse events in iCalendar format from the given 'text'. @@ -88,10 +97,9 @@ # Fill the StringIO with encoded plain string data. encoding = "utf-8" - calendar = parseEventsInCalendarFromResource(StringIO(text.encode(encoding)), encoding) - return calendar.getEvents() + return parseEventsInCalendarFromResource(StringIO(text.encode(encoding)), encoding, url) -def parseEventsInXMLCalendar(text): +def parseEventsInXMLCalendar(text, url): """ Parse events in xCalendar format from the given 'text'. @@ -100,8 +108,30 @@ # Fill the StringIO with encoded plain string data. encoding = "utf-8" - calendar = parseEventsInXMLCalendarFromResource(StringIO(text.encode(encoding)), encoding) - return calendar.getEvents() + return parseEventsInXMLCalendarFromResource(StringIO(text.encode(encoding)), encoding, url) + +def parseEventsInStore(store, url=None, request=None): + + """ + Parse events in the given item 'store'. + """ + + calendar = EventResourceCollection() + for text in store: + calendar.append(parseEventsInMessage(text, url, request)) + return calendar + +def parseEventsInMessage(text, url=None, request=None): + + """ + Parse events in the given 'text' in MIME e-mail message format. + """ + + message = Parser().parsestr(text) + return parseEventsInMessagePart(message, url=url, request=request) + +# Events originating from remote sources are typically made available within +# calendar objects, with collection objects used to group multiple calendars. def parseEventsInCalendarFromResource(f, encoding=None, url=None, metadata=None): @@ -132,35 +162,58 @@ else: return None -def parseEventsInXMLCalendarsFromResource(f, encoding=None, url=None, metadata=None): +def parseEventsInMailboxFromResource(f, encoding=None, url=None, metadata=None): """ - Parse a collection of events in xCalendar format from the given file-like - object 'f', with content having any specified 'encoding' and being described - by the given 'url' and 'metadata'. + Parse a compound message containing events in iCalendar or xCalendar format + from the given file-like object 'f', with content having any specified + 'encoding' and being described by the given 'url' and 'metadata'. """ - new_url = "" # hide the IMAP URL - message = Parser().parse(f) - resources = EventResourceCollection(new_url, metadata or {}) + new_url = "" # hide the IMAP URL + return parseEventsInMessagePart(message, new_url, metadata) + +def parseEventsInMessagePart(part, url=None, metadata=None, request=None): - for data in message.get_payload(): + """ + Parse events in the given message 'part'. + """ - # Find the calendar data. + if part.is_multipart(): + resources = EventResourceCollection() + + # Attempt to decrypt any encrypted message parts. - if data.is_multipart(): - for part in data.get_payload(): - if part.get_content_type() == "application/calendar+xml": - text = part + if MoinMessageSupport is not None: + try: + if MoinMessage.is_encrypted(part): + homedir = MoinMessageSupport.get_homedir(request) + gpg = MoinMessage.GPG(homedir) + text = gpg.decryptMessage(part) + part = Parser().parsestr(text) + except MoinMessage.MoinMessageError: + return resources + + for subpart in part.get_payload(): + resources.append(parseEventsInMessagePart(subpart, url, metadata, request)) + + return resources + + else: + content_type = part.get_content_type() + text = part.get_payload(decode=True) + + # Handle standard calendar formats. + + if content_type == "application/calendar+xml": + return parseEventsInXMLCalendarFromResource(StringIO(text), part.get_charset(), url) + elif content_type == "text/calendar": + return parseEventsInCalendarFromResource(StringIO(text), part.get_charset(), url) else: - text = data - - # Obtain a calendar and merge it into the collection. + return EventResourceCollection() - resources.append(parseEventsInXMLCalendarFromResource(StringIO(text.get_payload(decode=True)), part.get_charset(), new_url)) - - return resources +# Wiki-specific event parsing. def parseEvents(text, event_page, fragment=None): @@ -262,7 +315,9 @@ details[term] = desc raw_details[term] = raw_desc - return events + # Filter out incomplete events. + + return EventResource(event_page.getPageURL(), events=[e for e in events if e.as_timespan()]) # Event resources providing collections of events. @@ -270,10 +325,10 @@ "A resource providing event information." - def __init__(self, url, metadata=None): + def __init__(self, url, metadata=None, events=None): self.url = url self.metadata = metadata - self.events = None + self.events = events def getPageURL(self): @@ -328,10 +383,8 @@ "A collection of resources." - def __init__(self, url, metadata=None): - self.url = url - self.metadata = metadata - self.resources = [] + def __init__(self, resources=None): + self.resources = resources or [] def append(self, resource): self.resources.append(resource) @@ -615,7 +668,7 @@ "Return a list of events from this page." if self.events is None: - self.events = parseEventsInPage(self.page.data, self) + self.events = parseEventsInPage(self.page.data, self).getEvents() return self.events diff -r dbabdfec25c8 -r ae498249090e EventAggregatorSupport/View.py --- a/EventAggregatorSupport/View.py Sat Mar 29 19:42:14 2014 +0100 +++ b/EventAggregatorSupport/View.py Sun Mar 30 20:34:12 2014 +0200 @@ -763,6 +763,9 @@ "Return navigation links for a calendar." + if self.showing_everything: + return "" + page = self.page request = page.request fmt = request.formatter diff -r dbabdfec25c8 -r ae498249090e README.txt --- a/README.txt Sat Mar 29 19:42:14 2014 +0100 +++ b/README.txt Sun Mar 30 20:34:12 2014 +0200 @@ -275,6 +275,11 @@ http://moinmo.in/HelpOnXapian +For decryption of stored item content in a wiki, the MoinMessage extension is +required: + +http://hgweb.boddie.org.uk/MoinMessage + Troubleshooting: Categories --------------------------- diff -r dbabdfec25c8 -r ae498249090e TO_DO.txt --- a/TO_DO.txt Sat Mar 29 19:42:14 2014 +0100 +++ b/TO_DO.txt Sun Mar 30 20:34:12 2014 +0200 @@ -14,7 +14,9 @@ RECURRENCE-ID in an event is also useful where recurring events are being handled.) Here, a form of index needs to be supported for efficient access via event UIDs to event data. Other indexes might be supported for efficient -free/busy resource generation. +free/busy resource generation. Indexes could be supported using cache entries +just like the OpenID support (in MoinMoin.util.oid) uses them to track +associations. The actual sending and receiving of iTIP messages needs to be supported by other components such as MoinMessage. It might be interesting to support iTIP diff -r dbabdfec25c8 -r ae498249090e macros/EventAggregator.py --- a/macros/EventAggregator.py Sat Mar 29 19:42:14 2014 +0100 +++ b/macros/EventAggregator.py Sun Mar 30 20:34:12 2014 +0200 @@ -22,8 +22,8 @@ """ Execute the 'macro' with the given 'args': an optional list of selected - category names (categories whose pages are to be shown), together with - optional named arguments of the following forms: + category names (categories whose pages are to provide event information), + together with optional named arguments of the following forms: start=YYYY-MM shows event details starting from the specified month start=YYYY-MM-DD shows event details starting from the specified day diff -r dbabdfec25c8 -r ae498249090e parsers/calendar.py --- a/parsers/calendar.py Sat Mar 29 19:42:14 2014 +0100 +++ b/parsers/calendar.py Sun Mar 30 20:34:12 2014 +0200 @@ -9,8 +9,7 @@ from MoinSupport import parseAttributes, RawParser from EventAggregatorSupport.Formatting import formatEventsForOutputType, \ formatEvent -from EventAggregatorSupport.Types import parseEventsInPage, EventPage, \ - parseEventsInCalendar +from EventAggregatorSupport.Types import parseEventsInCalendar Dependencies = ["pages"] @@ -52,7 +51,7 @@ using the request. """ - for event in parseEventsInCalendar(self.raw): + for event in parseEventsInCalendar(self.raw).getEvents(): formatEvent(event, self.request, fmt, write=write, parser_cls=RawParser) # Extra API methods. @@ -70,7 +69,7 @@ if mimetype == "text/calendar": (write or request.write)(self.raw) else: - events = parseEventsInCalendar(self.raw) + events = parseEventsInCalendar(self.raw).getEvents() formatEventsForOutputType(events, self.request, mimetype, write=write) # Class methods. diff -r dbabdfec25c8 -r ae498249090e parsers/event.py --- a/parsers/event.py Sat Mar 29 19:42:14 2014 +0100 +++ b/parsers/event.py Sun Mar 30 20:34:12 2014 +0200 @@ -46,7 +46,7 @@ using the request. """ - for event in parseEventsInPage(self.raw, EventPage(self.request.page), self.fragment): + for event in parseEventsInPage(self.raw, EventPage(self.request.page), self.fragment).getEvents(): formatEvent(event, self.request, fmt, write=write) # Extra API methods. @@ -59,7 +59,7 @@ request. """ - events = parseEventsInPage(self.raw, EventPage(self.request.page), self.fragment) + events = parseEventsInPage(self.raw, EventPage(self.request.page), self.fragment).getEvents() formatEventsForOutputType(events, self.request, mimetype, write=write) # Class methods. diff -r dbabdfec25c8 -r ae498249090e parsers/xcalendar.py --- a/parsers/xcalendar.py Sat Mar 29 19:42:14 2014 +0100 +++ b/parsers/xcalendar.py Sun Mar 30 20:34:12 2014 +0200 @@ -9,8 +9,7 @@ from MoinSupport import parseAttributes, RawParser from EventAggregatorSupport.Formatting import formatEventsForOutputType, \ formatEvent -from EventAggregatorSupport.Types import parseEventsInPage, EventPage, \ - parseEventsInXMLCalendar +from EventAggregatorSupport.Types import parseEventsInXMLCalendar Dependencies = ["pages"] @@ -52,7 +51,7 @@ using the request. """ - for event in parseEventsInXMLCalendar(self.raw): + for event in parseEventsInXMLCalendar(self.raw).getEvents(): formatEvent(event, self.request, fmt, write=write, parser_cls=RawParser) # Extra API methods. @@ -70,7 +69,7 @@ if mimetype == "application/calendar+xml": (write or request.write)(self.raw) else: - events = parseEventsInXMLCalendar(self.raw) + events = parseEventsInXMLCalendar(self.raw).getEvents() formatEventsForOutputType(events, self.request, mimetype, write=write) # Class methods. diff -r dbabdfec25c8 -r ae498249090e resource_pages/EventSourcesDict --- a/resource_pages/EventSourcesDict Sat Mar 29 19:42:14 2014 +0100 +++ b/resource_pages/EventSourcesDict Sun Mar 30 20:34:12 2014 +0200 @@ -1,9 +1,33 @@ The following event sources are defined for !EventAggregator: - GriCal:: http://grical.org/s/?query=%23free-software+{start}+{end}&view=ical ical + GriCal:: url http://grical.org/s/?query=%23free-software+{start}+{end}&view=ical ical + +To add a new source, extend the above definition list, putting the name of the source (to be used with the !EventAggregator macro) as the definition title, and putting the source's URL and the format of the data provided by the source, separated by whitespace, as the definition body. An expected content type can also be specified if the source provides content labelled with an unconventional content type. + +Other types of sources can be specified by replacing `url` with another source type and using the appropriate parameters. See the syntax description below for details. + +== Syntax == + +For a `url` source indicating a source accessible via a specified URL: + +{{{ + name:: url [ ] +}}} -To add a new source, extend the above definition list, putting the name of the source (to be used with the !EventAggregator macro) as the definition title, and putting the source's URL and the format of the data provided by the source, separated by whitespace, as the definition body. +For `store` source types indicating a sources accessible via an item store associated with a wiki page: + +{{{ + name:: store [ ] +}}} + +== Formats == -Currently, `ical` is the only supported format, referring to iCalendar ([[http://tools.ietf.org/html/rfc2445|RFC 2445]] and [[http://tools.ietf.org/html/rfc5545|RFC 5545]]). +Currently, the supported formats are: + + * `ical`: iCalendar ([[http://tools.ietf.org/html/rfc5545|RFC 5545]]) + * `xcal`: xCalendar ([[http://tools.ietf.org/html/draft-hare-xcalendar-03|draft-hare-xcalendar-03]]) + * `mbox`: [[http://tools.ietf.org/html/rfc2045|multipart]] e-mail messages containing iCalendar or xCalendar content parts + +== Caching == Events from sources for a particular view are cached in !EventAggregator for a period of time. The default period of 300 seconds (5 minutes) can be changed by defining the `event_aggregator_max_cache_age` in the [[HelpOnConfiguration|Wiki configuration]], specifying a number of seconds to be used for the maximum age of a cache entry.