5 from pylons
import cache
, config
, request
, response
, session
, tmpl_context
as c
, url
6 from pylons
.controllers
.util
import abort
, redirect
7 from pylons
.decorators
.secure
import authenticate_form
8 from routes
import request_config
9 from sqlalchemy
.orm
import aliased
, contains_eager
, joinedload
10 from sqlalchemy
.orm
.exc
import NoResultFound
11 from sqlalchemy
.sql
import func
13 from wtforms
import fields
15 from spline
.model
import meta
16 from spline
.lib
import helpers
as h
17 from spline
.lib
.base
import BaseController
, render
18 import spline
.lib
.markdown
19 from splinext
.forum
import model
as forum_model
21 log
= logging
.getLogger(__name__
)
24 def forum_activity_score(forum
):
25 """Returns a number representing how active a forum is, based on the past
28 The calculation is arbitrary, but 0 is supposed to mean "dead" and 1 is
29 supposed to mean "healthy".
31 cutoff
= datetime
.datetime
.now() - datetime
.timedelta(days
=7)
33 post_count
= meta
.Session
.query(forum_model
.Post
) \
34 .join(forum_model
.Post
.thread
) \
35 .filter(forum_model
.Thread
.forum
== forum
) \
36 .filter(forum_model
.Post
.posted_time
>= cutoff
) \
39 # Avoid domain errors!
43 # The log is to scale 0 posts to 0.0, and 168 posts to 1.0.
44 # The square is really just to take the edge off the log curve; it
45 # accelerates to 1 very quickly, then slows considerably after that.
46 # Squaring helps both of these problems.
47 score
= (math
.log(post_count
) / math
.log(168)) ** 2
49 # TODO more threads and more new threads should boost the score slightly
53 def get_forum_activity():
54 """Returns a hash mapping forum ids to their level of 'activity'."""
55 forums_q
= meta
.Session
.query(forum_model
.Forum
)
58 for forum
in forums_q
:
59 activity
[forum
.id] = forum_activity_score(forum
)
63 def get_forum_volume():
64 """Returns a hash mapping forum ids to the percentage of all posts that
67 # Do a complicated-ass subquery to get a list of forums and postcounts
68 volume_q
= meta
.Session
.query(
69 forum_model
.Forum
.id.label('forum_id'),
70 func
.count(forum_model
.Post
.id).label('post_count'),
72 .outerjoin(forum_model
.Thread
) \
73 .outerjoin(forum_model
.Post
) \
74 .group_by(forum_model
.Forum
.id)
76 # Stick this into a hash, and count the number of total posts
79 for forum_id
, post_count
in volume_q
:
80 post_count
= float(post_count
or 0)
81 volume
[forum_id
] = post_count
82 total_posts
+= post_count
84 # Divide, to get a percentage
86 for forum_id
, post_count
in volume
.iteritems():
87 volume
[forum_id
] /= total_posts
92 class WritePostForm(wtforms
.Form
):
93 content
= fields
.TextAreaField('Content')
95 class WriteThreadForm(WritePostForm
):
96 subject
= fields
.TextField('Subject')
98 class ForumController(BaseController
):
101 c
.forums
= meta
.Session
.query(forum_model
.Forum
) \
102 .order_by(forum_model
.Forum
.id.asc()) \
105 # Get some forum stats. Cache them because they're a bit expensive to
106 # compute. Expire after an hour.
107 # XXX when there are admin controls, they'll need to nuke this cache
108 # when messing with the forum list
109 forum_cache
= cache
.get_cache('spline-forum', expiretime
=3600)
110 c
.forum_activity
= forum_cache
.get_value(
111 key
='forum_activity', createfunc
=get_forum_activity
)
112 c
.forum_volume
= forum_cache
.get_value(
113 key
='forum_volume', createfunc
=get_forum_volume
)
115 c
.max_volume
= max(c
.forum_volume
.itervalues()) or 1
117 # Need to know the last post for each forum, in realtime
119 last_post_subq
= meta
.Session
.query(
120 forum_model
.Forum
.id.label('forum_id'),
121 func
.max(forum_model
.Post
.posted_time
).label('posted_time'),
123 .outerjoin(forum_model
.Thread
) \
124 .outerjoin(forum_model
.Post
) \
125 .group_by(forum_model
.Forum
.id) \
127 last_post_q
= meta
.Session
.query(
129 last_post_subq
.c
.forum_id
,
133 forum_model
.Post
.posted_time
== last_post_subq
.c
.posted_time
,
136 joinedload('thread'),
137 joinedload('author'),
139 for post
, forum_id
in last_post_q
:
140 c
.last_post
[forum_id
] = post
142 return render('/forum/forums.mako')
144 def threads(self
, forum_id
):
145 c
.forum
= meta
.Session
.query(forum_model
.Forum
).get(forum_id
)
149 c
.write_thread_form
= WriteThreadForm()
151 # nb: This will never show post-less threads. Oh well!
152 last_post
= aliased(forum_model
.Post
)
153 threads_q
= c
.forum
.threads \
154 .join((last_post
, forum_model
.Thread
.last_post
)) \
155 .order_by(last_post
.posted_time
.desc()) \
157 contains_eager(forum_model
.Thread
.last_post
, alias
=last_post
),
158 joinedload('last_post.author'),
160 c
.num_threads
= threads_q
.count()
162 c
.skip
= int(request
.params
.get('skip', 0))
166 c
.threads
= threads_q
.offset(c
.skip
).limit(c
.per_page
)
168 return render('/forum/threads.mako')
170 def posts(self
, forum_id
, thread_id
):
172 c
.thread
= meta
.Session
.query(forum_model
.Thread
) \
173 .filter_by(id=thread_id
, forum_id
=forum_id
).one()
174 except NoResultFound
:
177 c
.write_post_form
= WritePostForm()
179 posts_q
= c
.thread
.posts \
180 .order_by(forum_model
.Post
.position
.asc()) \
181 .options(joinedload('author'))
182 c
.num_posts
= c
.thread
.post_count
184 c
.skip
= int(request
.params
.get('skip', 0))
188 c
.posts
= posts_q
.offset(c
.skip
).limit(c
.per_page
)
190 return render('/forum/posts.mako')
193 def write_thread(self
, forum_id
):
194 """Provides a form for posting a new thread."""
195 if not c
.user
.can('forum:create-thread'):
199 c
.forum
= meta
.Session
.query(forum_model
.Forum
) \
200 .filter_by(id=forum_id
).one()
201 except NoResultFound
:
204 c
.write_thread_form
= WriteThreadForm(request
.params
)
205 return render('/forum/write_thread.mako')
208 def write_thread_commit(self
, forum_id
):
209 """Posts a new thread."""
210 if not c
.user
.can('forum:create-thread'):
214 c
.forum
= meta
.Session
.query(forum_model
.Forum
) \
215 .filter_by(id=forum_id
).one()
216 except NoResultFound
:
219 c
.write_thread_form
= WriteThreadForm(request
.params
)
221 # Reshow the form on failure
222 if not c
.write_thread_form
.validate():
223 return render('/forum/write_thread.mako')
225 # Otherwise, add the post.
226 c
.forum
= meta
.Session
.query(forum_model
.Forum
) \
227 .with_lockmode('update') \
230 thread
= forum_model
.Thread(
231 forum_id
= c
.forum
.id,
232 subject
= c
.write_thread_form
.subject
.data
,
235 source
= c
.write_thread_form
.content
.data
236 post
= forum_model
.Post(
238 author_user_id
= c
.user
.id,
239 raw_content
= source
,
240 content
= spline
.lib
.markdown
.translate(source
),
243 thread
.posts
.append(post
)
244 c
.forum
.threads
.append(thread
)
246 meta
.Session
.commit()
248 # Redirect to the new thread
249 h
.flash("Contribution to the collective knowledge of the species successfully recorded.")
251 url(controller
='forum', action
='posts',
252 forum_id
=forum_id
, thread_id
=thread
.id),
256 def write(self
, forum_id
, thread_id
):
257 """Provides a form for posting to a thread."""
258 if not c
.user
.can('forum:create-post'):
262 c
.thread
= meta
.Session
.query(forum_model
.Thread
) \
263 .filter_by(id=thread_id
, forum_id
=forum_id
).one()
264 except NoResultFound
:
267 c
.write_post_form
= WritePostForm(request
.params
)
268 return render('/forum/write.mako')
271 def write_commit(self
, forum_id
, thread_id
):
272 """Post to a thread."""
273 if not c
.user
.can('forum:create-post'):
277 c
.thread
= meta
.Session
.query(forum_model
.Thread
) \
278 .filter_by(id=thread_id
, forum_id
=forum_id
).one()
279 except NoResultFound
:
282 c
.write_post_form
= WritePostForm(request
.params
)
284 # Reshow the form on failure
285 if not c
.write_post_form
.validate():
286 return render('/forum/write.mako')
288 # Otherwise, add the post.
289 c
.thread
= meta
.Session
.query(forum_model
.Thread
) \
290 .with_lockmode('update') \
293 source
= c
.write_post_form
.content
.data
294 post
= forum_model
.Post(
295 position
= c
.thread
.post_count
+ 1,
296 author_user_id
= c
.user
.id,
297 raw_content
= source
,
298 content
= spline
.lib
.markdown
.translate(source
),
301 c
.thread
.posts
.append(post
)
302 c
.thread
.post_count
+= 1
304 meta
.Session
.commit()
306 # Redirect to the thread
307 # XXX probably to the post instead; anchor? depends on paging scheme
308 h
.flash('Your uniqueness has been added to our own.')
310 url(controller
='forum', action
='posts',
311 forum_id
=forum_id
, thread_id
=thread_id
),