privatethreads.py 16 KB

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