privatethreads.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  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': _("Take thread over")
  106. })
  107. if is_owner:
  108. actions.append({
  109. 'action': 'participants',
  110. 'icon': 'users',
  111. 'name': _("Edit participants"),
  112. 'is_button': True
  113. })
  114. for participant in thread.participants_list:
  115. if not participant.is_owner:
  116. actions.append({
  117. 'action': 'make_owner:%s' % participant.user_id,
  118. 'is_hidden': True
  119. })
  120. if is_moderator:
  121. if thread.is_closed:
  122. actions.append({
  123. 'action': 'open',
  124. 'icon': 'unlock-alt',
  125. 'name': _("Open thread")
  126. })
  127. else:
  128. actions.append({
  129. 'action': 'close',
  130. 'icon': 'lock',
  131. 'name': _("Close thread")
  132. })
  133. actions.append({
  134. 'action': 'delete',
  135. 'icon': 'times',
  136. 'name': _("Delete thread"),
  137. 'confirmation': _("Are you sure you want to delete this "
  138. "thread? This action can't be undone.")
  139. })
  140. return actions
  141. @atomic
  142. def action_takeover(self, request, thread):
  143. participants.set_thread_owner(thread, request.user)
  144. messages.success(request, _("You are now owner of this thread."))
  145. message = _("%(user)s took over this thread.")
  146. record_event(request.user, thread, 'user', message, {
  147. 'user': request.user,
  148. })
  149. thread.save(update_fields=['has_events'])
  150. @atomic
  151. def action_make_owner(self, request, thread, new_owner_id):
  152. new_owner_id = int(new_owner_id)
  153. new_owner = None
  154. for participant in thread.participants_list:
  155. if participant.user.id == int(new_owner_id):
  156. new_owner = participant.user
  157. break
  158. if new_owner:
  159. participants.set_thread_owner(thread, new_owner)
  160. message = _("You have passed thread ownership to %(user)s.")
  161. messages.success(request, message % {'user': new_owner.username})
  162. message = _("%(user)s passed thread ownership to %(participant)s.")
  163. record_event(request.user, thread, 'user', message, {
  164. 'user': request.user,
  165. 'participant': new_owner
  166. })
  167. thread.save(update_fields=['has_events'])
  168. @private_threads_view
  169. class ThreadView(PrivateThreadsMixin, generic.ThreadView):
  170. template = 'misago/privatethreads/thread.html'
  171. ThreadActions = PrivateThreadActions
  172. @private_threads_view
  173. class ThreadParticipantsView(PrivateThreadsMixin, generic.ViewBase):
  174. template = 'misago/privatethreads/participants.html'
  175. def dispatch(self, request, *args, **kwargs):
  176. thread = self.get_thread(request, **kwargs)
  177. if not request.is_ajax():
  178. response = render(request, 'misago/errorpages/wrong_way.html')
  179. response.status_code = 405
  180. return response
  181. participants_qs = thread.threadparticipant_set
  182. participants_qs = participants_qs.select_related('user', 'user__rank')
  183. return self.render(request, {
  184. 'forum': thread.forum,
  185. 'thread': thread,
  186. 'participants': participants_qs.order_by('-is_owner', 'user__slug')
  187. })
  188. @private_threads_view
  189. class EditThreadParticipantsView(ThreadParticipantsView):
  190. template = 'misago/privatethreads/participants_modal.html'
  191. @private_threads_view
  192. class BaseEditThreadParticipantView(PrivateThreadsMixin, generic.ViewBase):
  193. @atomic
  194. def dispatch(self, request, *args, **kwargs):
  195. thread = self.get_thread(request, lock=True, **kwargs)
  196. if not request.is_ajax():
  197. response = render(request, 'misago/errorpages/wrong_way.html')
  198. response.status_code = 405
  199. return response
  200. if not request.method == "POST":
  201. raise AjaxError(_("Wrong action received."))
  202. if not thread.participant or not thread.participant.is_owner:
  203. raise AjaxError(_("Only thread owner can add or "
  204. "remove participants from thread."))
  205. return self.action(request, thread, kwargs)
  206. def action(self, request, thread, kwargs):
  207. raise NotImplementedError("views extending EditThreadParticipantView "
  208. "need to define custom action method")
  209. @private_threads_view
  210. class AddThreadParticipantsView(BaseEditThreadParticipantView):
  211. template = 'misago/privatethreads/participants_modal_list.html'
  212. def action(self, request, thread, kwargs):
  213. form = ThreadParticipantsForm(request.POST, user=request.user)
  214. if not form.is_valid():
  215. errors = []
  216. for field_errors in form.errors.as_data().values():
  217. errors.extend([unicode(e[0]) for e in field_errors])
  218. return JsonResponse({'message': errors[0], 'is_error': True})
  219. event_message = _("%(user)s added %(participant)s to this thread.")
  220. participants_list = [p.user for p in thread.participants_list]
  221. for user in form.users_cache:
  222. if user not in participants_list:
  223. participants.add_participant(request, thread, user)
  224. record_event(request.user, thread, 'user', event_message, {
  225. 'user': request.user,
  226. 'participant': user
  227. })
  228. thread.save(update_fields=['has_events'])
  229. participants_qs = thread.threadparticipant_set
  230. participants_qs = participants_qs.select_related('user', 'user__rank')
  231. participants_qs = participants_qs.order_by('-is_owner', 'user__slug')
  232. participants_list = [p for p in participants_qs]
  233. participants_list_html = self.render(request, {
  234. 'forum': thread.forum,
  235. 'thread': thread,
  236. 'participants': participants_list,
  237. }).content
  238. message = ungettext("%(users)s participant",
  239. "%(users)s participants",
  240. len(participants_list))
  241. message = message % {'users': len(participants_list)}
  242. return JsonResponse({
  243. 'is_error': False,
  244. 'message': message,
  245. 'list_html': participants_list_html
  246. })
  247. @private_threads_view
  248. class RemoveThreadParticipantView(BaseEditThreadParticipantView):
  249. def action(self, request, thread, kwargs):
  250. user_qs = thread.threadparticipant_set.select_related('user')
  251. try:
  252. participant = user_qs.get(user_id=kwargs['user_id'])
  253. except ThreadParticipant.DoesNotExist:
  254. return JsonResponse({
  255. 'message': _("Requested participant couldn't be found."),
  256. 'is_error': True,
  257. })
  258. if participant.user == request.user:
  259. return JsonResponse({
  260. 'message': _('To leave thread use "Leave thread" option.'),
  261. 'is_error': True,
  262. })
  263. participants_count = len(thread.participants_list) - 1
  264. if participants_count == 0:
  265. return JsonResponse({
  266. 'message': _("You can't remove last thread participant."),
  267. 'is_error': True,
  268. })
  269. participants.remove_participant(thread, participant.user)
  270. if not participants.thread_has_participants(thread):
  271. thread.delete()
  272. else:
  273. message = _("%(user)s removed %(participant)s from this thread.")
  274. record_event(request.user, thread, 'user', message, {
  275. 'user': request.user,
  276. 'participant': participant.user
  277. })
  278. thread.save(update_fields=['has_events'])
  279. participants_count = len(thread.participants_list) - 1
  280. message = ungettext("%(users)s participant",
  281. "%(users)s participants",
  282. participants_count)
  283. message = message % {'users': participants_count}
  284. return JsonResponse({'is_error': False, 'message': message})
  285. @private_threads_view
  286. class LeaveThreadView(BaseEditThreadParticipantView):
  287. @atomic
  288. def dispatch(self, request, *args, **kwargs):
  289. thread = self.get_thread(request, lock=True, **kwargs)
  290. try:
  291. if not request.method == "POST":
  292. raise RuntimeError(_("Wrong action received."))
  293. if not thread.participant:
  294. raise RuntimeError(_("You have to be thread participant in "
  295. "order to be able to leave thread."))
  296. user_qs = thread.threadparticipant_set.select_related('user')
  297. try:
  298. participant = user_qs.get(user_id=request.user.id)
  299. except ThreadParticipant.DoesNotExist:
  300. raise RuntimeError(_("You need to be thread "
  301. "participant to leave it."))
  302. except RuntimeError as e:
  303. messages.error(request, unicode(e))
  304. return redirect(thread.get_absolute_url())
  305. participants.remove_participant(thread, request.user)
  306. if not thread.threadparticipant_set.exists():
  307. thread.delete()
  308. elif thread.participant.is_owner:
  309. new_owner = user_qs.order_by('id')[:1][0].user
  310. participants.set_thread_owner(thread, new_owner)
  311. message = _("%(user)s left this thread. "
  312. "%(new_owner)s is now thread owner.")
  313. record_event(request.user, thread, 'user', message, {
  314. 'user': request.user,
  315. 'new_owner': new_owner
  316. })
  317. thread.save(update_fields=['has_events'])
  318. else:
  319. message = _("%(user)s left this thread.")
  320. record_event(request.user, thread, 'user', message, {
  321. 'user': request.user,
  322. })
  323. thread.save(update_fields=['has_events'])
  324. message = _('You have left "%(thread)s" thread.')
  325. message = message % {'thread': thread.title}
  326. messages.info(request, message)
  327. return redirect('misago:private_threads')
  328. """
  329. Generics
  330. """
  331. @private_threads_view
  332. class GotoLastView(PrivateThreadsMixin, generic.GotoLastView):
  333. pass
  334. @private_threads_view
  335. class GotoNewView(PrivateThreadsMixin, generic.GotoNewView):
  336. pass
  337. @private_threads_view
  338. class GotoPostView(PrivateThreadsMixin, generic.GotoPostView):
  339. pass
  340. @private_threads_view
  341. class ReportedPostsListView(PrivateThreadsMixin, generic.ReportedPostsListView):
  342. pass
  343. @private_threads_view
  344. class QuotePostView(PrivateThreadsMixin, generic.QuotePostView):
  345. pass
  346. @private_threads_view
  347. class UnhidePostView(PrivateThreadsMixin, generic.UnhidePostView):
  348. pass
  349. @private_threads_view
  350. class HidePostView(PrivateThreadsMixin, generic.HidePostView):
  351. pass
  352. @private_threads_view
  353. class DeletePostView(PrivateThreadsMixin, generic.DeletePostView):
  354. pass
  355. @private_threads_view
  356. class EventsView(PrivateThreadsMixin, generic.EventsView):
  357. pass
  358. @private_threads_view
  359. class PostingView(PrivateThreadsMixin, generic.PostingView):
  360. pass