threadslist.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  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.shortcuts import render
  7. from django.views.generic import View
  8. from django.utils import timezone
  9. from django.utils.translation import ugettext as _, ugettext_lazy
  10. from misago.categories.models import CATEGORIES_TREE_ID, Category
  11. from misago.categories.permissions import (
  12. allow_see_category, allow_browse_category)
  13. from misago.core.shortcuts import (
  14. get_object_or_404, paginate, pagination_dict, validate_slug)
  15. from misago.readtracker import threadstracker
  16. from misago.threads.models import Thread
  17. from misago.threads.permissions import exclude_invisible_threads
  18. from misago.threads.utils import add_categories_to_threads
  19. LISTS_NAMES = {
  20. 'my': ugettext_lazy("Your threads"),
  21. 'new': ugettext_lazy("New threads"),
  22. 'unread': ugettext_lazy("Unread threads"),
  23. 'subscribed': ugettext_lazy("Subscribed threads"),
  24. }
  25. def filter_threads_queryset(user, categories, list_type, queryset):
  26. if list_type == 'my':
  27. return queryset.filter(starter=user)
  28. elif list_type == 'subscribed':
  29. return queryset # TODO: filter(id__in=[subscribed threads])
  30. else:
  31. # grab cutoffs for categories
  32. cutoff_date = timezone.now() - timedelta(
  33. days=settings.MISAGO_FRESH_CONTENT_PERIOD
  34. )
  35. if cutoff_date < user.reads_cutoff:
  36. cutoff_date = user.reads_cutoff
  37. categories_dict = {}
  38. for record in user.categoryread_set.filter(category__in=categories):
  39. if record.last_read_on > cutoff_date:
  40. categories_dict[record.category_id] = record.last_read_on
  41. if list_type == 'new':
  42. # new threads have no entry in reads table
  43. # AND were started after cutoff date
  44. read_threads = user.threadread_set.filter(
  45. category__in=categories
  46. ).values('thread_id')
  47. if categories_dict:
  48. condition = Q(
  49. started_on__lte=cutoff_date,
  50. id__in=read_threads,
  51. )
  52. for category_id, category_cutoff in categories_dict.items():
  53. condition = condition | Q(
  54. category_id=category_id,
  55. started_on__lte=category_cutoff,
  56. )
  57. return queryset.exclude(condition)
  58. else:
  59. return queryset.exclude(
  60. started_on__lte=cutoff_date,
  61. id__in=read_threads,
  62. )
  63. elif list_type == 'unread':
  64. # unread threads were read in past but have new posts
  65. # after cutoff date
  66. read_threads = user.threadread_set.filter(
  67. category__in=categories,
  68. thread__last_post_on__gt=cutoff_date,
  69. last_read_on__lt=F('thread__last_post_on')
  70. ).values('thread_id')
  71. queryset = queryset.filter(id__in=read_threads)
  72. # unread threads have last reply after read/cutoff date
  73. if categories_dict:
  74. conditions = None
  75. for category_id, category_cutoff in categories_dict.items():
  76. condition = Q(
  77. category_id=category_id,
  78. last_post_on__lte=category_cutoff,
  79. )
  80. if conditions:
  81. conditions = conditions | condition
  82. else:
  83. conditions = condition
  84. return queryset.exclude(conditions)
  85. else:
  86. return queryset
  87. def get_threads_queryset(user, categories, list_type):
  88. queryset = Thread.objects
  89. queryset = exclude_invisible_threads(user, categories, queryset)
  90. if list_type == 'all':
  91. return queryset
  92. else:
  93. return filter_threads_queryset(user, categories, list_type, queryset)
  94. class BaseList(View):
  95. template_name = 'misago/threadslist/threads.html'
  96. preloaded_data_prefix = ''
  97. def allow_see_list(self, request, category, list_type):
  98. if request.user.is_anonymous():
  99. if list_type == 'my':
  100. raise PermissionDenied( _("You have to sign in to see list of "
  101. "threads that you have started."))
  102. if list_type == 'new':
  103. raise PermissionDenied(_("You have to sign in to see list of "
  104. "threads you haven't read."))
  105. if list_type == 'unread':
  106. raise PermissionDenied(_("You have to sign in to see list of "
  107. "threads with new replies."))
  108. if list_type == 'subscribed':
  109. raise PermissionDenied(_("You have to sign in to see list of "
  110. "threads you are subscribing."))
  111. def get_subcategories(self, request, category):
  112. if category.is_leaf_node():
  113. return []
  114. visible_categories = request.user.acl['visible_categories']
  115. queryset = category.get_descendants().filter(id__in=visible_categories)
  116. return list(queryset.order_by('lft'))
  117. def get_extra_context(self, request, category, subcategories, list_type):
  118. return {}
  119. def get(self, request, **kwargs):
  120. try:
  121. page = int(request.GET.get('page', 0))
  122. if page == 1:
  123. page = None
  124. except ValueError:
  125. raise Http404()
  126. list_type = kwargs['list_type']
  127. category = self.get_category(request, **kwargs)
  128. self.allow_see_list(request, category, list_type)
  129. subcategories = self.get_subcategories(request, category)
  130. categories = [category] + subcategories
  131. queryset = self.get_queryset(
  132. request, categories, list_type).order_by('-last_post_on')
  133. page = paginate(queryset, page, 24, 6)
  134. paginator = pagination_dict(page)
  135. if list_type in ('new', 'unread'):
  136. """we already know all threads on list are unread"""
  137. threadstracker.make_unread(page.object_list)
  138. else:
  139. threadstracker.make_threads_read_aware(
  140. request.user, page.object_list)
  141. add_categories_to_threads(categories, page.object_list)
  142. visible_subcategories = []
  143. for thread in page.object_list:
  144. if (thread.top_category and
  145. thread.top_category not in visible_subcategories):
  146. visible_subcategories.append(thread.top_category.pk)
  147. category.subcategories = []
  148. for subcategory in subcategories:
  149. if subcategory.pk in visible_subcategories:
  150. category.subcategories.append(subcategory)
  151. extra_context = self.get_extra_context(
  152. request, category, subcategories, list_type)
  153. show_toolbar = False
  154. if paginator['count']:
  155. if category.subcategories:
  156. show_toolbar = True
  157. if request.user.is_authenticated():
  158. show_toolbar = True
  159. return render(request, self.template_name, dict(
  160. category=category,
  161. show_toolbar=show_toolbar,
  162. list_type=list_type,
  163. list_name=LISTS_NAMES.get(list_type),
  164. threads=page.object_list,
  165. paginator=paginator,
  166. count=paginator['count'],
  167. **extra_context
  168. ))
  169. class ThreadsList(BaseList):
  170. template_name = 'misago/threadslist/threads.html'
  171. def get_category(self, request, **kwargs):
  172. return Category.objects.root_category()
  173. def get_queryset(self, request, categories, list_type):
  174. # [:1] cos we are cutting off root caregory on forum threads list
  175. # as it includes nedless extra condition to DB filter
  176. return get_threads_queryset(request.user, categories[1:], list_type)
  177. def get_extra_context(self, request, category, subcategories, list_type):
  178. return {
  179. 'is_index': not settings.MISAGO_CATEGORIES_ON_INDEX
  180. }
  181. class CategoryThreadsList(ThreadsList):
  182. template_name = 'misago/threadslist/category.html'
  183. preloaded_data_prefix = 'CATEGORY_'
  184. def get_category(self, request, **kwargs):
  185. category = get_object_or_404(Category.objects.select_related('parent'),
  186. tree_id=CATEGORIES_TREE_ID,
  187. id=kwargs['category_id'],
  188. )
  189. allow_see_category(request.user, category)
  190. allow_browse_category(request.user, category)
  191. validate_slug(category, kwargs['category_slug'])
  192. return category
  193. def get_queryset(self, request, categories, list_type):
  194. return get_threads_queryset(request.user, categories, list_type)
  195. class PrivateThreadsList(ThreadsList):
  196. template_name = 'misago/threadslist/private_threads.html'
  197. preloaded_data_prefix = 'PRIVATE_'
  198. def get_category(self, request, **kwargs):
  199. return Category.objects.private_threads()
  200. def get_subcategories(self, request, category):
  201. return []