summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAurélien Bompard <aurelien@bompard.org>2013-07-18 13:46:46 +0200
committerAurélien Bompard <aurelien@bompard.org>2013-07-18 13:50:08 +0200
commit63db25bb5e1a2605c7a2fdaabcba32422191f70b (patch)
treedf8d09107b5a8c06546ca99e991d6249d5e98960
parentfd5bdaf0bd47260e4a797ff2156036a80d3ac3ee (diff)
downloadhyperkitty-63db25bb5e1a2605c7a2fdaabcba32422191f70b.tar.gz
hyperkitty-63db25bb5e1a2605c7a2fdaabcba32422191f70b.tar.xz
hyperkitty-63db25bb5e1a2605c7a2fdaabcba32422191f70b.zip
Add a page to reattach a broken thread
-rw-r--r--hyperkitty/lib/view_helpers.py1
-rw-r--r--hyperkitty/static/hyperkitty/css/hyperkitty-message.css34
-rw-r--r--hyperkitty/static/hyperkitty/js/hyperkitty-thread.js30
-rw-r--r--hyperkitty/templates/ajax/reattach_suggest.html11
-rw-r--r--hyperkitty/templates/messages/message.html5
-rw-r--r--hyperkitty/templates/reattach.html69
-rw-r--r--hyperkitty/templates/threads/right_col.html6
-rw-r--r--hyperkitty/urls.py4
-rw-r--r--hyperkitty/views/thread.py87
9 files changed, 244 insertions, 3 deletions
diff --git a/hyperkitty/lib/view_helpers.py b/hyperkitty/lib/view_helpers.py
index a27baaf..1187335 100644
--- a/hyperkitty/lib/view_helpers.py
+++ b/hyperkitty/lib/view_helpers.py
@@ -32,6 +32,7 @@ from hyperkitty.views.forms import CategoryForm
FLASH_MESSAGES = {
"updated-ok": ("success", "The profile was successfully updated."),
"sent-ok": ("success", "The message has been sent successfully."),
+ "attached-ok": ("success", "Thread successfully re-attached."),
}
diff --git a/hyperkitty/static/hyperkitty/css/hyperkitty-message.css b/hyperkitty/static/hyperkitty/css/hyperkitty-message.css
index 2668f3a..769dc02 100644
--- a/hyperkitty/static/hyperkitty/css/hyperkitty-message.css
+++ b/hyperkitty/static/hyperkitty/css/hyperkitty-message.css
@@ -317,3 +317,37 @@
border-bottom: none;
background-color: #eee;
}
+
+
+/*
+ * Re-attach threads
+ */
+.reattach-thread form {
+ margin-bottom: 0;
+}
+.reattach-thread form.search p {
+ line-height: 22px;
+}
+.reattach-thread form.search input {
+ margin-left: 3em;
+}
+.reattach-thread form.search input,
+.reattach-thread form.search button {
+ font-size: 12px;
+ padding: 2px 8px;
+}
+.reattach-thread form img.ajaxloader {
+ margin-left: 1em;
+}
+.reattach-thread li.manual label {
+ display: inline;
+}
+.reattach-thread li.manual input {
+ margin-bottom: 0;
+}
+.reattach-thread li.manual input[type='text'] {
+ width: 22em;
+}
+.reattach-thread p.buttons {
+ margin-top: 2em;
+}
diff --git a/hyperkitty/static/hyperkitty/js/hyperkitty-thread.js b/hyperkitty/static/hyperkitty/js/hyperkitty-thread.js
index 069c4e9..e6c726f 100644
--- a/hyperkitty/static/hyperkitty/js/hyperkitty-thread.js
+++ b/hyperkitty/static/hyperkitty/js/hyperkitty-thread.js
@@ -331,3 +331,33 @@ function update_thread_replies(url) {
}
load_more(url);
}
+
+
+/*
+ * Re-attach threads
+ */
+function setup_reattach() {
+ $(".reattach-thread li.manual input[type='text']").focus( function() {
+ $(this).parents("li").first()
+ .find("input[type='radio']")
+ .prop("checked", true);
+ });
+ $(".reattach-thread form.search").submit(function (e) {
+ e.preventDefault();
+ var results_elem = $(this).parent().find("ul.suggestions");
+ var url = $(this).attr("action") + "?" + $(this).serialize();
+ results_elem.find("img.ajaxloader").show();
+ $.ajax({
+ url: url,
+ success: function(data) {
+ results_elem.html(data);
+ },
+ error: function(jqXHR, textStatus, errorThrown) {
+ alert(jqXHR.responseText);
+ },
+ complete: function(jqXHR, textStatus) {
+ results_elem.find("img.ajaxloader").hide();
+ }
+ });
+ }).submit();
+}
diff --git a/hyperkitty/templates/ajax/reattach_suggest.html b/hyperkitty/templates/ajax/reattach_suggest.html
new file mode 100644
index 0000000..7b94075
--- /dev/null
+++ b/hyperkitty/templates/ajax/reattach_suggest.html
@@ -0,0 +1,11 @@
+{% load hk_generic %}
+
+ {% for s_thread in suggested_threads %}
+ <li><label class="radio"><input type="radio" name="parent" value="{{ s_thread.thread_id }}" />
+ {{ s_thread.subject }}
+ <br/>(started {{ s_thread.starting_email|get_date }}, last active: {{ s_thread|get_date }})
+ </label>
+ </li>
+ {% empty %}
+ <li><em>(no suggestions)</em></li>
+ {% endfor %}
diff --git a/hyperkitty/templates/messages/message.html b/hyperkitty/templates/messages/message.html
index 2cfcdf8..453e248 100644
--- a/hyperkitty/templates/messages/message.html
+++ b/hyperkitty/templates/messages/message.html
@@ -27,7 +27,10 @@
{% gravatar email.sender_email 40 %}
</div>
<div class="email-author inline-block">
- <span class="name"><a href="{% url 'message_index' mlist_fqdn=mlist.name message_id_hash=email.message_id_hash %}">{{email.sender_name|escapeemail}}</a></span>
+ <span class="name"><a
+ href="{% url 'message_index' mlist_fqdn=mlist.name message_id_hash=email.message_id_hash %}"
+ title="{{ email.subject }}"
+ >{{email.sender_name|escapeemail}}</a></span>
{% if use_mockups %}
<br />
<span class="rank">
diff --git a/hyperkitty/templates/reattach.html b/hyperkitty/templates/reattach.html
new file mode 100644
index 0000000..b2419c9
--- /dev/null
+++ b/hyperkitty/templates/reattach.html
@@ -0,0 +1,69 @@
+{% extends "base.html" %}
+{% load url from future %}
+{% load hk_generic %}
+{% load storm %}
+
+
+{% block title %}
+Reattach a thread - {{ mlist.display_name|default:mlist.name|escapeemail }} - {{ app_name|title }}
+{% endblock %}
+
+{% block content %}
+
+<div class="row-fluid">
+
+{% include 'threads/month_list.html' %}
+
+ <div class="span7">
+
+ <h1>Re-attach a thread to another</h1>
+
+ <div class="reattach-thread">
+ <p>Thread to re-attach:
+ <a href="{% url 'thread' mlist_fqdn=mlist.name threadid=thread.thread_id %}"
+ >{{ thread.subject }}</a>
+ (started {{ thread.starting_email|get_date }}, last active: {{ thread|get_date }})
+ </p>
+ <form action="{% url 'thread_reattach_suggest' mlist_fqdn=mlist.name threadid=thread.thread_id %}"
+ method="GET" class="search">
+ <p>Re-attach it to:
+ <span class="input-append">
+ <input type="text" name="q" placeholder="Search for the parent thread"
+ /><button type="submit" class="btn">Search</button>
+ </span>
+ </p>
+ </form>
+ <form action="" method="POST">
+ {% csrf_token %}
+ <ul class="unstyled suggestions">
+ <img alt="Loading..." class="ajaxloader" src="{{ STATIC_URL }}hyperkitty/img/ajax-loader.gif" />
+ </ul>
+ <ul class="unstyled">
+ <li class="manual">
+ <input type="radio" name="parent" value="" />
+ <label>this thread ID:
+ <input type="text" name="parent-manual" size="32" placeholder="{{ thread.thread_id }}" />
+ </label>
+ </li>
+ </ul>
+ <p class="buttons">
+ <button type="submit" class="btn btn-primary">Do it</button> (there's no undoing!), or
+ <a href="{% url 'thread' mlist_fqdn=mlist.name threadid=thread.thread_id %}"
+ >go back to the thread</a>.
+ </p>
+ </form>
+ </div>
+
+ </div>
+
+</div>
+
+{% endblock %}
+
+{% block additionaljs %}
+<script type="text/javascript">
+ $(document).ready(function() {
+ setup_reattach();
+ });
+</script>
+{% endblock %}
diff --git a/hyperkitty/templates/threads/right_col.html b/hyperkitty/templates/threads/right_col.html
index 5384d03..c6d6d30 100644
--- a/hyperkitty/templates/threads/right_col.html
+++ b/hyperkitty/templates/threads/right_col.html
@@ -50,6 +50,12 @@
{{ addtag_form.as_p }}
</form>
</div>
+ {% if user.is_staff %}
+ <p><i class="icon-resize-small"></i>
+ <a href="{% url 'thread_reattach' mlist_fqdn=mlist.name threadid=threadid %}"
+ >Reattach this thread</a>
+ </p>
+ {% endif %}
<div id="participants">
<span id="participants_title">participants</span> ({{participants|length}})
<ul>
diff --git a/hyperkitty/urls.py b/hyperkitty/urls.py
index 44a6fdb..9dfe63c 100644
--- a/hyperkitty/urls.py
+++ b/hyperkitty/urls.py
@@ -86,6 +86,10 @@ urlpatterns = patterns('hyperkitty.views',
'thread.favorite', name='favorite'),
url(r'^list/(?P<mlist_fqdn>[^/@]+@[^/@]+)/thread/(?P<threadid>\w+)/category$',
'thread.set_category', name='thread_set_category'),
+ url(r'^list/(?P<mlist_fqdn>[^/@]+@[^/@]+)/thread/(?P<threadid>\w+)/reattach$',
+ 'thread.reattach', name='thread_reattach'),
+ url(r'^list/(?P<mlist_fqdn>[^/@]+@[^/@]+)/thread/(?P<threadid>\w+)/reattach-suggest$',
+ 'thread.reattach_suggest', name='thread_reattach_suggest'),
# Search
diff --git a/hyperkitty/views/thread.py b/hyperkitty/views/thread.py
index 1098302..5742703 100644
--- a/hyperkitty/views/thread.py
+++ b/hyperkitty/views/thread.py
@@ -21,13 +21,14 @@
#
import datetime
+import re
from collections import namedtuple
import django.utils.simplejson as json
from django.http import HttpResponse, Http404
from django.template import RequestContext, loader
-from django.shortcuts import render
+from django.shortcuts import render, redirect
from django.core.urlresolvers import reverse
from django.core.exceptions import SuspiciousOperation
from django.utils.timezone import utc
@@ -36,7 +37,8 @@ import robot_detection
from hyperkitty.models import Tag, Favorite, LastView, ThreadCategory
from hyperkitty.views.forms import AddTagForm, ReplyForm, CategoryForm
from hyperkitty.lib import get_store, stripped_subject
-from hyperkitty.lib.view_helpers import get_months, get_category_widget
+from hyperkitty.lib.view_helpers import (get_months, get_category_widget,
+ FLASH_MESSAGES)
from hyperkitty.lib.voting import set_message_votes
@@ -129,6 +131,14 @@ def thread_index(request, mlist_fqdn, threadid, month=None, year=None):
# XXX: Storm-specific
unread_count = thread.replies_after(last_view).count()
+ # Flash messages
+ flash_messages = []
+ flash_msg = request.GET.get("msg")
+ if flash_msg:
+ flash_msg = { "type": FLASH_MESSAGES[flash_msg][0],
+ "msg": FLASH_MESSAGES[flash_msg][1] }
+ flash_messages.append(flash_msg)
+
# TODO: eventually move to a middleware ?
# http://djangosnippets.org/snippets/1865/
is_bot = True
@@ -157,6 +167,7 @@ def thread_index(request, mlist_fqdn, threadid, month=None, year=None):
'unread_count': unread_count,
'category_form': category_form,
'category': category,
+ 'flash_messages': flash_messages,
}
context["participants"].sort(key=lambda x: x[0].lower())
@@ -320,3 +331,75 @@ def set_category(request, mlist_fqdn, threadid):
"category": category,
}
return render(request, "threads/category.html", context)
+
+
+def reattach(request, mlist_fqdn, threadid):
+ if not request.user.is_staff:
+ return HttpResponse('You must be a staff member to reattach a thread',
+ content_type="text/plain", status=403)
+ flash_messages = []
+ store = get_store(request)
+ mlist = store.get_list(mlist_fqdn)
+ thread = store.get_thread(mlist_fqdn, threadid)
+
+ if request.method == 'POST':
+ parent_tid = request.POST.get("parent")
+ if not parent_tid:
+ parent_tid = request.POST.get("parent-manual")
+ if not parent_tid or not re.match("\w{32}", parent_tid):
+ raise ValueError("Invalid thread id, it should look like "
+ "OUAASTM6GS4E5TEATD6R2VWMULG44NKJ")
+ flash_messages.append({"type": "warning",
+ "msg": "Invalid thread id, it should look "
+ "like OUAASTM6GS4E5TEATD6R2VWMULG44NKJ."})
+ elif parent_tid == threadid:
+ flash_messages.append({"type": "warning",
+ "msg": "Can't re-attach a thread to "
+ "itself, check your thread ID."})
+ else:
+ new_thread = store.get_thread(mlist_fqdn, parent_tid)
+ if new_thread is None:
+ flash_messages.append({"type": "warning",
+ "msg": "Unknown thread, check your "
+ "thread ID."})
+ elif thread.starting_email.date <= new_thread.starting_email.date:
+ flash_messages.append({"type": "error",
+ "msg": "Can't attach an older thread "
+ "to a newer thread."})
+ else:
+ for msg in thread.emails:
+ store.attach_to_thread(msg, new_thread)
+ store.delete_thread(mlist_fqdn, threadid)
+ return redirect(reverse(
+ 'thread', kwargs={
+ "mlist_fqdn": mlist_fqdn,
+ 'threadid': parent_tid,
+ })+"?msg=attached-ok")
+
+
+ context = {
+ 'mlist' : mlist,
+ 'thread': thread,
+ 'months_list': get_months(store, mlist.name),
+ 'flash_messages': flash_messages,
+ }
+ return render(request, "reattach.html", context)
+
+
+def reattach_suggest(request, mlist_fqdn, threadid):
+ store = get_store(request)
+ thread = store.get_thread(mlist_fqdn, threadid)
+
+ default_search_query = thread.subject.lower().replace("re:", "")
+ search_query = request.GET.get("q", default_search_query)
+ search_result = store.search(search_query, mlist_fqdn, 1, 50)
+ messages = search_result["results"]
+ suggested_threads = []
+ for msg in messages:
+ if msg.thread not in suggested_threads and msg.thread_id != threadid:
+ suggested_threads.append(msg.thread)
+
+ context = {
+ 'suggested_threads': suggested_threads[:10],
+ }
+ return render(request, "ajax/reattach_suggest.html", context)