annotate gpp/forums/views/main.py @ 348:d1b11096595b

Fix #168; when nailing a spammer, clear their profile text fields. Guard against topics and forums that don't exist when deleting posts in the signal handler. Make the forum stats template tag only display the latest active users.
author Brian Neal <bgneal@gmail.com>
date Wed, 02 Mar 2011 02:18:28 +0000
parents 000c006fee97
children a43add8af83d
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@232 184 user_profiles = dict((profile.user.id, profile) for profile in profiles)
bgneal@232 185
bgneal@232 186 for post in page.object_list:
bgneal@329 187 post.user.user_profile = user_profiles[post.user.id]
bgneal@285 188 post.attach_list = []
bgneal@285 189
bgneal@329 190 # Attach badge ownership info to the user profiles to avoid lots
bgneal@329 191 # of database hits in the template:
bgneal@329 192 bos_qs = BadgeOwnership.objects.filter(
bgneal@329 193 profile__id__in=user_profiles.keys()).select_related()
bgneal@329 194 bos = collections.defaultdict(list)
bgneal@329 195 for bo in bos_qs:
bgneal@329 196 bos[bo.profile.id].append(bo)
bgneal@329 197
bgneal@329 198 for pk, profile in user_profiles.iteritems():
bgneal@329 199 profile.badge_ownership = bos[pk]
bgneal@329 200
bgneal@285 201 # Attach any attachments
bgneal@285 202 post_ids = [post.pk for post in page.object_list]
bgneal@285 203 attachments = Attachment.objects.filter(post__in=post_ids).select_related(
bgneal@285 204 'embed').order_by('order')
bgneal@285 205
bgneal@285 206 post_dict = dict((post.pk, post) for post in page.object_list)
bgneal@285 207 for item in attachments:
bgneal@285 208 post_dict[item.post.id].attach_list.append(item.embed)
bgneal@232 209
bgneal@232 210 last_page = page_num == paginator.num_pages
bgneal@232 211
bgneal@232 212 if request.user.is_authenticated() and last_page:
bgneal@232 213 _update_last_visit(request.user, topic)
bgneal@232 214
bgneal@232 215 # we do this for the template since it is rendered twice
bgneal@232 216 page_nav = render_to_string('forums/pagination.html', {'page': page})
bgneal@232 217
bgneal@232 218 can_moderate = _can_moderate(topic.forum, request.user)
bgneal@232 219
bgneal@232 220 can_reply = request.user.is_authenticated() and (
bgneal@232 221 not topic.locked or can_moderate)
bgneal@232 222
bgneal@232 223 is_favorite = request.user.is_authenticated() and (
bgneal@232 224 topic in request.user.favorite_topics.all())
bgneal@232 225
bgneal@232 226 is_subscribed = request.user.is_authenticated() and (
bgneal@232 227 topic in request.user.subscriptions.all())
bgneal@232 228
bgneal@232 229 return render_to_response('forums/topic.html', {
bgneal@232 230 'forum': topic.forum,
bgneal@232 231 'topic': topic,
bgneal@232 232 'page': page,
bgneal@232 233 'page_nav': page_nav,
bgneal@232 234 'last_page': last_page,
bgneal@232 235 'can_moderate': can_moderate,
bgneal@232 236 'can_reply': can_reply,
bgneal@232 237 'form': NewPostForm(initial={'topic_id': topic.id}),
bgneal@232 238 'is_favorite': is_favorite,
bgneal@232 239 'is_subscribed': is_subscribed,
bgneal@232 240 },
bgneal@232 241 context_instance=RequestContext(request))
bgneal@232 242
bgneal@232 243
bgneal@232 244 @login_required
bgneal@232 245 def new_topic(request, slug):
bgneal@232 246 """
bgneal@232 247 This view handles the creation of new topics.
bgneal@232 248 """
bgneal@232 249 forum = get_object_or_404(Forum.objects.select_related(), slug=slug)
bgneal@232 250
bgneal@232 251 if not forum.category.can_access(request.user):
bgneal@232 252 return HttpResponseForbidden()
bgneal@232 253
bgneal@232 254 if request.method == 'POST':
bgneal@232 255 form = NewTopicForm(request.user, forum, request.POST)
bgneal@232 256 if form.is_valid():
bgneal@232 257 if antispam.utils.spam_check(request, form.cleaned_data['body']):
bgneal@232 258 return HttpResponseRedirect(reverse('antispam-suspended'))
bgneal@232 259
bgneal@232 260 topic = form.save(request.META.get("REMOTE_ADDR"))
bgneal@232 261 _bump_post_count(request.user)
bgneal@232 262 return HttpResponseRedirect(reverse('forums-new_topic_thanks',
bgneal@232 263 kwargs={'tid': topic.pk}))
bgneal@232 264 else:
bgneal@232 265 form = NewTopicForm(request.user, forum)
bgneal@286 266
bgneal@232 267 return render_to_response('forums/new_topic.html', {
bgneal@232 268 'forum': forum,
bgneal@232 269 'form': form,
bgneal@232 270 },
bgneal@232 271 context_instance=RequestContext(request))
bgneal@232 272
bgneal@232 273
bgneal@232 274 @login_required
bgneal@232 275 def new_topic_thanks(request, tid):
bgneal@232 276 """
bgneal@232 277 This view displays the success page for a newly created topic.
bgneal@232 278 """
bgneal@232 279 topic = get_object_or_404(Topic.objects.select_related(), pk=tid)
bgneal@232 280 return render_to_response('forums/new_topic_thanks.html', {
bgneal@232 281 'forum': topic.forum,
bgneal@232 282 'topic': topic,
bgneal@232 283 },
bgneal@232 284 context_instance=RequestContext(request))
bgneal@232 285
bgneal@232 286
bgneal@232 287 @require_POST
bgneal@232 288 def quick_reply_ajax(request):
bgneal@232 289 """
bgneal@232 290 This function handles the quick reply to a thread function. This
bgneal@232 291 function is meant to be the target of an AJAX post, and returns
bgneal@232 292 the HTML for the new post, which the client-side script appends
bgneal@232 293 to the document.
bgneal@232 294 """
bgneal@232 295 if not request.user.is_authenticated():
bgneal@232 296 return HttpResponseForbidden('Please login or register to post.')
bgneal@232 297
bgneal@232 298 form = NewPostForm(request.POST)
bgneal@232 299 if form.is_valid():
bgneal@232 300 if not _can_post_in_topic(form.topic, request.user):
bgneal@232 301 return HttpResponseForbidden("You don't have permission to post in this topic.")
bgneal@232 302 if antispam.utils.spam_check(request, form.cleaned_data['body']):
bgneal@232 303 return HttpResponseForbidden(antispam.BUSTED_MESSAGE)
bgneal@232 304
bgneal@232 305 post = form.save(request.user, request.META.get("REMOTE_ADDR", ""))
bgneal@232 306 post.unread = True
bgneal@329 307 post.user.user_profile = request.user.get_profile()
bgneal@285 308 post.attach_list = post.attachments.all()
bgneal@232 309 _bump_post_count(request.user)
bgneal@232 310 _update_last_visit(request.user, form.topic)
bgneal@285 311
bgneal@232 312 return render_to_response('forums/display_post.html', {
bgneal@232 313 'post': post,
bgneal@232 314 'can_moderate': _can_moderate(form.topic.forum, request.user),
bgneal@232 315 'can_reply': True,
bgneal@232 316 },
bgneal@232 317 context_instance=RequestContext(request))
bgneal@232 318
bgneal@286 319 return HttpResponseBadRequest("Oops, did you forget some text?");
bgneal@232 320
bgneal@232 321
bgneal@232 322 def goto_post(request, post_id):
bgneal@232 323 """
bgneal@232 324 This function calculates what page a given post is on, then redirects
bgneal@232 325 to that URL. This function is the target of get_absolute_url() for
bgneal@232 326 Post objects.
bgneal@232 327 """
bgneal@232 328 post = get_object_or_404(Post.objects.select_related(), pk=post_id)
bgneal@232 329 count = post.topic.posts.filter(creation_date__lt=post.creation_date).count()
bgneal@232 330 page = count / POSTS_PER_PAGE + 1
bgneal@232 331 url = reverse('forums-topic_index', kwargs={'id': post.topic.id}) + \
bgneal@232 332 '?page=%s#p%s' % (page, post.id)
bgneal@232 333 return HttpResponseRedirect(url)
bgneal@232 334
bgneal@232 335
bgneal@232 336 @require_POST
bgneal@232 337 def flag_post(request):
bgneal@232 338 """
bgneal@232 339 This function handles the flagging of posts by users. This function should
bgneal@232 340 be the target of an AJAX post.
bgneal@232 341 """
bgneal@232 342 if not request.user.is_authenticated():
bgneal@232 343 return HttpResponseForbidden('Please login or register to flag a post.')
bgneal@232 344
bgneal@232 345 id = request.POST.get('id')
bgneal@232 346 if id is None:
bgneal@232 347 return HttpResponseBadRequest('No post id')
bgneal@232 348
bgneal@232 349 try:
bgneal@232 350 post = Post.objects.get(pk=id)
bgneal@232 351 except Post.DoesNotExist:
bgneal@232 352 return HttpResponseBadRequest('No post with id %s' % id)
bgneal@232 353
bgneal@232 354 flag = FlaggedPost(user=request.user, post=post)
bgneal@232 355 flag.save()
bgneal@232 356 email_admins('A Post Has Been Flagged', """Hello,
bgneal@232 357
bgneal@232 358 A user has flagged a forum post for review.
bgneal@232 359 """)
bgneal@232 360 return HttpResponse('The post was flagged. A moderator will review the post shortly. ' \
bgneal@232 361 'Thanks for helping to improve the discussions on this site.')
bgneal@232 362
bgneal@232 363
bgneal@232 364 @login_required
bgneal@232 365 def edit_post(request, id):
bgneal@232 366 """
bgneal@232 367 This view function allows authorized users to edit posts.
bgneal@232 368 The superuser, forum moderators, and original author can edit posts.
bgneal@232 369 """
bgneal@232 370 post = get_object_or_404(Post.objects.select_related(), pk=id)
bgneal@232 371
bgneal@232 372 can_moderate = _can_moderate(post.topic.forum, request.user)
bgneal@232 373 can_edit = can_moderate or request.user == post.user
bgneal@232 374
bgneal@232 375 if not can_edit:
bgneal@232 376 return HttpResponseForbidden("You don't have permission to edit that post.")
bgneal@232 377
bgneal@295 378 topic_name = None
bgneal@295 379 first_post = Post.objects.filter(topic=post.topic).order_by('creation_date')[0]
bgneal@295 380 if first_post.id == post.id:
bgneal@295 381 topic_name = post.topic.name
bgneal@295 382
bgneal@232 383 if request.method == "POST":
bgneal@295 384 form = PostForm(request.POST, instance=post, topic_name=topic_name)
bgneal@232 385 if form.is_valid():
bgneal@232 386 if antispam.utils.spam_check(request, form.cleaned_data['body']):
bgneal@232 387 return HttpResponseRedirect(reverse('antispam-suspended'))
bgneal@232 388 post = form.save(commit=False)
bgneal@232 389 post.touch()
bgneal@232 390 post.save()
bgneal@285 391
bgneal@295 392 # if we are editing a first post, save the parent topic as well
bgneal@295 393 if topic_name:
bgneal@295 394 post.topic.save()
bgneal@295 395
bgneal@285 396 # Save any attachments
bgneal@286 397 form.attach_proc.save_attachments(post)
bgneal@285 398
bgneal@232 399 return HttpResponseRedirect(post.get_absolute_url())
bgneal@232 400 else:
bgneal@295 401 form = PostForm(instance=post, topic_name=topic_name)
bgneal@232 402
bgneal@329 403 post.user.user_profile = post.user.get_profile()
bgneal@232 404
bgneal@232 405 return render_to_response('forums/edit_post.html', {
bgneal@232 406 'forum': post.topic.forum,
bgneal@232 407 'topic': post.topic,
bgneal@232 408 'post': post,
bgneal@232 409 'form': form,
bgneal@232 410 'can_moderate': can_moderate,
bgneal@232 411 },
bgneal@232 412 context_instance=RequestContext(request))
bgneal@232 413
bgneal@232 414
bgneal@232 415 @require_POST
bgneal@232 416 def delete_post(request):
bgneal@232 417 """
bgneal@232 418 This view function allows superusers and forum moderators to delete posts.
bgneal@232 419 This function is the target of AJAX calls from the client.
bgneal@232 420 """
bgneal@232 421 if not request.user.is_authenticated():
bgneal@232 422 return HttpResponseForbidden('Please login to delete a post.')
bgneal@232 423
bgneal@232 424 id = request.POST.get('id')
bgneal@232 425 if id is None:
bgneal@232 426 return HttpResponseBadRequest('No post id')
bgneal@232 427
bgneal@232 428 post = get_object_or_404(Post.objects.select_related(), pk=id)
bgneal@232 429
bgneal@232 430 can_delete = request.user.is_superuser or \
bgneal@232 431 request.user in post.topic.forum.moderators.all()
bgneal@232 432
bgneal@232 433 if not can_delete:
bgneal@232 434 return HttpResponseForbidden("You don't have permission to delete that post.")
bgneal@232 435
bgneal@232 436 delete_single_post(post)
bgneal@232 437 return HttpResponse("The post has been deleted.")
bgneal@232 438
bgneal@232 439
bgneal@232 440 def delete_single_post(post):
bgneal@232 441 """
bgneal@232 442 This function deletes a single post. It handles the case of where
bgneal@232 443 a post is the sole post in a topic by deleting the topic also. It
bgneal@232 444 adjusts any foreign keys in Topic or Forum objects that might be pointing
bgneal@232 445 to this post before deleting the post to avoid a cascading delete.
bgneal@232 446 """
bgneal@232 447 if post.topic.post_count == 1 and post == post.topic.last_post:
bgneal@232 448 _delete_topic(post.topic)
bgneal@232 449 else:
bgneal@232 450 _delete_post(post)
bgneal@232 451
bgneal@232 452
bgneal@232 453 def _delete_post(post):
bgneal@232 454 """
bgneal@232 455 Internal function to delete a single post object.
bgneal@232 456 Decrements the post author's post count.
bgneal@232 457 Adjusts the parent topic and forum's last_post as needed.
bgneal@232 458 """
bgneal@232 459 # Adjust post creator's post count
bgneal@232 460 profile = post.user.get_profile()
bgneal@232 461 if profile.forum_post_count > 0:
bgneal@232 462 profile.forum_post_count -= 1
bgneal@232 463 profile.save()
bgneal@232 464
bgneal@232 465 # If this post is the last_post in a topic, we need to update
bgneal@232 466 # both the topic and parent forum's last post fields. If we don't
bgneal@232 467 # the cascading delete will delete them also!
bgneal@232 468
bgneal@232 469 topic = post.topic
bgneal@232 470 if topic.last_post == post:
bgneal@232 471 topic.last_post_pre_delete()
bgneal@232 472 topic.save()
bgneal@232 473
bgneal@232 474 forum = topic.forum
bgneal@232 475 if forum.last_post == post:
bgneal@232 476 forum.last_post_pre_delete()
bgneal@232 477 forum.save()
bgneal@232 478
bgneal@285 479 # delete any attachments
bgneal@285 480 post.attachments.clear()
bgneal@285 481
bgneal@232 482 # Should be safe to delete the post now:
bgneal@232 483 post.delete()
bgneal@232 484
bgneal@232 485
bgneal@232 486 def _delete_topic(topic):
bgneal@232 487 """
bgneal@232 488 Internal function to delete an entire topic.
bgneal@232 489 Deletes the topic and all posts contained within.
bgneal@232 490 Adjusts the parent forum's last_post as needed.
bgneal@232 491 Note that we don't bother adjusting all the users'
bgneal@232 492 post counts as that doesn't seem to be worth the effort.
bgneal@232 493 """
bgneal@318 494 parent_forum = topic.forum
bgneal@318 495 if parent_forum.last_post and parent_forum.last_post.topic == topic:
bgneal@318 496 parent_forum.last_post_pre_delete(deleting_topic=True)
bgneal@318 497 parent_forum.save()
bgneal@232 498
bgneal@232 499 # delete subscriptions to this topic
bgneal@232 500 topic.subscribers.clear()
bgneal@232 501 topic.bookmarkers.clear()
bgneal@232 502
bgneal@285 503 # delete all attachments
bgneal@285 504 posts = Post.objects.filter(topic=topic)
bgneal@285 505 for post in posts:
bgneal@285 506 post.attachments.clear()
bgneal@285 507
bgneal@318 508 # Null out the topic's last post so we don't have a foreign key pointing
bgneal@318 509 # to a post when we delete posts.
bgneal@318 510 topic.last_post = None
bgneal@318 511 topic.save()
bgneal@318 512
bgneal@318 513 # delete all posts in bulk
bgneal@318 514 posts.delete()
bgneal@318 515
bgneal@318 516 # It should be safe to just delete the topic now.
bgneal@232 517 topic.delete()
bgneal@232 518
bgneal@318 519 # Resync parent forum's post and topic counts
bgneal@318 520 parent_forum.sync()
bgneal@318 521 parent_forum.save()
bgneal@318 522
bgneal@232 523
bgneal@232 524 @login_required
bgneal@232 525 def new_post(request, topic_id):
bgneal@232 526 """
bgneal@232 527 This function is the view for creating a normal, non-quick reply
bgneal@232 528 to a topic.
bgneal@232 529 """
bgneal@232 530 topic = get_object_or_404(Topic.objects.select_related(), pk=topic_id)
bgneal@232 531 can_post = _can_post_in_topic(topic, request.user)
bgneal@232 532
bgneal@232 533 if can_post:
bgneal@232 534 if request.method == 'POST':
bgneal@232 535 form = PostForm(request.POST)
bgneal@232 536 if form.is_valid():
bgneal@232 537 if antispam.utils.spam_check(request, form.cleaned_data['body']):
bgneal@232 538 return HttpResponseRedirect(reverse('antispam-suspended'))
bgneal@232 539 post = form.save(commit=False)
bgneal@232 540 post.topic = topic
bgneal@232 541 post.user = request.user
bgneal@232 542 post.user_ip = request.META.get("REMOTE_ADDR", "")
bgneal@232 543 post.save()
bgneal@285 544
bgneal@285 545 # Save any attachments
bgneal@286 546 form.attach_proc.save_attachments(post)
bgneal@285 547
bgneal@232 548 _bump_post_count(request.user)
bgneal@232 549 _update_last_visit(request.user, topic)
bgneal@232 550 return HttpResponseRedirect(post.get_absolute_url())
bgneal@232 551 else:
bgneal@232 552 quote_id = request.GET.get('quote')
bgneal@232 553 if quote_id:
bgneal@232 554 quote_post = get_object_or_404(Post.objects.select_related(),
bgneal@232 555 pk=quote_id)
bgneal@232 556 form = PostForm(initial={'body': _quote_message(quote_post.user.username,
bgneal@232 557 quote_post.body)})
bgneal@232 558 else:
bgneal@232 559 form = PostForm()
bgneal@232 560 else:
bgneal@232 561 form = None
bgneal@232 562
bgneal@232 563 return render_to_response('forums/new_post.html', {
bgneal@232 564 'forum': topic.forum,
bgneal@232 565 'topic': topic,
bgneal@232 566 'form': form,
bgneal@232 567 'can_post': can_post,
bgneal@232 568 },
bgneal@232 569 context_instance=RequestContext(request))
bgneal@232 570
bgneal@232 571
bgneal@232 572 @login_required
bgneal@232 573 def mod_topic_stick(request, id):
bgneal@232 574 """
bgneal@232 575 This view function is for moderators to toggle the sticky status of a topic.
bgneal@232 576 """
bgneal@232 577 topic = get_object_or_404(Topic.objects.select_related(), pk=id)
bgneal@232 578 if _can_moderate(topic.forum, request.user):
bgneal@232 579 topic.sticky = not topic.sticky
bgneal@232 580 topic.save()
bgneal@232 581 return HttpResponseRedirect(topic.get_absolute_url())
bgneal@232 582
bgneal@232 583 return HttpResponseForbidden()
bgneal@232 584
bgneal@232 585
bgneal@232 586 @login_required
bgneal@232 587 def mod_topic_lock(request, id):
bgneal@232 588 """
bgneal@232 589 This view function is for moderators to toggle the locked status of a topic.
bgneal@232 590 """
bgneal@232 591 topic = get_object_or_404(Topic.objects.select_related(), pk=id)
bgneal@232 592 if _can_moderate(topic.forum, request.user):
bgneal@232 593 topic.locked = not topic.locked
bgneal@232 594 topic.save()
bgneal@232 595 return HttpResponseRedirect(topic.get_absolute_url())
bgneal@232 596
bgneal@232 597 return HttpResponseForbidden()
bgneal@232 598
bgneal@232 599
bgneal@232 600 @login_required
bgneal@232 601 def mod_topic_delete(request, id):
bgneal@232 602 """
bgneal@232 603 This view function is for moderators to delete an entire topic.
bgneal@232 604 """
bgneal@232 605 topic = get_object_or_404(Topic.objects.select_related(), pk=id)
bgneal@232 606 if _can_moderate(topic.forum, request.user):
bgneal@232 607 forum_url = topic.forum.get_absolute_url()
bgneal@232 608 _delete_topic(topic)
bgneal@232 609 return HttpResponseRedirect(forum_url)
bgneal@232 610
bgneal@232 611 return HttpResponseForbidden()
bgneal@232 612
bgneal@232 613
bgneal@232 614 @login_required
bgneal@232 615 def mod_topic_move(request, id):
bgneal@232 616 """
bgneal@232 617 This view function is for moderators to move a topic to a different forum.
bgneal@232 618 """
bgneal@232 619 topic = get_object_or_404(Topic.objects.select_related(), pk=id)
bgneal@232 620 if not _can_moderate(topic.forum, request.user):
bgneal@232 621 return HttpResponseForbidden()
bgneal@232 622
bgneal@232 623 if request.method == 'POST':
bgneal@232 624 form = MoveTopicForm(request.user, request.POST)
bgneal@232 625 if form.is_valid():
bgneal@232 626 new_forum = form.cleaned_data['forums']
bgneal@232 627 old_forum = topic.forum
bgneal@232 628 _move_topic(topic, old_forum, new_forum)
bgneal@232 629 return HttpResponseRedirect(topic.get_absolute_url())
bgneal@232 630 else:
bgneal@232 631 form = MoveTopicForm(request.user)
bgneal@232 632
bgneal@232 633 return render_to_response('forums/move_topic.html', {
bgneal@232 634 'forum': topic.forum,
bgneal@232 635 'topic': topic,
bgneal@232 636 'form': form,
bgneal@232 637 },
bgneal@232 638 context_instance=RequestContext(request))
bgneal@232 639
bgneal@232 640
bgneal@232 641 @login_required
bgneal@232 642 def mod_forum(request, slug):
bgneal@232 643 """
bgneal@232 644 Displays a view to allow moderators to perform various operations
bgneal@232 645 on topics in a forum in bulk. We currently support mass locking/unlocking,
bgneal@232 646 stickying and unstickying, moving, and deleting topics.
bgneal@232 647 """
bgneal@232 648 forum = get_object_or_404(Forum.objects.select_related(), slug=slug)
bgneal@232 649 if not _can_moderate(forum, request.user):
bgneal@232 650 return HttpResponseForbidden()
bgneal@232 651
bgneal@232 652 topics = forum.topics.select_related('user', 'last_post', 'last_post__user')
bgneal@232 653 paginator = create_topic_paginator(topics)
bgneal@232 654 page_num = get_page_num(request)
bgneal@232 655 try:
bgneal@232 656 page = paginator.page(page_num)
bgneal@232 657 except InvalidPage:
bgneal@232 658 raise Http404
bgneal@232 659
bgneal@232 660 # we do this for the template since it is rendered twice
bgneal@232 661 page_nav = render_to_string('forums/pagination.html', {'page': page})
bgneal@232 662 form = None
bgneal@232 663
bgneal@232 664 if request.method == 'POST':
bgneal@232 665 topic_ids = request.POST.getlist('topic_ids')
bgneal@232 666 url = reverse('forums-mod_forum', kwargs={'slug':forum.slug})
bgneal@232 667 url += '?page=%s' % page_num
bgneal@232 668
bgneal@232 669 if len(topic_ids):
bgneal@232 670 if request.POST.get('sticky'):
bgneal@232 671 _bulk_sticky(forum, topic_ids)
bgneal@232 672 return HttpResponseRedirect(url)
bgneal@232 673 elif request.POST.get('lock'):
bgneal@232 674 _bulk_lock(forum, topic_ids)
bgneal@232 675 return HttpResponseRedirect(url)
bgneal@232 676 elif request.POST.get('delete'):
bgneal@232 677 _bulk_delete(forum, topic_ids)
bgneal@232 678 return HttpResponseRedirect(url)
bgneal@232 679 elif request.POST.get('move'):
bgneal@232 680 form = MoveTopicForm(request.user, request.POST, hide_label=True)
bgneal@232 681 if form.is_valid():
bgneal@232 682 _bulk_move(topic_ids, forum, form.cleaned_data['forums'])
bgneal@232 683 return HttpResponseRedirect(url)
bgneal@286 684
bgneal@232 685 if form is None:
bgneal@232 686 form = MoveTopicForm(request.user, hide_label=True)
bgneal@232 687
bgneal@232 688 return render_to_response('forums/mod_forum.html', {
bgneal@232 689 'forum': forum,
bgneal@232 690 'page': page,
bgneal@232 691 'page_nav': page_nav,
bgneal@232 692 'form': form,
bgneal@232 693 },
bgneal@232 694 context_instance=RequestContext(request))
bgneal@232 695
bgneal@232 696
bgneal@232 697 @login_required
bgneal@232 698 @require_POST
bgneal@232 699 def forum_catchup(request, slug):
bgneal@232 700 """
bgneal@232 701 This view marks all the topics in the forum as being read.
bgneal@232 702 """
bgneal@232 703 forum = get_object_or_404(Forum.objects.select_related(), slug=slug)
bgneal@232 704
bgneal@232 705 if not forum.category.can_access(request.user):
bgneal@232 706 return HttpResponseForbidden()
bgneal@232 707
bgneal@232 708 forum.catchup(request.user)
bgneal@232 709 return HttpResponseRedirect(forum.get_absolute_url())
bgneal@232 710
bgneal@232 711
bgneal@232 712 @login_required
bgneal@232 713 def mod_topic_split(request, id):
bgneal@232 714 """
bgneal@232 715 This view function allows moderators to split posts off to a new topic.
bgneal@232 716 """
bgneal@232 717 topic = get_object_or_404(Topic.objects.select_related(), pk=id)
bgneal@232 718 if not _can_moderate(topic.forum, request.user):
bgneal@232 719 return HttpResponseRedirect(topic.get_absolute_url())
bgneal@232 720
bgneal@232 721 if request.method == "POST":
bgneal@232 722 form = SplitTopicForm(request.user, request.POST)
bgneal@232 723 if form.is_valid():
bgneal@232 724 if form.split_at:
bgneal@232 725 _split_topic_at(topic, form.post_ids[0],
bgneal@232 726 form.cleaned_data['forums'],
bgneal@232 727 form.cleaned_data['name'])
bgneal@232 728 else:
bgneal@232 729 _split_topic(topic, form.post_ids,
bgneal@232 730 form.cleaned_data['forums'],
bgneal@232 731 form.cleaned_data['name'])
bgneal@232 732
bgneal@232 733 return HttpResponseRedirect(topic.get_absolute_url())
bgneal@232 734 else:
bgneal@232 735 form = SplitTopicForm(request.user)
bgneal@232 736
bgneal@232 737 posts = topic.posts.select_related()
bgneal@232 738
bgneal@232 739 return render_to_response('forums/mod_split_topic.html', {
bgneal@232 740 'forum': topic.forum,
bgneal@232 741 'topic': topic,
bgneal@232 742 'posts': posts,
bgneal@232 743 'form': form,
bgneal@232 744 },
bgneal@232 745 context_instance=RequestContext(request))
bgneal@232 746
bgneal@232 747
bgneal@232 748 @login_required
bgneal@232 749 def unread_topics(request):
bgneal@232 750 """Displays the topics with unread posts for a given user."""
bgneal@232 751
bgneal@232 752 topics = get_unread_topics(request.user)
bgneal@232 753
bgneal@232 754 paginator = create_topic_paginator(topics)
bgneal@232 755 page_num = get_page_num(request)
bgneal@232 756 try:
bgneal@232 757 page = paginator.page(page_num)
bgneal@232 758 except InvalidPage:
bgneal@232 759 raise Http404
bgneal@232 760
bgneal@232 761 attach_topic_page_ranges(page.object_list)
bgneal@232 762
bgneal@232 763 # we do this for the template since it is rendered twice
bgneal@232 764 page_nav = render_to_string('forums/pagination.html', {'page': page})
bgneal@232 765
bgneal@232 766 return render_to_response('forums/topic_list.html', {
bgneal@232 767 'title': 'Topics With Unread Posts',
bgneal@232 768 'page': page,
bgneal@232 769 'page_nav': page_nav,
bgneal@232 770 },
bgneal@232 771 context_instance=RequestContext(request))
bgneal@232 772
bgneal@232 773
bgneal@232 774 def unanswered_topics(request):
bgneal@232 775 """Displays the topics with no replies."""
bgneal@232 776
bgneal@232 777 forum_ids = Forum.objects.forum_ids_for_user(request.user)
bgneal@232 778 topics = Topic.objects.filter(forum__id__in=forum_ids,
bgneal@232 779 post_count=1).select_related(
bgneal@232 780 'forum', 'user', 'last_post', 'last_post__user')
bgneal@232 781
bgneal@232 782 paginator = create_topic_paginator(topics)
bgneal@232 783 page_num = get_page_num(request)
bgneal@232 784 try:
bgneal@232 785 page = paginator.page(page_num)
bgneal@232 786 except InvalidPage:
bgneal@232 787 raise Http404
bgneal@232 788
bgneal@232 789 attach_topic_page_ranges(page.object_list)
bgneal@232 790
bgneal@232 791 # we do this for the template since it is rendered twice
bgneal@232 792 page_nav = render_to_string('forums/pagination.html', {'page': page})
bgneal@232 793
bgneal@232 794 return render_to_response('forums/topic_list.html', {
bgneal@232 795 'title': 'Unanswered Topics',
bgneal@232 796 'page': page,
bgneal@232 797 'page_nav': page_nav,
bgneal@232 798 },
bgneal@232 799 context_instance=RequestContext(request))
bgneal@232 800
bgneal@232 801
bgneal@232 802 @login_required
bgneal@232 803 def my_posts(request):
bgneal@232 804 """Displays a list of posts the requesting user made."""
bgneal@232 805 return _user_posts(request, request.user, request.user, 'My Posts')
bgneal@232 806
bgneal@232 807
bgneal@232 808 @login_required
bgneal@232 809 def posts_for_user(request, username):
bgneal@232 810 """Displays a list of posts by the given user.
bgneal@232 811 Only the forums that the requesting user can see are examined.
bgneal@232 812 """
bgneal@232 813 target_user = get_object_or_404(User, username=username)
bgneal@232 814 return _user_posts(request, target_user, request.user, 'Posts by %s' % username)
bgneal@232 815
bgneal@232 816
bgneal@232 817 @login_required
bgneal@232 818 def post_ip_info(request, post_id):
bgneal@232 819 """Displays information about the IP address the post was made from."""
bgneal@232 820 post = get_object_or_404(Post.objects.select_related(), pk=post_id)
bgneal@232 821
bgneal@232 822 if not _can_moderate(post.topic.forum, request.user):
bgneal@232 823 return HttpResponseForbidden("You don't have permission for this post.")
bgneal@232 824
bgneal@232 825 ip_users = sorted(set(Post.objects.filter(
bgneal@232 826 user_ip=post.user_ip).values_list('user__username', flat=True)))
bgneal@232 827
bgneal@232 828 return render_to_response('forums/post_ip.html', {
bgneal@232 829 'post': post,
bgneal@232 830 'ip_users': ip_users,
bgneal@232 831 },
bgneal@232 832 context_instance=RequestContext(request))
bgneal@232 833
bgneal@232 834
bgneal@232 835 def _user_posts(request, target_user, req_user, page_title):
bgneal@286 836 """Displays a list of posts made by the target user.
bgneal@232 837 req_user is the user trying to view the posts. Only the forums
bgneal@232 838 req_user can see are searched.
bgneal@232 839 """
bgneal@232 840 forum_ids = Forum.objects.forum_ids_for_user(req_user)
bgneal@232 841 posts = Post.objects.filter(user=target_user,
bgneal@232 842 topic__forum__id__in=forum_ids).order_by(
bgneal@232 843 '-creation_date').select_related()
bgneal@232 844
bgneal@232 845 paginator = create_post_paginator(posts)
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 # we do this for the template since it is rendered twice
bgneal@232 853 page_nav = render_to_string('forums/pagination.html', {'page': page})
bgneal@232 854
bgneal@232 855 return render_to_response('forums/post_list.html', {
bgneal@232 856 'title': page_title,
bgneal@232 857 'page': page,
bgneal@232 858 'page_nav': page_nav,
bgneal@232 859 },
bgneal@232 860 context_instance=RequestContext(request))
bgneal@232 861
bgneal@232 862
bgneal@232 863 def _can_moderate(forum, user):
bgneal@232 864 """
bgneal@232 865 Determines if a user has permission to moderate a given forum.
bgneal@232 866 """
bgneal@232 867 return user.is_authenticated() and (
bgneal@232 868 user.is_superuser or user in forum.moderators.all())
bgneal@232 869
bgneal@232 870
bgneal@232 871 def _can_post_in_topic(topic, user):
bgneal@232 872 """
bgneal@232 873 This function returns true if the given user can post in the given topic
bgneal@232 874 and false otherwise.
bgneal@232 875 """
bgneal@232 876 return (not topic.locked and topic.forum.category.can_access(user)) or \
bgneal@232 877 (user.is_superuser or user in topic.forum.moderators.all())
bgneal@232 878
bgneal@232 879
bgneal@232 880 def _bump_post_count(user):
bgneal@232 881 """
bgneal@232 882 Increments the forum_post_count for the given user.
bgneal@232 883 """
bgneal@232 884 profile = user.get_profile()
bgneal@232 885 profile.forum_post_count += 1
bgneal@232 886 profile.save()
bgneal@232 887
bgneal@232 888
bgneal@232 889 def _quote_message(who, message):
bgneal@232 890 """
bgneal@232 891 Builds a message reply by quoting the existing message in a
bgneal@232 892 typical email-like fashion. The quoting is compatible with Markdown.
bgneal@232 893 """
bgneal@232 894 header = '*%s wrote:*\n\n' % (who, )
bgneal@232 895 lines = wrap(message, 55).split('\n')
bgneal@232 896 for i, line in enumerate(lines):
bgneal@232 897 lines[i] = '> ' + line
bgneal@232 898 return header + '\n'.join(lines)
bgneal@232 899
bgneal@232 900
bgneal@232 901 def _move_topic(topic, old_forum, new_forum):
bgneal@232 902 if new_forum != old_forum:
bgneal@232 903 topic.forum = new_forum
bgneal@232 904 topic.save()
bgneal@232 905 # Have to adjust foreign keys to last_post, denormalized counts, etc.:
bgneal@232 906 old_forum.sync()
bgneal@232 907 old_forum.save()
bgneal@232 908 new_forum.sync()
bgneal@232 909 new_forum.save()
bgneal@232 910
bgneal@232 911
bgneal@232 912 def _bulk_sticky(forum, topic_ids):
bgneal@232 913 """
bgneal@232 914 Performs a toggle on the sticky status for a given list of topic ids.
bgneal@232 915 """
bgneal@232 916 topics = Topic.objects.filter(pk__in=topic_ids)
bgneal@232 917 for topic in topics:
bgneal@232 918 if topic.forum == forum:
bgneal@232 919 topic.sticky = not topic.sticky
bgneal@232 920 topic.save()
bgneal@232 921
bgneal@232 922
bgneal@232 923 def _bulk_lock(forum, topic_ids):
bgneal@232 924 """
bgneal@232 925 Performs a toggle on the locked status for a given list of topic ids.
bgneal@232 926 """
bgneal@232 927 topics = Topic.objects.filter(pk__in=topic_ids)
bgneal@232 928 for topic in topics:
bgneal@232 929 if topic.forum == forum:
bgneal@232 930 topic.locked = not topic.locked
bgneal@232 931 topic.save()
bgneal@232 932
bgneal@232 933
bgneal@232 934 def _bulk_delete(forum, topic_ids):
bgneal@232 935 """
bgneal@232 936 Deletes the list of topics.
bgneal@232 937 """
bgneal@235 938 # Because we are deleting stuff, retrieve each topic one at a
bgneal@235 939 # time since we are going to be adjusting de-normalized fields
bgneal@235 940 # during deletes. In particular, we can't do this:
bgneal@235 941 # topics = Topic.objects.filter(pk__in=topic_ids).select_related()
bgneal@235 942 # for topic in topics:
bgneal@235 943 # since topic.forum.last_post can go stale after a delete.
bgneal@235 944
bgneal@235 945 for id in topic_ids:
bgneal@235 946 try:
bgneal@235 947 topic = Topic.objects.select_related().get(pk=id)
bgneal@235 948 except Topic.DoesNotExist:
bgneal@235 949 continue
bgneal@235 950 _delete_topic(topic)
bgneal@232 951
bgneal@232 952
bgneal@232 953 def _bulk_move(topic_ids, old_forum, new_forum):
bgneal@232 954 """
bgneal@232 955 Moves the list of topics to a new forum.
bgneal@232 956 """
bgneal@232 957 topics = Topic.objects.filter(pk__in=topic_ids).select_related()
bgneal@232 958 for topic in topics:
bgneal@232 959 if topic.forum == old_forum:
bgneal@232 960 _move_topic(topic, old_forum, new_forum)
bgneal@232 961
bgneal@232 962
bgneal@232 963 def _update_last_visit(user, topic):
bgneal@232 964 """
bgneal@232 965 Does the bookkeeping for the last visit status for the user to the
bgneal@232 966 topic/forum.
bgneal@232 967 """
bgneal@232 968 now = datetime.datetime.now()
bgneal@232 969 try:
bgneal@232 970 flv = ForumLastVisit.objects.get(user=user, forum=topic.forum)
bgneal@232 971 except ForumLastVisit.DoesNotExist:
bgneal@232 972 flv = ForumLastVisit(user=user, forum=topic.forum)
bgneal@232 973 flv.begin_date = now
bgneal@232 974
bgneal@232 975 flv.end_date = now
bgneal@232 976 flv.save()
bgneal@232 977
bgneal@232 978 if topic.update_date > flv.begin_date:
bgneal@232 979 try:
bgneal@232 980 tlv = TopicLastVisit.objects.get(user=user, topic=topic)
bgneal@232 981 except TopicLastVisit.DoesNotExist:
bgneal@232 982 tlv = TopicLastVisit(user=user, topic=topic)
bgneal@232 983
bgneal@232 984 tlv.touch()
bgneal@232 985 tlv.save()
bgneal@232 986
bgneal@232 987
bgneal@232 988 def _split_topic_at(topic, post_id, new_forum, new_name):
bgneal@232 989 """
bgneal@232 990 This function splits the post given by post_id and all posts that come
bgneal@232 991 after it in the given topic to a new topic in a new forum.
bgneal@232 992 It is assumed the caller has been checked for moderator rights.
bgneal@232 993 """
bgneal@232 994 post = get_object_or_404(Post, id=post_id)
bgneal@232 995 if post.topic == topic:
bgneal@232 996 post_ids = Post.objects.filter(topic=topic,
bgneal@232 997 creation_date__gte=post.creation_date).values_list('id', flat=True)
bgneal@232 998 _split_topic(topic, post_ids, new_forum, new_name)
bgneal@232 999
bgneal@232 1000
bgneal@232 1001 def _split_topic(topic, post_ids, new_forum, new_name):
bgneal@232 1002 """
bgneal@232 1003 This function splits the posts given by the post_ids list in the
bgneal@232 1004 given topic to a new topic in a new forum.
bgneal@232 1005 It is assumed the caller has been checked for moderator rights.
bgneal@232 1006 """
bgneal@232 1007 posts = Post.objects.filter(topic=topic, id__in=post_ids)
bgneal@232 1008 if len(posts) > 0:
bgneal@232 1009 new_topic = Topic(forum=new_forum, name=new_name, user=posts[0].user)
bgneal@232 1010 new_topic.save()
bgneal@232 1011 for post in posts:
bgneal@232 1012 post.topic = new_topic
bgneal@232 1013 post.save()
bgneal@286 1014
bgneal@232 1015 topic.post_count_update()
bgneal@232 1016 topic.save()
bgneal@232 1017 new_topic.post_count_update()
bgneal@232 1018 new_topic.save()
bgneal@232 1019 topic.forum.sync()
bgneal@232 1020 topic.forum.save()
bgneal@232 1021 new_forum.sync()
bgneal@232 1022 new_forum.save()