Populate those empty "activity" and "volume" columns. Show last posts. #315 veekun-promotions/2010091201
authorEevee <git@veekun.com>
Thu, 9 Sep 2010 05:47:16 +0000 (22:47 -0700)
committerEevee <git@veekun.com>
Thu, 9 Sep 2010 05:47:16 +0000 (22:47 -0700)
splinext/forum/controllers/forum.py
splinext/forum/model/__init__.py
splinext/forum/templates/css/forum.mako
splinext/forum/templates/forum/forums.mako
splinext/forum/templates/forum/threads.mako

index b15302c..3628ff0 100644 (file)
@@ -1,9 +1,13 @@
+import datetime
 import logging
+import math
 
-from pylons import config, request, response, session, tmpl_context as c, url
+from pylons import cache, config, request, response, session, tmpl_context as c, url
 from pylons.controllers.util import abort, redirect
 from routes import request_config
+from sqlalchemy.orm import joinedload
 from sqlalchemy.orm.exc import NoResultFound
+from sqlalchemy.sql import func
 import wtforms
 from wtforms import fields
 
@@ -16,6 +20,74 @@ from splinext.forum import model as forum_model
 log = logging.getLogger(__name__)
 
 
+def forum_activity_score(forum):
+    """Returns a number representing how active a forum is, based on the past
+    week.
+
+    The calculation is arbitrary, but 0 is supposed to mean "dead" and 1 is
+    supposed to mean "healthy".
+    """
+    cutoff = datetime.datetime.now() - datetime.timedelta(days=7)
+
+    post_count = meta.Session.query(forum_model.Post) \
+        .join(forum_model.Post.thread) \
+        .filter(forum_model.Thread.forum == forum) \
+        .filter(forum_model.Post.posted_time >= cutoff) \
+        .count()
+
+    # Avoid domain errors!
+    if not post_count:
+        return 0.0
+
+    # The log is to scale 0 posts to 0.0, and 168 posts to 1.0.
+    # The square is really just to take the edge off the log curve; it
+    # accelerates to 1 very quickly, then slows considerably after that.
+    # Squaring helps both of these problems.
+    score = (math.log(post_count) / math.log(168)) ** 2
+
+    # TODO more threads and more new threads should boost the score slightly
+
+    return score
+
+def get_forum_activity():
+    """Returns a hash mapping forum ids to their level of 'activity'."""
+    forums_q = meta.Session.query(forum_model.Forum)
+
+    activity = {}
+    for forum in forums_q:
+        activity[forum.id] = forum_activity_score(forum)
+
+    return activity
+
+def get_forum_volume():
+    """Returns a hash mapping forum ids to the percentage of all posts that
+    reside in that forum.
+    """
+    # Do a complicated-ass subquery to get a list of forums and postcounts
+    volume_q = meta.Session.query(
+            forum_model.Forum.id.label('forum_id'),
+            func.count(forum_model.Post.id).label('post_count'),
+        ) \
+        .outerjoin(forum_model.Thread) \
+        .outerjoin(forum_model.Post) \
+        .group_by(forum_model.Forum.id)
+
+    # Stick this into a hash, and count the number of total posts
+    total_posts = 0
+    volume = {}
+    for forum_id, post_count in volume_q:
+        post_count = float(post_count or 0)
+        volume[forum_id] = post_count
+        total_posts += post_count
+
+    # Divide, to get a percentage
+    if total_posts:
+        for forum_id, post_count in volume.iteritems():
+            volume[forum_id] /= total_posts
+
+    return volume
+
+
 class WritePostForm(wtforms.Form):
     content = fields.TextAreaField('Content')
 
@@ -26,7 +98,46 @@ class ForumController(BaseController):
 
     def forums(self):
         c.forums = meta.Session.query(forum_model.Forum) \
