threads.py 8.6 KB

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