5 from pylons
import cache
, config
, request
, response
, session
, tmpl_context
as c
, url
6 from pylons
.controllers
.util
import abort
, redirect
7 from routes
import request_config
8 from sqlalchemy
.orm
import joinedload
9 from sqlalchemy
.orm
.exc
import NoResultFound
10 from sqlalchemy
.sql
import func
12 from wtforms
import fields
14 from spline
.model
import meta
15 from spline
.lib
import helpers
as h
16 from spline
.lib
.base
import BaseController
, render
17 import spline
.lib
.markdown
18 from splinext
.forum
import model
as forum_model
20 log
= logging
.getLogger(__name__
)
23 def forum_activity_score(forum
):
24 """Returns a number representing how active a forum is, based on the past
27 The calculation is arbitrary, but 0 is supposed to mean "dead" and 1 is
28 supposed to mean "healthy".
30 cutoff
= datetime
.datetime
.now() - datetime
.timedelta(days
=7)
32 post_count
= meta
.Session
.query(forum_model
.Post
) \
33 .join(forum_model
.Post
.thread
) \
34 .filter(forum_model
.Thread
.forum
== forum
) \
35 .filter(forum_model
.Post
.posted_time
>= cutoff
) \
38 # Avoid domain errors!
42 # The log is to scale 0 posts to 0.0, and 168 posts to 1.0.
43 # The square is really just to take the edge off the log curve; it
44 # accelerates to 1 very quickly, then slows considerably after that.
45 # Squaring helps both of these problems.
46 score
= (math
.log(post_count
) / math
.log(168)) ** 2
48 # TODO more threads and more new threads should boost the score slightly
52 def get_forum_activity():
53 """Returns a hash mapping forum ids to their level of 'activity'."""
54 forums_q
= meta
.Session
.query(forum_model
.Forum
)
57 for forum
in forums_q
:
58 activity
[forum
.id] = forum_activity_score(forum
)
62 def get_forum_volume():
63 """Returns a hash mapping forum ids to the percentage of all posts that
66 # Do a complicated-ass subquery to get a list of forums and postcounts
67 volume_q
= meta
.Session
.query(
68 forum_model
.Forum
.id.label('forum_id'),
69 func
.count(forum_model
.Post
.id).label('post_count'),
71 .outerjoin(forum_model
.Thread
) \
72 .outerjoin(forum_model
.Post
) \
73 .group_by(forum_model
.Forum
.id)
75 # Stick this into a hash, and count the number of total posts
78 for forum_id
, post_count
in volume_q
:
79 post_count
= float(post_count
or 0)
80 volume
[forum_id
] = post_count
81 total_posts
+= post_count
83 # Divide, to get a percentage
85 for forum_id
, post_count
in volume
.iteritems():
86 volume
[forum_id
] /= total_posts
91 class WritePostForm(wtforms
.Form
):
92 content
= fields
.TextAreaField('Content')
94 class WriteThreadForm(WritePostForm
):
95 subject
= fields
.TextField('Subject')
97 class ForumController(BaseController
):
100 c
.forums
= meta
.Session
.query(forum_model
.Forum
) \
101 .order_by(forum_model
.Forum
.id.asc()) \
104 # Get some forum stats. Cache them because they're a bit expensive to
105 # compute. Expire after an hour.
106 # XXX when there are admin controls, they'll need to nuke this cache
107 # when messing with the forum list
108 forum_cache
= cache
.get_cache('spline-forum', expiretime
=3600)
109 c
.forum_activity
= forum_cache
.get_value(
110 key
='forum_activity', createfunc
=get_forum_activity
)
111 c
.forum_volume
= forum_cache
.get_value(
112 key
='forum_volume', createfunc
=get_forum_volume
)
114 c
.max_volume
= max(c
.forum_volume
.itervalues()) or 1
116 # Need to know the last post for each forum, in realtime
118 last_post_subq
= meta
.Session
.query(
119 forum_model
.Forum
.id.label('forum_id'),
120 func
.max(forum_model
.Post
.posted_time
).label('posted_time'),
122 .outerjoin(forum_model
.Thread
) \
123 .outerjoin(forum_model
.Post
) \
124 .group_by(forum_model
.Forum
.id) \
126 last_post_q
= meta
.Session
.query(
128 last_post_subq
.c
.forum_id
,
132 forum_model
.Post
.posted_time
== last_post_subq
.c
.posted_time
,
135 joinedload('thread'),
136 joinedload('author'),
138 for post
, forum_id
in last_post_q
:
139 c
.last_post
[forum_id
] = post
141 return render('/forum/forums.mako')
143 def threads(self
, forum_id
):
144 c
.forum
= meta
.Session
.query(forum_model
.Forum
).get(forum_id
)
148 c
.write_thread_form
= WriteThreadForm()
150 # nb: This will never show post-less threads. Oh well!
151 threads_q
= c
.forum
.threads \
152 .join(forum_model
.Thread
.last_post
) \
153 .order_by(forum_model
.Post
.posted_time
.desc()) \
155 joinedload('last_post'),
156 joinedload('last_post.author'),
158 c
.num_threads
= threads_q
.count()
160 c
.skip
= int(request
.params
.get('skip', 0))
164 c
.threads
= threads_q
.offset(c
.skip
).limit(c
.per_page
)
166 return render('/forum/threads.mako')
168 def posts(self
, forum_id
, thread_id
):
170 c
.thread
= meta
.Session
.query(forum_model
.Thread
) \
171 .filter_by(id=thread_id
, forum_id
=forum_id
).one()
172 except NoResultFound
:
175 c
.write_post_form
= WritePostForm()
177 posts_q
= c
.thread
.posts \
178 .order_by(forum_model
.Post
.position
.asc()) \
179 .options(joinedload('author'))
180 c
.num_posts
= c
.thread
.post_count
182 c
.skip
= int(request
.params
.get('skip', 0))
186 c
.posts
= posts_q
.offset(c
.skip
).limit(c
.per_page
)
188 return render('/forum/posts.mako')
191 def write_thread(self
, forum_id
):
192 """Provides a form for posting a new thread."""
193 if not c
.user
.can('forum:create-thread'):
197 c
.forum
= meta
.Session
.query(forum_model
.Forum
) \
198 .filter_by(id=forum_id
).one()
199 except NoResultFound
:
202 c
.write_thread_form
= WriteThreadForm(request
.params
)
204 if request
.method
!= 'POST' or not c
.write_thread_form
.validate():
205 # Failure or initial request; show the form
206 return render('/forum/write_thread.mako')
209 # Otherwise, add the post.
210 c
.forum
= meta
.Session
.query(forum_model
.Forum
) \
211 .with_lockmode('update') \
214 thread
= forum_model
.Thread(
215 forum_id
= c
.forum
.id,
216 subject
= c
.write_thread_form
.subject
.data
,
219 source
= c
.write_thread_form
.content
.data
220 post
= forum_model
.Post(
222 author_user_id
= c
.user
.id,
223 raw_content
= source
,
224 content
= spline
.lib
.markdown
.translate(source
),
227 thread
.posts
.append(post
)
228 c
.forum
.threads
.append(thread
)
230 meta
.Session
.commit()
232 # Redirect to the new thread
233 h
.flash("Contribution to the collective knowledge of the species successfully recorded.")
235 url(controller
='forum', action
='posts',
236 forum_id
=forum_id
, thread_id
=thread
.id),
240 def write(self
, forum_id
, thread_id
):
241 """Provides a form for posting to a thread."""
242 if not c
.user
.can('forum:create-post'):
246 c
.thread
= meta
.Session
.query(forum_model
.Thread
) \
247 .filter_by(id=thread_id
, forum_id
=forum_id
).one()
248 except NoResultFound
:
251 c
.write_post_form
= WritePostForm(request
.params
)
253 if request
.method
!= 'POST' or not c
.write_post_form
.validate():
254 # Failure or initial request; show the form
255 return render('/forum/write.mako')
258 # Otherwise, add the post.
259 c
.thread
= meta
.Session
.query(forum_model
.Thread
) \
260 .with_lockmode('update') \
263 source
= c
.write_post_form
.content
.data
264 post
= forum_model
.Post(
265 position
= c
.thread
.post_count
+ 1,
266 author_user_id
= c
.user
.id,
267 raw_content
= source
,
268 content
= spline
.lib
.markdown
.translate(source
),
271 c
.thread
.posts
.append(post
)
272 c
.thread
.post_count
+= 1
274 meta
.Session
.commit()
276 # Redirect to the thread
277 # XXX probably to the post instead; anchor? depends on paging scheme
278 h
.flash('Your uniqueness has been added to our own.')
280 url(controller
='forum', action
='posts',
281 forum_id
=forum_id
, thread_id
=thread_id
),