install_requires = [
'spline',
+ 'feedparser',
],
include_package_data = True,
+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')
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),
]
+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
def index(self):
"""Magicaltastic front page.
- Plugins can register things to appear on it, somehow.
+ Plugins can register a hook called 'frontpage_updates_<type>' to add
+ updates to the front page. `<type>` 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')
.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; }
--- /dev/null
+<%page args="update" />
+
+<div class="frontpage-update">
+ <div class="header">
+ <div class="category"><img src="${h.static_uri('spline', "icons/{0}.png".format('gear--pencil'))}" alt=""> ${update.category}:</div>
+ <div class="date">${update.time}</div>
+ <div class="title">${update.tag}</div>
+ </div>
+ <div class="content">
+ <table class="frontpage-repository striped-rows">
+ <% last_repo = None %>\
+ % for commit in update.log:
+ % if commit.repo != last_repo:
+ <tr class="frontpage-repository-header">
+ <th colspan="4">${commit.repo}</th>
+ </tr>
+ % endif
+
+ <tr>
+ <td class="hash"><a href="${update.gitweb}?p=${commit.repo}.git;a=commit;h=${commit.hash}">${commit.hash}</a></td>
+ <td class="author">${commit.author}</td>
+ <td class="subject">${commit.subject}</td>
+ <td class="time">${commit.time}</td>
+ </tr>
+ <% last_repo = commit.repo %>\
+ % endfor
+ </table>
+ </div>
+</div>
--- /dev/null
+<%page args="update" />
+<%namespace name="userlib" file="/users/lib.mako" />
+
+<div class="frontpage-update">
+ <div class="header">
+ <div class="category"><img src="${h.static_uri('spline', "icons/{0}.png".format(update.icon or 'feed'))}" alt=""> ${update.category}:</div>
+ <div class="date">${update.time}</div>
+ <div class="title">
+ <a href="${update.entry.link}">${update.entry.title | n}</a>
+ </div>
+ </div>
+ <div class="content">${update.content}</div>
+</div>