annotate gpp/forums/views/main.py @ 265:1ba2c6bf6eb7

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