Added RSS and git support.
authorEevee <git@veekun.com>
Sun, 18 Jul 2010 07:20:47 +0000 (00:20 -0700)
committerEevee <git@veekun.com>
Sun, 18 Jul 2010 07:43:43 +0000 (00:43 -0700)
setup.py
splinext/frontpage/__init__.py
splinext/frontpage/controllers/frontpage.py
splinext/frontpage/templates/css/frontpage.mako
splinext/frontpage/templates/front_page/git.mako [new file with mode: 0644]
splinext/frontpage/templates/front_page/rss.mako [new file with mode: 0644]

index a256563..f1ef216 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -6,6 +6,7 @@ setup(
 
     install_requires = [
         'spline',
+        'feedparser',
     ],
 
     include_package_data = True,
index ceb606c..45b1587 100644 (file)
+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),
         ]
index 4bac44f..478c367 100644 (file)
@@ -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_<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')
index 03330c8..d7012d3 100644 (file)
@@ -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 (file)
index 0000000..ad3bcb4
--- /dev/null
@@ -0,0 +1,29 @@
+<%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>
diff --git a/splinext/frontpage/templates/front_page/rss.mako b/splinext/frontpage/templates/front_page/rss.mako
new file mode 100644 (file)
index 0000000..108455a
--- /dev/null
@@ -0,0 +1,13 @@
+<%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>