annotate gpp/forums/views/main.py @ 467:b910cc1460c8

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