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@437 | 82 | def formatMethod(resource, request, fmt, write=None): |
paul@437 | 83 | |
paul@437 | 84 | "Where the 'resource' has a method, offer iTIP actions." |
paul@437 | 85 | |
paul@437 | 86 | method = resource.getMethod() |
paul@437 | 87 | |
paul@437 | 88 | if method: |
paul@437 | 89 | _ = request.getText |
paul@437 | 90 | write = write or request.write |
paul@437 | 91 | write(fmt.paragraph(on=1)) |
paul@437 | 92 | write(fmt.span(on=1)) |
paul@437 | 93 | write(fmt.text(_("iTIP method"))) |
paul@437 | 94 | write(fmt.span(on=0)) |
paul@437 | 95 | write(" ") |
paul@437 | 96 | write(fmt.span(on=1)) |
paul@437 | 97 | write(fmt.text(method)) |
paul@437 | 98 | write(fmt.span(on=0)) |
paul@437 | 99 | write(fmt.paragraph(on=0)) |
paul@437 | 100 | |
paul@437 | 101 | def formatEventsForOutputType(events, request, mimetype, parent=None, descriptions=None, latest_timestamp=None, write=None, resource=None): |
paul@347 | 102 | |
paul@347 | 103 | """ |
paul@347 | 104 | Format the given 'events' using the 'request' for the given 'mimetype'. |
paul@347 | 105 | |
paul@347 | 106 | The optional 'parent' indicates the "natural" parent page of the events. Any |
paul@347 | 107 | event pages residing beneath the parent page will have their names |
paul@347 | 108 | reproduced as relative to the parent page. |
paul@347 | 109 | |
paul@347 | 110 | The optional 'descriptions' indicates the nature of any description given |
paul@347 | 111 | for events in the output resource. |
paul@347 | 112 | |
paul@347 | 113 | The optional 'latest_timestamp' indicates the timestamp of the latest edit |
paul@347 | 114 | of the page or event collection. |
paul@347 | 115 | |
paul@347 | 116 | If the 'write' parameter is specified, use it to write output. |
paul@437 | 117 | |
paul@437 | 118 | If the 'resource' parameter is specified, use it to write any resource-level |
paul@437 | 119 | information such as iTIP method information. |
paul@347 | 120 | """ |
paul@347 | 121 | |
paul@347 | 122 | write = write or request.write |
paul@347 | 123 | |
paul@347 | 124 | # Start the collection. |
paul@347 | 125 | |
paul@347 | 126 | if mimetype == "text/calendar" and vCalendar is not None: |
paul@364 | 127 | _write = vCalendar.iterwrite(write=write).write |
paul@364 | 128 | _write("BEGIN", {}, "VCALENDAR") |
paul@364 | 129 | _write("PRODID", {}, "-//MoinMoin//EventAggregatorSummary") |
paul@364 | 130 | _write("VERSION", {}, "2.0") |
paul@347 | 131 | |
paul@347 | 132 | elif mimetype == "application/rss+xml": |
paul@347 | 133 | |
paul@347 | 134 | # Using the page name and the page URL in the title, link and |
paul@347 | 135 | # description. |
paul@347 | 136 | |
paul@347 | 137 | path_info = getPathInfo(request) |
paul@347 | 138 | |
paul@365 | 139 | write('<rss version="2.0">\n') |
paul@365 | 140 | write('<channel>\n') |
paul@365 | 141 | write('<title>%s</title>\n' % path_info[1:]) |
paul@365 | 142 | write('<link>%s%s</link>\n' % (request.getBaseURL(), path_info)) |
paul@365 | 143 | write('<description>Events published on %s%s</description>\n' % (request.getBaseURL(), path_info)) |
paul@347 | 144 | |
paul@347 | 145 | if latest_timestamp is not None: |
paul@365 | 146 | write('<lastBuildDate>%s</lastBuildDate>\n' % latest_timestamp.as_HTTP_datetime_string()) |
paul@347 | 147 | |
paul@347 | 148 | # Sort the events by start date, reversed. |
paul@347 | 149 | |
paul@347 | 150 | ordered_events = getOrderedEvents(events) |
paul@347 | 151 | ordered_events.reverse() |
paul@347 | 152 | events = ordered_events |
paul@347 | 153 | |
paul@347 | 154 | elif mimetype == "text/html": |
paul@347 | 155 | write('<html>') |
paul@347 | 156 | write('<body>') |
paul@347 | 157 | |
paul@347 | 158 | # Output the collection one by one. |
paul@347 | 159 | |
paul@347 | 160 | for event in events: |
paul@437 | 161 | formatEventForOutputType(event, request, mimetype, parent, descriptions, write, resource) |
paul@347 | 162 | |
paul@347 | 163 | # End the collection. |
paul@347 | 164 | |
paul@347 | 165 | if mimetype == "text/calendar" and vCalendar is not None: |
paul@364 | 166 | _write("END", {}, "VCALENDAR") |
paul@347 | 167 | |
paul@347 | 168 | elif mimetype == "application/rss+xml": |
paul@365 | 169 | write('</channel>\n') |
paul@365 | 170 | write('</rss>\n') |
paul@347 | 171 | |
paul@347 | 172 | elif mimetype == "text/html": |
paul@347 | 173 | write('</body>') |
paul@347 | 174 | write('</html>') |
paul@347 | 175 | |
paul@437 | 176 | def formatEventForOutputType(event, request, mimetype, parent=None, descriptions=None, write=None, resource=None): |
paul@347 | 177 | |
paul@347 | 178 | """ |
paul@347 | 179 | Format the given 'event' using the 'request' for the given 'mimetype'. |
paul@347 | 180 | |
paul@347 | 181 | The optional 'parent' indicates the "natural" parent page of the events. Any |
paul@347 | 182 | event pages residing beneath the parent page will have their names |
paul@347 | 183 | reproduced as relative to the parent page. |
paul@347 | 184 | |
paul@347 | 185 | The optional 'descriptions' indicates the nature of any description given |
paul@347 | 186 | for events in the output resource. |
paul@347 | 187 | |
paul@347 | 188 | If the 'write' parameter is specified, use it to write output. |
paul@437 | 189 | |
paul@437 | 190 | If the 'resource' parameter is specified, use it to write any resource-level |
paul@437 | 191 | information such as iTIP method information. |
paul@347 | 192 | """ |
paul@347 | 193 | |
paul@347 | 194 | write = write or request.write |
paul@347 | 195 | event_details = event.getDetails() |
paul@347 | 196 | event_metadata = event.getMetadata() |
paul@347 | 197 | |
paul@347 | 198 | if mimetype == "text/calendar" and vCalendar is not None: |
paul@347 | 199 | |
paul@347 | 200 | # NOTE: A custom formatter making attributes for links and plain |
paul@347 | 201 | # NOTE: text for values could be employed here. |
paul@347 | 202 | |
paul@364 | 203 | _write = vCalendar.iterwrite(write=write).write |
paul@347 | 204 | |
paul@347 | 205 | # Get the summary details. |
paul@347 | 206 | |
paul@347 | 207 | event_summary = event.getSummary(parent) |
paul@347 | 208 | link = event.getEventURL() |
paul@347 | 209 | |
paul@347 | 210 | # Output the event details. |
paul@347 | 211 | |
paul@364 | 212 | _write("BEGIN", {}, "VEVENT") |
paul@364 | 213 | _write("UID", {}, link) |
paul@364 | 214 | _write("URL", {}, link) |
paul@364 | 215 | _write("DTSTAMP", {}, "%04d%02d%02dT%02d%02d%02dZ" % event_metadata["created"].as_tuple()[:6]) |
paul@364 | 216 | _write("LAST-MODIFIED", {}, "%04d%02d%02dT%02d%02d%02dZ" % event_metadata["last-modified"].as_tuple()[:6]) |
paul@364 | 217 | _write("SEQUENCE", {}, "%d" % event_metadata["sequence"]) |
paul@347 | 218 | |
paul@347 | 219 | start = event_details["start"] |
paul@347 | 220 | end = event_details["end"] |
paul@347 | 221 | |
paul@347 | 222 | if isinstance(start, DateTime): |
paul@347 | 223 | params, value = getCalendarDateTime(start) |
paul@347 | 224 | else: |
paul@347 | 225 | params, value = {"VALUE" : "DATE"}, "%04d%02d%02d" % start.as_date().as_tuple() |
paul@364 | 226 | _write("DTSTART", params, value) |
paul@347 | 227 | |
paul@347 | 228 | if isinstance(end, DateTime): |
paul@347 | 229 | params, value = getCalendarDateTime(end) |
paul@347 | 230 | else: |
paul@347 | 231 | params, value = {"VALUE" : "DATE"}, "%04d%02d%02d" % end.next_day().as_date().as_tuple() |
paul@364 | 232 | _write("DTEND", params, value) |
paul@347 | 233 | |
paul@364 | 234 | _write("SUMMARY", {}, event_summary) |
paul@347 | 235 | |
paul@347 | 236 | # Optional details. |
paul@347 | 237 | |
paul@347 | 238 | if event_details.get("topics") or event_details.get("categories"): |
paul@364 | 239 | _write("CATEGORIES", {}, event_details.get("topics") or event_details.get("categories")) |
paul@347 | 240 | if event_details.has_key("location"): |
paul@364 | 241 | _write("LOCATION", {}, event_details["location"]) |
paul@347 | 242 | if event_details.has_key("geo"): |
paul@364 | 243 | _write("GEO", {}, tuple([str(ref.to_degrees()) for ref in event_details["geo"]])) |
paul@347 | 244 | |
paul@364 | 245 | _write("END", {}, "VEVENT") |
paul@347 | 246 | |
paul@347 | 247 | elif mimetype == "application/rss+xml": |
paul@347 | 248 | |
paul@347 | 249 | event_page = event.getPage() |
paul@347 | 250 | event_details = event.getDetails() |
paul@347 | 251 | |
paul@347 | 252 | # Get a parser and formatter for the formatting of some attributes. |
paul@347 | 253 | |
paul@347 | 254 | fmt = request.html_formatter |
paul@347 | 255 | |
paul@347 | 256 | # Get the summary details. |
paul@347 | 257 | |
paul@347 | 258 | event_summary = event.getSummary(parent) |
paul@347 | 259 | link = event.getEventURL() |
paul@347 | 260 | |
paul@365 | 261 | write('<item>\n') |
paul@365 | 262 | write('<title>%s</title>\n' % escape(event_summary)) |
paul@365 | 263 | write('<link>%s</link>\n' % link) |
paul@347 | 264 | |
paul@347 | 265 | # Write a description according to the preferred source of |
paul@347 | 266 | # descriptions. |
paul@347 | 267 | |
paul@347 | 268 | if descriptions == "page": |
paul@347 | 269 | description = event_details.get("description", "") |
paul@347 | 270 | else: |
paul@347 | 271 | description = event_metadata["last-comment"] |
paul@347 | 272 | |
paul@365 | 273 | write('<description>%s</description>\n' % |
paul@347 | 274 | fmt.text(event_page.formatText(description, fmt))) |
paul@347 | 275 | |
paul@347 | 276 | for topic in event_details.get("topics") or event_details.get("categories") or []: |
paul@365 | 277 | write('<category>%s</category>\n' % |
paul@347 | 278 | fmt.text(event_page.formatText(topic, fmt))) |
paul@347 | 279 | |
paul@365 | 280 | write('<pubDate>%s</pubDate>\n' % event_metadata["created"].as_HTTP_datetime_string()) |
paul@365 | 281 | write('<guid>%s#%s</guid>\n' % (link, event_metadata["sequence"])) |
paul@365 | 282 | write('</item>\n') |
paul@347 | 283 | |
paul@347 | 284 | elif mimetype == "text/html": |
paul@347 | 285 | fmt = request.html_formatter |
paul@347 | 286 | fmt.setPage(request.page) |
paul@347 | 287 | formatEvent(event, request, fmt, write=write) |
paul@437 | 288 | if resource: |
paul@437 | 289 | formatMethod(resource, request, fmt, write) |
paul@347 | 290 | |
paul@347 | 291 | # iCalendar format helper functions. |
paul@347 | 292 | |
paul@347 | 293 | def getCalendarDateTime(datetime): |
paul@347 | 294 | |
paul@347 | 295 | """ |
paul@347 | 296 | Write to the given 'request' the 'datetime' using appropriate time zone |
paul@347 | 297 | information. |
paul@347 | 298 | """ |
paul@347 | 299 | |
paul@347 | 300 | utc_datetime = datetime.to_utc() |
paul@347 | 301 | if utc_datetime: |
paul@347 | 302 | return {"VALUE" : "DATE-TIME"}, "%04d%02d%02dT%02d%02d%02dZ" % utc_datetime.padded().as_tuple()[:-1] |
paul@347 | 303 | else: |
paul@347 | 304 | zone = datetime.time_zone() |
paul@347 | 305 | params = {"VALUE" : "DATE-TIME"} |
paul@347 | 306 | if zone: |
paul@347 | 307 | params["TZID"] = zone |
paul@347 | 308 | return params, "%04d%02d%02dT%02d%02d%02d" % datetime.padded().as_tuple()[:-1] |
paul@347 | 309 | |
paul@347 | 310 | # Helper functions. |
paul@347 | 311 | |
paul@347 | 312 | def getOrderedEvents(events): |
paul@347 | 313 | |
paul@347 | 314 | """ |
paul@347 | 315 | Return a list with the given 'events' ordered according to their start and |
paul@347 | 316 | end dates. |
paul@347 | 317 | """ |
paul@347 | 318 | |
paul@347 | 319 | ordered_events = events[:] |
paul@347 | 320 | ordered_events.sort() |
paul@347 | 321 | return ordered_events |
paul@347 | 322 | |
paul@347 | 323 | # vim: tabstop=4 expandtab shiftwidth=4 |