threads.py 8.4 KB

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