threads.py 9.5 KB

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