threads.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. from datetime import timedelta
  2. from django.core.exceptions import PermissionDenied
  3. from django.db.models import F, Q
  4. from django.http import Http404
  5. from django.utils import timezone
  6. from django.utils.translation import ugettext as _
  7. from django.utils.translation import ugettext_lazy
  8. from misago.acl import add_acl
  9. from misago.conf import settings
  10. from misago.core.shortcuts import paginate, pagination_dict
  11. from misago.readtracker import threadstracker
  12. from misago.readtracker.dates import get_cutoff_date
  13. from misago.threads.models import Post, Thread
  14. from misago.threads.participants import make_participants_aware
  15. from misago.threads.permissions import exclude_invisible_posts, exclude_invisible_threads
  16. from misago.threads.serializers import ThreadsListSerializer
  17. from misago.threads.subscriptions import make_subscription_aware
  18. from misago.threads.utils import add_categories_to_items
  19. __all__ = ['ForumThreads', 'PrivateThreads', 'filter_read_threads_queryset']
  20. LISTS_NAMES = {
  21. 'all': None,
  22. 'my': ugettext_lazy("Your threads"),
  23. 'new': ugettext_lazy("New threads"),
  24. 'unread': ugettext_lazy("Unread threads"),
  25. 'subscribed': ugettext_lazy("Subscribed threads"),
  26. 'unapproved': ugettext_lazy("Unapproved content"),
  27. }
  28. LIST_DENIED_MESSAGES = {
  29. 'my': ugettext_lazy("You have to sign in to see list of threads that you have started."),
  30. 'new': ugettext_lazy("You have to sign in to see list of threads you haven't read."),
  31. 'unread': ugettext_lazy("You have to sign in to see list of threads with new replies."),
  32. 'subscribed': ugettext_lazy("You have to sign in to see list of threads you are subscribing."),
  33. 'unapproved': ugettext_lazy("You have to sign in to see list of threads with unapproved posts."),
  34. }
  35. class ViewModel(object):
  36. def __init__(self, request, category, list_type, page):
  37. self.allow_see_list(request, category, list_type)
  38. category_model = category.unwrap()
  39. base_queryset = self.get_base_queryset(request, category.categories, list_type)
  40. base_queryset = base_queryset.select_related('starter', 'last_poster')
  41. threads_categories = [category_model] + category.subcategories
  42. threads_queryset = self.get_remaining_threads_queryset(
  43. base_queryset, category_model, threads_categories
  44. )
  45. list_page = paginate(
  46. threads_queryset, page, settings.MISAGO_THREADS_PER_PAGE, settings.MISAGO_THREADS_TAIL
  47. )
  48. paginator = pagination_dict(list_page)
  49. if list_page.number > 1:
  50. threads = list(list_page.object_list)
  51. else:
  52. pinned_threads = list(
  53. self.get_pinned_threads(base_queryset, category_model, threads_categories)
  54. )
  55. threads = list(pinned_threads) + list(list_page.object_list)
  56. add_categories_to_items(category_model, category.categories, threads)
  57. add_acl(request.user, threads)
  58. make_subscription_aware(request.user, threads)
  59. if list_type in ('new', 'unread'):
  60. # we already know all threads on list are unread
  61. for thread in threads:
  62. thread.is_read = False
  63. thread.is_new = True
  64. else:
  65. threadstracker.make_read_aware(request.user, threads)
  66. self.filter_threads(request, threads)
  67. # set state on object for easy access from hooks
  68. self.category = category
  69. self.threads = threads
  70. self.list_type = list_type
  71. self.paginator = paginator
  72. def allow_see_list(self, request, category, list_type):
  73. if list_type not in LISTS_NAMES:
  74. raise Http404()
  75. if request.user.is_anonymous:
  76. if list_type in LIST_DENIED_MESSAGES:
  77. raise PermissionDenied(LIST_DENIED_MESSAGES[list_type])
  78. else:
  79. has_permission = request.user.acl_cache['can_see_unapproved_content_lists']
  80. if list_type == 'unapproved' and not has_permission:
  81. raise PermissionDenied(
  82. _("You don't have permission to see unapproved content lists.")
  83. )
  84. def get_list_name(self, list_type):
  85. return LISTS_NAMES[list_type]
  86. def get_base_queryset(self, request, threads_categories, list_type):
  87. return get_threads_queryset(
  88. request.user,
  89. threads_categories,
  90. list_type,
  91. ).order_by('-last_post_id')
  92. def get_pinned_threads(self, queryset, category, threads_categories):
  93. return []
  94. def get_remaining_threads_queryset(self, queryset, category, threads_categories):
  95. return []
  96. def filter_threads(self, request, threads):
  97. pass # hook for custom thread types to add features to extend threads
  98. def get_frontend_context(self):
  99. context = {
  100. 'THREADS': {
  101. 'results': ThreadsListSerializer(self.threads, many=True).data,
  102. 'subcategories': [c.pk for c in self.category.children],
  103. },
  104. }
  105. context['THREADS'].update(self.paginator)
  106. return context
  107. def get_template_context(self):
  108. return {
  109. 'list_name': self.get_list_name(self.list_type),
  110. 'list_type': self.list_type,
  111. 'threads': self.threads,
  112. 'paginator': self.paginator,
  113. }
  114. class ForumThreads(ViewModel):
  115. def get_pinned_threads(self, queryset, category, threads_categories):
  116. if category.level:
  117. return list(queryset.filter(weight=2)
  118. ) + list(queryset.filter(weight=1, category__in=threads_categories))
  119. else:
  120. return queryset.filter(weight=2)
  121. def get_remaining_threads_queryset(self, queryset, category, threads_categories):
  122. if category.level:
  123. return queryset.filter(
  124. weight=0,
  125. category__in=threads_categories,
  126. )
  127. else:
  128. return queryset.filter(
  129. weight__lt=2,
  130. category__in=threads_categories,
  131. )
  132. class PrivateThreads(ViewModel):
  133. def get_base_queryset(self, request, threads_categories, list_type):
  134. queryset = super().get_base_queryset(request, threads_categories, list_type)
  135. # limit queryset to threads we are participant of
  136. participated_threads = request.user.threadparticipant_set.values('thread_id')
  137. if request.user.acl_cache['can_moderate_private_threads']:
  138. queryset = queryset.filter(Q(id__in=participated_threads) | Q(has_reported_posts=True))
  139. else:
  140. queryset = queryset.filter(id__in=participated_threads)
  141. return queryset
  142. def get_remaining_threads_queryset(self, queryset, category, threads_categories):
  143. return queryset.filter(category__in=threads_categories)
  144. def filter_threads(self, request, threads):
  145. make_participants_aware(request.user, threads)
  146. def get_threads_queryset(user, categories, list_type):
  147. queryset = exclude_invisible_threads(user, categories, Thread.objects)
  148. if list_type == 'all':
  149. return queryset
  150. else:
  151. return filter_threads_queryset(user, categories, list_type, queryset)
  152. def filter_threads_queryset(user, categories, list_type, queryset):
  153. if list_type == 'my':
  154. return queryset.filter(starter=user)
  155. elif list_type == 'subscribed':
  156. subscribed_threads = user.subscription_set.values('thread_id')
  157. return queryset.filter(id__in=subscribed_threads)
  158. elif list_type == 'unapproved':
  159. return queryset.filter(has_unapproved_posts=True)
  160. elif list_type in ('new', 'unread'):
  161. return filter_read_threads_queryset(user, categories, list_type, queryset)
  162. else:
  163. return queryset
  164. def filter_read_threads_queryset(user, categories, list_type, queryset):
  165. # grab cutoffs for categories
  166. cutoff_date = get_cutoff_date(user)
  167. visible_posts = Post.objects.filter(posted_on__gt=cutoff_date)
  168. visible_posts = exclude_invisible_posts(user, categories, visible_posts)
  169. queryset = queryset.filter(id__in=visible_posts.distinct().values('thread'))
  170. read_posts = visible_posts.filter(id__in=user.postread_set.values('post'))
  171. if list_type == 'new':
  172. # new threads have no entry in reads table
  173. return queryset.exclude(id__in=read_posts.distinct().values('thread'))
  174. if list_type == 'unread':
  175. # unread threads were read in past but have new posts
  176. unread_posts = visible_posts.exclude(id__in=user.postread_set.values('post'))
  177. queryset = queryset.filter(id__in=read_posts.distinct().values('thread'))
  178. queryset = queryset.filter(id__in=unread_posts.distinct().values('thread'))
  179. return queryset