From: Eevee Date: Sun, 18 Jul 2010 07:20:47 +0000 (-0700) Subject: Added RSS and git support. X-Git-Tag: veekun-promotions/2010080801~7 X-Git-Url: http://git.veekun.com/zzz-spline-frontpage.git/commitdiff_plain/4371e472c2c75a12b5e1e499a9df6e62d778c030 Added RSS and git support. --- diff --git a/setup.py b/setup.py index a256563..f1ef216 100644 --- a/setup.py +++ b/setup.py @@ -6,6 +6,7 @@ setup( install_requires = [ 'spline', + 'feedparser', ], include_package_data = True, diff --git a/splinext/frontpage/__init__.py b/splinext/frontpage/__init__.py index ceb606c..45b1587 100644 --- a/splinext/frontpage/__init__.py +++ b/splinext/frontpage/__init__.py @@ -1,10 +1,141 @@ +from collections import namedtuple +import datetime from pkg_resources import resource_filename +import subprocess -from spline.lib.plugin import PluginBase +import feedparser + +from spline.lib import helpers from spline.lib.plugin import PluginBase, PluginLink, Priority import splinext.frontpage.controllers.frontpage +class FrontPageUpdate(object): + """Base class ('interface') for an updated thing that may appear on the + front page. + + Subclasses should implement the `time` and `template` properties. + """ + pass + + +FrontPageRSS = namedtuple('FrontPageRSS', + ['time', 'entry', 'template', 'category', 'content', 'icon']) + +def rss_hook(limit, url, title, icon=None): + """Front page handler for news feeds.""" + feed = feedparser.parse(url) + + updates = [] + for entry in feed.entries: + # Try to find something to show! Default to the summary, if there is + # one, or try to generate one otherwise + content = u'' + if 'summary' in entry: + content = entry.summary + elif 'content' in entry: + content = entry.content[0].value + + content = helpers.literal(content) + + update = FrontPageRSS( + time = datetime.datetime(*entry.published_parsed[:6]), + entry = entry, + template = '/front_page/rss.mako', + category = title, + content = content, + icon = icon, + ) + updates.append(update) + + return updates + + +FrontPageGit = namedtuple('FrontPageGit', + ['time', 'gitweb', 'log', 'tag', 'template', 'category', 'icon']) +FrontPageGitCommit = namedtuple('FrontPageGitCommit', + ['hash', 'author', 'time', 'subject', 'repo']) + +def git_hook(limit, title, gitweb, repo_paths, repo_names, + tag_pattern=None, icon=None): + + """Front page handler for repository history.""" + # Repo stuff can be space-delimited lists... + repo_paths = repo_paths.split() + repo_names = repo_names.split() + + # Fetch the main repo's git tags + args = [ + 'git', + '--git-dir=' + repo_paths[0], + 'tag', '-l', + ] + if tag_pattern: + args.append(tag_pattern) + + proc = subprocess.Popen(args, stdout=subprocess.PIPE) + git_output, _ = proc.communicate() + tags = git_output.strip().split('\n') + + # Tags come out in alphabetical order, which means earliest first. Reverse + # it to make the slicing easier + tags.reverse() + # Only history from tag to tag is actually interesting, so get the most + # recent $limit tags but skip the earliest + interesting_tags = tags[:-1][:limit] + + updates = [] + for tag, since_tag in zip(interesting_tags, tags[1:]): + commits = [] + + for repo_path, repo_name in zip(repo_paths, repo_names): + # Grab an easily-parsed history: fields delimited by nulls. + # Hash, author's name, commit timestamp, subject. + git_log_args = [ + 'git', + '--git-dir=' + repo_path, + 'log', + '--pretty=%h%x00%an%x00%at%x00%s', + "{0}..{1}".format(since_tag, tag), + ] + proc = subprocess.Popen(git_log_args, stdout=subprocess.PIPE) + for line in proc.stdout: + hash, author, time, subject = line.strip().split('\x00') + commits.append( + FrontPageGitCommit( + hash = hash, + author = author, + time = datetime.datetime.fromtimestamp(int(time)), + subject = subject, + repo = repo_name, + ) + ) + + # LASTLY, get the date when this tag was actually created + args = [ + 'git', + 'for-each-ref', + '--format=%(taggerdate:raw)', + 'refs/tags/' + tag, + ] + tag_timestamp, _ = subprocess.Popen(args, stdout=subprocess.PIPE) \ + .communicate() + tag_unixtime, tag_timezone = tag_timestamp.split(None, 1) + + update = FrontPageGit( + time = datetime.datetime.fromtimestamp(int(tag_unixtime)), + gitweb = gitweb, + log = commits, + template = '/front_page/git.mako', + category = title, + tag = tag, + icon = icon, + ) + updates.append(update) + + return updates + + def add_routes_hook(map, *args, **kwargs): """Hook to inject some of our behavior into the routes configuration.""" map.connect('/', controller='frontpage', action='index') @@ -23,5 +154,7 @@ class FrontPagePlugin(PluginBase): def hooks(self): return [ - ('routes_mapping', Priority.NORMAL, add_routes_hook), + ('routes_mapping', Priority.NORMAL, add_routes_hook), + ('frontpage_updates_rss', Priority.NORMAL, rss_hook), + ('frontpage_updates_git', Priority.NORMAL, git_hook), ] diff --git a/splinext/frontpage/controllers/frontpage.py b/splinext/frontpage/controllers/frontpage.py index 4bac44f..478c367 100644 --- a/splinext/frontpage/controllers/frontpage.py +++ b/splinext/frontpage/controllers/frontpage.py @@ -1,4 +1,6 @@ +from collections import defaultdict import logging +import re from pylons import config, request, response, session, tmpl_context as c, url from pylons.controllers.util import abort, redirect_to @@ -16,18 +18,71 @@ class FrontPageController(BaseController): def index(self): """Magicaltastic front page. - Plugins can register things to appear on it, somehow. + Plugins can register a hook called 'frontpage_updates_' to add + updates to the front page. `` is an arbitrary string indicating + the sort of update the plugin knows how to handle; for example, + spline-forum has a `frontpage_updates_forum` hook for posting news from + a specific forum. + + Hook handlers should return a list of FrontPageUpdate objects. + + Standard hook parameters are `limit`, the maximum number of items that + should ever be returned. + + Updates are configured in the .ini like so: + + spline-frontpage.sources.foo = updatetype + spline-frontpage.sources.foo.opt1 = val1 + spline-frontpage.sources.foo.opt2 = val2 + + Note that the 'foo' name is completely arbitrary and is only used for + grouping options together. This will result in a call to: + + run_hooks('frontpage_updates_updatetype', opt1=val1, opt2=val2) + + Standard options are not shown and take precedence over whatever's in + the config file. Local plugins can override the fairly simple index.mako template to customize the front page layout. """ - # Hooks should return a list of FrontPageUpdate objects, making this - # return value a list of lists - updates_lol = run_hooks('frontpage_updates', limit=10) - updates = sum(updates_lol, []) + # XXX no reason to do this on the fly; cache it on server startup + update_config = defaultdict(dict) # source_name => config + key_rx = re.compile( + '(?x) ^ spline-frontpage [.] sources [.] (\w+) (?: [.] (\w+) )? $') + for key, val in config.iteritems(): + match = key_rx.match(key) + if not match: + continue + + source_name, subkey = match.groups() + if not subkey: + # This is the type declaration; use a special key + subkey = '__type__' + + update_config[source_name][subkey] = val + + + global_config = dict( + limit = 10, + ) + + # Ask plugins to deal with this stuff for us! + updates = [] + for source, source_config in update_config.iteritems(): + source_config2 = source_config.copy() + hook_name = 'frontpage_updates_' + source_config2.pop('__type__') + source_config2.update(global_config) + + # Hooks should return a list of FrontPageUpdate objects, making this + # return value a list of lists + updates_lol = run_hooks(hook_name, **source_config2) + updates += sum(updates_lol, []) + # Sort everything by descending time, then crop to the right number of + # items updates.sort(key=lambda obj: obj.time) updates.reverse() - c.updates = updates[0:10] + c.updates = updates[:global_config['limit']] return render('/index.mako') diff --git a/splinext/frontpage/templates/css/frontpage.mako b/splinext/frontpage/templates/css/frontpage.mako index 03330c8..d7012d3 100644 --- a/splinext/frontpage/templates/css/frontpage.mako +++ b/splinext/frontpage/templates/css/frontpage.mako @@ -1,10 +1,20 @@ .frontpage-update { position: relative; overflow: auto; margin: 1em 0; background: #f4f4f4; -moz-border-radius: 1em; -webkit-border-radius: 1em; } .frontpage-update:nth-child(2n) { background: #f0f0f0; } -.frontpage-update .header { padding: 0.5em 1em; border: 1px solid #b4c7e6; background: url(${h.static_uri('local', 'images/layout/th-background.png')}) left bottom repeat-x; -moz-border-radius-topleft: 1em; -moz-border-radius-topright: 1em; -webkit-border-top-left-radius: 0.5em; -webkit-border-top-right-radius: 0.5em; } -.frontpage-update .header .category { float: left; font-size: 1.33em; font-style: italic; color: #404040; } -.frontpage-update .header .title { float: left; font-size: 1.33em; margin-left: 0.25em; } -.frontpage-update .header .date { line-height: 1.33; text-align: right; } +.frontpage-update .header { white-space: nowrap; padding: 0.5em 1em; border: 1px solid #b4c7e6; background: url(${h.static_uri('local', 'images/layout/th-background.png')}) left bottom repeat-x; -moz-border-radius-topleft: 1em; -moz-border-radius-topright: 1em; -webkit-border-top-left-radius: 0.5em; -webkit-border-top-right-radius: 0.5em; } +.frontpage-update .header .category { float: left; font-size: 1.33em; margin-right: 0.25em; font-style: italic; color: #404040; vertical-align: bottom; } +.frontpage-update .header .category img { vertical-align: bottom; } +.frontpage-update .header .date { float: right; white-space: nowrap; line-height: 1.33; margin-left: 0.33em; vertical-align: bottom; } +.frontpage-update .header .title { overflow: hidden; font-size: 1.33em; height: 1em; vertical-align: bottom; text-overflow: ellipsis; font-weight: bold; color: #303030; } .frontpage-update .avatar { float: right; margin: 1em; } .frontpage-update .avatar img { -moz-box-shadow: 0 0 2px black; } -.frontpage-update .content { padding: 1em; padding-bottom: 3.5em; } +.frontpage-update .content { padding: 1em; line-height: 1.33; } +.frontpage-update .content.has-comments { padding-bottom: 3.5em; } .frontpage-update .comments { position: absolute; bottom: 0; left: 0; padding: 1em; } + +table.frontpage-repository { width: 100%; } +table.frontpage-repository tr.frontpage-repository-header { background: transparent !important; } +table.frontpage-repository th { font-size: 1.25em; padding: 0.5em 0 0; border-bottom: 1px solid #2457a0; text-align: left; font-style: italic; } +table.frontpage-repository tr:first-child th { padding-top: 0; } +table.frontpage-repository td.hash { width: 6em; text-align: center; font-family: monospace; } +table.frontpage-repository td.author { width: 10em; } +table.frontpage-repository td.time { width: 12em; } diff --git a/splinext/frontpage/templates/front_page/git.mako b/splinext/frontpage/templates/front_page/git.mako new file mode 100644 index 0000000..ad3bcb4 --- /dev/null +++ b/splinext/frontpage/templates/front_page/git.mako @@ -0,0 +1,29 @@ +<%page args="update" /> + +
+
+
${update.category}:
+
${update.time}
+
${update.tag}
+
+
+ + <% last_repo = None %>\ + % for commit in update.log: + % if commit.repo != last_repo: + + + + % endif + + + + + + + + <% last_repo = commit.repo %>\ + % endfor +
${commit.repo}
${commit.hash}${commit.author}${commit.subject}${commit.time}
+
+
diff --git a/splinext/frontpage/templates/front_page/rss.mako b/splinext/frontpage/templates/front_page/rss.mako new file mode 100644 index 0000000..108455a --- /dev/null +++ b/splinext/frontpage/templates/front_page/rss.mako @@ -0,0 +1,13 @@ +<%page args="update" /> +<%namespace name="userlib" file="/users/lib.mako" /> + +
+
+
${update.category}:
+
${update.time}
+ +
+
${update.content}
+