Support stuff-other-than-updates.
[zzz-spline-frontpage.git] / splinext / frontpage / controllers / frontpage.py
index 478c367..fdb0b4c 100644 (file)
@@ -1,15 +1,16 @@
-from collections import defaultdict
+import datetime
 import logging
 import logging
-import re
 
 from pylons import config, request, response, session, tmpl_context as c, url
 
 from pylons import config, request, response, session, tmpl_context as c, url
-from pylons.controllers.util import abort, redirect_to
+from pylons.controllers.util import abort, redirect
 from routes import request_config
 from sqlalchemy.orm.exc import NoResultFound
 
 from spline.lib import helpers as h
 from spline.lib.base import BaseController, render
 from spline.lib.plugin.load import run_hooks
 from routes import request_config
 from sqlalchemy.orm.exc import NoResultFound
 
 from spline.lib import helpers as h
 from spline.lib.base import BaseController, render
 from spline.lib.plugin.load import run_hooks
+from spline.model import meta
+from splinext.frontpage.sources import max_age_to_datetime
 
 log = logging.getLogger(__name__)
 
 
 log = logging.getLogger(__name__)
 
@@ -26,8 +27,13 @@ class FrontPageController(BaseController):
 
         Hook handlers should return a list of FrontPageUpdate objects.
 
 
         Hook handlers should return a list of FrontPageUpdate objects.
 
-        Standard hook parameters are `limit`, the maximum number of items that
-        should ever be returned.
+        Standard hook parameters are:
+        `limit`, the maximum number of items that should ever be returned.
+        `max_age`, the number of seconds after which items expire.
+        `title`, a name for the source.
+        `icon`, an icon to show next to its name.
+
+        `limit` and `max_age` are also global options.
 
         Updates are configured in the .ini like so:
 
 
         Updates are configured in the .ini like so:
 
@@ -40,49 +46,76 @@ class FrontPageController(BaseController):
 
             run_hooks('frontpage_updates_updatetype', opt1=val1, opt2=val2)
 
 
             run_hooks('frontpage_updates_updatetype', opt1=val1, opt2=val2)
 
-        Standard options are not shown and take precedence over whatever's in
-        the config file.
+        Plugins may also respond to the `frontpage_extras` hook with other
+        interesting things to put on the front page.  There's no way to
+        customize the order of these extras or which appear and which don't, at
+        the moment.  Such hooks should return an object with at least a
+        `template` attribute; the template will be called with the object
+        passed in as its `obj` argument.
 
         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.
         """
-        # 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 = []
         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[:global_config['limit']]
-
-        return render('/index.mako')
+        global_limit = config['spline-frontpage.limit']
+        global_max_age = max_age_to_datetime(
+            config['spline-frontpage.max_age'])
+
+        c.sources = config['spline-frontpage.sources']
+        for source in c.sources:
+            new_updates = source.poll(global_limit, global_max_age)
+            updates.extend(new_updates)
+
+            # Little optimization: once there are global_limit items, anything
+            # older than the oldest cannot possibly make it onto the list.  So,
+            # bump global_max_age to that oldest time if this is ever the case.
+            updates.sort(key=lambda obj: obj.time, reverse=True)
+            del updates[global_limit:]
+
+            if updates and len(updates) == global_limit:
+                global_max_age = updates[-1].time
+
+        # Find the oldest unseen item, to draw a divider after it.
+        # If this stays as None, the divider goes at the top
+        c.last_seen_item = None
+        # Could have a timestamp in the stash if this is a user, or in a cookie
+        # if this session has ever been logged out...
+        times = []
+        for source in (c.user.stash, request.cookies):
+            try:
+                times.append( int(source['frontpage-last-seen-time']) )
+            except (KeyError, ValueError):
+                pass
+
+        if times:
+            last_seen_time = datetime.datetime.fromtimestamp(max(times))
+            for update in updates:
+                if update.time > last_seen_time:
+                    c.last_seen_item = update
+                else:
+                    break
+
+        # Save ~now~ as the last-seen time
+        now = datetime.datetime.now().strftime('%s')
+        if c.user:
+            c.user.stash['frontpage-last-seen-time'] = now
+            meta.Session.add(c.user)
+        else:
+            response.set_cookie('frontpage-last-seen-time', now)
+
+        # Done!  Feed to template
+        c.updates = updates
+
+        # Hook for non-update interesting things to put on the front page.
+        # This hook should return objects with a 'template' attribute, and
+        # whatever else they need
+        c.extras = run_hooks('frontpage_extras')
+
+        ret = render('/index.mako')
+
+        # Commit AFTER rendering the template!  Committing invalidates
+        # everything in the session, undoing any eagerloading that may have
+        # been done by sources
+        meta.Session.commit()
+        return ret