annotate forums/views/main.py @ 661:15dbe0ccda95

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