From af143a4fe37f344708166fd59932c81f783dcc7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89lie=20Bouttier?= Date: Fri, 11 Aug 2017 23:34:12 +0200 Subject: [PATCH] removing old conversations app --- conversations/__init__.py | 1 - conversations/admin.py | 7 - conversations/apps.py | 8 -- conversations/emails.py | 76 ----------- conversations/forms.py | 5 - conversations/migrations/0001_initial.py | 64 --------- .../0002_conversationabouttalk_talk.py | 24 ---- conversations/migrations/__init__.py | 0 conversations/models.py | 127 ------------------ conversations/post-mail.sh | 10 -- conversations/sieve-filter | 24 ---- conversations/signals.py | 64 --------- .../conversations/_message_detail.html | 8 -- .../conversations/_message_form.html | 15 --- .../templates/conversations/conversation.html | 17 --- .../conversations/correspondent_list.html | 36 ----- .../conversations/emails/message.html | 8 -- .../conversations/emails/message.txt | 4 - .../emails/talk_notification.html | 11 -- .../emails/talk_notification.txt | 12 -- .../templates/conversations/inbox.html | 18 --- conversations/tests.py | 96 ------------- conversations/urls.py | 13 -- conversations/utils.py | 74 ---------- conversations/views.py | 100 -------------- mailing/tests.py | 93 +++++++++++++ 26 files changed, 93 insertions(+), 822 deletions(-) delete mode 100644 conversations/__init__.py delete mode 100644 conversations/admin.py delete mode 100644 conversations/apps.py delete mode 100644 conversations/emails.py delete mode 100644 conversations/forms.py delete mode 100644 conversations/migrations/0001_initial.py delete mode 100644 conversations/migrations/0002_conversationabouttalk_talk.py delete mode 100644 conversations/migrations/__init__.py delete mode 100644 conversations/models.py delete mode 100755 conversations/post-mail.sh delete mode 100755 conversations/sieve-filter delete mode 100644 conversations/signals.py delete mode 100644 conversations/templates/conversations/_message_detail.html delete mode 100644 conversations/templates/conversations/_message_form.html delete mode 100644 conversations/templates/conversations/conversation.html delete mode 100644 conversations/templates/conversations/correspondent_list.html delete mode 100644 conversations/templates/conversations/emails/message.html delete mode 100644 conversations/templates/conversations/emails/message.txt delete mode 100644 conversations/templates/conversations/emails/talk_notification.html delete mode 100644 conversations/templates/conversations/emails/talk_notification.txt delete mode 100644 conversations/templates/conversations/inbox.html delete mode 100644 conversations/tests.py delete mode 100644 conversations/urls.py delete mode 100644 conversations/utils.py delete mode 100644 conversations/views.py create mode 100644 mailing/tests.py diff --git a/conversations/__init__.py b/conversations/__init__.py deleted file mode 100644 index 113bdeb..0000000 --- a/conversations/__init__.py +++ /dev/null @@ -1 +0,0 @@ -default_app_config = 'conversations.apps.ConversationsConfig' diff --git a/conversations/admin.py b/conversations/admin.py deleted file mode 100644 index 431bad1..0000000 --- a/conversations/admin.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.contrib import admin - -from .models import ConversationAboutTalk, ConversationWithParticipant, Message - -admin.site.register(ConversationWithParticipant) -admin.site.register(ConversationAboutTalk) -admin.site.register(Message) diff --git a/conversations/apps.py b/conversations/apps.py deleted file mode 100644 index 7fc0e2b..0000000 --- a/conversations/apps.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.apps import AppConfig - - -class ConversationsConfig(AppConfig): - name = 'conversations' - - def ready(self): - import conversations.signals # noqa diff --git a/conversations/emails.py b/conversations/emails.py deleted file mode 100644 index ba00c6a..0000000 --- a/conversations/emails.py +++ /dev/null @@ -1,76 +0,0 @@ -import re -import chardet -import logging -from functools import reduce - -from email import policy -from email.parser import BytesParser -from email.message import EmailMessage - -from django.conf import settings -from django.contrib.auth.models import User -from django.core.exceptions import PermissionDenied -from django.http import Http404, HttpResponse -from django.shortcuts import get_object_or_404 -from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_http_methods - -from .models import Message -from .utils import hexdigest_sha256 - - -@csrf_exempt -@require_http_methods(["POST"]) -def email_recv(request): - - if not hasattr(settings, 'REPLY_EMAIL') \ - or not hasattr(settings, 'REPLY_KEY'): - return HttpResponse(status=501) # Not Implemented - - key = request.POST.get('key').strip() - if key != settings.REPLY_KEY: - raise PermissionDenied - - if 'email' not in request.FILES: - return HttpResponse(status=400) # Bad Request - - msg = request.FILES['email'] - - msg = BytesParser(policy=policy.default).parsebytes(msg.read()) - body = msg.get_body(preferencelist=('plain',)) - content = body.get_payload(decode=True) - - try: - content = content.decode(body.get_content_charset()) - except Exception: - encoding = chardet.detect(content)['encoding'] - content = content.decode(encoding) - - addr = settings.REPLY_EMAIL - pos = addr.find('@') - name = addr[:pos] - domain = addr[pos+1:] - - regexp = '^%s\+(?P[a-z0-9]{12})(?P[a-z0-9]{60})(?P[a-z0-9]{12})@%s$' % (name, domain) - p = re.compile(regexp) - m = None - addrs = map(lambda x: x.split(',') if x else [], [msg.get('To'), msg.get('Cc')]) - addrs = reduce(lambda x, y: x + y, addrs) - for _mto in map(lambda x: x.strip(), addrs): - m = p.match(_mto) - if m: - break - if not m: # no one matches - raise Http404 - - author = get_object_or_404(User, profile__email_token=m.group('dest')) - message = get_object_or_404(Message, token=m.group('token')) - key = hexdigest_sha256(settings.SECRET_KEY, message.token, author.pk)[0:12] - if key != m.group('key'): - raise PermissionDenied - - answer = Message(conversation=message.conversation, - author=author, content=content) - answer.save() - - return HttpResponse() diff --git a/conversations/forms.py b/conversations/forms.py deleted file mode 100644 index f84879c..0000000 --- a/conversations/forms.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.forms.models import modelform_factory - -from .models import Message - -MessageForm = modelform_factory(Message, fields=['content']) diff --git a/conversations/migrations/0001_initial.py b/conversations/migrations/0001_initial.py deleted file mode 100644 index 2784369..0000000 --- a/conversations/migrations/0001_initial.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.3 on 2017-01-13 10:49 -from __future__ import unicode_literals - -import conversations.utils -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('contenttypes', '0002_remove_content_type_name'), - ('accounts', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='ConversationAboutTalk', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('updated', models.DateTimeField(auto_now=True)), - ('subscribers', models.ManyToManyField(blank=True, related_name='_conversationabouttalk_subscribers_+', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='ConversationWithParticipant', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('updated', models.DateTimeField(auto_now=True)), - ('participation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='conversation', to='accounts.Participation')), - ('subscribers', models.ManyToManyField(blank=True, related_name='_conversationwithparticipant_subscribers_+', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Message', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('updated', models.DateTimeField(auto_now=True)), - ('object_id', models.PositiveIntegerField()), - ('token', models.CharField(default=conversations.utils.generate_message_token, max_length=64, unique=True)), - ('content', models.TextField(blank=True)), - ('system', models.BooleanField(default=False)), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), - ], - options={ - 'ordering': ['created'], - }, - ), - ] diff --git a/conversations/migrations/0002_conversationabouttalk_talk.py b/conversations/migrations/0002_conversationabouttalk_talk.py deleted file mode 100644 index ddaa634..0000000 --- a/conversations/migrations/0002_conversationabouttalk_talk.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.3 on 2017-01-13 10:49 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('proposals', '0001_initial'), - ('conversations', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='conversationabouttalk', - name='talk', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='conversation', to='proposals.Talk'), - ), - ] diff --git a/conversations/migrations/__init__.py b/conversations/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/conversations/models.py b/conversations/models.py deleted file mode 100644 index f05b7ef..0000000 --- a/conversations/models.py +++ /dev/null @@ -1,127 +0,0 @@ -from django.contrib.auth.models import User -from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation -from django.contrib.contenttypes.models import ContentType -from django.core.urlresolvers import reverse -from django.db import models - -from accounts.models import Participation -from ponyconf.utils import PonyConfModel -from proposals.models import Talk - -from .utils import generate_message_token, notify_by_email - - -class Message(PonyConfModel): - - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - object_id = models.PositiveIntegerField() - conversation = GenericForeignKey('content_type', 'object_id') - - token = models.CharField(max_length=64, default=generate_message_token, unique=True) - - author = models.ForeignKey(User) - content = models.TextField(blank=True) - system = models.BooleanField(default=False) - - class Meta: - ordering = ['created'] - - def __str__(self): - return "Message from %s" % self.author - - def get_absolute_url(self): - return self.conversation.get_absolute_url() - - -class Conversation(PonyConfModel): - - subscribers = models.ManyToManyField(User, related_name='+', blank=True) - - class Meta: - abstract = True - - -class ConversationWithParticipant(Conversation): - - participation = models.OneToOneField(Participation, related_name='conversation') - messages = GenericRelation(Message) - - uri = 'inbox' - template = 'participant_message' - - def __str__(self): - return "Conversation with %s" % self.participation.user - - def get_absolute_url(self): - return reverse('user-conversation', kwargs={'username': self.participation.user.username}) - - def get_site(self): - return self.participation.site - - def new_message(self, message): - site = self.get_site() - subject = '[%s] Conversation with %s' % (site.name, self.participation.user.profile) - recipients = list(self.subscribers.all()) - # Auto-subscribe - if message.author != self.participation.user and message.author not in recipients: - self.subscribers.add(message.author) - data = { - 'content': message.content, - 'uri': site.domain + reverse('user-conversation', args=[self.participation.user.username]), - } - first = self.messages.first() - if first != message: - ref = first.token - else: - ref = None - notify_by_email('message', data, subject, message.author, recipients, message.token, ref) - - if message.author != self.participation.user: - subject = '[%s] Message notification' % site.name - data.update({ - 'uri': site.domain + reverse('inbox') - }) - notify_by_email('message', data, subject, message.author, [self.participation.user], message.token, ref) - - -class ConversationAboutTalk(Conversation): - - talk = models.OneToOneField(Talk, related_name='conversation') - messages = GenericRelation(Message) - - uri = 'inbox' - template = 'talk_message' - - def __str__(self): - return "Conversation about %s" % self.talk.title - - def get_absolute_url(self): - return self.talk.get_absolute_url() - - def get_site(self): - return self.talk.site - - def new_message(self, message): - site = self.get_site() - first = self.messages.first() - if not message.system and message.author not in self.subscribers.all(): - self.subscribers.add(message.author) - recipients = self.subscribers.all() - data = { - 'uri': site.domain + reverse('show-talk', args=[self.talk.slug]), - } - if first == message: - subject = '[%s] Talk: %s' % (site.name, self.talk.title) - template = 'talk_notification' - ref = None - data.update({ - 'talk': self.talk, - 'proposer': message.author, - 'proposer_uri': site.domain + reverse('show-participant', args=[message.author.username]) - }) - else: - subject = 'Re: [%s] Talk: %s' % (site.name, self.talk.title) - template = 'message' - ref = first.token - data.update({'content': message.content}) - notify_by_email(template, data, subject, message.author, recipients, message.token, ref) diff --git a/conversations/post-mail.sh b/conversations/post-mail.sh deleted file mode 100755 index 3b04715..0000000 --- a/conversations/post-mail.sh +++ /dev/null @@ -1,10 +0,0 @@ -#! /bin/bash - -# Usage: cat email.txt | post-mail.sh REPLY_KEY@https://example.org/conversations/recv/ -# Get the value of REPLY_KEY from the django setting. - -# Postfix users can set up an alias file with this content: -# reply: "|/path/to/post-mail.sh mykey@https://example.org/conversations/recv/ -# don't forget to run postalias and to add the alias file to main.cf under alias_map. - -curl ${@#*\@} -F key=${@%\@*} -F "email=@-;filename=email.txt" diff --git a/conversations/sieve-filter b/conversations/sieve-filter deleted file mode 100755 index c64b574..0000000 --- a/conversations/sieve-filter +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python - -import sys - -import requests - -if len(sys.argv) != 2: - print("Usage: %s KEY@URL" % sys.argv[0]) - sys.exit(1) - -key, url = sys.argv[1].split('@') - -email = sys.stdin.buffer.raw.read() -sys.stdout.buffer.write(email) # DO NOT REMOVE - -requests.post( - url, - data={ - 'key': key, - }, - files={ - 'email': ('email.txt', email), - } -) diff --git a/conversations/signals.py b/conversations/signals.py deleted file mode 100644 index e3c7d4c..0000000 --- a/conversations/signals.py +++ /dev/null @@ -1,64 +0,0 @@ -from django.contrib.auth.models import User -from django.db.models import Q -from django.db.models.signals import post_save -from django.dispatch import receiver - -from ponyconf.decorators import disable_for_loaddata -from accounts.models import Participation -from proposals.models import Talk -from proposals.signals import talk_added, talk_edited - -from .models import ConversationAboutTalk, ConversationWithParticipant, Message - - -@receiver(post_save, sender=Participation, dispatch_uid="Create ConversationWithParticipant") -@disable_for_loaddata -def create_conversation_with_participant(sender, instance, created, **kwargs): - if not created: - return - conversation = ConversationWithParticipant(participation=instance) - conversation.save() - - -@receiver(post_save, sender=Talk, dispatch_uid="Create ConversationAboutTalk") -@disable_for_loaddata -def create_conversation_about_talk(sender, instance, created, **kwargs): - if not created: - return - conversation = ConversationAboutTalk(talk=instance) - conversation.save() - - -def check_talk(talk): - reviewers = User.objects.filter(Q(topic__talk=talk) | Q(participation__site=talk.site, participation__orga=True)) - # Subscribe the reviewers to the conversation about the talk - talk.conversation.subscribers.add(*reviewers) - # Subscribe the reviewers to the conversations with each speaker - for user in talk.speakers.all(): - participation, created = Participation.objects.get_or_create(user=user, site=talk.site) - participation.conversation.subscribers.add(*reviewers) - - -@receiver(talk_added, dispatch_uid="Notify talk added") -def notify_talk_added(sender, instance, author, **kwargs): - check_talk(instance) - message = Message(conversation=instance.conversation, author=author, - content='The talk has been proposed.', system=True) - message.save() - - -@receiver(talk_edited, dispatch_uid="Notify talk edited") -def notify_talk_edited(sender, instance, author, **kwargs): - check_talk(instance) - message = Message(conversation=instance.conversation, author=author, - content='The talk has been modified.', system=True) - message.save() - - -@receiver(post_save, sender=Message, dispatch_uid="Notify new message") -@disable_for_loaddata -def notify_new_message(sender, instance, created, **kwargs): - if not created: - # Possibly send a modification notification? - return - instance.conversation.new_message(instance) diff --git a/conversations/templates/conversations/_message_detail.html b/conversations/templates/conversations/_message_detail.html deleted file mode 100644 index 3766a26..0000000 --- a/conversations/templates/conversations/_message_detail.html +++ /dev/null @@ -1,8 +0,0 @@ -
-
- {{ message.created }} | {{ message.author.profile }} -
-
- {{ message.content|linebreaksbr }} -
-
diff --git a/conversations/templates/conversations/_message_form.html b/conversations/templates/conversations/_message_form.html deleted file mode 100644 index 7ef8e91..0000000 --- a/conversations/templates/conversations/_message_form.html +++ /dev/null @@ -1,15 +0,0 @@ -{% load i18n %} -
-
- {% trans "Send a message" %} -
-
-
- {% csrf_token %} -
- -
- -
-
-
diff --git a/conversations/templates/conversations/conversation.html b/conversations/templates/conversations/conversation.html deleted file mode 100644 index 47438f6..0000000 --- a/conversations/templates/conversations/conversation.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'staff.html' %} - -{% load i18n %} - -{% block correspondentstab %} class="active"{% endblock %} - -{% block content %} - -

