comparison forums/views/main.py @ 581:ee87ea74d46b

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