privatethreads.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. from django.contrib import messages
  2. from django.contrib.auth import get_user_model
  3. from django.db.transaction import atomic
  4. from django.http import Http404, JsonResponse
  5. from django.shortcuts import get_object_or_404, redirect, render
  6. from django.utils.translation import ugettext as _, ungettext
  7. from misago.acl import add_acl
  8. from misago.core.exceptions import AjaxError
  9. from misago.forums.models import Forum
  10. from misago.threads import participants
  11. from misago.threads.events import record_event
  12. from misago.threads.forms.posting import ThreadParticipantsForm
  13. from misago.threads.models import Thread, ThreadParticipant
  14. from misago.threads.permissions import (allow_use_private_threads,
  15. allow_see_private_thread,
  16. allow_see_private_post,
  17. exclude_invisible_private_threads)
  18. from misago.threads.views import generic
  19. def private_threads_view(klass):
  20. """
  21. decorator for making views check allow_use_private_threads
  22. """
  23. def decorator(f):
  24. def dispatch(self, request, *args, **kwargs):
  25. allow_use_private_threads(request.user)
  26. return f(self, request, *args, **kwargs)
  27. return dispatch
  28. klass.dispatch = decorator(klass.dispatch)
  29. return klass
  30. class PrivateThreadsMixin(object):
  31. """
  32. Mixin is used to make views use different permission tests
  33. """
  34. def get_forum(self, request, lock=False, **kwargs):
  35. forum = Forum.objects.private_threads()
  36. add_acl(request.user, forum)
  37. return forum
  38. def check_forum_permissions(self, request, forum):
  39. add_acl(request.user, forum)
  40. allow_use_private_threads(request.user)
  41. def fetch_thread(self, request, lock=False, select_related=None,
  42. queryset=None, **kwargs):
  43. queryset = queryset or Thread.objects
  44. if lock:
  45. queryset = queryset.select_for_update()
  46. select_related = select_related or []
  47. if not 'forum' in select_related:
  48. select_related.append('forum')
  49. queryset = queryset.select_related(*select_related)
  50. where = {'id': kwargs.get('thread_id')}
  51. thread = get_object_or_404(queryset, **where)
  52. if thread.forum.special_role != 'private_threads':
  53. raise Http404()
  54. return thread
  55. def check_thread_permissions(self, request, thread):
  56. add_acl(request.user, thread.forum)
  57. add_acl(request.user, thread)
  58. participants.make_thread_participants_aware(request.user, thread)
  59. allow_see_private_thread(request.user, thread)
  60. allow_use_private_threads(request.user)
  61. def check_post_permissions(self, request, post):
  62. add_acl(request.user, post.forum)
  63. add_acl(request.user, post.thread)
  64. add_acl(request.user, post)
  65. participants.make_thread_participants_aware(request.user, thread)
  66. allow_see_private_post(request.user, post)
  67. allow_see_private_thread(request.user, post.thread)
  68. allow_use_private_threads(request.user)
  69. def exclude_invisible_posts(self, queryset, user, forum, thread):
  70. return queryset
  71. class PrivateThreads(generic.Threads):
  72. def get_queryset(self):
  73. threads_qs = Forum.objects.private_threads().thread_set
  74. return exclude_invisible_private_threads(threads_qs, self.user)
  75. class PrivateThreadsFiltering(generic.ThreadsFiltering):
  76. def get_available_filters(self):
  77. filters = super(PrivateThreadsFiltering, self).get_available_filters()
  78. if self.user.acl['can_moderate_private_threads']:
  79. filters.append({
  80. 'type': 'reported',
  81. 'name': _("With reported posts"),
  82. 'is_label': False,
  83. })
  84. return filters
  85. @private_threads_view
  86. class PrivateThreadsView(generic.ThreadsView):
  87. link_name = 'misago:private_threads'
  88. template = 'misago/privatethreads/list.html'
  89. Threads = PrivateThreads
  90. Filtering = PrivateThreadsFiltering
  91. class PrivateThreadActions(generic.ThreadActions):
  92. def get_available_actions(self, kwargs):
  93. user = kwargs['user']
  94. thread = kwargs['thread']
  95. is_moderator = user.acl['can_moderate_private_threads']
  96. if thread.participant and thread.participant.is_owner:
  97. is_owner = True
  98. else:
  99. is_owner = False
  100. actions = []
  101. if is_moderator and not is_owner:
  102. actions.append({
  103. 'action': 'takeover',
  104. 'icon': 'level-up',
  105. 'name': _("Takeover thread")
  106. })
  107. if is_owner:
  108. actions.append({
  109. 'action': 'participants',
  110. 'icon': 'users',
  111. 'name': _("Edit participants"),
  112. 'is_button': True
  113. })
  114. if is_moderator:
  115. if thread.is_closed:
  116. actions.append({
  117. 'action': 'open',
  118. 'icon': 'unlock-alt',
  119. 'name': _("Open thread")
  120. })
  121. else:
  122. actions.append({
  123. 'action': 'close',
  124. 'icon': 'lock',
  125. 'name': _("Close thread")
  126. })
  127. actions.append({
  128. 'action': 'delete',
  129. 'icon': 'times',
  130. 'name': _("Delete thread"),
  131. 'confirmation': _("Are you sure you want to delete this "
  132. "thread? This action can't be undone.")
  133. })
  134. return actions
  135. def action_takeover(self, request, thread):
  136. participants.set_thread_owner(thread, request.user)
  137. messages.success(request, _("You are now owner of this thread."))
  138. @private_threads_view
  139. class ThreadView(PrivateThreadsMixin, generic.ThreadView):
  140. template = 'misago/privatethreads/thread.html'
  141. ThreadActions = PrivateThreadActions
  142. @private_threads_view
  143. class ThreadParticipantsView(PrivateThreadsMixin, generic.ViewBase):
  144. template = 'misago/privatethreads/participants.html'
  145. def dispatch(self, request, *args, **kwargs):
  146. thread = self.get_thread(request, **kwargs)
  147. if not request.is_ajax():
  148. response = render(request, 'misago/errorpages/wrong_way.html')
  149. response.status_code = 405
  150. return response
  151. participants_qs = thread.threadparticipant_set
  152. participants_qs = participants_qs.select_related('user', 'user__rank')
  153. return self.render(request, {
  154. 'forum': thread.forum,
  155. 'thread': thread,
  156. 'participants': participants_qs.order_by('-is_owner', 'user__slug')
  157. })
  158. @private_threads_view
  159. class EditThreadParticipantsView(ThreadParticipantsView):
  160. template = 'misago/privatethreads/participants_modal.html'
  161. @private_threads_view
  162. class BaseEditThreadParticipantView(PrivateThreadsMixin, generic.ViewBase):
  163. @atomic
  164. def dispatch(self, request, *args, **kwargs):
  165. thread = self.get_thread(request, lock=True, **kwargs)
  166. if not request.is_ajax():
  167. response = render(request, 'misago/errorpages/wrong_way.html')
  168. response.status_code = 405
  169. return response
  170. if not request.method == "POST":
  171. raise AjaxError(_("Wrong action received."))
  172. if not thread.participant or not thread.participant.is_owner:
  173. raise AjaxError(_("Only thread owner can add or "
  174. "remove participants from thread."))
  175. return self.action(request, thread, kwargs)
  176. def action(self, request, thread, kwargs):
  177. raise NotImplementedError("views extending EditThreadParticipantView "
  178. "need to define custom action method")
  179. @private_threads_view
  180. class AddThreadParticipantsView(BaseEditThreadParticipantView):
  181. template = 'misago/privatethreads/participants_modal_list.html'
  182. def action(self, request, thread, kwargs):
  183. form = ThreadParticipantsForm(request.POST, user=request.user)
  184. if not form.is_valid():
  185. errors = []
  186. for field_errors in form.errors.as_data().values():
  187. errors.extend([unicode(e[0]) for e in field_errors])
  188. return JsonResponse({'message': errors[0], 'is_error': True})
  189. event_message = _("%(user)s added %(participant)s to this thread.")
  190. participants_list = [p.user for p in thread.participants_list]
  191. for user in form.users_cache:
  192. if user not in participants_list:
  193. participants.add_participant(request, thread, user)
  194. record_event(request.user, thread, 'user', event_message, {
  195. 'user': request.user,
  196. 'participant': user
  197. })
  198. thread.save(update_fields=['has_events'])
  199. participants_qs = thread.threadparticipant_set
  200. participants_qs = participants_qs.select_related('user', 'user__rank')
  201. participants_qs = participants_qs.order_by('-is_owner', 'user__slug')
  202. participants_list = [p for p in participants_qs]
  203. participants_list_html = self.render(request, {
  204. 'forum': thread.forum,
  205. 'thread': thread,
  206. 'participants': participants_list,
  207. }).content
  208. message = ungettext("%(users)s participant",
  209. "%(users)s participants",
  210. len(participants_list))
  211. message = message % {'users': len(participants_list)}
  212. return JsonResponse({
  213. 'is_error': False,
  214. 'message': message,
  215. 'list_html': participants_list_html
  216. })
  217. @private_threads_view
  218. class RemoveThreadParticipantView(BaseEditThreadParticipantView):
  219. def action(self, request, thread, kwargs):
  220. user_qs = thread.threadparticipant_set.select_related('user')
  221. try:
  222. participant = user_qs.get(user_id=kwargs['user_id'])
  223. except ThreadParticipant.DoesNotExist:
  224. return JsonResponse({
  225. 'message': _("Requested participant couldn't be found."),
  226. 'is_error': True,
  227. })
  228. if participant.user == request.user:
  229. return JsonResponse({
  230. 'message': _('To leave thread use "Leave thread" option.'),
  231. 'is_error': True,
  232. })
  233. participants_count = len(thread.participants_list) - 1
  234. if participants_count == 0:
  235. return JsonResponse({
  236. 'message': _("You can't remove last thread participant."),
  237. 'is_error': True,
  238. })
  239. participants.remove_participant(thread, participant.user)
  240. if not participants.thread_has_participants(thread):
  241. thread.delete()
  242. else:
  243. message = _("%(user)s removed %(participant)s from this thread.")
  244. record_event(request.user, thread, 'user', message, {
  245. 'user': request.user,
  246. 'participant': participant.user
  247. })
  248. thread.save(update_fields=['has_events'])
  249. participants_count = len(thread.participants_list) - 1
  250. message = ungettext("%(users)s participant",
  251. "%(users)s participants",
  252. participants_count)
  253. message = message % {'users': participants_count}
  254. return JsonResponse({'is_error': False, 'message': message})
  255. @private_threads_view
  256. class LeaveThreadView(BaseEditThreadParticipantView):
  257. @atomic
  258. def dispatch(self, request, *args, **kwargs):
  259. thread = self.get_thread(request, lock=True, **kwargs)
  260. try:
  261. if not request.method == "POST":
  262. raise RuntimeError(_("Wrong action received."))
  263. if not thread.participant:
  264. raise RuntimeError(_("You have to be thread participant in "
  265. "order to be able to leave thread."))
  266. user_qs = thread.threadparticipant_set.select_related('user')
  267. try:
  268. participant = user_qs.get(user_id=request.user.id)
  269. except ThreadParticipant.DoesNotExist:
  270. raise RuntimeError(_("You need to be thread "
  271. "participant to leave it."))
  272. except RuntimeError as e:
  273. messages.error(request, unicode(e))
  274. return redirect(thread.get_absolute_url())
  275. participants.remove_participant(thread, request.user)
  276. if not thread.threadparticipant_set.exists():
  277. thread.delete()
  278. elif thread.participant.is_owner:
  279. new_owner = user_qs.order_by('id')[:1][0].user
  280. participants.set_thread_owner(thread, new_owner)
  281. message = _("%(user)s left this thread. "
  282. "%(new_owner)s is now thread owner.")
  283. record_event(request.user, thread, 'user', message, {
  284. 'user': request.user,
  285. 'new_owner': new_owner
  286. })
  287. thread.save(update_fields=['has_events'])
  288. else:
  289. message = _("%(user)s left this thread.")
  290. record_event(request.user, thread, 'user', message, {
  291. 'user': request.user,
  292. })
  293. thread.save(update_fields=['has_events'])
  294. message = _('You have left "%(thread)s" thread.')
  295. message = message % {'thread': thread.title}
  296. messages.info(request, message)
  297. return redirect('misago:private_threads')
  298. """
  299. Generics
  300. """
  301. @private_threads_view
  302. class GotoLastView(PrivateThreadsMixin, generic.GotoLastView):
  303. pass
  304. @private_threads_view
  305. class GotoNewView(PrivateThreadsMixin, generic.GotoNewView):
  306. pass
  307. @private_threads_view
  308. class GotoPostView(PrivateThreadsMixin, generic.GotoPostView):
  309. pass
  310. @private_threads_view
  311. class ReportedPostsListView(PrivateThreadsMixin, generic.ReportedPostsListView):
  312. pass
  313. @private_threads_view
  314. class QuotePostView(PrivateThreadsMixin, generic.QuotePostView):
  315. pass
  316. @private_threads_view
  317. class UnhidePostView(PrivateThreadsMixin, generic.UnhidePostView):
  318. pass
  319. @private_threads_view
  320. class HidePostView(PrivateThreadsMixin, generic.HidePostView):
  321. pass
  322. @private_threads_view
  323. class DeletePostView(PrivateThreadsMixin, generic.DeletePostView):
  324. pass
  325. @private_threads_view
  326. class EventsView(PrivateThreadsMixin, generic.EventsView):
  327. pass
  328. @private_threads_view
  329. class PostingView(PrivateThreadsMixin, generic.PostingView):
  330. pass