2.1 --- a/macros/SharedContent.py Tue May 07 20:01:49 2013 +0200
2.2 +++ b/macros/SharedContent.py Tue May 07 20:03:45 2013 +0200
2.3 @@ -6,9 +6,11 @@
2.4 @license: GNU GPL (v2 or later), see COPYING.txt for details.
2.5 """
2.6
2.7 +from DateSupport import getDateTime, DateTime
2.8 from MoinMoin.Page import Page
2.9 from MoinRemoteSupport import *
2.10 from MoinSupport import parseMacroArguments
2.11 +from email.utils import parsedate
2.12 import xml.dom.pulldom
2.13
2.14 try:
2.15 @@ -21,6 +23,8 @@
2.16 MAX_ENTRIES = 5
2.17 ATOM_NS = "http://www.w3.org/2005/Atom"
2.18
2.19 +# Utility functions.
2.20 +
2.21 def text(element):
2.22 nodes = []
2.23 for node in element.childNodes:
2.24 @@ -37,64 +41,78 @@
2.25 else:
2.26 return element.getAttribute("href")
2.27
2.28 -def execute(macro, args):
2.29 - request = macro.request
2.30 - fmt = macro.formatter
2.31 - _ = request.getText
2.32 +# Error classes.
2.33 +
2.34 +class FeedError(Exception):
2.35 + pass
2.36 +
2.37 +class FeedMissingError(FeedError):
2.38 + pass
2.39
2.40 - feed_url = None
2.41 - show_content = None
2.42 - max_entries = None
2.43 +class FeedContentTypeError(FeedError):
2.44 + pass
2.45 +
2.46 +# Entry/update classes.
2.47 +
2.48 +class Update:
2.49 +
2.50 + "A feed update entry."
2.51
2.52 - for arg, value in parseMacroArguments(args):
2.53 - if arg == "url":
2.54 - feed_url = value
2.55 - elif arg == "show":
2.56 - show_content = value in ("true", "True", "yes")
2.57 - elif arg == "limit":
2.58 - try:
2.59 - max_entries = int(value)
2.60 - except ValueError:
2.61 - return fmt.text(_("SharedContent: limit must be set to the maximum number of entries to be shown"))
2.62 + def __init__(self):
2.63 + self.title = None
2.64 + self.link = None
2.65 + self.content = None
2.66 + self.content_type = None
2.67 + self.updated = None
2.68
2.69 - if not feed_url:
2.70 - return fmt.text(_("SharedContent: a feed URL must be specified"))
2.71 + def __cmp__(self, other):
2.72 + if self.updated is None and other.updated is not None:
2.73 + return 1
2.74 + elif self.updated is not None and other.updated is None:
2.75 + return -1
2.76 + else:
2.77 + return cmp(self.updated, other.updated)
2.78 +
2.79 +# Feed retrieval.
2.80
2.81 - show_content = show_content or False
2.82 - max_entries = max_entries or MAX_ENTRIES
2.83 +def getUpdates(request, feed_url, max_entries):
2.84 +
2.85 + """
2.86 + Using the given 'request', retrieve from 'feed_url' up to the given number
2.87 + 'max_entries' of update entries.
2.88 +
2.89 + A tuple of the form ((feed_type, channel_title, channel_link), updates) is
2.90 + returned.
2.91 + """
2.92 +
2.93 + feed_updates = []
2.94
2.95 # Obtain the resource, using a cached version if appropriate.
2.96
2.97 max_cache_age = int(getattr(request.cfg, "moin_share_max_cache_age", "300"))
2.98 data = getCachedResource(request, feed_url, "MoinShare", "wiki", max_cache_age)
2.99 if not data:
2.100 - return fmt.text(_("SharedContent: updates could not be retrieved for %s") % feed_url)
2.101 + raise FeedMissingError
2.102 +
2.103 + # Interpret the cached feed.
2.104
2.105 feed = StringIO(data)
2.106 -
2.107 _url, content_type, _encoding, _metadata = getCachedResourceMetadata(feed)
2.108
2.109 if content_type not in ("application/atom+xml", "application/rss+xml"):
2.110 - return fmt.text(_("SharedContent: updates for %s were not provided in Atom or RSS format") % feed_url)
2.111 + raise FeedContentTypeError
2.112
2.113 try:
2.114 # Parse each node from the feed.
2.115
2.116 - title = link = content = content_type = None
2.117 channel_title = channel_link = None
2.118
2.119 - output = []
2.120 - append = output.append
2.121 -
2.122 feed_type = None
2.123 - in_item = False
2.124 + update = None
2.125 nentries = 0
2.126
2.127 events = xml.dom.pulldom.parse(feed)
2.128
2.129 - if not show_content:
2.130 - append(fmt.bullet_list(on=1))
2.131 -
2.132 for event, value in events:
2.133
2.134 if event == xml.dom.pulldom.START_ELEMENT:
2.135 @@ -113,27 +131,38 @@
2.136 elif feed_type == "rss" and tagname == "item" or \
2.137 feed_type == "atom" and tagname == "entry":
2.138
2.139 - in_item = True
2.140 + update = Update()
2.141
2.142 elif tagname == "title":
2.143 events.expandNode(value)
2.144 - if in_item:
2.145 - title = value
2.146 + if update:
2.147 + update.title = text(value)
2.148 else:
2.149 - channel_title = value
2.150 + channel_title = text(value)
2.151
2.152 elif tagname == "link":
2.153 events.expandNode(value)
2.154 - if in_item:
2.155 - link = value
2.156 + if update:
2.157 + update.link = linktext(value, feed_type)
2.158 else:
2.159 - channel_link = value
2.160 + channel_link = linktext(value, feed_type)
2.161
2.162 elif feed_type == "atom" and tagname == "content":
2.163 events.expandNode(value)
2.164 - if in_item:
2.165 - content = value
2.166 - content_type = value.getAttribute("type")
2.167 + if update:
2.168 + update.content = text(value)
2.169 + update.content_type = value.getAttribute("type")
2.170 +
2.171 + elif feed_type == "atom" and tagname == "updated" or \
2.172 + feed_type == "rss" and tagname == "pubDate":
2.173 + events.expandNode(value)
2.174 +
2.175 + if update:
2.176 + if feed_type == "atom":
2.177 + value = getDateTime(text(value))
2.178 + else:
2.179 + value = DateTime(parsedate(text(value)))
2.180 + update.updated = value
2.181
2.182 elif event == xml.dom.pulldom.END_ELEMENT:
2.183 tagname = value.localName
2.184 @@ -141,39 +170,104 @@
2.185 if feed_type == "rss" and tagname == "item" or \
2.186 feed_type == "atom" and tagname == "entry":
2.187
2.188 - in_item = False
2.189 -
2.190 - # Emit content where appropriate.
2.191 - # NOTE: HTML should be sanitised.
2.192 -
2.193 - if show_content:
2.194 - if content and content_type == "html":
2.195 - append(fmt.rawHTML(unescape(text(content))))
2.196 -
2.197 - # Or emit title and link information for items.
2.198 + if nentries < max_entries:
2.199 + feed_updates.append(update)
2.200
2.201 - elif title and link and nentries < max_entries:
2.202 - link_text = linktext(link, feed_type)
2.203 -
2.204 - append(fmt.listitem(on=1))
2.205 - append(fmt.url(on=1, href=link_text))
2.206 - append(fmt.icon('www'))
2.207 - append(fmt.text(" " + text(title)))
2.208 - append(fmt.url(on=0))
2.209 - append(fmt.listitem(on=0))
2.210 -
2.211 - title = link = content = content_type = None
2.212 + update = None
2.213 nentries += 1
2.214
2.215 - if not show_content:
2.216 - append(fmt.bullet_list(on=0))
2.217 + finally:
2.218 + feed.close()
2.219 +
2.220 + return (feed_type, channel_title, channel_link), feed_updates
2.221 +
2.222 +# The macro itself.
2.223 +
2.224 +def execute(macro, args):
2.225 + request = macro.request
2.226 + fmt = macro.formatter
2.227 + _ = request.getText
2.228 +
2.229 + feed_urls = []
2.230 + show_content = None
2.231 + max_entries = None
2.232 +
2.233 + for arg, value in parseMacroArguments(args):
2.234 + if arg == "url":
2.235 + feed_urls.append(value)
2.236 + elif arg == "show":
2.237 + show_content = value in ("true", "True", "yes")
2.238 + elif arg == "limit":
2.239 + try:
2.240 + max_entries = int(value)
2.241 + except ValueError:
2.242 + return fmt.text(_("SharedContent: limit must be set to the maximum number of entries to be shown"))
2.243 +
2.244 + if not feed_urls:
2.245 + return fmt.text(_("SharedContent: a feed URL must be specified"))
2.246 +
2.247 + show_content = show_content or False
2.248 + max_entries = max_entries or MAX_ENTRIES
2.249 +
2.250 + updates = []
2.251 + feeds = []
2.252 + missing = []
2.253 + bad_content = []
2.254
2.255 + for feed_url in feed_urls:
2.256 + try:
2.257 + feed_info, feed_updates = getUpdates(request, feed_url, max_entries)
2.258 + updates += feed_updates
2.259 + feeds.append(feed_info)
2.260 + except FeedMissingError:
2.261 + missing.append(feed_url)
2.262 + except FeedContentTypeError:
2.263 + bad_content.append(feed_url)
2.264 +
2.265 + output = []
2.266 + append = output.append
2.267 +
2.268 + # Show the updates.
2.269 +
2.270 + if not show_content:
2.271 + append(fmt.bullet_list(on=1))
2.272 +
2.273 + # NOTE: Permit configurable sorting.
2.274 +
2.275 + updates.sort()
2.276 + updates.reverse()
2.277 +
2.278 + for update in updates:
2.279 +
2.280 + # Emit content where appropriate.
2.281 + # NOTE: HTML should be sanitised.
2.282 +
2.283 + if show_content:
2.284 + append(fmt.div(on=1, css_class="moinshare-update"))
2.285 + if update.content and update.content_type == "html":
2.286 + append(fmt.rawHTML(unescape(update.content)))
2.287 + append(fmt.div(on=0))
2.288 +
2.289 + # Or emit title and link information for items.
2.290 +
2.291 + elif update.title and update.link:
2.292 + append(fmt.listitem(on=1, css_class="moinshare-update"))
2.293 + append(fmt.url(on=1, href=update.link))
2.294 + append(fmt.icon('www'))
2.295 + append(fmt.text(" " + update.title))
2.296 + append(fmt.url(on=0))
2.297 + append(fmt.listitem(on=0))
2.298 +
2.299 + if not show_content:
2.300 + append(fmt.bullet_list(on=0))
2.301 +
2.302 + # Show the feeds.
2.303 +
2.304 + for feed_type, channel_title, channel_link in feeds:
2.305 if channel_title and channel_link:
2.306 - channel_link_text = linktext(channel_link, feed_type)
2.307 -
2.308 - append(fmt.paragraph(on=1))
2.309 - append(fmt.url(on=1, href=channel_link_text))
2.310 - append(fmt.text(text(channel_title)))
2.311 + append(fmt.paragraph(on=1, css_class="moinshare-feed"))
2.312 + append(fmt.url(on=1, href=channel_link))
2.313 + append(fmt.text(channel_title))
2.314 append(fmt.url(on=0))
2.315 append(fmt.text(" "))
2.316 append(fmt.url(on=1, href=feed_url))
2.317 @@ -181,8 +275,17 @@
2.318 append(fmt.url(on=0))
2.319 append(fmt.paragraph(on=0))
2.320
2.321 - finally:
2.322 - feed.close()
2.323 + # Show errors.
2.324 +
2.325 + for feed_url in missing:
2.326 + append(fmt.paragraph(on=1, css_class="moinshare-missing-feed-error"))
2.327 + append(fmt.text(_("SharedContent: updates could not be retrieved for %s") % feed_url))
2.328 + append(fmt.paragraph(on=0))
2.329 +
2.330 + for feed_url in bad_content:
2.331 + append(fmt.paragraph(on=1, css_class="moinshare-content-type-feed-error"))
2.332 + return fmt.text(_("SharedContent: updates for %s were not provided in Atom or RSS format") % feed_url)
2.333 + append(fmt.paragraph(on=0))
2.334
2.335 return ''.join(output)
2.336