Crash fix: allow creating threads, oops.
[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 c.threads = c.forum.threads.options(
151 joinedload('last_post'),
152 joinedload('last_post.author'),
153 )
154
155 return render('/forum/threads.mako')
156
157 def posts(self, forum_id, thread_id):
158 try:
159 c.thread = meta.Session.query(forum_model.Thread) \
160 .filter_by(id=thread_id, forum_id=forum_id).one()
161 except NoResultFound:
162 abort(404)
163
164 c.write_post_form = WritePostForm()
165
166 return render('/forum/posts.mako')
167
168
169 def write_thread(self, forum_id):
170 """Provides a form for posting a new thread."""
171 if not c.user.can('forum:create-thread'):
172 abort(403)
173
174 try:
175 c.forum = meta.Session.query(forum_model.Forum) \
176 .filter_by(id=forum_id).one()
177 except NoResultFound:
178 abort(404)
179
180 c.write_thread_form = WriteThreadForm(request.params)
181
182 if request.method != 'POST' or not c.write_thread_form.validate():
183 # Failure or initial request; show the form
184 return render('/forum/write_thread.mako')
185
186
187 # Otherwise, add the post.
188 c.forum = meta.Session.query(forum_model.Forum) \
189 .with_lockmode('update') \
190 .get(c.forum.id)
191
192 thread = forum_model.Thread(
193 forum_id = c.forum.id,
194 subject = c.write_thread_form.subject.data,
195 post_count = 1,
196 )
197 source = c.write_thread_form.content.data
198 post = forum_model.Post(
199 position = 1,
200 author_user_id = c.user.id,
201 raw_content = source,
202 content = spline.lib.markdown.translate(source),
203 )
204
205 thread.posts.append(post)
206 c.forum.threads.append(thread)
207
208 meta.Session.commit()
209
210 # Redirect to the new thread
211 h.flash("Contribution to the collective knowledge of the species successfully recorded.")
212 redirect(
213 url(controller='forum', action='posts',
214 forum_id=forum_id, thread_id=thread.id),
215 code=303,
216 )
217
218 def write(self, forum_id, thread_id):
219 """Provides a form for posting to a thread."""
220 if not c.user.can('forum:create-post'):
221 abort(403)
222
223 try:
224 c.thread = meta.Session.query(forum_model.Thread) \
225 .filter_by(id=thread_id, forum_id=forum_id).one()
226 except NoResultFound:
227 abort(404)
228
229 c.write_post_form = WritePostForm(request.params)
230
231 if request.method != 'POST' or not c.write_post_form.validate():
232 # Failure or initial request; show the form
233 return render('/forum/write.mako')
234
235
236 # Otherwise, add the post.
237 c.thread = meta.Session.query(forum_model.Thread) \
238 .with_lockmode('update') \
239 .get(c.thread.id)
240
241 source = c.write_post_form.content.data
242 post = forum_model.Post(
243 position = c.thread.post_count + 1,
244 author_user_id = c.user.id,
245 raw_content = source,
246 content = spline.lib.markdown.translate(source),
247 )
248
249 c.thread.posts.append(post)
250 c.thread.post_count += 1
251
252 meta.Session.commit()
253
254 # Redirect to the thread
255 # XXX probably to the post instead; anchor? depends on paging scheme
256 h.flash('Your uniqueness has been added to our own.')
257 redirect(
258 url(controller='forum', action='posts',
259 forum_id=forum_id, thread_id=thread_id),
260 code=303,
261 )