{% blocktrans with correspondent=correspondent.profile %}Conversation with {{ correspondent }}{% endblocktrans %}

- -{% for message in message_list %} -{% include 'conversations/_message_detail.html' %} -{% endfor %} - -{% include 'conversations/_message_form.html' %} - -{% endblock %} diff --git a/conversations/templates/conversations/correspondent_list.html b/conversations/templates/conversations/correspondent_list.html deleted file mode 100644 index d4590fa..0000000 --- a/conversations/templates/conversations/correspondent_list.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends 'staff.html' %} - -{% load bootstrap3 i18n %} - -{% block correspondentstab %} class="active"{% endblock %} - -{% block content %} - -

{% trans "Correspondents" %}

-

{% trans "This is the list of participants that you follow." %}

- - - - - - - - - {% for correspondent in correspondent_list %} - - - - - - - {% endfor %} -
#UsernameFull nameAdministration
{{ forloop.counter }}{{ correspondent.user.username }}{{ correspondent.user.get_full_name }} - - {% if request.user in correspondent.conversation.subscribers.all %} - - {% else %} - - {% endif %} -
- -{% endblock %} diff --git a/conversations/templates/conversations/emails/message.html b/conversations/templates/conversations/emails/message.html deleted file mode 100644 index af1a03d..0000000 --- a/conversations/templates/conversations/emails/message.html +++ /dev/null @@ -1,8 +0,0 @@ -{{ content|linebreaksbr }} - -
-{% if answering %} -Reply to this email directly or view it online. -{% else %} -Reply online. -{% endif %} diff --git a/conversations/templates/conversations/emails/message.txt b/conversations/templates/conversations/emails/message.txt deleted file mode 100644 index 060506a..0000000 --- a/conversations/templates/conversations/emails/message.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ content|safe }} - --- -Reply {% if answering %}to this email directly or view it {% endif %}online: https://{{ uri }} diff --git a/conversations/templates/conversations/emails/talk_notification.html b/conversations/templates/conversations/emails/talk_notification.html deleted file mode 100644 index 1db0dae..0000000 --- a/conversations/templates/conversations/emails/talk_notification.html +++ /dev/null @@ -1,11 +0,0 @@ -Hi!
-
-A new {{ talk.event }} has been proposed by {{ proposer.profile }}!
-
-Title: {{ talk.title }}
-
-Description:
-

