| 1 | package Vee::Controller::Forum; |
|---|
| 2 | |
|---|
| 3 | use strict; |
|---|
| 4 | use warnings; |
|---|
| 5 | use base 'Catalyst::Controller'; |
|---|
| 6 | |
|---|
| 7 | use Scalar::Util qw/weaken/; |
|---|
| 8 | use List::Util qw/min/; |
|---|
| 9 | use Vee::Utils::Forum; |
|---|
| 10 | |
|---|
| 11 | # TODO for flags: |
|---|
| 12 | # - consolidate checking for deletion/hiding and get it out of the main code |
|---|
| 13 | # - styling |
|---|
| 14 | |
|---|
| 15 | =head1 NAME |
|---|
| 16 | |
|---|
| 17 | Vee::Controller::Forum - Forum Controller |
|---|
| 18 | |
|---|
| 19 | =head1 SYNOPSIS |
|---|
| 20 | |
|---|
| 21 | See L<Vee> |
|---|
| 22 | |
|---|
| 23 | =head1 DESCRIPTION |
|---|
| 24 | |
|---|
| 25 | Catalyst Controller for forum viewing. |
|---|
| 26 | |
|---|
| 27 | =head1 METHODS |
|---|
| 28 | |
|---|
| 29 | =cut |
|---|
| 30 | |
|---|
| 31 | =head2 auto |
|---|
| 32 | |
|---|
| 33 | =cut |
|---|
| 34 | |
|---|
| 35 | sub auto : Private { |
|---|
| 36 | my ($self, $c) = @_; |
|---|
| 37 | my $s = $c->stash; |
|---|
| 38 | |
|---|
| 39 | # TODO: hacky |
|---|
| 40 | weaken $c; |
|---|
| 41 | $s->{can_post} = sub { Vee::Utils::Forum::can_post($c, @_) }; |
|---|
| 42 | } |
|---|
| 43 | |
|---|
| 44 | =head2 list |
|---|
| 45 | |
|---|
| 46 | List of forums. |
|---|
| 47 | |
|---|
| 48 | =cut |
|---|
| 49 | |
|---|
| 50 | sub list : Path : Args(0) { |
|---|
| 51 | my ($self, $c) = @_; |
|---|
| 52 | my $s = $c->stash; |
|---|
| 53 | |
|---|
| 54 | $s->{page_title} = 'Forums'; |
|---|
| 55 | $s->{link_name} = 'forum'; |
|---|
| 56 | $s->{crumbs} = [ '<a href="/forum">Forum Index</a>' ]; |
|---|
| 57 | $s->{extra_css} = 'forum'; |
|---|
| 58 | |
|---|
| 59 | $s->{announcements_rs} = $c->model('DBIC::Threads')->search_announcements; |
|---|
| 60 | $s->{forums_rs} = $c->model('DBIC::Forums')->search(undef, { prefetch => { last_post => 'user' } }); |
|---|
| 61 | |
|---|
| 62 | # Grab a list of forums with unread threads |
|---|
| 63 | if ($c->user) { |
|---|
| 64 | my @unread_ids = $c->model('DBIC::Forums')->unread_ids($c->user->obj); |
|---|
| 65 | my %unread_hash = map { $_ => 1 } @unread_ids; |
|---|
| 66 | |
|---|
| 67 | $s->{is_unread} = sub { $unread_hash{$_[0]->id} }; |
|---|
| 68 | } |
|---|
| 69 | |
|---|
| 70 | $s->{template} = 'forum/index.tt'; |
|---|
| 71 | } |
|---|
| 72 | |
|---|
| 73 | =head2 view |
|---|
| 74 | |
|---|
| 75 | Forum view: list of threads. |
|---|
| 76 | |
|---|
| 77 | =cut |
|---|
| 78 | |
|---|
| 79 | sub view : Path : Args(1) { |
|---|
| 80 | my ($self, $c, $id) = @_; |
|---|
| 81 | my $s = $c->stash; |
|---|
| 82 | |
|---|
| 83 | my $forum = $c->model('DBIC::Forums')->find($id) |
|---|
| 84 | or $c->vee_abort('There is no forum with id ', $id, '. You may have followed an old link, or might be playing with URLs.'); |
|---|
| 85 | |
|---|
| 86 | my $skip = $c->req->params->{skip} || 0; |
|---|
| 87 | my $perpage = $c->site_opts->{page_sizes}{threads}; |
|---|
| 88 | |
|---|
| 89 | $s->{page_title} = $forum->name . ' - Forums'; |
|---|
| 90 | $s->{page_header} = $forum->name; |
|---|
| 91 | $s->{link_name} = 'forum'; |
|---|
| 92 | $s->{extra_css} = 'forum'; |
|---|
| 93 | $s->{crumbs} = [ '<a href="/forum">Forum Index</a>', '<a href="/forum/'.$forum->id.'">'.$forum->name.'</a>' ]; |
|---|
| 94 | |
|---|
| 95 | $s->{announcements_rs} = $c->model('DBIC::Threads')->search_announcements; |
|---|
| 96 | $s->{forum} = $forum; |
|---|
| 97 | $s->{skip} = $skip; |
|---|
| 98 | $s->{threads_rs} = $forum->search_related('threads', undef, { |
|---|
| 99 | order_by => 'FIND_IN_SET("sticky", me.flags) > 0 DESC, last_post.time DESC', |
|---|
| 100 | prefetch => { first_post => 'user', last_post => 'user' }, |
|---|
| 101 | offset => $skip, |
|---|
| 102 | rows => $perpage, |
|---|
| 103 | } ); |
|---|
| 104 | |
|---|
| 105 | # TODO: ugly but for now I don't know the best way to clean it up |
|---|
| 106 | $s->{threads_rs} = $s->{threads_rs}->search(\ 'NOT FIND_IN_SET("deleted", me.flags)') unless $c->can_i(override_thread_deleted => $forum->id); |
|---|
| 107 | |
|---|
| 108 | # Grab a list of unread threads in this forum |
|---|
| 109 | if ($c->user) { |
|---|
| 110 | my @unread_ids = $forum->search_related('threads') |
|---|
| 111 | ->unread_ids($c->user->obj); |
|---|
| 112 | my %unread_hash = map { $_ => 1 } @unread_ids; |
|---|
| 113 | |
|---|
| 114 | $s->{is_unread} = sub { $unread_hash{$_[0]->id} }; |
|---|
| 115 | } |
|---|
| 116 | |
|---|
| 117 | # form generation stuff |
|---|
| 118 | my $reply_fields = { |
|---|
| 119 | content => { type => 'textarea', rows => '10', cols => '100' }, |
|---|
| 120 | id => { type => 'hidden' }, |
|---|
| 121 | subject => { type => 'text', maxlength => 48 }, |
|---|
| 122 | blurb => { type => 'text', maxlength => 96 }, |
|---|
| 123 | }; |
|---|
| 124 | |
|---|
| 125 | my $form = new Vee::Form( |
|---|
| 126 | id => 'reply', |
|---|
| 127 | fields => $reply_fields, |
|---|
| 128 | params => $c->req->params, |
|---|
| 129 | copy_params => 1, |
|---|
| 130 | ); |
|---|
| 131 | $form->force( id => $forum->id ); |
|---|
| 132 | |
|---|
| 133 | $s->{form} = $form; |
|---|
| 134 | $s->{template} = 'forum/view.tt'; |
|---|
| 135 | } |
|---|
| 136 | |
|---|
| 137 | =head2 thread |
|---|
| 138 | |
|---|
| 139 | Thread view: list of posts. |
|---|
| 140 | |
|---|
| 141 | =cut |
|---|
| 142 | |
|---|
| 143 | # TODO: spit out an error if we're not actually inside the thread! |
|---|
| 144 | sub thread : Local : Args(1) { |
|---|
| 145 | my ($self, $c, $id) = @_; |
|---|
| 146 | my $s = $c->stash; |
|---|
| 147 | |
|---|
| 148 | my $thread = $c->model('DBIC::Threads')->find($id, { prefetch => 'forum' }) |
|---|
| 149 | or $c->vee_abort('There is no thread with id ', $id, '.'); |
|---|
| 150 | $thread->view_count( $thread->view_count + 1 ); $thread->update; |
|---|
| 151 | |
|---|
| 152 | if ($thread->flags =~ /deleted/ and not $c->can_i(override_thread_deleted => $thread->forum->id)) { |
|---|
| 153 | $c->vee_abort('This thread has been deleted.'); |
|---|
| 154 | } |
|---|
| 155 | |
|---|
| 156 | my $filter = $c->req->params->{filter}; |
|---|
| 157 | my $skip = $c->req->params->{skip} || 0; |
|---|
| 158 | |
|---|
| 159 | my $filter_user; |
|---|
| 160 | if ($filter) { |
|---|
| 161 | $filter_user = $c->model('DBIC::Users')->search({ name => $filter })->first; |
|---|
| 162 | if ($filter_user) { |
|---|
| 163 | my $posts_shown = $c->model('DBIC::Posts')->count({ thread_id => $id, user_id => $filter_user->id }); |
|---|
| 164 | } else { |
|---|
| 165 | push @{ $s->{error_msg} }, "Can't find user " . $c->vee_cleanse($filter) . " in the database."; |
|---|
| 166 | undef $filter; |
|---|
| 167 | } |
|---|
| 168 | } |
|---|
| 169 | |
|---|
| 170 | my $perpage = $c->site_opts->{page_sizes}{posts}; |
|---|
| 171 | my $lastpage = ($skip + $perpage >= $thread->post_count); |
|---|
| 172 | my $posts_rs = $c->model('DBIC::Posts')->search( |
|---|
| 173 | { 'me.thread_id' => $id, ( $filter_user ? ( 'me.user_id' => $filter_user->id ) : () ) }, |
|---|
| 174 | { prefetch => [ 'user', { 'lastedit', 'user' } ], order_by => 'me.time ASC', offset => $skip, rows => $perpage + 1 } |
|---|
| 175 | ); |
|---|
| 176 | my $post_count = $c->model('DBIC::Posts')->count({ 'me.thread_id' => $id, ( $filter_user ? ( 'me.user_id' => $filter_user->id ) : () ) }); |
|---|
| 177 | my @flags = split /,/, $thread->flags; |
|---|
| 178 | |
|---|
| 179 | $s->{physical_lastpost} = $thread->search_related('posts', undef, { order_by => [ 'time DESC', 'id DESC' ] })->single; |
|---|
| 180 | |
|---|
| 181 | ### Last-viewed time |
|---|
| 182 | |
|---|
| 183 | if ($c->user and not $filter) { |
|---|
| 184 | my $thread_view = $c->model('DBIC::ThreadViews')->search({ |
|---|
| 185 | thread_id => $thread->id, |
|---|
| 186 | user_id => $c->user->obj->id, |
|---|
| 187 | })->single; |
|---|
| 188 | |
|---|
| 189 | if ($thread_view) { |
|---|
| 190 | $s->{last_viewed} = $thread_view->last_viewed; |
|---|
| 191 | |
|---|
| 192 | if ($s->{physical_lastpost} and |
|---|
| 193 | $thread_view->last_viewed < $s->{physical_lastpost}->time) |
|---|
| 194 | { |
|---|
| 195 | $thread_view->last_viewed( $s->{physical_lastpost}->time ); |
|---|
| 196 | $thread_view->update; |
|---|
| 197 | } |
|---|
| 198 | } elsif ($s->{physical_lastpost}) { |
|---|
| 199 | # eval'd due to very improbable but possible race condition: user |
|---|
| 200 | # loads the same thread twice simultaneously |
|---|
| 201 | eval { |
|---|
| 202 | $c->model('DBIC::ThreadViews')->create({ |
|---|
| 203 | thread_id => $thread->id, |
|---|
| 204 | user_id => $c->user->obj->id, |
|---|
| 205 | last_viewed => $s->{physical_lastpost}->time, |
|---|
| 206 | }); |
|---|
| 207 | }; |
|---|
| 208 | } |
|---|
| 209 | } |
|---|
| 210 | |
|---|
| 211 | # form generation stuff |
|---|
| 212 | my $reply_fields = { |
|---|
| 213 | content => { type => 'textarea', rows => '10', cols => '100' }, |
|---|
| 214 | id => { type => 'hidden' }, |
|---|
| 215 | }; |
|---|
| 216 | |
|---|
| 217 | my $form = new Vee::Form( |
|---|
| 218 | id => 'reply', |
|---|
| 219 | fields => $reply_fields, |
|---|
| 220 | params => $c->req->params, |
|---|
| 221 | copy_params => 1, |
|---|
| 222 | ); |
|---|
| 223 | $form->force( id => $thread->id ); |
|---|
| 224 | |
|---|
| 225 | $s->{page_title} = $thread->subject . ' - Forums'; |
|---|
| 226 | $s->{page_header} = $thread->subject; |
|---|
| 227 | $s->{link_name} = 'forum'; |
|---|
| 228 | $s->{crumbs} = [ '<a href="/forum">Forum Index</a>', '<a href="/forum/'.$thread->forum->id.'">'.$thread->forum->name.'</a>', qq'<a href="/forum/thread/'.$thread->id.'">'.$thread->subject.'</a>' ]; |
|---|
| 229 | if ($filter_user) { push @{ $s->{crumbs} }, '<a href="/user/'.$filter_user->id.'">'.$filter_user->name.'</a>\'s posts' } |
|---|
| 230 | $s->{extra_css} = [ 'forum', 'bbcode' ]; |
|---|
| 231 | $s->{form} = $form; |
|---|
| 232 | $s->{flags} = \@flags; |
|---|
| 233 | |
|---|
| 234 | $s->{forum} = $thread->forum; |
|---|
| 235 | $s->{thread} = $thread; |
|---|
| 236 | $s->{page_islast} = $lastpage; |
|---|
| 237 | $s->{posts_rs} = $posts_rs; |
|---|
| 238 | $s->{post_count} = $post_count; |
|---|
| 239 | $s->{skip} = $skip; |
|---|
| 240 | $s->{filter} = $filter; |
|---|
| 241 | |
|---|
| 242 | $s->{template} = 'forum/thread/view.tt'; |
|---|
| 243 | } |
|---|
| 244 | |
|---|
| 245 | =head2 post |
|---|
| 246 | |
|---|
| 247 | Post "view": link to the corresponding post. |
|---|
| 248 | |
|---|
| 249 | =cut |
|---|
| 250 | |
|---|
| 251 | sub post : Local : Args(1) { |
|---|
| 252 | my ($self, $c, $id) = @_; |
|---|
| 253 | |
|---|
| 254 | my $post = $c->model('DBIC::Posts')->find($id, { prefetch => { thread => 'forum' } }) |
|---|
| 255 | or $c->vee_abort('There is no post with id ', $id, '.'); |
|---|
| 256 | |
|---|
| 257 | # slurp up the number of (visible) posts before the requested one |
|---|
| 258 | my $offset = $c->model('DBIC::Posts')->count({ |
|---|
| 259 | thread_id => $post->thread_id, |
|---|
| 260 | -or => [ |
|---|
| 261 | { time => $post->time, id => { '<', $post->id } }, |
|---|
| 262 | { time => { '<', $post->time } }, |
|---|
| 263 | ], |
|---|
| 264 | }); |
|---|
| 265 | |
|---|
| 266 | my $perpage = $c->site_opts->{page_sizes}{posts}; |
|---|
| 267 | my $skip = $offset - $offset % $perpage; |
|---|
| 268 | $c->res->redirect( $c->uri('Forum', 'thread', $post->thread_id, ($skip ? { skip => $skip } : ()) ) . "#p$id" ); |
|---|
| 269 | } |
|---|
| 270 | |
|---|
| 271 | =head2 flags |
|---|
| 272 | |
|---|
| 273 | Set flags for a thread. |
|---|
| 274 | |
|---|
| 275 | =cut |
|---|
| 276 | |
|---|
| 277 | sub thread_flags : LocalRegex('^thread/(\d*)/(announcement|sticky|locked)') : Args(0) { |
|---|
| 278 | my ($self, $c) = @_; |
|---|
| 279 | my ($thread_id, $flag) = @{ $c->req->captures }; |
|---|
| 280 | |
|---|
| 281 | my $thread = $c->model('DBIC::Threads')->find($thread_id); |
|---|
| 282 | |
|---|
| 283 | toggleflag($thread, $flag) |
|---|
| 284 | or $c->vee_abort("Whoops! Error setting flag '", $flag, "'. Not much more can be said about this."); |
|---|
| 285 | $c->res->redirect($c->uri('Forum', 'thread', $thread_id)); |
|---|
| 286 | } |
|---|
| 287 | |
|---|
| 288 | =head2 mark_read |
|---|
| 289 | |
|---|
| 290 | Mark everything on the whole forum read. |
|---|
| 291 | |
|---|
| 292 | =cut |
|---|
| 293 | |
|---|
| 294 | sub mark_read : Local : Args(0) { |
|---|
| 295 | my ($self, $c) = @_; |
|---|
| 296 | |
|---|
| 297 | if ($c->req->method ne 'POST') { |
|---|
| 298 | $c->vee_abort('This URL may only be requested by POST.'); |
|---|
| 299 | } elsif (not $c->user) { |
|---|
| 300 | $c->vee_abort('You must be logged in to use this function.'); |
|---|
| 301 | } |
|---|
| 302 | |
|---|
| 303 | $c->user->thread_view_cutoff(time); |
|---|
| 304 | $c->user->update; |
|---|
| 305 | |
|---|
| 306 | $c->model('DBIC::ThreadViews')->search({ |
|---|
| 307 | user_id => $c->user->obj->id |
|---|
| 308 | })->delete; |
|---|
| 309 | |
|---|
| 310 | $c->res->redirect( $c->req->referer || $c->uri_for('/') ); |
|---|
| 311 | } |
|---|
| 312 | |
|---|
| 313 | =head1 AUTHOR |
|---|
| 314 | |
|---|
| 315 | Maintainer: Alex "Eevee" Munroe (C<veekun@veekun.com>) |
|---|
| 316 | |
|---|
| 317 | See the included F<AUTHORS> file for a full list of contributers. |
|---|
| 318 | |
|---|
| 319 | =head1 LICENSE |
|---|
| 320 | |
|---|
| 321 | See the included F<LICENSE> file. |
|---|
| 322 | |
|---|
| 323 | =cut |
|---|
| 324 | |
|---|
| 325 | 1; |
|---|