-            .order_by(forum_model.Forum.id.asc())
+            .order_by(forum_model.Forum.id.asc()) \
+            .all()
+
+        # Get some forum stats.  Cache them because they're a bit expensive to
+        # compute.  Expire after an hour.
+        # XXX when there are admin controls, they'll need to nuke this cache
+        # when messing with the forum list
+        forum_cache = cache.get_cache('spline-forum', expiretime=3600)
+        c.forum_activity = forum_cache.get_value(
+            key='forum_activity', createfunc=get_forum_activity)
+        c.forum_volume = forum_cache.get_value(
+            key='forum_volume', createfunc=get_forum_volume)
+
+        c.max_volume = max(c.forum_volume.itervalues()) or 1
+
+        # Need to know the last post for each forum, in realtime
+        c.last_post = {}
+        last_post_subq = meta.Session.query(
+                forum_model.Forum.id.label('forum_id'),
+                func.max(forum_model.Post.posted_time).label('posted_time'),
+            ) \
+            .outerjoin(forum_model.Thread) \
+            .outerjoin(forum_model.Post) \
+            .group_by(forum_model.Forum.id) \
+            .subquery()
+        last_post_q = meta.Session.query(
+                forum_model.Post,
+                last_post_subq.c.forum_id,
+            ) \
+            .join((
+                last_post_subq,
+                forum_model.Post.posted_time == last_post_subq.c.posted_time,
+            )) \
+            .options(
+                joinedload('thread'),
+                joinedload('author'),
+            )
+        for post, forum_id in last_post_q:
+            c.last_post[forum_id] = post
+
         return render('/forum/forums.mako')
 
     def threads(self, forum_id):
@@ -36,7 +147,10 @@ class ForumController(BaseController):
 
         c.write_thread_form = WriteThreadForm()
 
-        c.threads = c.forum.threads
+        c.threads = c.forum.threads.options(
+            joinedload('last_post'),
+            joinedload('last_post.author'),
+        )
 
         return render('/forum/threads.mako')
 
