paul@347 | 1 | # -*- coding: iso-8859-1 -*- |
paul@347 | 2 | """ |
paul@347 | 3 | MoinMoin - EventAggregator event formatting |
paul@347 | 4 | |
paul@393 | 5 | @copyright: 2008, 2009, 2010, 2011, 2012, 2013, 2014 by Paul Boddie <paul@boddie.org.uk> |
paul@347 | 6 | @copyright: 2000-2004 Juergen Hermann <jh@web.de>, |
paul@347 | 7 | 2005-2008 MoinMoin:ThomasWaldmann. |
paul@347 | 8 | @license: GNU GPL (v2 or later), see COPYING.txt for details. |
paul@347 | 9 | """ |
paul@347 | 10 | |
paul@347 | 11 | from MoinSupport import * |
paul@347 | 12 | from MoinMoin.wikiutil import escape |
paul@347 | 13 | |
paul@347 | 14 | try: |
paul@347 | 15 | import vCalendar |
paul@347 | 16 | except ImportError: |
paul@347 | 17 | vCalendar = None |
paul@347 | 18 | |
paul@347 | 19 | # Event-only formatting. |
paul@347 | 20 | |
paul@393 | 21 | def formatEvent(event, request, fmt, write=None, parser_cls=None): |
paul@347 | 22 | |
paul@347 | 23 | """ |
paul@347 | 24 | Format the given 'event' using the 'request' and formatter 'fmt'. If the |
paul@347 | 25 | 'write' parameter is specified, use it to write output. |
paul@393 | 26 | |
paul@393 | 27 | Where 'parser_cls' is specified, override the parser used to format text. |
paul@393 | 28 | This is essential when dealing with calendar format pages since the page |
paul@393 | 29 | parser will be unable to handle arbitrary fragments of text. |
paul@347 | 30 | """ |
paul@347 | 31 | |
paul@347 | 32 | details = event.getDetails() |
paul@347 | 33 | raw_details = event.getRawDetails() |
paul@347 | 34 | write = write or request.write |
paul@347 | 35 | |
paul@347 | 36 | if details.has_key("fragment"): |
paul@347 | 37 | write(fmt.anchordef(details["fragment"])) |
paul@347 | 38 | |
paul@347 | 39 | # Promote any title to a heading above the event details. |
paul@347 | 40 | |
paul@347 | 41 | if raw_details.has_key("title"): |
paul@393 | 42 | write(formatText(raw_details["title"], request, fmt, parser_cls=parser_cls)) |
paul@347 | 43 | elif details.has_key("title"): |
paul@347 | 44 | write(fmt.heading(on=1, depth=1)) |
paul@347 | 45 | write(fmt.text(details["title"])) |
paul@347 | 46 | write(fmt.heading(on=0, depth=1)) |
paul@347 | 47 | |
paul@347 | 48 | # Produce a definition list for the rest of the details. |
paul@347 | 49 | |
paul@347 | 50 | write(fmt.definition_list(on=1)) |
paul@347 | 51 | |
paul@347 | 52 | for term in event.all_terms: |
paul@347 | 53 | if term == "title": |
paul@347 | 54 | continue |
paul@347 | 55 | |
paul@347 | 56 | raw_value = raw_details.get(term) |
paul@347 | 57 | value = details.get(term) |
paul@347 | 58 | |
paul@347 | 59 | if raw_value or value: |
paul@347 | 60 | write(fmt.definition_term(on=1)) |
paul@347 | 61 | write(fmt.text(term)) |
paul@347 | 62 | write(fmt.definition_term(on=0)) |
paul@347 | 63 | write(fmt.definition_desc(on=1)) |
paul@347 | 64 | |
paul@347 | 65 | # Try and use the raw details, if available. |
paul@347 | 66 | |
paul@347 | 67 | if raw_value: |
paul@393 | 68 | write(formatText(raw_value, request, fmt, parser_cls=parser_cls)) |
paul@347 | 69 | |
paul@347 | 70 | # Otherwise, format the processed details. |
paul@347 | 71 | |
paul@347 | 72 | else: |
paul@347 | 73 | if term in event.list_terms: |
paul@393 | 74 | write(", ".join([formatText(unicode(v), request, fmt, parser_cls=parser_cls) for v in value])) |
paul@347 | 75 | else: |
paul@366 | 76 | write(fmt.text(unicode(value))) |
paul@347 | 77 | |
paul@347 | 78 | write(fmt.definition_desc(on=0)) |
paul@347 | 79 | |
paul@347 | 80 | write(fmt.definition_list(on=0)) |
paul@347 | 81 | |
paul@347 | 82 | def formatEventsForOutputType(events, request, mimetype, parent=None, descriptions=None, latest_timestamp=None, write=None): |
paul@347 | 83 | |
paul@347 | 84 | """ |
paul@347 | 85 | Format the given 'events' using the 'request' for the given 'mimetype'. |
paul@347 | 86 | |
paul@347 | 87 | The optional 'parent' indicates the "natural" parent page of the events. Any |
paul@347 | 88 | event pages residing beneath the parent page will have their names |
paul@347 | 89 | reproduced as relative to the parent page. |
paul@347 | 90 | |
paul@347 | 91 | The optional 'descriptions' indicates the nature of any description given |
paul@347 | 92 | for events in the output resource. |
paul@347 | 93 | |
paul@347 | 94 | The optional 'latest_timestamp' indicates the timestamp of the latest edit |
paul@347 | 95 | of the page or event collection. |
paul@347 | 96 | |
paul@347 | 97 | If the 'write' parameter is specified, use it to write output. |
paul@347 | 98 | """ |
paul@347 | 99 | |
paul@347 | 100 | write = write or request.write |
paul@347 | 101 | |
paul@347 | 102 | # Start the collection. |
paul@347 | 103 | |
paul@347 | 104 | if mimetype == "text/calendar" and vCalendar is not None: |
paul@364 | 105 | _write = vCalendar.iterwrite(write=write).write |
paul@364 | 106 | _write("BEGIN", {}, "VCALENDAR") |
paul@364 | 107 | _write("PRODID", {}, "-//MoinMoin//EventAggregatorSummary") |
paul@364 | 108 | _write("VERSION", {}, "2.0") |
paul@347 | 109 | |
paul@347 | 110 | elif mimetype == "application/rss+xml": |
paul@347 | 111 | |
paul@347 | 112 | # Using the page name and the page URL in the title, link and |
paul@347 | 113 | # description. |
paul@347 | 114 | |
paul@347 | 115 | path_info = getPathInfo(request) |
paul@347 | 116 | |
paul@365 | 117 | write('<rss version="2.0">\n') |
paul@365 | 118 | write('<channel>\n') |
paul@365 | 119 | write('<title>%s</title>\n' % path_info[1:]) |
paul@365 | 120 | write('<link>%s%s</link>\n' % (request.getBaseURL(), path_info)) |
paul@365 | 121 | write('<description>Events published on %s%s</description>\n' % (request.getBaseURL(), path_info)) |
paul@347 | 122 | |
paul@347 | 123 | if latest_timestamp is not None: |
paul@365 | 124 | write('<lastBuildDate>%s</lastBuildDate>\n' % latest_timestamp.as_HTTP_datetime_string()) |
paul@347 | 125 | |
paul@347 | 126 | # Sort the events by start date, reversed. |
paul@347 | 127 | |
paul@347 | 128 | ordered_events = getOrderedEvents(events) |
paul@347 | 129 | ordered_events.reverse() |
paul@347 | 130 | events = ordered_events |
paul@347 | 131 | |
paul@347 | 132 | elif mimetype == "text/html": |
paul@347 | 133 | write('<html>') |
paul@347 | 134 | write('<body>') |
paul@347 | 135 | |
paul@347 | 136 | # Output the collection one by one. |
paul@347 | 137 | |
paul@347 | 138 | for event in events: |
paul@360 | 139 | formatEventForOutputType(event, request, mimetype, parent, descriptions, write) |
paul@347 | 140 | |
paul@347 | 141 | # End the collection. |
paul@347 | 142 | |
paul@347 | 143 | if mimetype == "text/calendar" and vCalendar is not None: |
paul@364 | 144 | _write("END", {}, "VCALENDAR") |
paul@347 | 145 | |
paul@347 | 146 | elif mimetype == "application/rss+xml": |
paul@365 | 147 | write('</channel>\n') |
paul@365 | 148 | write('</rss>\n') |
paul@347 | 149 | |
paul@347 | 150 | elif mimetype == "text/html": |
paul@347 | 151 | write('</body>') |
paul@347 | 152 | write('</html>') |
paul@347 | 153 | |
paul@347 | 154 | def formatEventForOutputType(event, request, mimetype, parent=None, descriptions=None, write=None): |
paul@347 | 155 | |
paul@347 | 156 | """ |
paul@347 | 157 | Format the given 'event' using the 'request' for the given 'mimetype'. |
paul@347 | 158 | |
paul@347 | 159 | The optional 'parent' indicates the "natural" parent page of the events. Any |
paul@347 | 160 | event pages residing beneath the parent page will have their names |
paul@347 | 161 | reproduced as relative to the parent page. |
paul@347 | 162 | |
paul@347 | 163 | The optional 'descriptions' indicates the nature of any description given |
paul@347 | 164 | for events in the output resource. |
paul@347 | 165 | |
paul@347 | 166 | If the 'write' parameter is specified, use it to write output. |
paul@347 | 167 | """ |
paul@347 | 168 | |
paul@347 | 169 | write = write or request.write |
paul@347 | 170 | event_details = event.getDetails() |
paul@347 | 171 | event_metadata = event.getMetadata() |
paul@347 | 172 | |
paul@347 | 173 | if mimetype == "text/calendar" and vCalendar is not None: |
paul@347 | 174 | |
paul@347 | 175 | # NOTE: A custom formatter making attributes for links and plain |
paul@347 | 176 | # NOTE: text for values could be employed here. |
paul@347 | 177 | |
paul@364 | 178 | _write = vCalendar.iterwrite(write=write).write |
paul@347 | 179 | |
paul@347 | 180 | # Get the summary details. |
paul@347 | 181 | |
paul@347 | 182 | event_summary = event.getSummary(parent) |
paul@347 | 183 | link = event.getEventURL() |
paul@347 | 184 | |
paul@347 | 185 | # Output the event details. |
paul@347 | 186 | |
paul@364 | 187 | _write("BEGIN", {}, "VEVENT") |
paul@364 | 188 | _write("UID", {}, link) |
paul@364 | 189 | _write("URL", {}, link) |
paul@364 | 190 | _write("DTSTAMP", {}, "%04d%02d%02dT%02d%02d%02dZ" % event_metadata["created"].as_tuple()[:6]) |
paul@364 | 191 | _write("LAST-MODIFIED", {}, "%04d%02d%02dT%02d%02d%02dZ" % event_metadata["last-modified"].as_tuple()[:6]) |
paul@364 | 192 | _write("SEQUENCE", {}, "%d" % event_metadata["sequence"]) |
paul@347 | 193 | |
paul@347 | 194 | start = event_details["start"] |
paul@347 | 195 | end = event_details["end"] |
paul@347 | 196 | |
paul@347 | 197 | if isinstance(start, DateTime): |
paul@347 | 198 | params, value = getCalendarDateTime(start) |
paul@347 | 199 | else: |
paul@347 | 200 | params, value = {"VALUE" : "DATE"}, "%04d%02d%02d" % start.as_date().as_tuple() |
paul@364 | 201 | _write("DTSTART", params, value) |
paul@347 | 202 | |
paul@347 | 203 | if isinstance(end, DateTime): |
paul@347 | 204 | params, value = getCalendarDateTime(end) |
paul@347 | 205 | else: |
paul@347 | 206 | params, value = {"VALUE" : "DATE"}, "%04d%02d%02d" % end.next_day().as_date().as_tuple() |
paul@364 | 207 | _write("DTEND", params, value) |
paul@347 | 208 | |
paul@364 | 209 | _write("SUMMARY", {}, event_summary) |
paul@347 | 210 | |
paul@347 | 211 | # Optional details. |
paul@347 | 212 | |
paul@347 | 213 | if event_details.get("topics") or event_details.get("categories"): |
paul@364 | 214 | _write("CATEGORIES", {}, event_details.get("topics") or event_details.get("categories")) |
paul@347 | 215 | if event_details.has_key("location"): |
paul@364 | 216 | _write("LOCATION", {}, event_details["location"]) |
paul@347 | 217 | if event_details.has_key("geo"): |
paul@364 | 218 | _write("GEO", {}, tuple([str(ref.to_degrees()) for ref in event_details["geo"]])) |
paul@347 | 219 | |
paul@364 | 220 | _write("END", {}, "VEVENT") |
paul@347 | 221 | |
paul@347 | 222 | elif mimetype == "application/rss+xml": |
paul@347 | 223 | |
paul@347 | 224 | event_page = event.getPage() |
paul@347 | 225 | event_details = event.getDetails() |
paul@347 | 226 | |
paul@347 | 227 | # Get a parser and formatter for the formatting of some attributes. |
paul@347 | 228 | |
paul@347 | 229 | fmt = request.html_formatter |
paul@347 | 230 | |
paul@347 | 231 | # Get the summary details. |
paul@347 | 232 | |
paul@347 | 233 | event_summary = event.getSummary(parent) |
paul@347 | 234 | link = event.getEventURL() |
paul@347 | 235 | |
paul@365 | 236 | write('<item>\n') |
paul@365 | 237 | write('<title>%s</title>\n' % escape(event_summary)) |
paul@365 | 238 | write('<link>%s</link>\n' % link) |
paul@347 | 239 | |
paul@347 | 240 | # Write a description according to the preferred source of |
paul@347 | 241 | # descriptions. |
paul@347 | 242 | |
paul@347 | 243 | if descriptions == "page": |
paul@347 | 244 | description = event_details.get("description", "") |
paul@347 | 245 | else: |
paul@347 | 246 | description = event_metadata["last-comment"] |
paul@347 | 247 | |
paul@365 | 248 | write('<description>%s</description>\n' % |
paul@347 | 249 | fmt.text(event_page.formatText(description, fmt))) |
paul@347 | 250 | |
paul@347 | 251 | for topic in event_details.get("topics") or event_details.get("categories") or []: |
paul@365 | 252 | write('<category>%s</category>\n' % |
paul@347 | 253 | fmt.text(event_page.formatText(topic, fmt))) |
paul@347 | 254 | |
paul@365 | 255 | write('<pubDate>%s</pubDate>\n' % event_metadata["created"].as_HTTP_datetime_string()) |
paul@365 | 256 | write('<guid>%s#%s</guid>\n' % (link, event_metadata["sequence"])) |
paul@365 | 257 | write('</item>\n') |
paul@347 | 258 | |
paul@347 | 259 | elif mimetype == "text/html": |
paul@347 | 260 | fmt = request.html_formatter |
paul@347 | 261 | fmt.setPage(request.page) |
paul@347 | 262 | formatEvent(event, request, fmt, write=write) |
paul@347 | 263 | |
paul@347 | 264 | # iCalendar format helper functions. |
paul@347 | 265 | |
paul@347 | 266 | def getCalendarDateTime(datetime): |
paul@347 | 267 | |
paul@347 | 268 | """ |
paul@347 | 269 | Write to the given 'request' the 'datetime' using appropriate time zone |
paul@347 | 270 | information. |
paul@347 | 271 | """ |
paul@347 | 272 | |
paul@347 | 273 | utc_datetime = datetime.to_utc() |
paul@347 | 274 | if utc_datetime: |
paul@347 | 275 | return {"VALUE" : "DATE-TIME"}, "%04d%02d%02dT%02d%02d%02dZ" % utc_datetime.padded().as_tuple()[:-1] |
paul@347 | 276 | else: |
paul@347 | 277 | zone = datetime.time_zone() |
paul@347 | 278 | params = {"VALUE" : "DATE-TIME"} |
paul@347 | 279 | if zone: |
paul@347 | 280 | params["TZID"] = zone |
paul@347 | 281 | return params, "%04d%02d%02dT%02d%02d%02d" % datetime.padded().as_tuple()[:-1] |
paul@347 | 282 | |
paul@347 | 283 | # Helper functions. |
paul@347 | 284 | |
paul@347 | 285 | def getOrderedEvents(events): |
paul@347 | 286 | |
paul@347 | 287 | """ |
paul@347 | 288 | Return a list with the given 'events' ordered according to their start and |
paul@347 | 289 | end dates. |
paul@347 | 290 | """ |
paul@347 | 291 | |
paul@347 | 292 | ordered_events = events[:] |
paul@347 | 293 | ordered_events.sort() |
paul@347 | 294 | return ordered_events |
paul@347 | 295 | |
paul@347 | 296 | # vim: tabstop=4 expandtab shiftwidth=4 |