1.1 --- a/macros/SharedContent.py Tue May 07 20:01:49 2013 +0200
1.2 +++ b/macros/SharedContent.py Tue May 07 20:03:45 2013 +0200
1.3 @@ -6,9 +6,11 @@
1.4 @license: GNU GPL (v2 or later), see COPYING.txt for details.
1.5 """
1.6
1.7 +from DateSupport import getDateTime, DateTime
1.8 from MoinMoin.Page import Page
1.9 from MoinRemoteSupport import *
1.10 from MoinSupport import parseMacroArguments
1.11 +from email.utils import parsedate
1.12 import xml.dom.pulldom
1.13
1.14 try:
1.15 @@ -21,6 +23,8 @@
1.16 MAX_ENTRIES = 5
1.17 ATOM_NS = "http://www.w3.org/2005/Atom"
1.18
1.19 +# Utility functions.
1.20 +
1.21 def text(element):
1.22 nodes = []
1.23 for node in element.childNodes:
1.24 @@ -37,64 +41,78 @@
1.25 else:
1.26 return element.getAttribute("href")
1.27
1.28 -def execute(macro, args):
1.29 - request = macro.request
1.30 - fmt = macro.formatter
1.31 - _ = request.getText
1.32 +# Error classes.
1.33 +
1.34 +class FeedError(Exception):
1.35 + pass
1.36 +
1.37 +class FeedMissingError(FeedError):
1.38 + pass
1.39
1.40 - feed_url = None
1.41 - show_content = None
1.42 - max_entries = None
1.43 +class FeedContentTypeError(FeedError):
1.44 + pass
1.45 +
1.46 +# Entry/update classes.
1.47 +
1.48 +class Update:
1.49 +
1.50 + "A feed update entry."
1.51
1.52 - for arg, value in parseMacroArguments(args):
1.53 - if arg == "url":
1.54 - feed_url = value
1.55 - elif arg == "show":
1.56 - show_content = value in ("true", "True", "yes")
1.57 - elif arg == "limit":
1.58 - try:
1.59 - max_entries = int(value)
1.60 - except ValueError:
1.61 - return fmt.text(_("SharedContent: limit must be set to the maximum number of entries to be shown"))
1.62 + def __init__(self):
1.63 + self.title = None
1.64 + self.link = None
1.65 + self.content = None
1.66 + self.content_type = None
1.67 + self.updated = None
1.68
1.69 - if not feed_url:
1.70 - return fmt.text(_("SharedContent: a feed URL must be specified"))
1.71 + def __cmp__(self, other):
1.72 + if self.updated is None and other.updated is not None:
1.73 + return 1
1.74 + elif self.updated is not None and other.updated is None:
1.75 + return -1
1.76 + else:
1.77 + return cmp(self.updated, other.updated)
1.78 +
1.79 +# Feed retrieval.
1.80
1.81 - show_content = show_content or False
1.82 - max_entries = max_entries or MAX_ENTRIES
1.83 +def getUpdates(request, feed_url, max_entries):
1.84 +
1.85 + """
1.86 + Using the given 'request', retrieve from 'feed_url' up to the given number
1.87 + 'max_entries' of update entries.
1.88 +
1.89 + A tuple of the form ((feed_type, channel_title, channel_link), updates) is
1.90 + returned.
1.91 + """
1.92 +
1.93 + feed_updates = []
1.94
1.95 # Obtain the resource, using a cached version if appropriate.
1.96
1.97 max_cache_age = int(getattr(request.cfg, "moin_share_max_cache_age", "300"))
1.98 data = getCachedResource(request, feed_url, "MoinShare", "wiki", max_cache_age)
1.99 if not data:
1.100 - return fmt.text(_("SharedContent: updates could not be retrieved for %s") % feed_url)
1.101 + raise FeedMissingError
1.102 +
1.103 + # Interpret the cached feed.
1.104
1.105 feed = StringIO(data)
1.106 -
1.107 _url, content_type, _encoding, _metadata = getCachedResourceMetadata(feed)
1.108
1.109 if content_type not in ("application/atom+xml", "application/rss+xml"):
1.110 - return fmt.text(_("SharedContent: updates for %s were not provided in Atom or RSS format") % feed_url)
1.111 + raise FeedContentTypeError
1.112
1.113 try:
1.114 # Parse each node from the feed.
1.115
1.116 - title = link = content = content_type = None
1.117 channel_title = channel_link = None
1.118
1.119 - output = []
1.120 - append = output.append
1.121 -
1.122 feed_type = None
1.123 - in_item = False
1.124 + update = None
1.125 nentries = 0
1.126
1.127 events = xml.dom.pulldom.parse(feed)
1.128
1.129 - if not show_content:
1.130 - append(fmt.bullet_list(on=1))
1.131 -
1.132 for event, value in events:
1.133
1.134 if event == xml.dom.pulldom.START_ELEMENT:
1.135 @@ -113,27 +131,38 @@
1.136 elif feed_type == "rss" and tagname == "item" or \
1.137 feed_type == "atom" and tagname == "entry":
1.138
1.139 - in_item = True
1.140 + update = Update()
1.141
1.142 elif tagname == "title":
1.143 events.expandNode(value)
1.144 - if in_item:
1.145 - title = value
1.146 + if update:
1.147 + update.title = text(value)
1.148 else:
1.149 - channel_title = value
1.150 + channel_title = text(value)
1.151
1.152 elif tagname == "link":
1.153 events.expandNode(value)
1.154 - if in_item:
1.155 - link = value
1.156 + if update:
1.157 + update.link = linktext(value, feed_type)
1.158 else:
1.159 - channel_link = value
1.160 + channel_link = linktext(value, feed_type)
1.161
1.162 elif feed_type == "atom" and tagname == "content":
1.163 events.expandNode(value)
1.164 - if in_item:
1.165 - content = value
1.166 - content_type = value.getAttribute("type")
1.167 + if update:
1.168 + update.content = text(value)
1.169 + update.content_type = value.getAttribute("type")
1.170 +
1.171 + elif feed_type == "atom" and tagname == "updated" or \
1.172 + feed_type == "rss" and tagname == "pubDate":
1.173 + events.expandNode(value)
1.174 +
1.175 + if update:
1.176 + if feed_type == "atom":
1.177 + value = getDateTime(text(value))
1.178 + else:
1.179 + value = DateTime(parsedate(text(value)))
1.180 + update.updated = value
1.181
1.182 elif event == xml.dom.pulldom.END_ELEMENT:
1.183 tagname = value.localName
1.184 @@ -141,39 +170,104 @@
1.185 if feed_type == "rss" and tagname == "item" or \
1.186 feed_type == "atom" and tagname == "entry":
1.187
1.188 - in_item = False
1.189 -
1.190 - # Emit content where appropriate.
1.191 - # NOTE: HTML should be sanitised.
1.192 -
1.193 - if show_content:
1.194 - if content and content_type == "html":
1.195 - append(fmt.rawHTML(unescape(text(content))))
1.196 -
1.197 - # Or emit title and link information for items.
1.198 + if nentries < max_entries:
1.199 + feed_updates.append(update)
1.200
1.201 - elif title and link and nentries < max_entries:
1.202 - link_text = linktext(link, feed_type)
1.203 -
1.204 - append(fmt.listitem(on=1))
1.205 - append(fmt.url(on=1, href=link_text))
1.206 - append(fmt.icon('www'))
1.207 - append(fmt.text(" " + text(title)))
1.208 - append(fmt.url(on=0))
1.209 - append(fmt.listitem(on=0))
1.210 -
1.211 - title = link = content = content_type = None
1.212 + update = None
1.213 nentries += 1
1.214
1.215 - if not show_content:
1.216 - append(fmt.bullet_list(on=0))
1.217 + finally:
1.218 + feed.close()
1.219 +
1.220 + return (feed_type, channel_title, channel_link), feed_updates
1.221 +
1.222 +# The macro itself.
1.223 +
1.224 +def execute(macro, args):
1.225 + request = macro.request
1.226 + fmt = macro.formatter
1.227 + _ = request.getText
1.228 +
1.229 + feed_urls = []
1.230 + show_content = None
1.231 + max_entries = None
1.232 +
1.233 + for arg, value in parseMacroArguments(args):
1.234 + if arg == "url":
1.235 + feed_urls.append(value)
1.236 + elif arg == "show":
1.237 + show_content = value in ("true", "True", "yes")
1.238 + elif arg == "limit":
1.239 + try:
1.240 + max_entries = int(value)
1.241 + except ValueError:
1.242 + return fmt.text(_("SharedContent: limit must be set to the maximum number of entries to be shown"))
1.243 +
1.244 + if not feed_urls:
1.245 + return fmt.text(_("SharedContent: a feed URL must be specified"))
1.246 +
1.247 + show_content = show_content or False
1.248 + max_entries = max_entries or MAX_ENTRIES
1.249 +
1.250 + updates = []
1.251 + feeds = []
1.252 + missing = []
1.253 + bad_content = []
1.254
1.255 + for feed_url in feed_urls:
1.256 + try:
1.257 + feed_info, feed_updates = getUpdates(request, feed_url, max_entries)
1.258 + updates += feed_updates
1.259 + feeds.append(feed_info)
1.260 + except FeedMissingError:
1.261 + missing.append(feed_url)
1.262 + except FeedContentTypeError:
1.263 + bad_content.append(feed_url)
1.264 +
1.265 + output = []
1.266 + append = output.append
1.267 +
1.268 + # Show the updates.
1.269 +
1.270 + if not show_content:
1.271 + append(fmt.bullet_list(on=1))
1.272 +
1.273 + # NOTE: Permit configurable sorting.
1.274 +
1.275 + updates.sort()
1.276 + updates.reverse()
1.277 +
1.278 + for update in updates:
1.279 +
1.280 + # Emit content where appropriate.
1.281 + # NOTE: HTML should be sanitised.
1.282 +
1.283 + if show_content:
1.284 + append(fmt.div(on=1, css_class="moinshare-update"))
1.285 + if update.content and update.content_type == "html":
1.286 + append(fmt.rawHTML(unescape(update.content)))
1.287 + append(fmt.div(on=0))
1.288 +
1.289 + # Or emit title and link information for items.
1.290 +
1.291 + elif update.title and update.link:
1.292 + append(fmt.listitem(on=1, css_class="moinshare-update"))
1.293 + append(fmt.url(on=1, href=update.link))
1.294 + append(fmt.icon('www'))
1.295 + append(fmt.text(" " + update.title))
1.296 + append(fmt.url(on=0))
1.297 + append(fmt.listitem(on=0))
1.298 +
1.299 + if not show_content:
1.300 + append(fmt.bullet_list(on=0))
1.301 +
1.302 + # Show the feeds.
1.303 +
1.304 + for feed_type, channel_title, channel_link in feeds:
1.305 if channel_title and channel_link:
1.306 - channel_link_text = linktext(channel_link, feed_type)
1.307 -
1.308 - append(fmt.paragraph(on=1))
1.309 - append(fmt.url(on=1, href=channel_link_text))
1.310 - append(fmt.text(text(channel_title)))
1.311 + append(fmt.paragraph(on=1, css_class="moinshare-feed"))
1.312 + append(fmt.url(on=1, href=channel_link))
1.313 + append(fmt.text(channel_title))
1.314 append(fmt.url(on=0))
1.315 append(fmt.text(" "))
1.316 append(fmt.url(on=1, href=feed_url))
1.317 @@ -181,8 +275,17 @@
1.318 append(fmt.url(on=0))
1.319 append(fmt.paragraph(on=0))
1.320
1.321 - finally:
1.322 - feed.close()
1.323 + # Show errors.
1.324 +
1.325 + for feed_url in missing:
1.326 + append(fmt.paragraph(on=1, css_class="moinshare-missing-feed-error"))
1.327 + append(fmt.text(_("SharedContent: updates could not be retrieved for %s") % feed_url))
1.328 + append(fmt.paragraph(on=0))
1.329 +
1.330 + for feed_url in bad_content:
1.331 + append(fmt.paragraph(on=1, css_class="moinshare-content-type-feed-error"))
1.332 + return fmt.text(_("SharedContent: updates for %s were not provided in Atom or RSS format") % feed_url)
1.333 + append(fmt.paragraph(on=0))
1.334
1.335 return ''.join(output)
1.336