Crash fix: allow creating threads, oops.
[zzz-spline-forum.git] / splinext / forum / controllers / forum.py
index 342e8e6..5d962e0 100644 (file)
@@ -1,20 +1,93 @@
+import datetime
 import logging
+import math
 
-from pylons import config, request, response, session, tmpl_context as c, url
-from pylons.controllers.util import abort, redirect_to
+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
 
 from spline.model import meta
 from spline.lib import helpers as h
 from spline.lib.base import BaseController, render
+import spline.lib.markdown
 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')
 
@@ -25,18 +98,59 @@ 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):
-        try:
-            c.forum = meta.Session.query(forum_model.Forum).get(forum_id)
-        except NoResultFound:
+        c.forum = meta.Session.query(forum_model.Forum).get(forum_id)
+        if not c.forum:
             abort(404)
 
         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')
 
@@ -80,10 +194,12 @@ class ForumController(BaseController):
             subject = c.write_thread_form.subject.data,
             post_count = 1,
         )
+        source = c.write_thread_form.content.data
         post = forum_model.Post(
             position = 1,
             author_user_id = c.user.id,
-            content = c.write_thread_form.content.data,
+            raw_content = source,
+            content = spline.lib.markdown.translate(source),
         )
 
         thread.posts.append(post)
@@ -93,9 +209,11 @@ class ForumController(BaseController):
 
         # Redirect to the new thread
         h.flash("Contribution to the collective knowledge of the species successfully recorded.")
-        redirect_to(controller='forum', action='posts',
-            forum_id=forum_id, thread_id=thread.id,
-            _code=303)
+        redirect(
+            url(controller='forum', action='posts',
+                forum_id=forum_id, thread_id=thread.id),
+            code=303,
+        )
 
     def write(self, forum_id, thread_id):
         """Provides a form for posting to a thread."""
@@ -120,10 +238,12 @@ class ForumController(BaseController):
             .with_lockmode('update') \
             .get(c.thread.id)
 
+        source = c.write_post_form.content.data
         post = forum_model.Post(
             position = c.thread.post_count + 1,
             author_user_id = c.user.id,
-            content = c.write_post_form.content.data,
+            raw_content = source,
+            content = spline.lib.markdown.translate(source),
         )
 
         c.thread.posts.append(post)
@@ -134,6 +254,8 @@ class ForumController(BaseController):
         # Redirect to the thread
         # XXX probably to the post instead; anchor?  depends on paging scheme
         h.flash('Your uniqueness has been added to our own.')
-        redirect_to(controller='forum', action='posts',
-            forum_id=forum_id, thread_id=thread_id,
-            _code=303)
+        redirect(
+            url(controller='forum', action='posts',
+                forum_id=forum_id, thread_id=thread_id),
+            code=303,
+        )