0d1207f4675126cf147aea547743daafa5ee6311
[zzz-spline-forum.git] / splinext / forum / controllers / forum.py
1 import datetime
2 import logging
3 import math
4
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
11 import wtforms
12 from wtforms import fields
13
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
19
20 log = logging.getLogger(__name__)
21
22
23 def forum_activity_score(forum):
24 """Returns a number representing how active a forum is, based on the past
25 week.
26
27 The calculation is arbitrary, but 0 is supposed to mean "dead" and 1 is
28 supposed to mean "healthy".
29 """
30 cutoff = datetime.datetime.now() - datetime.timedelta(days=7)
31
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) \
36 .count()
37
38 # Avoid domain errors!
39 if not post_count:
40 return 0.0
41
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
47
48 # TODO more threads and more new threads should boost the score slightly
49
50 return score
51
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)
55
56 activity = {}
57 for forum in forums_q:
58 activity[forum.id] = forum_activity_score(forum)
59
60 return activity
61
62 def get_forum_volume():
63 """Returns a hash mapping forum ids to the percentage of all posts that
64 reside in that forum.
65 """
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'),
70 ) \
71 .outerjoin(forum_model.Thread) \
72 .outerjoin(forum_model.Post) \
73 .group_by(forum_model.Forum.id)
74
75 # Stick this into a hash, and count the number of total posts
76 total_posts = 0
77 volume = {}
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
82
83 # Divide, to get a percentage
84 if total_posts:
85 for forum_id, post_count in volume.iteritems():
86 volume[forum_id] /= total_posts
87
88 return volume
89
90
91 class WritePostForm(wtforms.Form):
92 content = fields.TextAreaField('Content')
93
94 class WriteThreadForm(WritePostForm):
95 subject = fields.TextField('Subject')
96
97 class ForumController(BaseController):
98
99 def forums(self):
100 c.forums = meta.Session.query(forum_model.Forum) \
101 .order_by(forum_model.Forum.id.asc()) \
102 .all()
103
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)
113
114 c.max_volume = max(c.forum_volume.itervalues()) or 1
115
116 # Need to know the last post for each forum, in realtime
117 c.last_post = {}
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'),
121 ) \
122 .outerjoin(forum_model.Thread) \
123 .outerjoin(forum_model.Post) \
124 .group_by(forum_model.Forum.id) \
125 .subquery()
126 last_post_q = meta.Session.query(
127 forum_model.Post,
128 last_post_subq.c.forum_id,
129 ) \
130 .join((
131 last_post_subq,
132 forum_model.Post.posted_time == last_post_subq.c.posted_time,
133 )) \
134 .options(
135 joinedload('thread'),
136 joinedload('author'),
137 )
138 for post, forum_id in last_post_q:
139 c.last_post[forum_id] = post
140
141 return render('/forum/forums.mako')
142
143 def threads(self, forum_id):
144 c.forum = meta.Session.query(forum_model.Forum).get(forum_id)
145 if not c.forum:
146 abort(404)
147
148 c.write_thread_form = WriteThreadForm()
149
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()) \
154 .options(joinedload('last_post.author'))
155 c.num_threads = threads_q.count()
156 try:
157 c.skip = int(request.params.get('skip', 0))
158 except ValueError:
159 abort(404)
160 c.per_page = 89
161 c.threads = threads_q.offset(c.skip).limit(c.per_page)
162
163 return render('/forum/threads.mako')
164
165 def posts(self, forum_id, thread_id):
166 try:
167 c.thread = meta.Session.query(forum_model.Thread) \
168 .filter_by(id=thread_id, forum_id=forum_id).one()
169 except NoResultFound:
170 abort(404)
171
172 c.write_post_form = WritePostForm()
173
174 posts_q = c.thread.posts \
175 .order_by(forum_model.Post.position.asc()) \
176 .options(joinedload('author'))
177 c.num_posts = c.thread.post_count
178 try:
179 c.skip = int(request.params.get('skip', 0))
180 except ValueError:
181 abort(404)
182 c.per_page = 89
183 c.posts = posts_q.offset(c.skip).limit(c.per_page)
184
185 return render('/forum/posts.mako')
186
187
188 def write_thread(self, forum_id):
189 """Provides a form for posting a new thread."""
190 if not c.user.can('forum:create-thread'):
191 abort(403)
192
193 try:
194 c.forum = meta.Session.query(forum_model.Forum) \
195 .filter_by(id=forum_id).one()
196 except NoResultFound:
197 abort(404)
198
199 c.write_thread_form = WriteThreadForm(request.params)
200
201 if request.method != 'POST' or not c.write_thread_form.validate():
202 # Failure or initial request; show the form
203 return render('/forum/write_thread.mako')
204
205
206 # Otherwise, add the post.
207 c.forum = meta.Session.query(forum_model.Forum) \
208 .with_lockmode('update') \
209 .get(c.forum.id)
210
211 thread = forum_model.Thread(
212 forum_id = c.forum.id,
213 subject = c.write_thread_form.subject.data,
214 post_count = 1,
215 )
216 source = c.write_thread_form.content.data
217 post = forum_model.Post(
218 position = 1,
219 author_user_id = c.user.id,
220 raw_content = source,
221 content = spline.lib.markdown.translate(source),
222 )
223
224 thread.posts.append(post)
225 c.forum.threads.append(thread)
226
227 meta.Session.commit()
228
229 # Redirect to the new thread
230 h.flash("Contribution to the collective knowledge of the species successfully recorded.")
231 redirect(
232 url(controller='forum', action='posts',
233 forum_id=forum_id, thread_id=thread.id),
234 code=303,
235 )
236
237 def write(self, forum_id, thread_id):
238 """Provides a form for posting to a thread."""
239 if not c.user.can('forum:create-post'):
240 abort(403)
241
242 try:
243 c.thread = meta.Session.query(forum_model.Thread) \
244 .filter_by(id=thread_id, forum_id=forum_id).one()
245 except NoResultFound:
246 abort(404)
247
248 c.write_post_form = WritePostForm(request.params)
249
250 if request.method != 'POST' or not c.write_post_form.validate():
251 # Failure or initial request; show the form
252 return render('/forum/write.mako')
253
254
255 # Otherwise, add the post.
256 c.thread = meta.Session.query(forum_model.Thread) \
257 .with_lockmode('update') \
258 .get(c.thread.id)
259
260 source = c.write_post_form.content.data
261 post = forum_model.Post(
262 position = c.thread.post_count + 1,
263 author_user_id = c.user.id,
264 raw_content = source,
265 content = spline.lib.markdown.translate(source),
266 )
267
268 c.thread.posts.append(post)
269 c.thread.post_count += 1
270
271 meta.Session.commit()
272
273 # Redirect to the thread
274 # XXX probably to the post instead; anchor? depends on paging scheme
275 h.flash('Your uniqueness has been added to our own.')
276 redirect(
277 url(controller='forum', action='posts',
278 forum_id=forum_id, thread_id=thread_id),
279 code=303,
280 )