# HG changeset patch # User Paul Boddie # Date 1390948277 -3600 # Node ID a748da07fd84e5c8d79f4586f44c6220facbf849 # Parent 7f5a761329953e088f06fa19dc61a68f228d88cf# Parent d35aea22f824804437fcb8bd227faa92e3d05643 Merged with default. diff -r 7f5a76132995 -r a748da07fd84 EventAggregatorSupport/Resources.py --- a/EventAggregatorSupport/Resources.py Tue Jan 28 01:50:22 2014 +0100 +++ b/EventAggregatorSupport/Resources.py Tue Jan 28 23:31:17 2014 +0100 @@ -110,6 +110,9 @@ if format == "ical": parser = parseEventsInCalendarFromResource required_content_type = expected_content_type or "text/calendar" + elif format == "xcal": + parser = parseEventsInXMLCalendarFromResource + required_content_type = expected_content_type or "application/calendar+xml" else: return None diff -r 7f5a76132995 -r a748da07fd84 EventAggregatorSupport/Types.py --- a/EventAggregatorSupport/Types.py Tue Jan 28 01:50:22 2014 +0100 +++ b/EventAggregatorSupport/Types.py Tue Jan 28 23:31:17 2014 +0100 @@ -28,6 +28,13 @@ except NameError: from sets import Set as set +# Import libxml2dom for xCalendar parsing. + +try: + import libxml2dom +except ImportError: + libxml2dom = None + # Page parsing. definition_list_regexp = re.compile(ur'(?P^(?P#*)\s+(?P.*?):: )(?P.*?)$', re.UNICODE | re.MULTILINE) @@ -46,6 +53,11 @@ if page.getFormat() == "calendar": return parseEventsInCalendar(text) + # xCalendar-format pages are parsed directly by the iCalendar parser. + + elif page.getFormat() == "xcalendar": + return parseEventsInXMLCalendar(text) + # Wiki-format pages are parsed region-by-region using the special markup. elif page.getFormat() == "wiki": @@ -78,6 +90,18 @@ calendar = parseEventsInCalendarFromResource(StringIO(text.encode(encoding)), encoding) return calendar.getEvents() +def parseEventsInXMLCalendar(text): + + """ + Parse events in xCalendar format from the given 'text'. + """ + + # Fill the StringIO with encoded plain string data. + + encoding = "utf-8" + calendar = parseEventsInXMLCalendarFromResource(StringIO(text.encode(encoding)), encoding) + return calendar.getEvents() + def parseEventsInCalendarFromResource(f, encoding=None, url=None, metadata=None): """ @@ -94,6 +118,19 @@ finally: uf.close() +def parseEventsInXMLCalendarFromResource(f, encoding=None, url=None, metadata=None): + + """ + Parse 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'. + """ + + if libxml2dom is not None: + return EventXMLCalendar(url or "", libxml2dom.parse(f), metadata or {}) + else: + return None + def parseEvents(text, event_page, fragment=None): """ @@ -254,13 +291,12 @@ return fmt.text(text) -class EventCalendar(EventResource): - - "An iCalendar resource." +class EventCalendarResource(EventResource): - def __init__(self, url, calendar, metadata): + "A generic calendar resource." + + def __init__(self, url, metadata): EventResource.__init__(self, url) - self.calendar = calendar self.metadata = metadata self.events = None @@ -280,6 +316,24 @@ return self.metadata +class EventCalendar(EventCalendarResource): + + "An iCalendar resource." + + def __init__(self, url, calendar, metadata): + EventCalendarResource.__init__(self, url, metadata) + self.calendar = calendar + + def getMetadata(self): + + """ + Return a dictionary containing items describing the page's "created" + time, "last-modified" time, "sequence" (or revision number) and the + "last-comment" made about the last edit. + """ + + return self.metadata + def getEvents(self): "Return a list of events from this resource." @@ -348,6 +402,110 @@ return self.events +class EventXMLCalendar(EventCalendarResource): + + "An xCalendar resource." + + XCAL = {"xcal" : "urn:ietf:params:xml:ns:icalendar-2.0"} + + # See: http://tools.ietf.org/html/draft-daboo-et-al-icalendar-in-xml-11#section-3.4 + + properties = [ + ("summary", "xcal:properties/xcal:summary", "getText"), + ("location", "xcal:properties/xcal:location", "getText"), + ("start", "xcal:properties/xcal:dtstart", "getDateTime"), + ("end", "xcal:properties/xcal:dtend", "getDateTime"), + ("created", "xcal:properties/xcal:created", "getDateTime"), + ("dtstamp", "xcal:properties/xcal:dtstamp", "getDateTime"), + ("last-modified", "xcal:properties/xcal:last-modified", "getDateTime"), + ("sequence", "xcal:properties/xcal:sequence", "getInteger"), + ("categories", "xcal:properties/xcal:categories", "getCollection"), + ("geo", "xcal:properties/xcal:geo", "getGeo"), + ("url", "xcal:properties/xcal:url", "getURI"), + ] + + def __init__(self, url, doc, metadata): + EventCalendarResource.__init__(self, url, metadata) + self.doc = doc + + def getEvents(self): + + "Return a list of events from this resource." + + if self.events is None: + self.events = [] + + for event in self.doc.xpath("//xcal:vevent", namespaces=self.XCAL): + details = {} + + for property, path, converter in self.properties: + values = event.xpath(path, namespaces=self.XCAL) + + try: + value = getattr(self, converter)(property, values) + details[property] = value + except (IndexError, ValueError): + pass + + self.events.append(CalendarEvent(self, details)) + + return self.events + + def _getValue(self, values, type): + for element in values[0].xpath("xcal:%s" % type, namespaces=self.XCAL): + return element.textContent + else: + return None + + def getText(self, property, values): + return self._getValue(values, "text") + + def getDateTime(self, property, values): + element = values[0] + for dtelement in element.xpath("xcal:date-time|xcal:date", namespaces=self.XCAL): + dt = getDateTimeFromISO8601(dtelement.textContent) + break + else: + return None + + tzid = self._getValue(element.xpath("xcal:parameters", namespaces=self.XCAL), "tzid") + if tzid and isinstance(dt, DateTime): + zone = "/".join(tzid.rsplit("/", 2)[-2:]) + dt.set_time_zone(zone) + + if dtelement.localName == "date" and property == "end": + dt = dt.previous_day() + + return dt + + def getInteger(self, property, values): + value = self._getValue(values, "integer") + if value is not None: + return int(value) + else: + return None + + def getCollection(self, property, values): + return [n.textContent for n in values[0].xpath("xcal:text", namespaces=self.XCAL)] + + def getGeo(self, property, values): + geo = [None, None] + + for geoelement in values[0].xpath("xcal:latitude|xcal:longitude", namespaces=self.XCAL): + value = geoelement.textContent + if geoelement.localName == "latitude": + geo[0] = value + else: + geo[1] = value + + if None not in geo: + return map(getMapReferenceFromDecimal, geo) + else: + return None + + def getURI(self, property, values): + return self._getValue(values, "uri") + class EventPage: "An event page acting as an event resource." diff -r 7f5a76132995 -r a748da07fd84 parsers/xcalendar.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/parsers/xcalendar.py Tue Jan 28 23:31:17 2014 +0100 @@ -0,0 +1,83 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - xcalendar (EventAggregator) + + @copyright: 2012, 2013, 2014 by Paul Boddie + @license: GNU GPL (v2 or later), see COPYING.txt for details. +""" + +from MoinSupport import parseAttributes, RawParser +from EventAggregatorSupport.Formatting import formatEventsForOutputType, \ + formatEvent +from EventAggregatorSupport.Types import parseEventsInPage, EventPage, \ + parseEventsInXMLCalendar + +Dependencies = ["pages"] + +# Parser support. + +class Parser: + + "Interpret and show calendar information in different ways." + + Dependencies = Dependencies + extensions = [".xcs"] + + # Input content types understood by this parser. + + input_mimetypes = ["application/calendar+xml"] + + # Output content types preferred by this parser. + + output_mimetypes = ["text/html", "application/calendar+xml"] + + def __init__(self, raw, request, **kw): + + """ + Initialise the parser with the given 'raw' data, 'request' and any + keyword arguments that may have been supplied. + """ + + self.raw = raw + self.request = request + attrs = parseAttributes(kw.get("format_args", ""), False) + + self.fragment = attrs.get("fragment") + + def format(self, fmt, write=None): + + """ + Format a calendar using the given formatter 'fmt'. If the 'write' + parameter is specified, use it to write output; otherwise, write output + using the request. + """ + + for event in parseEventsInXMLCalendar(self.raw): + formatEvent(event, self.request, fmt, write=write, parser_cls=RawParser) + + # Extra API methods. + + def formatForOutputType(self, mimetype, write=None): + + """ + Format a calendar for the given 'mimetype'. If the 'write' parameter is + specified, use it to write output; otherwise, write output using the + request. + """ + + # Write raw calendar information unchanged. + + if mimetype == "application/calendar+xml": + (write or request.write)(self.raw) + else: + events = parseEventsInXMLCalendar(self.raw) + formatEventsForOutputType(events, self.request, mimetype, write=write) + + # Class methods. + + def getOutputTypes(self): + return self.output_mimetypes + + getOutputTypes = classmethod(getOutputTypes) + +# vim: tabstop=4 expandtab shiftwidth=4