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',
 
     install_requires = [
         'spline',
+        'feedparser',
     ],
 
     include_package_data = True,
     ],
 
     include_package_data = True,
index ceb606c..45b1587 100644 (file)
+from collections import namedtuple
+import datetime
 from pkg_resources import resource_filename
 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
 
 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 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 [
 
     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 logging
+import re
 
 from pylons import config, request, response, session, tmpl_context as c, url
 from pylons.controllers.util import abort, redirect_to
 
 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.
 
     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.
         """
 
         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()
         updates.sort(key=lambda obj: obj.time)
         updates.reverse()
-        c.updates = updates[0:10]
+        c.updates = updates[:global_config['limit']]
 
         return render('/index.mako')
 
         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 { 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 .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; }
 .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>