index 80ca57e..6dc6bfd 100644 (file)
@@ -84,6 +84,7 @@ Forum.threads = relation(Thread, order_by=Thread.id.desc(), lazy='dynamic', back
 
 Thread.posts = relation(Post, order_by=Post.position.asc(), lazy='dynamic', backref='thread')
 Thread.first_post = relation(Post, primaryjoin=and_(Post.thread_id == Thread.id, Post.position == 1), uselist=False)
+# XXX THIS WILL NEED TO CHANGE when posts can be deleted!  Or change what 'position' means
 Thread.last_post = relation(Post, primaryjoin=and_(Post.thread_id == Thread.id, Post.position == Thread.post_count), uselist=False)
 
 Post.author = relation(users_model.User, backref='posts')
index dd35bfd..6400dd9 100644 (file)
@@ -9,7 +9,16 @@ table.forum-list .header-row th { vertical-align: middle; }
 table.forum-list .name { text-align: left; }
 table.forum-list td.name a { display: block; font-size: 1.5em; padding: 0.33em; }
 table.forum-list td.name .forum-description { padding: 0.33em 0.5em; color: #404040; }
-table.forum-list .stats { width: 10em; text-align: center; }
+table.forum-list .last-post { width: 20em; }
+table.forum-list td.last-post { line-height: 1.33; text-align: left; vertical-align: top; }
+table.forum-list .stats { width: 8em; text-align: center; }
+table.forum-list td.stats { line-height: 1.33; vertical-align: top; }
+table.forum-list td.stats.verylow   { font-weight: bold; color: #aaaaaa; }
+table.forum-list td.stats.low       { font-weight: bold; color: #aa5555; }
+table.forum-list td.stats.okay      { font-weight: bold; color: #aa9555; }
+table.forum-list td.stats.high      { font-weight: bold; color: #78aa55; }
+table.forum-list td.stats.veryhigh  { font-weight: bold; color: #559eaa; }
+table.forum-list td.stats.whoanelly { font-weight: bold; color: #6855aa; }
 
 .forum-post-container { }
 .forum-post { position: relative; margin: 1em 0; background: #fcfcfc; -moz-border-radius: 1em; -webkit-border-radius: 1em; }
index 00310d0..2cf6451 100644 (file)
@@ -1,5 +1,6 @@
 <%inherit file="/base.mako" />
 <%namespace name="forumlib" file="/forum/lib.mako" />
+<%namespace name="userlib" file="/users/lib.mako" />
 
 <%def name="title()">Forums</%def>
 
@@ -11,6 +12,7 @@
             <img src="${h.static_uri('spline', 'icons/folders-stack.png')}" alt="">
             Forum
         </th>
+        <th class="last-post">Last post</th>
         <th class="stats">Volume</th>
         <th class="stats">Activity</th>
     </tr>
             </div>
             % endif
         </td>
-        <td class="stats">xxx</td>
-        <td class="stats">xxx</th>
+
+        <td class="last-post">
+            <% last_post = c.last_post.get(forum.id, None) %> \
+            % if last_post:
+            ## XXX should do direct post link
+            <a href="${url(controller='forum', action='posts', forum_id=forum.id, thread_id=last_post.thread_id)}">${last_post.posted_time}</a>
+            <br> in <a href="${url(controller='forum', action='posts', forum_id=forum.id, thread_id=last_post.thread_id)}">${last_post.thread.subject}</a>
+            <br> by <a href="${url(controller='users', action='profile', id=last_post.author.id, name=last_post.author.name)}">${userlib.color_bar(last_post.author)} ${last_post.author.name}</a>
+            % else:
+            —
+            % endif
+        </td>
+
+        <% relative_volume = c.forum_volume[forum.id] / c.max_volume %>\
+        <td class="stats
+            % if relative_volume < 0.1:
+            verylow
+            % elif relative_volume < 0.33:
+            low
+            % elif relative_volume < 0.5:
+            okay
+            % elif relative_volume < 0.67:
+            high
+            % elif relative_volume < 0.9:
+            veryhigh
+            % else:
+            whoanelly
+            % endif
+        ">
+            ${"{0:3.1f}".format(c.forum_volume[forum.id] * 100)}%
+        </th>
+        <% activity = c.forum_activity[forum.id] %>\
+        <td class="stats
+            % if activity < 0.25:
+            verylow
+            % elif activity < 0.5:
+            low
+            % elif activity < 1.0:
+            okay
+            % elif activity < 2.0:
+            high
+            % elif activity < 4.0:
+            veryhigh
+            % else:
+            whoanelly
+            % endif
+        ">
+            ${"{0:0.3f}".format(activity)}
+        </td>
     </tr>
     % endfor
 </tbody>
index 7df962b..cf510e7 100644 (file)
@@ -1,5 +1,6 @@
 <%inherit file="/base.mako" />
 <%namespace name="forumlib" file="/forum/lib.mako" />
+<%namespace name="userlib" file="/users/lib.mako" />
 
 <%def name="title()">${c.forum.name} - Forums</%def>
 
@@ -20,6 +21,7 @@ ${forumlib.hierarchy(c.forum)}
             <img src="${h.static_uri('spline', 'icons/folder-open-document-text.png')}" alt="">
             Thread
         </th>
+        <th class="stats">Last post</th>
         <th class="stats">Posts</th>
     </tr>
 </thead>
@@ -27,6 +29,15 @@ ${forumlib.hierarchy(c.forum)}
     % for thread in c.threads:
     <tr>
         <td class="name"><a href="${url(controller='forum', action='posts', forum_id=c.forum.id, thread_id=thread.id)}">${thread.subject}</a></td>
+        <td class="last-post">
+            % if thread.last_post:
+            ## XXX should do direct post link
+            <a href="${url(controller='forum', action='posts', forum_id=c.forum.id, thread_id=thread.last_post.thread_id)}">${thread.last_post.posted_time}</a>
+            <br> by <a href="${url(controller='users', action='profile', id=thread.last_post.author.id, name=thread.last_post.author.name)}">${userlib.color_bar(thread.last_post.author)} ${thread.last_post.author.name}</a>
+            % else:
+            —
+            % endif
+        </td>
         <td class="stats">${thread.post_count}</td>
     </tr>
     % endfor