annotate gpp/forums/views.py @ 169:7071b196ddd5

Implement #54; add a forum query to display a user's posts.
author Brian Neal <bgneal@gmail.com>
date Sun, 31 Jan 2010 04:52:08 +0000
parents e6d4dfdfbc64
children 6f14970b103a
rev   line source
bgneal@81 1 """
bgneal@81 2 Views for the forums application.
bgneal@81 3 """
bgneal@113 4 import datetime
bgneal@113 5
bgneal@83 6 from django.contrib.auth.decorators import login_required
bgneal@82 7 from django.http import Http404
bgneal@98 8 from django.http import HttpResponse
bgneal@89 9 from django.http import HttpResponseBadRequest
bgneal@90 10 from django.http import HttpResponseForbidden
bgneal@83 11 from django.http import HttpResponseRedirect
bgneal@83 12 from django.core.urlresolvers import reverse
bgneal@91 13 from django.core.paginator import InvalidPage
bgneal@82 14 from django.shortcuts import get_object_or_404
bgneal@81 15 from django.shortcuts import render_to_response
bgneal@169 16 from django.shortcuts import redirect
bgneal@97 17 from django.template.loader import render_to_string
bgneal@81 18 from django.template import RequestContext
bgneal@89 19 from django.views.decorators.http import require_POST
bgneal@108 20 from django.utils.text import wrap
bgneal@81 21
bgneal@90 22 from core.paginator import DiggPaginator
bgneal@98 23 from core.functions import email_admins
bgneal@113 24 from forums.models import Forum, Topic, Post, FlaggedPost, TopicLastVisit, \
bgneal@113 25 ForumLastVisit
bgneal@115 26 from forums.forms import NewTopicForm, NewPostForm, PostForm, MoveTopicForm, \
bgneal@115 27 SplitTopicForm
bgneal@114 28 from forums.unread import get_forum_unread_status, get_topic_unread_status, \
bgneal@167 29 get_post_unread_status, get_unread_topics
bgneal@81 30
bgneal@117 31 from bio.models import UserProfile
bgneal@90 32 #######################################################################
bgneal@90 33
bgneal@93 34 TOPICS_PER_PAGE = 50
bgneal@113 35 POSTS_PER_PAGE = 20
bgneal@90 36
bgneal@167 37
bgneal@167 38 def get_page_num(request):
bgneal@167 39 """Returns the value of the 'page' variable in GET if it exists, or 1
bgneal@167 40 if it does not."""
bgneal@167 41
bgneal@167 42 try:
bgneal@167 43 page_num = int(request.GET.get('page', 1))
bgneal@167 44 except ValueError:
bgneal@167 45 page_num = 1
bgneal@167 46
bgneal@167 47 return page_num
bgneal@167 48
bgneal@167 49
bgneal@93 50 def create_topic_paginator(topics):
bgneal@93 51 return DiggPaginator(topics, TOPICS_PER_PAGE, body=5, tail=2, margin=3, padding=2)
bgneal@93 52
bgneal@93 53 def create_post_paginator(posts):
bgneal@93 54 return DiggPaginator(posts, POSTS_PER_PAGE, body=5, tail=2, margin=3, padding=2)
bgneal@90 55
bgneal@167 56
bgneal@167 57 def attach_topic_page_ranges(topics):
bgneal@167 58 """Attaches a page_range attribute to each topic in the supplied list.
bgneal@167 59 This attribute will be None if it is a single page topic. This is used
bgneal@167 60 by the templates to generate "goto page x" links.
bgneal@167 61 """
bgneal@167 62 for topic in topics:
bgneal@167 63 if topic.post_count > POSTS_PER_PAGE:
bgneal@167 64 pp = DiggPaginator(range(topic.post_count), POSTS_PER_PAGE,
bgneal@167 65 body=2, tail=3, margin=1)
bgneal@167 66 topic.page_range = pp.page(1).page_range
bgneal@167 67 else:
bgneal@167 68 topic.page_range = None
bgneal@167 69
bgneal@90 70 #######################################################################
bgneal@81 71
bgneal@81 72 def index(request):
bgneal@82 73 """
bgneal@82 74 This view displays all the forums available, ordered in each category.
bgneal@82 75 """
bgneal@167 76 # check for special forum queries
bgneal@167 77 query = request.GET.get("query")
bgneal@167 78 if query == "unread":
bgneal@169 79 return redirect('forums-unread_topics')
bgneal@168 80 elif query == "unanswered":
bgneal@169 81 return redirect('forums-unanswered_topics')
bgneal@169 82 elif query == "mine":
bgneal@169 83 return redirect('forums-my_posts')
bgneal@167 84
bgneal@100 85 forums = Forum.objects.forums_for_user(request.user)
bgneal@113 86 get_forum_unread_status(forums, request.user)
bgneal@81 87 cats = {}
bgneal@81 88 for forum in forums:
bgneal@81 89 cat = cats.setdefault(forum.category.id, {
bgneal@81 90 'cat': forum.category,
bgneal@81 91 'forums': [],
bgneal@81 92 })
bgneal@81 93 cat['forums'].append(forum)
bgneal@81 94
bgneal@81 95 cmpdef = lambda a, b: cmp(a['cat'].position, b['cat'].position)
bgneal@81 96 cats = sorted(cats.values(), cmpdef)
bgneal@81 97
bgneal@81 98 return render_to_response('forums/index.html', {
bgneal@81 99 'cats': cats,
bgneal@81 100 },
bgneal@81 101 context_instance=RequestContext(request))
bgneal@81 102
bgneal@82 103
bgneal@81 104 def forum_index(request, slug):
bgneal@82 105 """
bgneal@82 106 Displays all the topics in a forum.
bgneal@82 107 """
bgneal@101 108 forum = get_object_or_404(Forum.objects.select_related(), slug=slug)
bgneal@100 109
bgneal@100 110 if not forum.category.can_access(request.user):
bgneal@100 111 return HttpResponseForbidden()
bgneal@100 112
bgneal@107 113 topics = forum.topics.select_related('user', 'last_post', 'last_post__user')
bgneal@114 114 get_topic_unread_status(forum, topics, request.user)
bgneal@114 115
bgneal@93 116 paginator = create_topic_paginator(topics)
bgneal@167 117 page_num = get_page_num(request)
bgneal@93 118 try:
bgneal@93 119 page = paginator.page(page_num)
bgneal@93 120 except InvalidPage:
bgneal@93 121 raise Http404
bgneal@97 122
bgneal@167 123 attach_topic_page_ranges(page.object_list)
bgneal@161 124
bgneal@97 125 # we do this for the template since it is rendered twice
bgneal@97 126 page_nav = render_to_string('forums/pagination.html', {'page': page})
bgneal@111 127
bgneal@111 128 can_moderate = _can_moderate(forum, request.user)
bgneal@82 129
bgneal@82 130 return render_to_response('forums/forum_index.html', {
bgneal@82 131 'forum': forum,
bgneal@93 132 'page': page,
bgneal@97 133 'page_nav': page_nav,
bgneal@111 134 'can_moderate': can_moderate,
bgneal@82 135 },
bgneal@82 136 context_instance=RequestContext(request))
bgneal@82 137
bgneal@82 138
bgneal@82 139 def topic_index(request, id):
bgneal@82 140 """
bgneal@82 141 Displays all the posts in a topic.
bgneal@82 142 """
bgneal@101 143 topic = get_object_or_404(Topic.objects.select_related(), pk=id)
bgneal@100 144
bgneal@100 145 if not topic.forum.category.can_access(request.user):
bgneal@100 146 return HttpResponseForbidden()
bgneal@100 147
bgneal@86 148 topic.view_count += 1
bgneal@86 149 topic.save()
bgneal@86 150
bgneal@86 151 posts = topic.posts.select_related()
bgneal@114 152
bgneal@93 153 paginator = create_post_paginator(posts)
bgneal@167 154 page_num = get_page_num(request)
bgneal@90 155 try:
bgneal@90 156 page = paginator.page(page_num)
bgneal@90 157 except InvalidPage:
bgneal@90 158 raise Http404
bgneal@117 159 get_post_unread_status(topic, page.object_list, request.user)
bgneal@117 160
bgneal@117 161 # Attach user profiles to each post to avoid using get_user_profile() in
bgneal@117 162 # the template.
bgneal@117 163 users = set(post.user.id for post in page.object_list)
bgneal@117 164
bgneal@117 165 profiles = UserProfile.objects.filter(user__id__in=users).select_related()
bgneal@117 166 user_profiles = dict((profile.user.id, profile) for profile in profiles)
bgneal@117 167
bgneal@117 168 for post in page.object_list:
bgneal@117 169 post.user_profile = user_profiles[post.user.id]
bgneal@90 170
bgneal@90 171 last_page = page_num == paginator.num_pages
bgneal@86 172
bgneal@113 173 if request.user.is_authenticated() and last_page:
bgneal@113 174 _update_last_visit(request.user, topic)
bgneal@113 175
bgneal@97 176 # we do this for the template since it is rendered twice
bgneal@97 177 page_nav = render_to_string('forums/pagination.html', {'page': page})
bgneal@97 178
bgneal@109 179 can_moderate = _can_moderate(topic.forum, request.user)
bgneal@104 180
bgneal@104 181 can_reply = request.user.is_authenticated() and (
bgneal@104 182 not topic.locked or can_moderate)
bgneal@104 183
bgneal@86 184 return render_to_response('forums/topic.html', {
bgneal@86 185 'forum': topic.forum,
bgneal@86 186 'topic': topic,
bgneal@90 187 'page': page,
bgneal@97 188 'page_nav': page_nav,
bgneal@87 189 'last_page': last_page,
bgneal@104 190 'can_moderate': can_moderate,
bgneal@104 191 'can_reply': can_reply,
bgneal@106 192 'form': NewPostForm(initial={'topic_id': topic.id}),
bgneal@86 193 },
bgneal@86 194 context_instance=RequestContext(request))
bgneal@83 195
bgneal@83 196
bgneal@83 197 @login_required
bgneal@83 198 def new_topic(request, slug):
bgneal@83 199 """
bgneal@83 200 This view handles the creation of new topics.
bgneal@83 201 """
bgneal@101 202 forum = get_object_or_404(Forum.objects.select_related(), slug=slug)
bgneal@100 203
bgneal@100 204 if not forum.category.can_access(request.user):
bgneal@100 205 return HttpResponseForbidden()
bgneal@100 206
bgneal@83 207 if request.method == 'POST':
bgneal@102 208 form = NewTopicForm(request.user, forum, request.POST)
bgneal@83 209 if form.is_valid():
bgneal@102 210 topic = form.save(request.META.get("REMOTE_ADDR"))
bgneal@108 211 _bump_post_count(request.user)
bgneal@83 212 return HttpResponseRedirect(reverse('forums-new_topic_thanks',
bgneal@83 213 kwargs={'tid': topic.pk}))
bgneal@83 214 else:
bgneal@102 215 form = NewTopicForm(request.user, forum)
bgneal@83 216
bgneal@83 217 return render_to_response('forums/new_topic.html', {
bgneal@83 218 'forum': forum,
bgneal@83 219 'form': form,
bgneal@83 220 },
bgneal@83 221 context_instance=RequestContext(request))
bgneal@83 222
bgneal@83 223
bgneal@83 224 @login_required
bgneal@83 225 def new_topic_thanks(request, tid):
bgneal@83 226 """
bgneal@83 227 This view displays the success page for a newly created topic.
bgneal@83 228 """
bgneal@101 229 topic = get_object_or_404(Topic.objects.select_related(), pk=tid)
bgneal@83 230 return render_to_response('forums/new_topic_thanks.html', {
bgneal@83 231 'forum': topic.forum,
bgneal@83 232 'topic': topic,
bgneal@83 233 },
bgneal@83 234 context_instance=RequestContext(request))
bgneal@89 235
bgneal@89 236
bgneal@89 237 @require_POST
bgneal@89 238 def quick_reply_ajax(request):
bgneal@89 239 """
bgneal@89 240 This function handles the quick reply to a thread function. This
bgneal@89 241 function is meant to be the target of an AJAX post, and returns
bgneal@89 242 the HTML for the new post, which the client-side script appends
bgneal@89 243 to the document.
bgneal@89 244 """
bgneal@90 245 if not request.user.is_authenticated():
bgneal@108 246 return HttpResponseForbidden('Please login or register to post.')
bgneal@90 247
bgneal@106 248 form = NewPostForm(request.POST)
bgneal@89 249 if form.is_valid():
bgneal@108 250 if not _can_post_in_topic(form.topic, request.user):
bgneal@108 251 return HttpResponseForbidden("You don't have permission to post in this topic.")
bgneal@100 252
bgneal@108 253 post = form.save(request.user, request.META.get("REMOTE_ADDR", ""))
bgneal@114 254 post.unread = True
bgneal@122 255 post.user_profile = request.user.get_profile()
bgneal@108 256 _bump_post_count(request.user)
bgneal@113 257 _update_last_visit(request.user, form.topic)
bgneal@89 258 return render_to_response('forums/display_post.html', {
bgneal@89 259 'post': post,
bgneal@113 260 'can_moderate': _can_moderate(form.topic.forum, request.user),
bgneal@120 261 'can_reply': True,
bgneal@89 262 },
bgneal@89 263 context_instance=RequestContext(request))
bgneal@89 264
bgneal@108 265 return HttpResponseBadRequest("Invalid post.");
bgneal@89 266
bgneal@91 267
bgneal@91 268 def goto_post(request, post_id):
bgneal@91 269 """
bgneal@91 270 This function calculates what page a given post is on, then redirects
bgneal@91 271 to that URL. This function is the target of get_absolute_url() for
bgneal@91 272 Post objects.
bgneal@91 273 """
bgneal@101 274 post = get_object_or_404(Post.objects.select_related(), pk=post_id)
bgneal@91 275 count = post.topic.posts.filter(creation_date__lt=post.creation_date).count()
bgneal@91 276 page = count / POSTS_PER_PAGE + 1
bgneal@91 277 url = reverse('forums-topic_index', kwargs={'id': post.topic.id}) + \
bgneal@91 278 '?page=%s#p%s' % (page, post.id)
bgneal@91 279 return HttpResponseRedirect(url)
bgneal@91 280
bgneal@98 281
bgneal@98 282 @require_POST
bgneal@98 283 def flag_post(request):
bgneal@98 284 """
bgneal@98 285 This function handles the flagging of posts by users. This function should
bgneal@98 286 be the target of an AJAX post.
bgneal@98 287 """
bgneal@98 288 if not request.user.is_authenticated():
bgneal@99 289 return HttpResponseForbidden('Please login or register to flag a post.')
bgneal@98 290
bgneal@98 291 id = request.POST.get('id')
bgneal@98 292 if id is None:
bgneal@98 293 return HttpResponseBadRequest('No post id')
bgneal@98 294
bgneal@98 295 try:
bgneal@98 296 post = Post.objects.get(pk=id)
bgneal@98 297 except Post.DoesNotExist:
bgneal@98 298 return HttpResponseBadRequest('No post with id %s' % id)
bgneal@98 299
bgneal@98 300 flag = FlaggedPost(user=request.user, post=post)
bgneal@98 301 flag.save()
bgneal@98 302 email_admins('A Post Has Been Flagged', """Hello,
bgneal@98 303
bgneal@98 304 A user has flagged a forum post for review.
bgneal@98 305 """)
bgneal@98 306 return HttpResponse('The post was flagged. A moderator will review the post shortly. ' \
bgneal@98 307 'Thanks for helping to improve the discussions on this site.')
bgneal@106 308
bgneal@106 309
bgneal@106 310 @login_required
bgneal@106 311 def edit_post(request, id):
bgneal@106 312 """
bgneal@106 313 This view function allows authorized users to edit posts.
bgneal@106 314 The superuser, forum moderators, and original author can edit posts.
bgneal@106 315 """
bgneal@106 316 post = get_object_or_404(Post.objects.select_related(), pk=id)
bgneal@108 317
bgneal@109 318 can_moderate = _can_moderate(post.topic.forum, request.user)
bgneal@108 319 can_edit = can_moderate or request.user == post.user
bgneal@106 320
bgneal@106 321 if not can_edit:
bgneal@106 322 return HttpResponseForbidden("You don't have permission to edit that post.")
bgneal@106 323
bgneal@106 324 if request.method == "POST":
bgneal@106 325 form = PostForm(request.POST, instance=post)
bgneal@106 326 if form.is_valid():
bgneal@115 327 post = form.save(commit=False)
bgneal@115 328 post.touch()
bgneal@115 329 post.save()
bgneal@106 330 return HttpResponseRedirect(post.get_absolute_url())
bgneal@106 331 else:
bgneal@106 332 form = PostForm(instance=post)
bgneal@106 333
bgneal@123 334 post.user_profile = request.user.get_profile()
bgneal@123 335
bgneal@106 336 return render_to_response('forums/edit_post.html', {
bgneal@106 337 'forum': post.topic.forum,
bgneal@106 338 'topic': post.topic,
bgneal@106 339 'post': post,
bgneal@106 340 'form': form,
bgneal@108 341 'can_moderate': can_moderate,
bgneal@106 342 },
bgneal@106 343 context_instance=RequestContext(request))
bgneal@107 344
bgneal@107 345
bgneal@107 346 @require_POST
bgneal@107 347 def delete_post(request):
bgneal@107 348 """
bgneal@107 349 This view function allows superusers and forum moderators to delete posts.
bgneal@107 350 This function is the target of AJAX calls from the client.
bgneal@107 351 """
bgneal@107 352 if not request.user.is_authenticated():
bgneal@107 353 return HttpResponseForbidden('Please login to delete a post.')
bgneal@107 354
bgneal@107 355 id = request.POST.get('id')
bgneal@107 356 if id is None:
bgneal@107 357 return HttpResponseBadRequest('No post id')
bgneal@107 358
bgneal@107 359 post = get_object_or_404(Post.objects.select_related(), pk=id)
bgneal@107 360
bgneal@107 361 can_delete = request.user.is_superuser or \
bgneal@107 362 request.user in post.topic.forum.moderators.all()
bgneal@107 363
bgneal@107 364 if not can_delete:
bgneal@107 365 return HttpResponseForbidden("You don't have permission to delete that post.")
bgneal@107 366
bgneal@147 367 delete_single_post(post)
bgneal@147 368 return HttpResponse("The post has been deleted.")
bgneal@147 369
bgneal@147 370
bgneal@147 371 def delete_single_post(post):
bgneal@147 372 """
bgneal@147 373 This function deletes a single post. It handles the case of where
bgneal@147 374 a post is the sole post in a topic by deleting the topic also. It
bgneal@147 375 adjusts any foreign keys in Topic or Forum objects that might be pointing
bgneal@147 376 to this post before deleting the post to avoid a cascading delete.
bgneal@147 377 """
bgneal@107 378 if post.topic.post_count == 1 and post == post.topic.last_post:
bgneal@107 379 _delete_topic(post.topic)
bgneal@107 380 else:
bgneal@107 381 _delete_post(post)
bgneal@107 382
bgneal@107 383
bgneal@107 384 def _delete_post(post):
bgneal@107 385 """
bgneal@107 386 Internal function to delete a single post object.
bgneal@107 387 Decrements the post author's post count.
bgneal@107 388 Adjusts the parent topic and forum's last_post as needed.
bgneal@107 389 """
bgneal@107 390 # Adjust post creator's post count
bgneal@107 391 profile = post.user.get_profile()
bgneal@107 392 if profile.forum_post_count > 0:
bgneal@107 393 profile.forum_post_count -= 1
bgneal@107 394 profile.save()
bgneal@107 395
bgneal@107 396 # If this post is the last_post in a topic, we need to update
bgneal@107 397 # both the topic and parent forum's last post fields. If we don't
bgneal@107 398 # the cascading delete will delete them also!
bgneal@107 399
bgneal@107 400 topic = post.topic
bgneal@107 401 if topic.last_post == post:
bgneal@107 402 topic.last_post_pre_delete()
bgneal@107 403 topic.save()
bgneal@107 404
bgneal@107 405 forum = topic.forum
bgneal@107 406 if forum.last_post == post:
bgneal@107 407 forum.last_post_pre_delete()
bgneal@107 408 forum.save()
bgneal@107 409
bgneal@107 410 # Should be safe to delete the post now:
bgneal@107 411 post.delete()
bgneal@107 412
bgneal@107 413
bgneal@107 414 def _delete_topic(topic):
bgneal@107 415 """
bgneal@107 416 Internal function to delete an entire topic.
bgneal@107 417 Deletes the topic and all posts contained within.
bgneal@107 418 Adjusts the parent forum's last_post as needed.
bgneal@107 419 Note that we don't bother adjusting all the users'
bgneal@107 420 post counts as that doesn't seem to be worth the effort.
bgneal@107 421 """
bgneal@147 422 if topic.forum.last_post and topic.forum.last_post.topic == topic:
bgneal@107 423 topic.forum.last_post_pre_delete()
bgneal@107 424 topic.forum.save()
bgneal@107 425
bgneal@107 426 # It should be safe to just delete the topic now. This will
bgneal@107 427 # automatically delete all posts in the topic.
bgneal@107 428 topic.delete()
bgneal@108 429
bgneal@108 430
bgneal@108 431 @login_required
bgneal@108 432 def new_post(request, topic_id):
bgneal@108 433 """
bgneal@108 434 This function is the view for creating a normal, non-quick reply
bgneal@108 435 to a topic.
bgneal@108 436 """
bgneal@108 437 topic = get_object_or_404(Topic.objects.select_related(), pk=topic_id)
bgneal@108 438 can_post = _can_post_in_topic(topic, request.user)
bgneal@108 439
bgneal@108 440 if can_post:
bgneal@108 441 if request.method == 'POST':
bgneal@108 442 form = PostForm(request.POST)
bgneal@108 443 if form.is_valid():
bgneal@108 444 post = form.save(commit=False)
bgneal@108 445 post.topic = topic
bgneal@108 446 post.user = request.user
bgneal@108 447 post.user_ip = request.META.get("REMOTE_ADDR", "")
bgneal@108 448 post.save()
bgneal@108 449 _bump_post_count(request.user)
bgneal@113 450 _update_last_visit(request.user, topic)
bgneal@108 451 return HttpResponseRedirect(post.get_absolute_url())
bgneal@108 452 else:
bgneal@108 453 quote_id = request.GET.get('quote')
bgneal@108 454 if quote_id:
bgneal@108 455 quote_post = get_object_or_404(Post.objects.select_related(),
bgneal@108 456 pk=quote_id)
bgneal@108 457 form = PostForm(initial={'body': _quote_message(quote_post.user.username,
bgneal@108 458 quote_post.body)})
bgneal@108 459 else:
bgneal@108 460 form = PostForm()
bgneal@108 461 else:
bgneal@108 462 form = None
bgneal@108 463
bgneal@108 464 return render_to_response('forums/new_post.html', {
bgneal@108 465 'forum': topic.forum,
bgneal@108 466 'topic': topic,
bgneal@108 467 'form': form,
bgneal@108 468 'can_post': can_post,
bgneal@108 469 },
bgneal@108 470 context_instance=RequestContext(request))
bgneal@108 471
bgneal@108 472
bgneal@109 473 @login_required
bgneal@109 474 def mod_topic_stick(request, id):
bgneal@109 475 """
bgneal@109 476 This view function is for moderators to toggle the sticky status of a topic.
bgneal@109 477 """
bgneal@109 478 topic = get_object_or_404(Topic.objects.select_related(), pk=id)
bgneal@109 479 if _can_moderate(topic.forum, request.user):
bgneal@109 480 topic.sticky = not topic.sticky
bgneal@109 481 topic.save()
bgneal@109 482 return HttpResponseRedirect(topic.get_absolute_url())
bgneal@109 483
bgneal@110 484 return HttpResponseForbidden()
bgneal@109 485
bgneal@109 486
bgneal@109 487 @login_required
bgneal@109 488 def mod_topic_lock(request, id):
bgneal@109 489 """
bgneal@109 490 This view function is for moderators to toggle the locked status of a topic.
bgneal@109 491 """
bgneal@109 492 topic = get_object_or_404(Topic.objects.select_related(), pk=id)
bgneal@109 493 if _can_moderate(topic.forum, request.user):
bgneal@109 494 topic.locked = not topic.locked
bgneal@109 495 topic.save()
bgneal@109 496 return HttpResponseRedirect(topic.get_absolute_url())
bgneal@109 497
bgneal@110 498 return HttpResponseForbidden()
bgneal@109 499
bgneal@109 500
bgneal@109 501 @login_required
bgneal@109 502 def mod_topic_delete(request, id):
bgneal@109 503 """
bgneal@109 504 This view function is for moderators to delete an entire topic.
bgneal@109 505 """
bgneal@109 506 topic = get_object_or_404(Topic.objects.select_related(), pk=id)
bgneal@109 507 if _can_moderate(topic.forum, request.user):
bgneal@109 508 forum_url = topic.forum.get_absolute_url()
bgneal@109 509 _delete_topic(topic)
bgneal@109 510 return HttpResponseRedirect(forum_url)
bgneal@109 511
bgneal@110 512 return HttpResponseForbidden()
bgneal@110 513
bgneal@110 514
bgneal@110 515 @login_required
bgneal@110 516 def mod_topic_move(request, id):
bgneal@110 517 """
bgneal@110 518 This view function is for moderators to move a topic to a different forum.
bgneal@110 519 """
bgneal@110 520 topic = get_object_or_404(Topic.objects.select_related(), pk=id)
bgneal@110 521 if not _can_moderate(topic.forum, request.user):
bgneal@110 522 return HttpResponseForbidden()
bgneal@110 523
bgneal@110 524 if request.method == 'POST':
bgneal@110 525 form = MoveTopicForm(request.user, request.POST)
bgneal@110 526 if form.is_valid():
bgneal@110 527 new_forum = form.cleaned_data['forums']
bgneal@110 528 old_forum = topic.forum
bgneal@111 529 _move_topic(topic, old_forum, new_forum)
bgneal@110 530 return HttpResponseRedirect(topic.get_absolute_url())
bgneal@110 531 else:
bgneal@110 532 form = MoveTopicForm(request.user)
bgneal@110 533
bgneal@110 534 return render_to_response('forums/move_topic.html', {
bgneal@110 535 'forum': topic.forum,
bgneal@110 536 'topic': topic,
bgneal@110 537 'form': form,
bgneal@110 538 },
bgneal@110 539 context_instance=RequestContext(request))
bgneal@109 540
bgneal@109 541
bgneal@111 542 @login_required
bgneal@111 543 def mod_forum(request, slug):
bgneal@111 544 """
bgneal@111 545 Displays a view to allow moderators to perform various operations
bgneal@111 546 on topics in a forum in bulk. We currently support mass locking/unlocking,
bgneal@111 547 stickying and unstickying, moving, and deleting topics.
bgneal@111 548 """
bgneal@111 549 forum = get_object_or_404(Forum.objects.select_related(), slug=slug)
bgneal@111 550 if not _can_moderate(forum, request.user):
bgneal@111 551 return HttpResponseForbidden()
bgneal@111 552
bgneal@111 553 topics = forum.topics.select_related('user', 'last_post', 'last_post__user')
bgneal@111 554 paginator = create_topic_paginator(topics)
bgneal@167 555 page_num = get_page_num(request)
bgneal@111 556 try:
bgneal@111 557 page = paginator.page(page_num)
bgneal@111 558 except InvalidPage:
bgneal@111 559 raise Http404
bgneal@111 560
bgneal@111 561 # we do this for the template since it is rendered twice
bgneal@111 562 page_nav = render_to_string('forums/pagination.html', {'page': page})
bgneal@111 563 form = None
bgneal@111 564
bgneal@111 565 if request.method == 'POST':
bgneal@111 566 topic_ids = request.POST.getlist('topic_ids')
bgneal@111 567 url = reverse('forums-mod_forum', kwargs={'slug':forum.slug})
bgneal@111 568 url += '?page=%s' % page_num
bgneal@111 569
bgneal@111 570 if len(topic_ids):
bgneal@111 571 if request.POST.get('sticky'):
bgneal@111 572 _bulk_sticky(forum, topic_ids)
bgneal@111 573 return HttpResponseRedirect(url)
bgneal@111 574 elif request.POST.get('lock'):
bgneal@111 575 _bulk_lock(forum, topic_ids)
bgneal@111 576 return HttpResponseRedirect(url)
bgneal@111 577 elif request.POST.get('delete'):
bgneal@111 578 _bulk_delete(forum, topic_ids)
bgneal@111 579 return HttpResponseRedirect(url)
bgneal@111 580 elif request.POST.get('move'):
bgneal@111 581 form = MoveTopicForm(request.user, request.POST, hide_label=True)
bgneal@111 582 if form.is_valid():
bgneal@111 583 _bulk_move(topic_ids, forum, form.cleaned_data['forums'])
bgneal@111 584 return HttpResponseRedirect(url)
bgneal@111 585
bgneal@111 586 if form is None:
bgneal@111 587 form = MoveTopicForm(request.user, hide_label=True)
bgneal@111 588
bgneal@111 589 return render_to_response('forums/mod_forum.html', {
bgneal@111 590 'forum': forum,
bgneal@111 591 'page': page,
bgneal@111 592 'page_nav': page_nav,
bgneal@111 593 'form': form,
bgneal@111 594 },
bgneal@111 595 context_instance=RequestContext(request))
bgneal@111 596
bgneal@111 597
bgneal@113 598 @login_required
bgneal@113 599 @require_POST
bgneal@113 600 def forum_catchup(request, slug):
bgneal@113 601 """
bgneal@113 602 This view marks all the topics in the forum as being read.
bgneal@113 603 """
bgneal@113 604 forum = get_object_or_404(Forum.objects.select_related(), slug=slug)
bgneal@113 605
bgneal@113 606 if not forum.category.can_access(request.user):
bgneal@113 607 return HttpResponseForbidden()
bgneal@113 608
bgneal@113 609 forum.catchup(request.user)
bgneal@113 610 return HttpResponseRedirect(forum.get_absolute_url())
bgneal@113 611
bgneal@113 612
bgneal@115 613 @login_required
bgneal@115 614 def mod_topic_split(request, id):
bgneal@115 615 """
bgneal@115 616 This view function allows moderators to split posts off to a new topic.
bgneal@115 617 """
bgneal@115 618 topic = get_object_or_404(Topic.objects.select_related(), pk=id)
bgneal@115 619 if not _can_moderate(topic.forum, request.user):
bgneal@115 620 return HttpResponseRedirect(topic.get_absolute_url())
bgneal@115 621
bgneal@115 622 if request.method == "POST":
bgneal@115 623 form = SplitTopicForm(request.user, request.POST)
bgneal@115 624 if form.is_valid():
bgneal@115 625 if form.split_at:
bgneal@115 626 _split_topic_at(topic, form.post_ids[0],
bgneal@115 627 form.cleaned_data['forums'],
bgneal@115 628 form.cleaned_data['name'])
bgneal@115 629 else:
bgneal@115 630 _split_topic(topic, form.post_ids,
bgneal@115 631 form.cleaned_data['forums'],
bgneal@115 632 form.cleaned_data['name'])
bgneal@115 633
bgneal@115 634 return HttpResponseRedirect(topic.get_absolute_url())
bgneal@115 635 else:
bgneal@115 636 form = SplitTopicForm(request.user)
bgneal@115 637
bgneal@115 638 posts = topic.posts.select_related()
bgneal@115 639
bgneal@115 640 return render_to_response('forums/mod_split_topic.html', {
bgneal@115 641 'forum': topic.forum,
bgneal@115 642 'topic': topic,
bgneal@115 643 'posts': posts,
bgneal@115 644 'form': form,
bgneal@115 645 },
bgneal@115 646 context_instance=RequestContext(request))
bgneal@115 647
bgneal@115 648
bgneal@167 649 @login_required
bgneal@167 650 def unread_topics(request):
bgneal@168 651 """Displays the topics with unread posts for a given user."""
bgneal@168 652
bgneal@167 653 topics = get_unread_topics(request.user)
bgneal@167 654
bgneal@167 655 paginator = create_topic_paginator(topics)
bgneal@167 656 page_num = get_page_num(request)
bgneal@167 657 try:
bgneal@167 658 page = paginator.page(page_num)
bgneal@167 659 except InvalidPage:
bgneal@167 660 raise Http404
bgneal@167 661
bgneal@167 662 attach_topic_page_ranges(page.object_list)
bgneal@167 663
bgneal@167 664 # we do this for the template since it is rendered twice
bgneal@167 665 page_nav = render_to_string('forums/pagination.html', {'page': page})
bgneal@167 666
bgneal@167 667 return render_to_response('forums/topic_list.html', {
bgneal@167 668 'title': 'Topics With Unread Posts',
bgneal@167 669 'page': page,
bgneal@167 670 'page_nav': page_nav,
bgneal@167 671 },
bgneal@167 672 context_instance=RequestContext(request))
bgneal@167 673
bgneal@167 674
bgneal@168 675 def unanswered_topics(request):
bgneal@168 676 """Displays the topics with no replies."""
bgneal@168 677
bgneal@168 678 forum_ids = Forum.objects.forum_ids_for_user(request.user)
bgneal@168 679 topics = Topic.objects.filter(forum__id__in=forum_ids,
bgneal@168 680 post_count=1).select_related(
bgneal@168 681 'forum', 'user', 'last_post', 'last_post__user')
bgneal@168 682
bgneal@168 683 paginator = create_topic_paginator(topics)
bgneal@168 684 page_num = get_page_num(request)
bgneal@168 685 try:
bgneal@168 686 page = paginator.page(page_num)
bgneal@168 687 except InvalidPage:
bgneal@168 688 raise Http404
bgneal@168 689
bgneal@168 690 attach_topic_page_ranges(page.object_list)
bgneal@168 691
bgneal@168 692 # we do this for the template since it is rendered twice
bgneal@168 693 page_nav = render_to_string('forums/pagination.html', {'page': page})
bgneal@168 694
bgneal@168 695 return render_to_response('forums/topic_list.html', {
bgneal@168 696 'title': 'Unanswered Topics',
bgneal@168 697 'page': page,
bgneal@168 698 'page_nav': page_nav,
bgneal@168 699 },
bgneal@168 700 context_instance=RequestContext(request))
bgneal@168 701
bgneal@168 702
bgneal@169 703 @login_required
bgneal@169 704 def my_posts(request):
bgneal@169 705 """Displays a list of posts the requesting user made."""
bgneal@169 706
bgneal@169 707 forum_ids = Forum.objects.forum_ids_for_user(request.user)
bgneal@169 708 posts = Post.objects.filter(user=request.user,
bgneal@169 709 topic__forum__id__in=forum_ids).order_by(
bgneal@169 710 '-creation_date').select_related()
bgneal@169 711
bgneal@169 712 paginator = create_post_paginator(posts)
bgneal@169 713 page_num = get_page_num(request)
bgneal@169 714 try:
bgneal@169 715 page = paginator.page(page_num)
bgneal@169 716 except InvalidPage:
bgneal@169 717 raise Http404
bgneal@169 718
bgneal@169 719 # we do this for the template since it is rendered twice
bgneal@169 720 page_nav = render_to_string('forums/pagination.html', {'page': page})
bgneal@169 721
bgneal@169 722 return render_to_response('forums/post_list.html', {
bgneal@169 723 'title': 'My Posts',
bgneal@169 724 'page': page,
bgneal@169 725 'page_nav': page_nav,
bgneal@169 726 },
bgneal@169 727 context_instance=RequestContext(request))
bgneal@169 728
bgneal@169 729
bgneal@109 730 def _can_moderate(forum, user):
bgneal@109 731 """
bgneal@109 732 Determines if a user has permission to moderate a given forum.
bgneal@109 733 """
bgneal@109 734 return user.is_authenticated() and (
bgneal@109 735 user.is_superuser or user in forum.moderators.all())
bgneal@109 736
bgneal@109 737
bgneal@108 738 def _can_post_in_topic(topic, user):
bgneal@108 739 """
bgneal@108 740 This function returns true if the given user can post in the given topic
bgneal@108 741 and false otherwise.
bgneal@108 742 """
bgneal@108 743 return (not topic.locked and topic.forum.category.can_access(user)) or \
bgneal@108 744 (user.is_superuser or user in topic.forum.moderators.all())
bgneal@108 745
bgneal@108 746
bgneal@108 747 def _bump_post_count(user):
bgneal@108 748 """
bgneal@108 749 Increments the forum_post_count for the given user.
bgneal@108 750 """
bgneal@108 751 profile = user.get_profile()
bgneal@108 752 profile.forum_post_count += 1
bgneal@108 753 profile.save()
bgneal@108 754
bgneal@108 755
bgneal@108 756 def _quote_message(who, message):
bgneal@111 757 """
bgneal@111 758 Builds a message reply by quoting the existing message in a
bgneal@111 759 typical email-like fashion. The quoting is compatible with Markdown.
bgneal@111 760 """
bgneal@111 761 header = '*%s wrote:*\n\n' % (who, )
bgneal@111 762 lines = wrap(message, 55).split('\n')
bgneal@111 763 for i, line in enumerate(lines):
bgneal@111 764 lines[i] = '> ' + line
bgneal@111 765 return header + '\n'.join(lines)
bgneal@111 766
bgneal@111 767
bgneal@111 768 def _move_topic(topic, old_forum, new_forum):
bgneal@111 769 if new_forum != old_forum:
bgneal@111 770 topic.forum = new_forum
bgneal@111 771 topic.save()
bgneal@111 772 # Have to adjust foreign keys to last_post, denormalized counts, etc.:
bgneal@112 773 old_forum.sync()
bgneal@111 774 old_forum.save()
bgneal@112 775 new_forum.sync()
bgneal@111 776 new_forum.save()
bgneal@111 777
bgneal@111 778
bgneal@111 779 def _bulk_sticky(forum, topic_ids):
bgneal@111 780 """
bgneal@111 781 Performs a toggle on the sticky status for a given list of topic ids.
bgneal@111 782 """
bgneal@111 783 topics = Topic.objects.filter(pk__in=topic_ids)
bgneal@111 784 for topic in topics:
bgneal@111 785 if topic.forum == forum:
bgneal@111 786 topic.sticky = not topic.sticky
bgneal@111 787 topic.save()
bgneal@111 788
bgneal@111 789
bgneal@111 790 def _bulk_lock(forum, topic_ids):
bgneal@111 791 """
bgneal@111 792 Performs a toggle on the locked status for a given list of topic ids.
bgneal@111 793 """
bgneal@111 794 topics = Topic.objects.filter(pk__in=topic_ids)
bgneal@111 795 for topic in topics:
bgneal@111 796 if topic.forum == forum:
bgneal@111 797 topic.locked = not topic.locked
bgneal@111 798 topic.save()
bgneal@111 799
bgneal@111 800
bgneal@111 801 def _bulk_delete(forum, topic_ids):
bgneal@111 802 """
bgneal@111 803 Deletes the list of topics.
bgneal@111 804 """
bgneal@111 805 topics = Topic.objects.filter(pk__in=topic_ids).select_related()
bgneal@111 806 for topic in topics:
bgneal@111 807 if topic.forum == forum:
bgneal@111 808 _delete_topic(topic)
bgneal@111 809
bgneal@111 810
bgneal@111 811 def _bulk_move(topic_ids, old_forum, new_forum):
bgneal@111 812 """
bgneal@111 813 Moves the list of topics to a new forum.
bgneal@111 814 """
bgneal@111 815 topics = Topic.objects.filter(pk__in=topic_ids).select_related()
bgneal@111 816 for topic in topics:
bgneal@111 817 if topic.forum == old_forum:
bgneal@111 818 _move_topic(topic, old_forum, new_forum)
bgneal@111 819
bgneal@113 820
bgneal@113 821 def _update_last_visit(user, topic):
bgneal@113 822 """
bgneal@113 823 Does the bookkeeping for the last visit status for the user to the
bgneal@113 824 topic/forum.
bgneal@113 825 """
bgneal@113 826 now = datetime.datetime.now()
bgneal@113 827 try:
bgneal@113 828 flv = ForumLastVisit.objects.get(user=user, forum=topic.forum)
bgneal@113 829 except ForumLastVisit.DoesNotExist:
bgneal@113 830 flv = ForumLastVisit(user=user, forum=topic.forum)
bgneal@113 831 flv.begin_date = now
bgneal@113 832
bgneal@113 833 flv.end_date = now
bgneal@113 834 flv.save()
bgneal@113 835
bgneal@113 836 if topic.update_date > flv.begin_date:
bgneal@113 837 try:
bgneal@113 838 tlv = TopicLastVisit.objects.get(user=user, topic=topic)
bgneal@113 839 except TopicLastVisit.DoesNotExist:
bgneal@113 840 tlv = TopicLastVisit(user=user, topic=topic)
bgneal@113 841
bgneal@113 842 tlv.touch()
bgneal@113 843 tlv.save()
bgneal@113 844
bgneal@115 845
bgneal@115 846 def _split_topic_at(topic, post_id, new_forum, new_name):
bgneal@115 847 """
bgneal@115 848 This function splits the post given by post_id and all posts that come
bgneal@115 849 after it in the given topic to a new topic in a new forum.
bgneal@115 850 It is assumed the caller has been checked for moderator rights.
bgneal@115 851 """
bgneal@115 852 post = get_object_or_404(Post, id=post_id)
bgneal@115 853 if post.topic == topic:
bgneal@115 854 post_ids = Post.objects.filter(topic=topic,
bgneal@115 855 creation_date__gte=post.creation_date).values_list('id', flat=True)
bgneal@115 856 _split_topic(topic, post_ids, new_forum, new_name)
bgneal@115 857
bgneal@115 858
bgneal@115 859 def _split_topic(topic, post_ids, new_forum, new_name):
bgneal@115 860 """
bgneal@115 861 This function splits the posts given by the post_ids list in the
bgneal@115 862 given topic to a new topic in a new forum.
bgneal@115 863 It is assumed the caller has been checked for moderator rights.
bgneal@115 864 """
bgneal@115 865 posts = Post.objects.filter(topic=topic, id__in=post_ids)
bgneal@115 866 if len(posts) > 0:
bgneal@115 867 new_topic = Topic(forum=new_forum, name=new_name, user=posts[0].user)
bgneal@115 868 new_topic.save()
bgneal@115 869 for post in posts:
bgneal@115 870 post.topic = new_topic
bgneal@115 871 post.save()
bgneal@115 872
bgneal@115 873 topic.post_count_update()
bgneal@115 874 topic.save()
bgneal@115 875 new_topic.post_count_update()
bgneal@115 876 new_topic.save()
bgneal@115 877 topic.forum.sync()
bgneal@115 878 topic.forum.save()
bgneal@115 879 new_forum.sync()
bgneal@115 880 new_forum.save()