annotate gpp/forums/views/main.py @ 429:d0f0800eef0c

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