threads.py 8.3 KB

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