{{ talk.description|linebreaksbr }}

-{% if answering %} -
-Reply to this email directly to comment this talk.{% endif %} diff --git a/conversations/templates/conversations/emails/talk_notification.txt b/conversations/templates/conversations/emails/talk_notification.txt deleted file mode 100644 index e02d1cc..0000000 --- a/conversations/templates/conversations/emails/talk_notification.txt +++ /dev/null @@ -1,12 +0,0 @@ -Hi! - -A new talk has been proposed by {{ proposer.profile }}! -See it online: https://{{ uri }} - -Title: {{ talk.title }} - -Description: -{{ talk.description }} -{% if answering %} --- -Reply to this email directly to comment this talk.{% endif %} diff --git a/conversations/templates/conversations/inbox.html b/conversations/templates/conversations/inbox.html deleted file mode 100644 index 4fc83df..0000000 --- a/conversations/templates/conversations/inbox.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends 'base.html' %} - -{% load i18n %} - -{% block inboxtab %} class="active"{% endblock %} - -{% block content %} - -

{% trans "Messaging" %}

-

{% trans "You can use this page to communicate with the staff." %}

- -{% for message in message_list %} -{% include 'conversations/_message_detail.html' %} -{% endfor %} - -{% include 'conversations/_message_form.html' %} - -{% endblock %} diff --git a/conversations/tests.py b/conversations/tests.py deleted file mode 100644 index 54acbce..0000000 --- a/conversations/tests.py +++ /dev/null @@ -1,96 +0,0 @@ -from django.contrib.auth.models import User -from django.contrib.sites.models import Site -from django.core.urlresolvers import reverse -from django.test import TestCase, override_settings -from django.core import mail -from django.conf import settings - -from accounts.models import Participation -from proposals.models import Topic, Talk, Event - -from .models import ConversationAboutTalk, ConversationWithParticipant, Message - - -class ConversationTests(TestCase): - def setUp(self): - a, b, c, d = (User.objects.create_user(guy, email='%s@example.org' % guy, password=guy) for guy in 'abcd') - d.is_superuser = True - d.save() - pa, _ = Participation.objects.get_or_create(user=a, site=Site.objects.first()) - conversation, _ = ConversationWithParticipant.objects.get_or_create(participation=pa) - Message.objects.create(content='allo', conversation=conversation, author=b) - Message.objects.create(content='aluil', conversation=conversation, author=a) - site = Site.objects.first() - Talk.objects.get_or_create(site=site, proposer=a, title='a talk', description='yay', event=Event.objects.get(site=site, name='other')) - - def test_models(self): - talk, participant, message = (model.objects.first() for model in - (ConversationAboutTalk, ConversationWithParticipant, Message)) - self.assertEqual(str(talk), 'Conversation about a talk') - self.assertEqual(str(participant), 'Conversation with a') - self.assertEqual(str(message), 'Message from b') - self.assertEqual(message.get_absolute_url(), '/conversations/with/a/') - self.assertEqual(talk.get_absolute_url(), '/talk/details/a-talk') - - def test_views(self): - url = ConversationWithParticipant.objects.first().get_absolute_url() - self.assertEqual(self.client.get(url).status_code, 302) - self.client.login(username='c', password='c') - self.assertEqual(self.client.get(url).status_code, 403) - self.assertEqual(self.client.get(reverse('list-correspondents')).status_code, 403) # c is not staff - self.assertEqual(self.client.get(reverse('inbox')).status_code, 200) - self.client.post(reverse('inbox'), {'content': 'coucou'}) - self.client.login(username='d', password='d') - self.client.post(url, {'content': 'im superuser'}) - self.assertEqual(Message.objects.last().content, 'im superuser') - self.client.login(username='d', password='d') - self.assertEqual(self.client.get(reverse('list-correspondents')).status_code, 200) - - -@override_settings(DEFAULT_FROM_EMAIL='noreply@example.org', - REPLY_EMAIL='reply@example.org', - REPLY_KEY='secret') -class EmailTests(TestCase): - def setUp(self): - for guy in 'abcd': - setattr(self, guy, User.objects.create_user(guy, email='%s@example.org' % guy, password=guy)) - a_p = Participation(user=self.a, site=Site.objects.first()) - a_p.orga = True - a_p.save() - t = Topic(name='Topic 1', site=Site.objects.first()) - t.save() - t.reviewers.add(self.b) - - - def test_talk_notification(self): - self.client.login(username='c', password='c') - # Check that login create participation - self.assertTrue(Participation.objects.filter(user=self.c, site=Site.objects.first()).exists()) - # Propose new talk - topic = Topic.objects.get(name='Topic 1') - response = self.client.post(reverse('add-talk'), { - 'title': 'Talk 1', - 'description': 'This is the first talk', - 'topics': (topic.pk,), - 'event': 1, - 'speakers': (self.c.pk, self.d.pk), - }, follow=True) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'Talk proposed') # check messages notification - talk = Talk.objects.get(site=Site.objects.first(), title='Talk 1') - conv = ConversationAboutTalk.objects.get(talk=talk) - # Orga and reviewer should have been subscribed to the conversation about the talk - self.assertEqual(set([self.a, self.b]), set(conv.subscribers.all())) - # Both should have received an email notification - self.assertEqual(len(mail.outbox), 2) - for m in mail.outbox: - self.assertEqual(m.from_email, '%s <%s>' % (self.c.profile, settings.DEFAULT_FROM_EMAIL)) - self.assertTrue('Talk: %s' % talk.title) - self.assertTrue(len(m.to), 1) - self.assertTrue(m.to[0] in [ self.a.email, self.b.email ]) - # Both should have been subscribed to conversations with each speakers - for user in [self.c, self.d]: - # Participation should have been created as the user is a speaker - p = Participation.objects.get(user=user, site=Site.objects.first()) - conv = ConversationWithParticipant.objects.get(participation=p) - self.assertEqual(set([self.a, self.b]), set(conv.subscribers.all())) diff --git a/conversations/urls.py b/conversations/urls.py deleted file mode 100644 index 34fd082..0000000 --- a/conversations/urls.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.conf.urls import url - -from conversations import emails, views - -urlpatterns = [ - url(r'^recv/$', emails.email_recv), # API - url(r'^inbox/$', views.user_conversation, name='inbox'), - url(r'^$', views.correspondent_list, name='list-correspondents'), - url(r'^with/(?P[\w.@+-]+)/$', views.user_conversation, name='user-conversation'), - url(r'^about/(?P[\w.@+-]+)/$', views.talk_conversation, name='talk-conversation'), - url(r'^subscribe/(?P[\w.@+-]+)/$', views.subscribe, name='subscribe-conversation'), - url(r'^unsubscribe/(?P[\w.@+-]+)/$', views.unsubscribe, name='unsubscribe-conversation'), -] diff --git a/conversations/utils.py b/conversations/utils.py deleted file mode 100644 index 1a48cda..0000000 --- a/conversations/utils.py +++ /dev/null @@ -1,74 +0,0 @@ -import hashlib - -from django.conf import settings -from django.core import mail -from django.core.mail import EmailMultiAlternatives -from django.template.loader import render_to_string -from django.utils.crypto import get_random_string - - -def hexdigest_sha256(*args): - - r = hashlib.sha256() - for arg in args: - r.update(str(arg).encode('utf-8')) - - return r.hexdigest() - - -def get_reply_addr(message_id, dest): - - if not hasattr(settings, 'REPLY_EMAIL'): - return [] - - addr = settings.REPLY_EMAIL - pos = addr.find('@') - name = addr[:pos] - domain = addr[pos:] - key = hexdigest_sha256(settings.SECRET_KEY, message_id, dest.pk)[0:12] - - return ['%s+%s%s%s%s' % (name, dest.profile.email_token, message_id, key, domain)] - - -def generate_message_token(): - return get_random_string(length=60, allowed_chars='abcdefghijklmnopqrstuvwxyz0123456789') - - -def notify_by_email(template, data, subject, sender, dests, message_id, ref=None): - - if hasattr(settings, 'REPLY_EMAIL') and hasattr(settings, 'REPLY_KEY'): - data.update({'answering': True}) - - text_message = render_to_string('conversations/emails/%s.txt' % template, data) - html_message = render_to_string('conversations/emails/%s.html' % template, data) - - from_email = '{name} <{email}>'.format( - name=sender.get_full_name() or sender.username, - email=settings.DEFAULT_FROM_EMAIL) - - # Generating headers - headers = {'Message-ID': "<%s.%s>" % (message_id, settings.DEFAULT_FROM_EMAIL)} - if ref: - # This email reference a previous one - headers.update({ - 'References': '<%s.%s>' % (ref, settings.DEFAULT_FROM_EMAIL), - }) - - mails = [] - for dest in dests: - if not dest.email: - continue - - reply_to = get_reply_addr(message_id, dest) - - mails += [(subject, (text_message, html_message), from_email, [dest.email], reply_to, headers)] - - messages = [] - for subject, message, from_email, dest_emails, reply_to, headers in mails: - text_message, html_message = message - msg = EmailMultiAlternatives(subject, text_message, from_email, dest_emails, reply_to=reply_to, - headers=headers) - msg.attach_alternative(html_message, 'text/html') - messages += [msg] - with mail.get_connection() as connection: - connection.send_messages(messages) diff --git a/conversations/views.py b/conversations/views.py deleted file mode 100644 index 476876c..0000000 --- a/conversations/views.py +++ /dev/null @@ -1,100 +0,0 @@ -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.contrib.auth.models import User -from django.contrib.sites.shortcuts import get_current_site -from django.core.exceptions import PermissionDenied -from django.core.urlresolvers import reverse -from django.shortcuts import get_object_or_404, redirect, render -from django.utils.translation import ugettext_lazy as _ - -from accounts.decorators import staff_required -from accounts.models import Participation -from proposals.models import Talk - -from .forms import MessageForm - - -@login_required -def user_conversation(request, username=None): - - if username: - p = Participation.objects.get(user=request.user, site=get_current_site(request)) - if not p.is_staff() and not p.is_orga(): - raise PermissionDenied() - user = get_object_or_404(User, username=username) - template = 'conversations/conversation.html' - else: - user = request.user - template = 'conversations/inbox.html' - - participation = get_object_or_404(Participation, user=user, site=get_current_site(request)) - conversation = participation.conversation - message_list = conversation.messages.all() - - form = MessageForm(request.POST or None) - - if request.method == 'POST' and form.is_valid(): - form.instance.conversation = conversation - form.instance.author = request.user - form.save() - messages.success(request, _('Message sent!')) - if username: - return redirect(reverse('user-conversation', args=[username])) - else: - return redirect('inbox') - - return render(request, template, { - 'correspondent': user, - 'message_list': message_list, - 'form': form, - }) - - -@login_required -def talk_conversation(request, talk): - - talk = get_object_or_404(Talk, slug=talk) - form = MessageForm(request.POST or None) - - if request.method == 'POST' and form.is_valid(): - form.instance.conversation = talk.conversation - form.instance.author = request.user - form.save() - messages.success(request, 'Message sent!') - - return redirect(talk.get_absolute_url()) - - -@staff_required -def correspondent_list(request): - - correspondent_list = Participation.objects.filter(site=get_current_site(request), - conversation__subscribers=request.user) - - return render(request, 'conversations/correspondent_list.html', { - 'correspondent_list': correspondent_list, - }) - - -@staff_required -def subscribe(request, username): - - participation = get_object_or_404(Participation, user__username=username, site=get_current_site(request)) - participation.conversation.subscribers.add(request.user) - messages.success(request, 'Subscribed.') - - next_url = request.GET.get('next') or reverse('user-conversation', args=[username]) - - return redirect(next_url) - - -@staff_required -def unsubscribe(request, username): - - participation = get_object_or_404(Participation, user__username=username, site=get_current_site(request)) - participation.conversation.subscribers.remove(request.user) - messages.success(request, 'Unsubscribed.') - - next_url = request.GET.get('next') or reverse('user-conversation', args=[username]) - - return redirect(next_url) diff --git a/mailing/tests.py b/mailing/tests.py new file mode 100644 index 0000000..5b67ee3 --- /dev/null +++ b/mailing/tests.py @@ -0,0 +1,93 @@ +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.core.urlresolvers import reverse +from django.test import TestCase, override_settings +from django.core import mail +from django.conf import settings + +from .models import Message, MessageThread, MessageCorrespondent + + +#class MailingTests(TestCase): +# def setUp(self): +# a, b, c, d = (User.objects.create_user(guy, email='%s@example.org' % guy, password=guy) for guy in 'abcd') +# d.is_superuser = True +# d.save() +# pa, _ = Participation.objects.get_or_create(user=a, site=Site.objects.first()) +# conversation, _ = ConversationWithParticipant.objects.get_or_create(participation=pa) +# Message.objects.create(content='allo', conversation=conversation, author=b) +# Message.objects.create(content='aluil', conversation=conversation, author=a) +# site = Site.objects.first() +# Talk.objects.get_or_create(site=site, proposer=a, title='a talk', description='yay', event=Event.objects.get(site=site, name='other')) +# +# def test_models(self): +# talk, participant, message = (model.objects.first() for model in +# (ConversationAboutTalk, ConversationWithParticipant, Message)) +# self.assertEqual(str(talk), 'Conversation about a talk') +# self.assertEqual(str(participant), 'Conversation with a') +# self.assertEqual(str(message), 'Message from b') +# self.assertEqual(message.get_absolute_url(), '/conversations/with/a/') +# self.assertEqual(talk.get_absolute_url(), '/talk/details/a-talk') +# +# def test_views(self): +# url = ConversationWithParticipant.objects.first().get_absolute_url() +# self.assertEqual(self.client.get(url).status_code, 302) +# self.client.login(username='c', password='c') +# self.assertEqual(self.client.get(url).status_code, 403) +# self.assertEqual(self.client.get(reverse('list-correspondents')).status_code, 403) # c is not staff +# self.assertEqual(self.client.get(reverse('inbox')).status_code, 200) +# self.client.post(reverse('inbox'), {'content': 'coucou'}) +# self.client.login(username='d', password='d') +# self.client.post(url, {'content': 'im superuser'}) +# self.assertEqual(Message.objects.last().content, 'im superuser') +# self.client.login(username='d', password='d') +# self.assertEqual(self.client.get(reverse('list-correspondents')).status_code, 200) +# +# +#@override_settings(DEFAULT_FROM_EMAIL='noreply@example.org', +# REPLY_EMAIL='reply@example.org', +# REPLY_KEY='secret') +#class EmailTests(TestCase): +# def setUp(self): +# for guy in 'abcd': +# setattr(self, guy, User.objects.create_user(guy, email='%s@example.org' % guy, password=guy)) +# a_p = Participation(user=self.a, site=Site.objects.first()) +# a_p.orga = True +# a_p.save() +# t = Topic(name='Topic 1', site=Site.objects.first()) +# t.save() +# t.reviewers.add(self.b) +# +# +# def test_talk_notification(self): +# self.client.login(username='c', password='c') +# # Check that login create participation +# self.assertTrue(Participation.objects.filter(user=self.c, site=Site.objects.first()).exists()) +# # Propose new talk +# topic = Topic.objects.get(name='Topic 1') +# response = self.client.post(reverse('add-talk'), { +# 'title': 'Talk 1', +# 'description': 'This is the first talk', +# 'topics': (topic.pk,), +# 'event': 1, +# 'speakers': (self.c.pk, self.d.pk), +# }, follow=True) +# self.assertEqual(response.status_code, 200) +# self.assertContains(response, 'Talk proposed') # check messages notification +# talk = Talk.objects.get(site=Site.objects.first(), title='Talk 1') +# conv = ConversationAboutTalk.objects.get(talk=talk) +# # Orga and reviewer should have been subscribed to the conversation about the talk +# self.assertEqual(set([self.a, self.b]), set(conv.subscribers.all())) +# # Both should have received an email notification +# self.assertEqual(len(mail.outbox), 2) +# for m in mail.outbox: +# self.assertEqual(m.from_email, '%s <%s>' % (self.c.profile, settings.DEFAULT_FROM_EMAIL)) +# self.assertTrue('Talk: %s' % talk.title) +# self.assertTrue(len(m.to), 1) +# self.assertTrue(m.to[0] in [ self.a.email, self.b.email ]) +# # Both should have been subscribed to conversations with each speakers +# for user in [self.c, self.d]: +# # Participation should have been created as the user is a speaker +# p = Participation.objects.get(user=user, site=Site.objects.first()) +# conv = ConversationWithParticipant.objects.get(participation=p) +# self.assertEqual(set([self.a, self.b]), set(conv.subscribers.all()))