summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--hyperkitty/static/css/hyperkitty.css34
-rw-r--r--hyperkitty/static/js/hyperkitty.js14
-rw-r--r--hyperkitty/templates/message_new.html42
-rw-r--r--hyperkitty/templates/thread_list.html9
-rw-r--r--hyperkitty/urls.py2
-rw-r--r--hyperkitty/views/forms.py4
-rw-r--r--hyperkitty/views/list.py9
-rw-r--r--hyperkitty/views/message.py75
-rw-r--r--hyperkitty/views/thread.py10
-rw-r--r--requirements.txt1
10 files changed, 176 insertions, 24 deletions
diff --git a/hyperkitty/static/css/hyperkitty.css b/hyperkitty/static/css/hyperkitty.css
index 5381e86..f08baaa 100644
--- a/hyperkitty/static/css/hyperkitty.css
+++ b/hyperkitty/static/css/hyperkitty.css
@@ -134,6 +134,16 @@ table.user-data {
list-style-type: none;
}
+.flashmsg {
+ position: absolute;
+ left: 0;
+ width: 100%;
+ text-align: center;
+}
+.flashmsg .alert {
+ display: inline-block;
+}
+
/* Add icons to some text */
.participant, .discussion, .saved, .notsaved {
@@ -394,6 +404,15 @@ form.likeform {
margin-right: 4em;
}
+#thread-list .thread-list-header .thread-new {
+ float: right;
+}
+#thread-list .thread-list-header .thread-new strong {
+ font-size: 150%;
+ font-weight: bold;
+ margin-right: 0.2em;
+}
+
/* Thread view */
@@ -902,3 +921,18 @@ a.reply.disabled {
white-space: pre;
text-align: left;
}
+
+
+/*
+ * New thread form
+ */
+.new-thread-form {
+ margin-left: 2em;
+}
+.new-thread-form #id_subject,
+.new-thread-form textarea {
+ width: 90%;
+}
+.new-thread-form .buttons .submit {
+ margin-right: 2em;
+}
diff --git a/hyperkitty/static/js/hyperkitty.js b/hyperkitty/static/js/hyperkitty.js
index 87d6987..5371657 100644
--- a/hyperkitty/static/js/hyperkitty.js
+++ b/hyperkitty/static/js/hyperkitty.js
@@ -61,7 +61,6 @@ function vote(elem, value) {
function setup_vote() {
- $("a.youlike.disabled").add("a.youdislike.disabled").tooltip();
$("a.youlike").click(function(e) { e.preventDefault(); vote(this, 1); });
$("a.youdislike").click(function(e) { e.preventDefault(); vote(this, -1); });
}
@@ -107,7 +106,6 @@ function setup_favorites() {
form.find("a.saved").show();
}
}).trigger("change");
- $(".favorite a.disabled").tooltip();
$(".favorite a").bind("click", function(e) {
e.preventDefault();
if ($(elem).hasClass("disabled")) {
@@ -145,7 +143,6 @@ function setup_favorites() {
*/
function setup_replies() {
- $("a.reply.disabled").tooltip();
$("a.reply").click(function(e) {
e.preventDefault();
if (!$(this).hasClass("disabled")) {
@@ -301,6 +298,15 @@ function setup_months_list() {
$("#months-list").accordion({ collapsible: true, active: current });
}
+function setup_disabled_tooltips() {
+ $("a.disabled").tooltip().click(function (e) {
+ e.preventDefault();
+ });
+}
+
+function setup_flash_messages() {
+ $('.flashmsg').delay(3000).fadeOut('slow');
+}
/*
@@ -315,4 +321,6 @@ $(document).ready(function() {
setup_months_list();
setup_favorites();
setup_replies();
+ setup_disabled_tooltips();
+ setup_flash_messages();
});
diff --git a/hyperkitty/templates/message_new.html b/hyperkitty/templates/message_new.html
new file mode 100644
index 0000000..c764c1a
--- /dev/null
+++ b/hyperkitty/templates/message_new.html
@@ -0,0 +1,42 @@
+{% extends "base.html" %}
+{% load gravatar %}
+{% load hk_generic %}
+{% load storm %}
+{% load crispy_forms_tags %}
+
+
+{% block title %}
+Create a new 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">
+
+ <div class="message-header">
+ <h1>Create a new thread</h1>
+ </div>
+
+ <div class="new-thread-form">
+ <form method="post"
+ action="{% url message_new mlist_fqdn=mlist.name %}">
+ {% csrf_token %}
+ {{ post_form|crispy }}
+ <p class="buttons">
+ <button type="submit" class="submit btn btn-primary">Send</button>
+ or <a class="cancel" href="{% url archives_latest mlist_fqdn=mlist.name %}">cancel</a>
+ </p>
+ </form>
+ </div>
+
+ </div>
+
+</div>
+
+{% endblock %}
+
+{# vim: set noet: #}
diff --git a/hyperkitty/templates/thread_list.html b/hyperkitty/templates/thread_list.html
index cb95daa..2b61792 100644
--- a/hyperkitty/templates/thread_list.html
+++ b/hyperkitty/templates/thread_list.html
@@ -15,7 +15,16 @@
<div id="thread-list" class="span8">
+ {% if flash_msg %}
+ <div class="flashmsg">
+ <div class="alert alert-success">{{ flash_msg }}</div>
+ </div>
+ {% endif %}
+
<div class="thread-list-header page-header">
+ <a href="{% url message_new mlist_fqdn=mlist.name %}"
+ class="thread-new btn{% if not user.is_authenticated %} disabled" title="You must be logged-in to create a thread.{% endif %}"
+ ><strong>+</strong> Start a new thread</a>
<h1>{{ mlist.display_name|default:mlist.name|escapeemail }}
<small>{{ list_title }}</small>
</h1>
diff --git a/hyperkitty/urls.py b/hyperkitty/urls.py
index ac1e4d7..ddf13bc 100644
--- a/hyperkitty/urls.py
+++ b/hyperkitty/urls.py
@@ -64,6 +64,8 @@ urlpatterns = patterns('hyperkitty.views',
'message.vote', name='message_vote'),
url(r'^list/(?P<mlist_fqdn>[^/@]+@[^/@]+)/message/(?P<message_id_hash>\w+)/reply$',
'message.reply', name='message_reply'),
+ url(r'^list/(?P<mlist_fqdn>[^/@]+@[^/@]+)/message/new$',
+ 'message.new_message', name='message_new'),
# Thread
url(r'^list/(?P<mlist_fqdn>[^/@]+@[^/@]+)/thread/(?P<threadid>\w+)/$',
diff --git a/hyperkitty/views/forms.py b/hyperkitty/views/forms.py
index 90c288f..3fe6e7e 100644
--- a/hyperkitty/views/forms.py
+++ b/hyperkitty/views/forms.py
@@ -103,3 +103,7 @@ class SearchForm(forms.Form):
class ReplyForm(forms.Form):
message = forms.CharField(widget=forms.Textarea, label="")
+
+class PostForm(forms.Form):
+ subject = forms.CharField()
+ message = forms.CharField(widget=forms.Textarea)
diff --git a/hyperkitty/views/list.py b/hyperkitty/views/list.py
index 6efd0b3..c8439ef 100644
--- a/hyperkitty/views/list.py
+++ b/hyperkitty/views/list.py
@@ -52,6 +52,10 @@ if settings.USE_MOCKUPS:
from hyperkitty.lib.mockup import generate_top_author, generate_thread_per_category
+FLASH_MESSAGES = {
+ "sent-ok": "The message has been sent successfully.",
+}
+
def archives(request, mlist_fqdn, year=None, month=None, day=None):
if year is None and month is None:
@@ -132,6 +136,10 @@ def _thread_list(request, mlist, threads, template_name='thread_list.html', extr
# If page is out of range (e.g. 9999), deliver last page of results.
threads = paginator.page(paginator.num_pages)
+ flash_msg = request.GET.get("msg")
+ if flash_msg:
+ flash_msg = FLASH_MESSAGES[flash_msg]
+
context = {
'mlist' : mlist,
'current_page': page_num,
@@ -139,6 +147,7 @@ def _thread_list(request, mlist, threads, template_name='thread_list.html', extr
'threads': threads,
'participants': len(participants),
'months_list': get_months(store, mlist.name),
+ 'flash_msg': flash_msg,
'use_mockups': settings.USE_MOCKUPS,
}
context.update(extra_context)
diff --git a/hyperkitty/views/message.py b/hyperkitty/views/message.py
index e64ac5f..de2da05 100644
--- a/hyperkitty/views/message.py
+++ b/hyperkitty/views/message.py
@@ -23,15 +23,17 @@
import re
import os
import urllib
+import datetime
import django.utils.simplejson as simplejson
from django.http import HttpResponse, HttpResponseRedirect, Http404
-from django.shortcuts import redirect, render
+from django.shortcuts import redirect, render, render_to_response
from django.template import RequestContext, loader
from django.conf import settings
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage
from django.core.urlresolvers import reverse
from django.core.mail import EmailMessage
+from django.core.exceptions import SuspiciousOperation
from django.contrib.auth.decorators import (login_required,
permission_required,
user_passes_test)
@@ -142,26 +144,69 @@ def reply(request, mlist_fqdn, message_id_hash):
TODO: unit tests
"""
if request.method != 'POST':
- return HttpResponse("Something went wrong", content_type="text/plain", status=500)
+ raise SuspiciousOperation
form = ReplyForm(request.POST)
if not form.is_valid():
- return HttpResponse(form.errors.as_text(), content_type="text/plain", status=400)
+ return HttpResponse(form.errors.as_text(),
+ content_type="text/plain", status=400)
store = get_store(request)
- message = store.get_message_by_hash_from_list(mlist_fqdn, message_id_hash)
+ mlist = store.get_list(mlist_fqdn)
+ message = store.get_message_by_hash_from_list(mlist.name, message_id_hash)
subject = message.subject
if not message.subject.lower().startswith("re:"):
subject = "Re: %s" % subject
- reply = EmailMessage(
- subject=subject,
- body=form.cleaned_data["message"],
- from_email='"%s %s" <%s>' %
- (request.user.first_name, request.user.last_name,
- request.user.email),
- to=[mlist_fqdn],
- cc=['aurelien@bompard.org'],
- headers={
+ _send_email(request, mlist, subject, form.cleaned_data["message"], {
"In-Reply-To": "<%s>" % message.message_id,
"References": "<%s>" % message.message_id,
})
- reply.send()
- return HttpResponse("The reply has been sent successfully.", mimetype="text/plain")
+ return HttpResponse("The reply has been sent successfully.",
+ mimetype="text/plain")
+
+
+@login_required
+def new_message(request, mlist_fqdn):
+ """ Sends a new thread-starting message to the list.
+ TODO: unit tests
+ """
+ store = get_store(request)
+ mlist = store.get_list(mlist_fqdn)
+ if request.method == 'POST':
+ form = PostForm(request.POST)
+ if form.is_valid():
+ _send_email(request, mlist, form.cleaned_data['subject'],
+ form.cleaned_data["message"])
+ today = datetime.date.today()
+ redirect_url = reverse(
+ 'archives_with_month', kwargs={
+ "mlist_fqdn": mlist_fqdn,
+ 'year': today.year,
+ 'month': today.month})
+ redirect_url += "?msg=sent-ok"
+ return HttpResponseRedirect(redirect_url)
+ else:
+ form = PostForm()
+ context = {
+ "mlist": mlist,
+ "post_form": form,
+ 'months_list': get_months(store, mlist.name),
+ 'use_mockups': settings.USE_MOCKUPS,
+ }
+ return render_to_response("message_new.html", context,
+ context_instance=RequestContext(request))
+
+
+def _send_email(request, mlist, subject, message, headers={}):
+ if not mlist:
+ # Make sure the list exists to avoid posting to any email addess
+ raise SuspiciousOperation("I don't know this mailing-list")
+ headers["User-Agent"] = "HyperKitty on %s" % request.build_absolute_uri("/")
+ msg = EmailMessage(
+ subject=subject,
+ body=message,
+ from_email='"%s %s" <%s>' %
+ (request.user.first_name, request.user.last_name,
+ request.user.email),
+ to=[mlist.name],
+ headers=headers,
+ )
+ msg.send()
diff --git a/hyperkitty/views/thread.py b/hyperkitty/views/thread.py
index c819792..5375bea 100644
--- a/hyperkitty/views/thread.py
+++ b/hyperkitty/views/thread.py
@@ -29,6 +29,7 @@ from django.template import RequestContext, loader
from django.conf import settings
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage
from django.core.urlresolvers import reverse
+from django.core.exceptions import SuspiciousOperation
from django.utils.datastructures import SortedDict
from django.contrib.auth.decorators import (login_required,
permission_required,
@@ -136,8 +137,7 @@ def add_tag(request, mlist_fqdn, threadid):
content_type="text/plain", status=403)
if request.method != 'POST':
- return HttpResponse("Something went wrong here",
- content_type="text/plain", status=500)
+ raise SuspiciousOperation
form = AddTagForm(request.POST)
if not form.is_valid():
@@ -171,8 +171,7 @@ def favorite(request, mlist_fqdn, threadid):
content_type="text/plain", status=403)
if request.method != 'POST':
- return HttpResponse("Something went wrong here",
- content_type="text/plain", status=500)
+ raise SuspiciousOperation
props = dict(list_address=mlist_fqdn, threadid=threadid, user=request.user)
if request.POST["action"] == "add":
@@ -189,7 +188,6 @@ def favorite(request, mlist_fqdn, threadid):
else:
fav.delete()
else:
- return HttpResponse("Something went wrong here",
- content_type="text/plain", status=500)
+ raise SuspiciousOperation
return HttpResponse("success", mimetype='text/plain')
diff --git a/requirements.txt b/requirements.txt
index 6b5b0e3..4a3d139 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,3 +6,4 @@ mailman>=3.0.0b2
kittystore
networkx
South
+django-crispy-forms