Commit 6e3e6a52 authored by Aurélien Bompard's avatar Aurélien Bompard

Merge branch 'delete-email'

parents d2208872 2c3e0a14
......@@ -30,7 +30,7 @@ from django.utils.translation import ugettext_lazy as _
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit
from hyperkitty.models import Profile
from hyperkitty.models.profile import Profile
# pylint: disable=too-few-public-methods
......@@ -169,3 +169,9 @@ class PostForm(forms.Form):
class CategoryForm(forms.Form):
category = forms.ChoiceField(label="", required=False)
class MessageDeleteForm(forms.Form):
email = forms.ModelMultipleChoiceField(
queryset=None, widget=forms.ModelMultipleChoiceField.hidden_widget,
)
......@@ -35,7 +35,7 @@ def compute_thread_order_and_depth(thread):
obj = graph.node[msgid]["obj"]
obj.thread_depth = thread_pos["d"]
obj.thread_order = thread_pos["o"]
obj.save()
obj.save(update_fields=["thread_depth", "thread_order"])
thread_pos["d"] += 1
thread_pos["o"] += 1
for succ in sorted(graph.successors(msgid),
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('hyperkitty', '0005_MailingList_list_id'),
]
operations = [
migrations.AlterField(
model_name='thread',
name='category',
field=models.ForeignKey(related_name='threads', on_delete=django.db.models.deletion.SET_NULL, to='hyperkitty.ThreadCategory', null=True),
),
migrations.AlterField(
model_name='thread',
name='starting_email',
field=models.OneToOneField(related_name='started_thread', null=True, on_delete=django.db.models.deletion.SET_NULL, to='hyperkitty.Email'),
),
]
......@@ -268,7 +268,7 @@ class Email(models.Model):
self.save(update_fields=["parent"])
starter = children[0]
starter.parent = None
starter.save()
starter.save(update_fields=["parent"])
children.all().update(parent=starter)
else:
children.update(parent=self.parent)
......@@ -286,7 +286,7 @@ class Email(models.Model):
else:
if thread.starting_email is None:
thread.find_starting_email()
thread.save()
thread.save(update_fields=["starting_email"])
compute_thread_order_and_depth(thread)
......
......@@ -48,9 +48,10 @@ class Thread(models.Model):
mailinglist = models.ForeignKey("MailingList", related_name="threads")
thread_id = models.CharField(max_length=255, db_index=True)
date_active = models.DateTimeField(db_index=True, default=now)
category = models.ForeignKey("ThreadCategory", related_name="threads", null=True)
category = models.ForeignKey("ThreadCategory",
related_name="threads", null=True, on_delete=models.SET_NULL)
starting_email = models.OneToOneField("Email",
related_name="started_thread", null=True)
related_name="started_thread", null=True, on_delete=models.SET_NULL)
class Meta:
unique_together = ("mailinglist", "thread_id")
......
......@@ -137,21 +137,9 @@ ul.list-stats {
li+li {
padding-left: 0.75em;
}
}
.participant:before {
content: "\e609";
color: #BFBFBF;
font-size: 1.14em;
line-height: 1em;
}
.discussion:before {
content: "\e607";
color: #BFBFBF;
font-size: 1.14em;
line-height: 1em;
}
.fa-envelope {
color: #BFBFBF;
.fa {
color: #BFBFBF;
}
}
......
......@@ -163,6 +163,9 @@ $headerNavPadding: 7px;
.list-name {
color: $subheadingColor;
}
ul.list-stats .fa {
color: $subheadingColor;
}
}
// 'new' label in front of list name
......
......@@ -246,6 +246,19 @@ $oddEmailColor: rgb(238, 238, 238);
font-size: 70%;
}
#thread-overview-info, #message-overview-info {
a {
color: #999;
}
.fa {
color: #BFBFBF;
}
.postorius .fa {
color: black;
}
}
#thread-overview-info {
#thread-date-info {
padding-bottom: 8px;
......@@ -261,7 +274,6 @@ $oddEmailColor: rgb(238, 238, 238);
margin-bottom: 0;
}
.favorite a {
color: #999;
i {
margin-right: 0.3em;
}
......
......@@ -29,11 +29,11 @@
{% endif %}
<ul class="list-unstyled pull-right list-stats">
<li>
<i class="icomoon participant"></i>
<i class="fa fa-user"></i>
{{ thread.participants_count }}
</li>
<li>
<i class="icomoon discussion"></i>
<i class="fa fa-comment"></i>
{{ thread|num_comments }}
</li>
<li class="hidden-tn">
......
......@@ -137,7 +137,7 @@
</div>
<ul class="list-stats list-unstyled">
<li>
<i class="icomoon participant"></i>
<i class="fa fa-user"></i>
{% if mlist.can_view %}
{{ mlist.recent_participants_count|default_if_none:"..." }}
{% else %}
......@@ -146,7 +146,7 @@
{% trans 'participants' %}
</li>
<li>
<i class="icomoon discussion"></i>
<i class="fa fa-comments"></i>
{% if mlist.can_view %}
{{ mlist.recent_threads_count|default_if_none:"..." }}
{% else %}
......@@ -221,7 +221,7 @@
</div>
<ul class="list-stats list-unstyled">
<li>
<i class="icomoon participant"></i>
<i class="fa fa-user"></i>
{% if mlist.can_view %}
{{ mlist.recent_participants_count|default_if_none:"..." }}
{% else %}
......@@ -230,7 +230,7 @@
{% trans 'participants' %}
</li>
<li>
<i class="icomoon discussion"></i>
<i class="fa fa-comments"></i>
{% if mlist.can_view %}
{{ mlist.recent_threads_count|default_if_none:"..." }}
{% else %}
......
......@@ -14,7 +14,7 @@
{% include 'hyperkitty/fragments/month_list.html' %}
<div class="col-md-7">
<div class="col-tn-12 col-sm-10">
<div class="message-header row">
<div class="col-tn-2 message-back">
......@@ -28,9 +28,16 @@
</div>
</div>
<section id="thread-content">
{% include 'hyperkitty/messages/message.html' with email=message unfolded='True' %}
</section>
<div class="row">
<div class="col-sm-9">
<section id="thread-content">
{% include 'hyperkitty/messages/message.html' with email=message unfolded='True' %}
</section>
</div>
<div class="col-sm-3">
{% include 'hyperkitty/messages/right_col.html' %}
</div>
</div>
</div>
......
{% extends "hyperkitty/base.html" %}
{% load i18n %}
{% load gravatar %}
{% load hk_generic %}
{% block title %}
{% trans "Delete message(s)" %} - {{ block.super }}
{% endblock %}
{% block content %}
<div class="row">
{# {% include 'hyperkitty/fragments/month_list.html' %} #}
<div class="col-sm-10">
<div class="message-header">
<h1>
{% trans "Delete message(s)" %}
</h1>
</div>
<p>
{% blocktrans with count=form.initial.email|length %}
{{ count }} message(s) will be deleted. Do you want to continue?
{% endblocktrans %}
</p>
<form method="post" enctype="multipart/form-data" action="{{ form_action }}">
{% csrf_token %}
<ul class="list-unstyled">
{% for error in form.non_field_errors %}
<li><i class="fa-li fa fa-times-circle"></i> {{ error }}</li>
{% endfor %}
</ul>
<div class="form-group {% if form.email.errors %}has-error{% endif %}">
{{ form.email }}
<span class="help-block">{{ form.email.errors }}</span>
</div>
<p class="buttons">
<button type="submit" class="submit btn btn-primary">{% trans "Delete" %}</button>
{% trans "or" %} <a class="cancel" href="{{ cancel_url }}">{% trans "cancel" %}</a>
</p>
</form>
</div>
</div>
{% endblock %}
{% load i18n %}
{% load gravatar %}
{% load hk_generic %}
{% load cache %}
<!-- right column -->
<section id="message-overview-info">
<p>
<a href="{% url 'hk_thread' threadid=message.thread.thread_id mlist_fqdn=mlist.name %}#{{message.message_id_hash}}">
<i class="fa fa-fw fa-comments"></i>
<span class="hidden-tn hidden-xs">{% trans "Back to the thread" %}</span>
</a>
</p>
<p>
<a href="{% url 'hk_list_overview' mlist_fqdn=mlist.name %}">
<i class="fa fa-fw fa-envelope-o"></i>
{% trans "Back to the list" %}
</a>
</p>
{% if user.is_staff or user.is_superuser %}
<div class="panel panel-danger">
<div class="panel-body">
<div><i class="fa fa-fw fa-trash"></i>
<a href="{% url 'hk_message_delete' mlist_fqdn=mlist.name message_id_hash=message.message_id_hash %}"
>{% trans "Delete this message" %}</a>
</div>
</div>
</div>
{% endif %}
</section>
......@@ -158,8 +158,8 @@
{% trans "the past <strong>30</strong> days:" %}
</p>
<ul class="list-stats list-unstyled">
<li><i class="icomoon participant"></i>{{ mlist.recent_participants_count }} {% trans "participants" %}</li>
<li><i class="icomoon discussion"></i>{{ mlist.recent_threads.count }} {% trans "discussions" %}</li>
<li><i class="fa fa-user"></i>{{ mlist.recent_participants_count }} {% trans "participants" %}</li>
<li><i class="fa fa-comments"></i>{{ mlist.recent_threads.count }} {% trans "discussions" %}</li>
</ul>
<div class="row">
......
......@@ -40,7 +40,7 @@
{% endif %}
<ul class="list-unstyled list-stats col-tn-12 col-sm-6 col-md-4">
<li>
<span class="icomoon discussion"></span>
<span class="fa fa-comment"></span>
{{ emails.paginator.count }} {% trans "messages" %}
</li>
</ul>
......
......@@ -119,8 +119,8 @@
<div class="col-sm-3">
<div class="anchor-link">
<a id="stats"></a>
</div>
<a id="stats"></a>
</div>
{% include 'hyperkitty/threads/right_col.html' %}
</div>
......
......@@ -35,12 +35,12 @@
<ul class="list-unstyled list-stats thread-list-info col-tn-6 col-xs-8">
{% if participants_count %}
<li>
<i class="icomoon participant"></i>
<i class="fa fa-user"></i>
{{ participants_count }} {% trans "participants" %}
</li>
{% endif %}
<li>
<i class="icomoon discussion"></i>
<i class="fa fa-comment"></i>
{{ threads.paginator.count }} {% trans "discussions" %}
</li>
</ul>
......
......@@ -28,25 +28,25 @@
</p>
{% if postorius_installed %}
<p>
<p class="postorius">
<a class="btn btn-default btn-sm" href="{{ postorius_installed }}/{{ mlist.name }}">
<i class="fa fa-inbox"></i>
<i class="fa fa-fw fa-inbox"></i>
Manage subscription</a>
</p>
{% endif %}
<p>
<p class="thread-overview-details">
<div>
<i class="icomoon discussion"></i>
<i class="fa fa-fw fa-comment"></i>
{{ num_comments }} {% trans "comments" %}
</div>
<div>
<i class="icomoon participant"></i>
<i class="fa fa-fw fa-user"></i>
{{ thread.participants_count }} {% trans "participants" %}
</div>
{% if user.is_authenticated %}
<div>
<i class="fa fa-envelope"></i> {{ unread_count }} {% trans "unread" %}
<i class="fa fa-fw fa-envelope"></i> {{ unread_count }} {% trans "unread" %}
<span class="hidden-sm"> {% trans "messages" %}</span>
</div>
{% endif %}
......@@ -58,17 +58,25 @@
<input type="hidden" name="action" value="{{ fav_action }}" />
<p>
<a href="#AddFav" class="notsaved{% if not user.is_authenticated %} disabled" title="{% trans 'You must be logged-in to have favorites.' %}{% endif %}">
<i class="fa fa-star"></i>{% trans "Add to favorites" %}</a>
<i class="fa fa-fw fa-star"></i>{% trans "Add to favorites" %}</a>
<a href="#RmFav" class="saved">
<i class="fa fa-star"></i>{% trans "Remove from favorites" %}</a>
<i class="fa fa-fw fa-star"></i>{% trans "Remove from favorites" %}</a>
</p>
</form>
{% if user.is_staff %}
<p><i class="icon-resize-small"></i>
<a href="{% url 'hk_thread_reattach' mlist_fqdn=mlist.name threadid=thread.thread_id %}"
>{% trans "Reattach this thread" %}</a>
</p>
{% if user.is_staff or user.is_superuser %}
<div class="panel panel-danger">
<div class="panel-body">
<div><i class="fa fa-fw fa-plug"></i>
<a href="{% url 'hk_thread_reattach' mlist_fqdn=mlist.name threadid=thread.thread_id %}"
>{% trans "Reattach this thread" %}</a>
</div>
<div><i class="fa fa-fw fa-trash"></i>
<a href="{% url 'hk_thread_delete' mlist_fqdn=mlist.name threadid=thread.thread_id %}"
>{% trans "Delete this thread" %}</a>
</div>
</div>
</div>
{% endif %}
<div id="tags">
......
......@@ -46,11 +46,11 @@
</div>
<ul class="list-unstyled list-stats col-tn-12 col-sm-5 col-xl-3">
<li>
<i class="icomoon participant"></i>
<i class="fa fa-user"></i>
{{ thread.participants_count }} {% trans "participants" %}
</li>
<li>
<i class="icomoon discussion"></i>
<i class="fa fa-comment"></i>
{{ thread|num_comments }} {% trans "comments" %}
</li>
</ul>
......
......@@ -290,7 +290,37 @@ HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# the site admins on every HTTP 500 error when DEBUG=False.
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {}
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'loggers': {
'django': {
'handlers': ['console'],
'level': 'INFO',
'propagate': True,
},
'hyperkitty': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': True,
},
},
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s: %(message)s'
},
},
'root': {
'handlers': ['console'],
'level': 'INFO',
},
}
#
# HyperKitty-specific
......
......@@ -35,7 +35,9 @@ from django_gravatar.helpers import get_gravatar_url
from hyperkitty.lib.utils import get_message_id_hash
from hyperkitty.lib.incoming import add_to_list
from hyperkitty.models import Email, MailingList
from hyperkitty.models.email import Email
from hyperkitty.models.mailinglist import MailingList
from hyperkitty.models.thread import Thread
from hyperkitty.tests.utils import TestCase, get_flash_messages
......@@ -321,3 +323,89 @@ class MessageViewsTestCase(TestCase):
get_message_id_hash("msg2")))
response = self.client.get(url)
self.assertNotContains(response, "[email protected]", status_code=200)
def test_delete_forbidden(self):
url = reverse('hk_message_delete', args=("[email protected]",
get_message_id_hash("msg")))
response = self.client.post(url)
self.assertEqual(response.status_code, 403)
def test_delete_single_message(self):
self.user.is_staff = True
self.user.save()
msg = Email.objects.get(message_id="msg")
thread_id = msg.thread.pk
url = reverse('hk_message_delete',
args=("[email protected]", msg.message_id_hash))
response = self.client.post(url, {"email": msg.pk})
self.assertRedirects(
response, reverse('hk_list_overview', kwargs={
"mlist_fqdn": "[email protected]"}))
# Flash message
messages = get_flash_messages(response)
self.assertEqual(len(messages), 1)
self.assertEqual(messages[0].tags, "success")
# The message and the thread must be deleted.
self.assertFalse(Email.objects.filter(message_id="msg").exists())
self.assertFalse(Thread.objects.filter(pk=thread_id).exists())
def test_delete_single_in_thread(self):
# Delete an email in a thread that contains other emails
self.user.is_staff = True
self.user.save()
msg = Email.objects.get(message_id="msg")
msg2 = Message()
msg2["From"] = "[email protected]"
msg2["Message-ID"] = "<msg2>"
msg2["In-Reply-To"] = "<msg>"
msg2.set_payload("Dummy message")
add_to_list("[email protected]", msg2)
msg2 = Email.objects.get(message_id="msg2")
thread_id = msg.thread.thread_id
url = reverse('hk_message_delete',
args=("[email protected]", msg.message_id_hash))
response = self.client.post(url, {"email": msg.pk})
self.assertRedirects(
response, reverse('hk_thread', kwargs={
"mlist_fqdn": "[email protected]",
"threadid": thread_id}))
# Flash message
messages = get_flash_messages(response)
self.assertEqual(len(messages), 1)
self.assertEqual(messages[0].tags, "success")
# The message must be deleted, but not the other message or the thread.
self.assertFalse(Email.objects.filter(message_id="msg").exists())
self.assertTrue(Email.objects.filter(message_id="msg2").exists())
thread = Thread.objects.get(thread_id=thread_id)
self.assertIsNotNone(thread)
# msg2 must now be the thread starter.
msg2.refresh_from_db()
self.assertIsNone(msg2.parent_id)
self.assertEqual(thread.starting_email.message_id, "msg2")
def test_delete_all_messages_in_thread(self):
self.user.is_staff = True
self.user.save()
msg = Email.objects.get(message_id="msg")
msg2 = Message()
msg2["From"] = "[email protected]"
msg2["Message-ID"] = "<msg2>"
msg2["In-Reply-To"] = "<msg>"
msg2.set_payload("Dummy message")
add_to_list("[email protected]", msg2)
msg2 = Email.objects.get(message_id="msg2")
thread_id = msg.thread.pk
url = reverse('hk_thread_delete',
args=("[email protected]", msg.thread.thread_id))
response = self.client.post(url, {"email": [msg.pk, msg2.pk]})
self.assertRedirects(
response, reverse('hk_list_overview', kwargs={
"mlist_fqdn": "[email protected]"}))
# Flash message
messages = get_flash_messages(response)
self.assertEqual(len(messages), 1)
self.assertEqual(messages[0].tags, "success")
# Alls messages and the thread must be deleted.
self.assertFalse(Email.objects.filter(message_id="msg").exists())
self.assertFalse(Email.objects.filter(message_id="msg2").exists())
self.assertFalse(Thread.objects.filter(pk=thread_id).exists())
......@@ -77,6 +77,8 @@ urlpatterns = [
message.vote, name='hk_message_vote'),