1.1 --- a/EventAggregatorSupport.py Mon Jun 04 21:07:26 2012 +0200
1.2 +++ b/EventAggregatorSupport.py Mon Jun 18 01:09:14 2012 +0200
1.3 @@ -323,6 +323,73 @@
1.4
1.5 return [Page(request, page.page_name) for page in result_pages]
1.6
1.7 +# Event parsing from page texts.
1.8 +
1.9 +def parseEvents(text, page):
1.10 +
1.11 + """
1.12 + Parse events in the given 'text', returning a list of event objects for the
1.13 + given 'page'.
1.14 + """
1.15 +
1.16 + details = {}
1.17 + events = [Event(page, details)]
1.18 +
1.19 + for match in definition_list_regexp.finditer(text):
1.20 +
1.21 + # Skip commented-out items.
1.22 +
1.23 + if match.group("optcomment"):
1.24 + continue
1.25 +
1.26 + # Permit case-insensitive list terms.
1.27 +
1.28 + term = match.group("term").lower()
1.29 + desc = match.group("desc")
1.30 +
1.31 + # Special value type handling.
1.32 +
1.33 + # Dates.
1.34 +
1.35 + if term in ("start", "end"):
1.36 + desc = getDateTime(desc)
1.37 +
1.38 + # Lists (whose elements may be quoted).
1.39 +
1.40 + elif term in ("topics", "categories"):
1.41 + desc = map(getSimpleWikiText, to_list(desc, ","))
1.42 +
1.43 + # Position details.
1.44 +
1.45 + elif term == "geo":
1.46 + try:
1.47 + desc = map(getMapReference, to_list(desc, None))
1.48 + if len(desc) != 2:
1.49 + continue
1.50 + except (KeyError, ValueError):
1.51 + continue
1.52 +
1.53 + # Labels which may well be quoted.
1.54 +
1.55 + elif term in ("title", "summary", "description", "location"):
1.56 + desc = getSimpleWikiText(desc.strip())
1.57 +
1.58 + if desc is not None:
1.59 +
1.60 + # Handle apparent duplicates by creating a new set of
1.61 + # details.
1.62 +
1.63 + if details.has_key(term):
1.64 +
1.65 + # Make a new event.
1.66 +
1.67 + details = {}
1.68 + events.append(Event(page, details))
1.69 +
1.70 + details[term] = desc
1.71 +
1.72 + return events
1.73 +
1.74 # Event resources providing collections of events.
1.75
1.76 class EventResource:
1.77 @@ -535,62 +602,10 @@
1.78 "Return a list of events from this page."
1.79
1.80 if self.events is None:
1.81 - details = {}
1.82 - self.events = [Event(self, details)]
1.83 -
1.84 if self.getFormat() == "wiki":
1.85 - for match in definition_list_regexp.finditer(self.getBody()):
1.86 -
1.87 - # Skip commented-out items.
1.88 -
1.89 - if match.group("optcomment"):
1.90 - continue
1.91 -
1.92 - # Permit case-insensitive list terms.
1.93 -
1.94 - term = match.group("term").lower()
1.95 - desc = match.group("desc")
1.96 -
1.97 - # Special value type handling.
1.98 -
1.99 - # Dates.
1.100 -
1.101 - if term in ("start", "end"):
1.102 - desc = getDateTime(desc)
1.103 -
1.104 - # Lists (whose elements may be quoted).
1.105 -
1.106 - elif term in ("topics", "categories"):
1.107 - desc = map(getSimpleWikiText, to_list(desc, ","))
1.108 -
1.109 - # Position details.
1.110 -
1.111 - elif term == "geo":
1.112 - try:
1.113 - desc = map(getMapReference, to_list(desc, None))
1.114 - if len(desc) != 2:
1.115 - continue
1.116 - except (KeyError, ValueError):
1.117 - continue
1.118 -
1.119 - # Labels which may well be quoted.
1.120 -
1.121 - elif term in ("title", "summary", "description", "location"):
1.122 - desc = getSimpleWikiText(desc.strip())
1.123 -
1.124 - if desc is not None:
1.125 -
1.126 - # Handle apparent duplicates by creating a new set of
1.127 - # details.
1.128 -
1.129 - if details.has_key(term):
1.130 -
1.131 - # Make a new event.
1.132 -
1.133 - details = {}
1.134 - self.events.append(Event(self, details))
1.135 -
1.136 - details[term] = desc
1.137 + self.events = parseEvents(self.getBody(), self)
1.138 + else:
1.139 + self.events = []
1.140
1.141 return self.events
1.142
1.143 @@ -1676,8 +1691,10 @@
1.144 self.calendar_start, self.calendar_end
1.145 )
1.146
1.147 - return "action=EventAggregatorNewEvent&%s&%s&template=%s&parent=%s&%s" % (
1.148 - args, self.category_name_parameters, self.template_name, self.parent_name or "",
1.149 + return "action=EventAggregatorNewEvent%s%s&template=%s&parent=%s&%s" % (
1.150 + args and "&%s" % args,
1.151 + self.category_name_parameters and "&%s" % self.category_name_parameters,
1.152 + self.template_name, self.parent_name or "",
1.153 navigation_link)
1.154
1.155 def getFullDateLabel(self, date):
1.156 @@ -1747,11 +1764,11 @@
1.157
1.158 # Generate the links.
1.159
1.160 - download_dialogue_link = "action=EventAggregatorSummary&parent=%s&resolution=%s&%s&%s" % (
1.161 + download_dialogue_link = "action=EventAggregatorSummary&parent=%s&resolution=%s%s%s" % (
1.162 self.parent_name or "",
1.163 self.resolution,
1.164 - self.category_name_parameters,
1.165 - self.remote_source_parameters
1.166 + self.category_name_parameters and "&%s" % self.category_name_parameters,
1.167 + self.remote_source_parameters and "&%s" % self.remote_source_parameters
1.168 )
1.169 download_all_link = download_dialogue_link + "&doit=1"
1.170 download_link = download_all_link + ("&%s&%s" % (
1.171 @@ -3362,4 +3379,191 @@
1.172 </script>
1.173 """
1.174
1.175 +# Event-only formatting.
1.176 +
1.177 +def formatEventsForOutputType(events, request, mimetype, parent=None, descriptions=None, latest_timestamp=None):
1.178 +
1.179 + """
1.180 + Format the given 'events' using the 'request' for the given 'mimetype'.
1.181 +
1.182 + The optional 'parent' indicates the "natural" parent page of the events. Any
1.183 + event pages residing beneath the parent page will have their names
1.184 + reproduced as relative to the parent page.
1.185 +
1.186 + The optional 'descriptions' indicates the nature of any description given
1.187 + for events in the output resource.
1.188 +
1.189 + The optional 'latest_timestamp' indicates the timestamp of the latest edit
1.190 + of the page or event collection.
1.191 + """
1.192 +
1.193 + # Start the collection.
1.194 +
1.195 + if mimetype == "text/calendar":
1.196 + request.write("BEGIN:VCALENDAR\r\n")
1.197 + request.write("PRODID:-//MoinMoin//EventAggregatorSummary\r\n")
1.198 + request.write("VERSION:2.0\r\n")
1.199 +
1.200 + elif mimetype == "application/rss+xml":
1.201 +
1.202 + # Using the page name and the page URL in the title, link and
1.203 + # description.
1.204 +
1.205 + path_info = getPathInfo(request)
1.206 +
1.207 + request.write('<rss version="2.0">\r\n')
1.208 + request.write('<channel>\r\n')
1.209 + request.write('<title>%s</title>\r\n' % path_info[1:])
1.210 + request.write('<link>%s%s</link>\r\n' % (request.getBaseURL(), path_info))
1.211 + request.write('<description>Events published on %s%s</description>\r\n' % (request.getBaseURL(), path_info))
1.212 +
1.213 + if latest_timestamp is not None:
1.214 + request.write('<lastBuildDate>%s</lastBuildDate>\r\n' % latest_timestamp.as_HTTP_datetime_string())
1.215 +
1.216 + # Sort the events by start date, reversed.
1.217 +
1.218 + ordered_events = getOrderedEvents(events)
1.219 + ordered_events.reverse()
1.220 + events = ordered_events
1.221 +
1.222 + # Output the collection one by one.
1.223 +
1.224 + for event in events:
1.225 + formatEventForOutputType(event, request, mimetype, parent, descriptions)
1.226 +
1.227 + # End the collection.
1.228 +
1.229 + if mimetype == "text/calendar":
1.230 + request.write("END:VCALENDAR\r\n")
1.231 +
1.232 + elif mimetype == "application/rss+xml":
1.233 + request.write('</channel>\r\n')
1.234 + request.write('</rss>\r\n')
1.235 +
1.236 +def formatEventForOutputType(event, request, mimetype, parent=None, descriptions=None):
1.237 +
1.238 + """
1.239 + Format the given 'event' using the 'request' for the given 'mimetype'.
1.240 +
1.241 + The optional 'parent' indicates the "natural" parent page of the events. Any
1.242 + event pages residing beneath the parent page will have their names
1.243 + reproduced as relative to the parent page.
1.244 +
1.245 + The optional 'descriptions' indicates the nature of any description given
1.246 + for events in the output resource.
1.247 + """
1.248 +
1.249 + event_details = event.getDetails()
1.250 +
1.251 + if mimetype == "text/calendar":
1.252 +
1.253 + # NOTE: A custom formatter making attributes for links and plain
1.254 + # NOTE: text for values could be employed here.
1.255 +
1.256 + # Get the summary details.
1.257 +
1.258 + event_summary = event.getSummary(parent)
1.259 + link = event.getEventURL()
1.260 +
1.261 + # Output the event details.
1.262 +
1.263 + request.write("BEGIN:VEVENT\r\n")
1.264 + request.write("UID:%s\r\n" % link)
1.265 + request.write("URL:%s\r\n" % link)
1.266 + request.write("DTSTAMP:%04d%02d%02dT%02d%02d%02dZ\r\n" % event_details["created"].as_tuple()[:6])
1.267 + request.write("LAST-MODIFIED:%04d%02d%02dT%02d%02d%02dZ\r\n" % event_details["last-modified"].as_tuple()[:6])
1.268 + request.write("SEQUENCE:%d\r\n" % event_details["sequence"])
1.269 +
1.270 + start = event_details["start"]
1.271 + end = event_details["end"]
1.272 +
1.273 + if isinstance(start, DateTime):
1.274 + request.write("DTSTART")
1.275 + write_calendar_datetime(request, start)
1.276 + else:
1.277 + request.write("DTSTART;VALUE=DATE:%04d%02d%02d\r\n" % start.as_date().as_tuple())
1.278 +
1.279 + if isinstance(end, DateTime):
1.280 + request.write("DTEND")
1.281 + write_calendar_datetime(request, end)
1.282 + else:
1.283 + request.write("DTEND;VALUE=DATE:%04d%02d%02d\r\n" % end.next_day().as_date().as_tuple())
1.284 +
1.285 + request.write("SUMMARY:%s\r\n" % getQuotedText(event_summary))
1.286 +
1.287 + # Optional details.
1.288 +
1.289 + if event_details.get("topics") or event_details.get("categories"):
1.290 + request.write("CATEGORIES:%s\r\n" % ",".join(
1.291 + [getQuotedText(topic)
1.292 + for topic in event_details.get("topics") or event_details.get("categories")]
1.293 + ))
1.294 + if event_details.has_key("location"):
1.295 + request.write("LOCATION:%s\r\n" % getQuotedText(event_details["location"]))
1.296 + if event_details.has_key("geo"):
1.297 + request.write("GEO:%s\r\n" % getQuotedText(";".join([str(ref.to_degrees()) for ref in event_details["geo"]])))
1.298 +
1.299 + request.write("END:VEVENT\r\n")
1.300 +
1.301 + elif mimetype == "application/rss+xml":
1.302 +
1.303 + event_page = event.getPage()
1.304 + event_details = event.getDetails()
1.305 +
1.306 + # Get a parser and formatter for the formatting of some attributes.
1.307 +
1.308 + fmt = request.html_formatter
1.309 +
1.310 + # Get the summary details.
1.311 +
1.312 + event_summary = event.getSummary(parent)
1.313 + link = event.getEventURL()
1.314 +
1.315 + request.write('<item>\r\n')
1.316 + request.write('<title>%s</title>\r\n' % wikiutil.escape(event_summary))
1.317 + request.write('<link>%s</link>\r\n' % link)
1.318 +
1.319 + # Write a description according to the preferred source of
1.320 + # descriptions.
1.321 +
1.322 + if descriptions == "page":
1.323 + description = event_details.get("description", "")
1.324 + else:
1.325 + description = event_details["last-comment"]
1.326 +
1.327 + request.write('<description>%s</description>\r\n' %
1.328 + fmt.text(event_page.formatText(description, fmt)))
1.329 +
1.330 + for topic in event_details.get("topics") or event_details.get("categories") or []:
1.331 + request.write('<category>%s</category>\r\n' %
1.332 + fmt.text(event_page.formatText(topic, fmt)))
1.333 +
1.334 + request.write('<pubDate>%s</pubDate>\r\n' % event_details["created"].as_HTTP_datetime_string())
1.335 + request.write('<guid>%s#%s</guid>\r\n' % (link, event_details["sequence"]))
1.336 + request.write('</item>\r\n')
1.337 +
1.338 +# iCalendar format helper functions.
1.339 +
1.340 +def write_calendar_datetime(request, datetime):
1.341 +
1.342 + """
1.343 + Write to the given 'request' the 'datetime' using appropriate time zone
1.344 + information.
1.345 + """
1.346 +
1.347 + utc_datetime = datetime.to_utc()
1.348 + if utc_datetime:
1.349 + request.write(";VALUE=DATE-TIME:%04d%02d%02dT%02d%02d%02dZ\r\n" % utc_datetime.padded().as_tuple()[:-1])
1.350 + else:
1.351 + zone = datetime.time_zone()
1.352 + if zone:
1.353 + request.write(";TZID=/%s" % zone)
1.354 + request.write(";VALUE=DATE-TIME:%04d%02d%02dT%02d%02d%02d\r\n" % datetime.padded().as_tuple()[:-1])
1.355 +
1.356 +def getQuotedText(text):
1.357 +
1.358 + "Return the 'text' quoted for iCalendar purposes."
1.359 +
1.360 + return text.replace(";", r"\;").replace(",", r"\,")
1.361 +
1.362 # vim: tabstop=4 expandtab shiftwidth=4