PageRenderTime 53ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/hyperkitty/views/thread.py

https://gitlab.com/liuzceecs/hyperkitty
Python | 417 lines | 380 code | 9 blank | 28 comment | 10 complexity | c49b0a303f6703d1cc668e43ffd2bdfd MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. # Copyright (C) 2014-2015 by the Free Software Foundation, Inc.
  3. #
  4. # This file is part of HyperKitty.
  5. #
  6. # HyperKitty is free software: you can redistribute it and/or modify it under
  7. # the terms of the GNU General Public License as published by the Free
  8. # Software Foundation, either version 3 of the License, or (at your option)
  9. # any later version.
  10. #
  11. # HyperKitty is distributed in the hope that it will be useful, but WITHOUT
  12. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13. # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
  14. # more details.
  15. #
  16. # You should have received a copy of the GNU General Public License along with
  17. # HyperKitty. If not, see <http://www.gnu.org/licenses/>.
  18. #
  19. # Author: Aamir Khan <syst3m.w0rm@gmail.com>
  20. # Author: Aurelien Bompard <abompard@fedoraproject.org>
  21. #
  22. from __future__ import absolute_import, unicode_literals
  23. import datetime
  24. import re
  25. import json
  26. import robot_detection
  27. from django.contrib import messages
  28. from django.core.urlresolvers import reverse
  29. from django.core.exceptions import SuspiciousOperation
  30. from django.http import HttpResponse, Http404
  31. from django.shortcuts import render, redirect, get_object_or_404
  32. from django.template import RequestContext, loader
  33. from django.utils.timezone import utc
  34. from django.utils.translation import gettext as _
  35. from haystack.query import SearchQuerySet
  36. from hyperkitty.models import Tag, Tagging, Favorite, LastView, Thread, MailingList
  37. from hyperkitty.forms import AddTagForm, ReplyForm
  38. from hyperkitty.lib.utils import stripped_subject
  39. from hyperkitty.lib.view_helpers import (get_months, get_category_widget,
  40. check_mlist_private, get_posting_form)
  41. REPLY_RE = re.compile(r'^(re:\s*)*', re.IGNORECASE)
  42. def _get_thread_replies(request, thread, limit, offset=1):
  43. '''
  44. Get and sort the replies for a thread.
  45. By default, offset = 1 to skip the original message.
  46. '''
  47. if not thread:
  48. raise Http404
  49. sort_mode = request.GET.get("sort", "thread")
  50. if sort_mode not in ("date", "thread"):
  51. raise SuspiciousOperation
  52. if sort_mode == "thread":
  53. sort_mode = "thread_order"
  54. mlist = thread.mailinglist
  55. initial_subject = stripped_subject(mlist, thread.starting_email.subject)
  56. emails = list(thread.emails.order_by(sort_mode)[offset:offset+limit])
  57. for email in emails:
  58. # Extract all the votes for this message
  59. if request.user.is_authenticated():
  60. email.myvote = email.votes.filter(user=request.user).first()
  61. else:
  62. email.myvote = None
  63. # Threading position
  64. if sort_mode == "thread_order":
  65. email.level = email.thread_depth - 1 # replies start ragged left
  66. if email.level > 5:
  67. email.level = 5
  68. elif email.level < 0:
  69. email.level = 0
  70. # Subject change
  71. subject = email.subject.strip()
  72. if mlist.subject_prefix:
  73. subject = subject.replace(mlist.subject_prefix, "")
  74. subject = REPLY_RE.sub("", subject.strip())
  75. if subject == initial_subject.strip():
  76. email.changed_subject = False
  77. else:
  78. email.changed_subject = subject
  79. return emails
  80. @check_mlist_private
  81. def thread_index(request, mlist_fqdn, threadid, month=None, year=None):
  82. ''' Displays all the email for a given thread identifier '''
  83. # pylint: disable=unused-argument
  84. mlist = get_object_or_404(MailingList, name=mlist_fqdn)
  85. thread = get_object_or_404(Thread, mailinglist=mlist, thread_id=threadid)
  86. starting_email = thread.starting_email
  87. sort_mode = request.GET.get("sort", "thread")
  88. if request.user.is_authenticated():
  89. starting_email.myvote = starting_email.votes.filter(
  90. user=request.user).first()
  91. else:
  92. starting_email.myvote = None
  93. # Tags
  94. tag_form = AddTagForm()
  95. # Favorites
  96. fav_action = "add"
  97. if request.user.is_authenticated() and Favorite.objects.filter(
  98. thread=thread, user=request.user).exists():
  99. fav_action = "rm"
  100. # Category
  101. category, category_form = get_category_widget(request, thread.category)
  102. # Extract relative dates
  103. today = datetime.date.today()
  104. days_old = today - starting_email.date.date()
  105. days_inactive = today - thread.date_active.date()
  106. subject = stripped_subject(mlist, starting_email.subject)
  107. # Last view
  108. last_view = None
  109. if request.user.is_authenticated():
  110. last_view_obj, created = LastView.objects.get_or_create(
  111. thread=thread, user=request.user)
  112. if not created:
  113. last_view = last_view_obj.view_date
  114. last_view_obj.save() # update timestamp
  115. # get the number of unread messages
  116. if last_view is None:
  117. if request.user.is_authenticated():
  118. unread_count = thread.emails_count
  119. else:
  120. unread_count = 0
  121. else:
  122. unread_count = thread.emails.filter(date__gt=last_view).count()
  123. # TODO: eventually move to a middleware ?
  124. # http://djangosnippets.org/snippets/1865/
  125. user_agent = request.META.get('HTTP_USER_AGENT', None)
  126. if user_agent:
  127. is_bot = robot_detection.is_robot(user_agent)
  128. else:
  129. is_bot = True
  130. # Export button
  131. export = {
  132. "url": "%s?thread=%s" % (
  133. reverse("hk_list_export_mbox", kwargs={
  134. "mlist_fqdn": mlist.name,
  135. "filename": "%s-%s" % (mlist.name, thread.thread_id)}),
  136. thread.thread_id),
  137. "message": _("Download"),
  138. "title": _("This thread in gzipped mbox format"),
  139. }
  140. context = {
  141. 'mlist': mlist,
  142. 'thread': thread,
  143. 'starting_email': starting_email,
  144. 'subject': subject,
  145. 'addtag_form': tag_form,
  146. 'month': thread.date_active,
  147. 'months_list': get_months(mlist),
  148. 'days_inactive': days_inactive.days,
  149. 'days_old': days_old.days,
  150. 'sort_mode': sort_mode,
  151. 'fav_action': fav_action,
  152. 'reply_form': get_posting_form(ReplyForm, request, mlist),
  153. 'is_bot': is_bot,
  154. 'num_comments': thread.emails_count - 1,
  155. 'last_view': last_view,
  156. 'unread_count': unread_count,
  157. 'category_form': category_form,
  158. 'category': category,
  159. 'export': export,
  160. }
  161. if is_bot:
  162. # Don't rely on AJAX to load the replies
  163. # The limit is a safety measure, don't let a bot kill the DB
  164. context["replies"] = _get_thread_replies(request, thread, limit=1000)
  165. return render(request, "hyperkitty/thread.html", context)
  166. @check_mlist_private
  167. def replies(request, mlist_fqdn, threadid):
  168. """Get JSON encoded lists with the replies and the participants"""
  169. chunk_size = 6 # must be an even number, or the even/odd cycle will be broken
  170. offset = int(request.GET.get("offset", "1"))
  171. mlist = get_object_or_404(MailingList, name=mlist_fqdn)
  172. thread = get_object_or_404(Thread,
  173. mailinglist__name=mlist_fqdn, thread_id=threadid)
  174. # Last view
  175. last_view = request.GET.get("last_view")
  176. if last_view:
  177. try:
  178. last_view = datetime.datetime.fromtimestamp(int(last_view), utc)
  179. except ValueError:
  180. last_view = None
  181. context = {
  182. 'threadid': thread,
  183. 'reply_form': get_posting_form(ReplyForm, request, mlist),
  184. 'last_view': last_view,
  185. }
  186. context["replies"] = _get_thread_replies(request, thread, offset=offset,
  187. limit=chunk_size)
  188. replies_tpl = loader.get_template('hyperkitty/ajax/replies.html')
  189. replies_html = replies_tpl.render(RequestContext(request, context))
  190. response = {"replies_html": replies_html,
  191. "more_pending": False,
  192. "next_offset": None,
  193. }
  194. if len(context["replies"]) == chunk_size:
  195. response["more_pending"] = True
  196. response["next_offset"] = offset + chunk_size
  197. return HttpResponse(json.dumps(response),
  198. content_type='application/javascript')
  199. @check_mlist_private
  200. def tags(request, mlist_fqdn, threadid):
  201. """ Add or remove one or more tags on a given thread. """
  202. if not request.user.is_authenticated():
  203. return HttpResponse('You must be logged in to add a tag',
  204. content_type="text/plain", status=403)
  205. thread = get_object_or_404(Thread,
  206. mailinglist__name=mlist_fqdn, thread_id=threadid)
  207. if request.method != 'POST':
  208. raise SuspiciousOperation
  209. action = request.POST.get("action")
  210. if action == "add":
  211. form = AddTagForm(request.POST)
  212. if not form.is_valid():
  213. return HttpResponse("Error adding tag: invalid data",
  214. content_type="text/plain", status=500)
  215. tagname = form.data['tag']
  216. elif action == "rm":
  217. tagname = request.POST.get('tag')
  218. else:
  219. raise SuspiciousOperation
  220. tagnames = [ t.strip() for t in re.findall(r"[\w'_ -]+", tagname) ]
  221. for tagname in tagnames:
  222. if action == "add":
  223. tag = Tag.objects.get_or_create(name=tagname)[0]
  224. Tagging.objects.get_or_create(
  225. tag=tag, thread=thread, user=request.user)
  226. elif action == "rm":
  227. try:
  228. Tagging.objects.get(tag__name=tagname, thread=thread,
  229. user=request.user).delete()
  230. except Tagging.DoesNotExist:
  231. raise Http404("No such tag: %s" % tagname)
  232. # cleanup
  233. if not Tagging.objects.filter(tag__name=tagname).exists():
  234. Tag.objects.filter(name=tagname).delete()
  235. # Now refresh the tag list
  236. tpl = loader.get_template('hyperkitty/threads/tags.html')
  237. html = tpl.render(RequestContext(request, {
  238. "thread": thread,
  239. }))
  240. response = {"tags": [ t.name for t in thread.tags.distinct() ],
  241. "html": html}
  242. return HttpResponse(json.dumps(response),
  243. content_type='application/javascript')
  244. @check_mlist_private
  245. def suggest_tags(request, mlist_fqdn, threadid):
  246. term = request.GET.get("term")
  247. current_tags = Tag.objects.filter(
  248. threads__mailinglist__name=mlist_fqdn,
  249. threads__thread_id=threadid
  250. ).values_list("name", flat=True)
  251. if term:
  252. tags_db = Tag.objects.filter(
  253. threads__mailinglist__name=mlist_fqdn,
  254. name__istartswith=term)
  255. else:
  256. tags_db = Tag.objects.all()
  257. tag_names = [ t.encode("utf-8") for t in
  258. tags_db.exclude(name__in=current_tags
  259. ).values_list("name", flat=True)[:20] ]
  260. return HttpResponse(json.dumps(tag_names),
  261. content_type='application/javascript')
  262. @check_mlist_private
  263. def favorite(request, mlist_fqdn, threadid):
  264. """ Add or remove from favorites"""
  265. if not request.user.is_authenticated():
  266. return HttpResponse('You must be logged in to have favorites',
  267. content_type="text/plain", status=403)
  268. if request.method != 'POST':
  269. raise SuspiciousOperation
  270. thread = get_object_or_404(Thread,
  271. mailinglist__name=mlist_fqdn, thread_id=threadid)
  272. if request.POST["action"] == "add":
  273. Favorite.objects.get_or_create(thread=thread, user=request.user)
  274. elif request.POST["action"] == "rm":
  275. Favorite.objects.filter(thread=thread, user=request.user).delete()
  276. else:
  277. raise SuspiciousOperation
  278. return HttpResponse("success", content_type='text/plain')
  279. @check_mlist_private
  280. def set_category(request, mlist_fqdn, threadid):
  281. """ Set the category for a given thread. """
  282. if not request.user.is_authenticated():
  283. return HttpResponse('You must be logged in to add a tag',
  284. content_type="text/plain", status=403)
  285. if request.method != 'POST':
  286. raise SuspiciousOperation
  287. thread = get_object_or_404(Thread,
  288. mailinglist__name=mlist_fqdn, thread_id=threadid)
  289. category, category_form = get_category_widget(request)
  290. if not category and thread.category:
  291. thread.category = None
  292. thread.save()
  293. elif category and category.name != thread.category:
  294. thread.category = category
  295. thread.save()
  296. # Now refresh the category widget
  297. context = {
  298. "category_form": category_form,
  299. "thread": thread,
  300. "category": category,
  301. }
  302. return render(request, "hyperkitty/threads/category.html", context)
  303. @check_mlist_private
  304. def reattach(request, mlist_fqdn, threadid):
  305. if not request.user.is_staff and not request.user.is_superuser:
  306. return HttpResponse('You must be a staff member to reattach a thread',
  307. content_type="text/plain", status=403)
  308. mlist = get_object_or_404(MailingList, name=mlist_fqdn)
  309. thread = get_object_or_404(Thread, mailinglist=mlist, thread_id=threadid)
  310. context = {
  311. 'mlist': mlist,
  312. 'thread': thread,
  313. 'months_list': get_months(mlist),
  314. }
  315. template_name = "hyperkitty/reattach.html"
  316. if request.method == 'POST':
  317. parent_tid = request.POST.get("parent")
  318. if not parent_tid:
  319. parent_tid = request.POST.get("parent-manual")
  320. if not parent_tid or not re.match(r"\w{32}", parent_tid):
  321. messages.warning(request,
  322. "Invalid thread id, it should look "
  323. "like OUAASTM6GS4E5TEATD6R2VWMULG44NKJ.")
  324. return render(request, template_name, context)
  325. if parent_tid == threadid:
  326. messages.warning(request,
  327. "Can't re-attach a thread to itself, check your thread ID.")
  328. return render(request, template_name, context)
  329. try:
  330. parent = Thread.objects.get(
  331. mailinglist=thread.mailinglist, thread_id=parent_tid)
  332. except Thread.DoesNotExist:
  333. messages.warning(request,
  334. "Unknown thread, check your thread ID.")
  335. return render(request, template_name, context)
  336. try:
  337. thread.starting_email.set_parent(parent.starting_email)
  338. except ValueError as e:
  339. messages.error(request, str(e))
  340. return render(request, template_name, context)
  341. messages.success(request, "Thread successfully re-attached.")
  342. return redirect(reverse(
  343. 'hk_thread', kwargs={
  344. "mlist_fqdn": mlist_fqdn,
  345. 'threadid': parent_tid,
  346. }))
  347. return render(request, template_name, context)
  348. @check_mlist_private
  349. def reattach_suggest(request, mlist_fqdn, threadid):
  350. mlist = get_object_or_404(MailingList, name=mlist_fqdn)
  351. thread = get_object_or_404(Thread, mailinglist=mlist, thread_id=threadid)
  352. default_search_query = stripped_subject(
  353. mlist, thread.subject).lower().replace("re:", "")
  354. search_query = request.GET.get("q")
  355. if not search_query:
  356. search_query = default_search_query
  357. search_query = search_query.strip()
  358. emails = SearchQuerySet().filter(
  359. mailinglist__exact=mlist_fqdn, content=search_query)[:50]
  360. suggested_threads = []
  361. for msg in emails:
  362. sugg_thread = msg.object.thread
  363. if sugg_thread not in suggested_threads \
  364. and sugg_thread.thread_id != threadid:
  365. suggested_threads.append(sugg_thread)
  366. context = {
  367. 'mlist' : mlist,
  368. 'suggested_threads': suggested_threads[:10],
  369. }
  370. return render(request, "hyperkitty/ajax/reattach_suggest.html", context)