privatethreads.py 17 KB

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