diff options
author | Aurélien Bompard <aurelien@bompard.org> | 2013-07-18 13:46:46 +0200 |
---|---|---|
committer | Aurélien Bompard <aurelien@bompard.org> | 2013-07-18 13:50:08 +0200 |
commit | 63db25bb5e1a2605c7a2fdaabcba32422191f70b (patch) | |
tree | df8d09107b5a8c06546ca99e991d6249d5e98960 | |
parent | fd5bdaf0bd47260e4a797ff2156036a80d3ac3ee (diff) | |
download | hyperkitty-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.py | 1 | ||||
-rw-r--r-- | hyperkitty/static/hyperkitty/css/hyperkitty-message.css | 34 | ||||
-rw-r--r-- | hyperkitty/static/hyperkitty/js/hyperkitty-thread.js | 30 | ||||
-rw-r--r-- | hyperkitty/templates/ajax/reattach_suggest.html | 11 | ||||
-rw-r--r-- | hyperkitty/templates/messages/message.html | 5 | ||||
-rw-r--r-- | hyperkitty/templates/reattach.html | 69 | ||||
-rw-r--r-- | hyperkitty/templates/threads/right_col.html | 6 | ||||
-rw-r--r-- | hyperkitty/urls.py | 4 | ||||
-rw-r--r-- | hyperkitty/views/thread.py | 87 |
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) |