removing old conversations app
This commit is contained in:
parent
2452a3497c
commit
af143a4fe3
|
@ -1 +0,0 @@
|
||||||
default_app_config = 'conversations.apps.ConversationsConfig'
|
|
|
@ -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)
|
|
|
@ -1,8 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationsConfig(AppConfig):
|
|
||||||
name = 'conversations'
|
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
import conversations.signals # noqa
|
|
|
@ -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<dest>[a-z0-9]{12})(?P<token>[a-z0-9]{60})(?P<key>[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()
|
|
|
@ -1,5 +0,0 @@
|
||||||
from django.forms.models import modelform_factory
|
|
||||||
|
|
||||||
from .models import Message
|
|
||||||
|
|
||||||
MessageForm = modelform_factory(Message, fields=['content'])
|
|
|
@ -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'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -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)
|
|
|
@ -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"
|
|
|
@ -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),
|
|
||||||
}
|
|
||||||
)
|
|
|
@ -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)
|
|
|
@ -1,8 +0,0 @@
|
||||||
<div class="panel panel-{% if message.author == message.conversation.participation.user %}info{% else %}default{% endif %}">
|
|
||||||
<div class="panel-heading">
|
|
||||||
{{ message.created }} | <a href="{% url 'show-participant' message.author.username %}">{{ message.author.profile }}</a>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
{{ message.content|linebreaksbr }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,15 +0,0 @@
|
||||||
{% load i18n %}
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
{% trans "Send a message" %}
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<form action="{{ form_url }}" method="post" role="form">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="form-group">
|
|
||||||
<textarea class="form-control" name="content" rows="6" required></textarea>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-envelope"></span> {% trans "Send" %}</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,17 +0,0 @@
|
||||||
{% extends 'staff.html' %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block correspondentstab %} class="active"{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<h1>{% blocktrans with correspondent=correspondent.profile %}Conversation with {{ correspondent }}{% endblocktrans %}</h1>
|
|
||||||
|
|
||||||
{% for message in message_list %}
|
|
||||||
{% include 'conversations/_message_detail.html' %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% include 'conversations/_message_form.html' %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,36 +0,0 @@
|
||||||
{% extends 'staff.html' %}
|
|
||||||
|
|
||||||
{% load bootstrap3 i18n %}
|
|
||||||
|
|
||||||
{% block correspondentstab %} class="active"{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<h1>{% trans "Correspondents" %}</h1>
|
|
||||||
<p>{% trans "This is the list of participants that you follow." %}</p>
|
|
||||||
|
|
||||||
<table class="table table-striped">
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Username</th>
|
|
||||||
<th>Full name</th>
|
|
||||||
<th>Administration</th>
|
|
||||||
</tr>
|
|
||||||
{% for correspondent in correspondent_list %}
|
|
||||||
<tr>
|
|
||||||
<th>{{ forloop.counter }}</th>
|
|
||||||
<td>{{ correspondent.user.username }}</td>
|
|
||||||
<td>{{ correspondent.user.get_full_name }}</td>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'user-conversation' correspondent.user.username %}"><span class="glyphicon glyphicon-envelope"></span></a>
|
|
||||||
{% if request.user in correspondent.conversation.subscribers.all %}
|
|
||||||
<a href="{% url 'unsubscribe-conversation' correspondent.user.username %}?next={% url 'list-correspondents' %}" data-toggle="tooltip" data-placement="bottom" title="{% trans "Unsubscribe from the conversation" %}"><span class="glyphicon glyphicon-star"></span></a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'subscribe-conversation' correspondent.user.username %}?next={% url 'list-correspondents' %}" data-toggle="tooltip" data-placement="bottom" title="{% trans "Subscribe to the conversation" %}"><span class="glyphicon glyphicon-star-empty"></span></a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{{ content|linebreaksbr }}
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
{% if answering %}
|
|
||||||
Reply to this email directly or <a href="https://{{ uri }}">view it online</a>.
|
|
||||||
{% else %}
|
|
||||||
<a href="https://{{ uri }}">Reply online</a>.
|
|
||||||
{% endif %}
|
|
|
@ -1,4 +0,0 @@
|
||||||
{{ content|safe }}
|
|
||||||
|
|
||||||
--
|
|
||||||
Reply {% if answering %}to this email directly or view it {% endif %}online: https://{{ uri }}
|
|
|
@ -1,11 +0,0 @@
|
||||||
Hi!<br />
|
|
||||||
<br />
|
|
||||||
A <a href="https://{{ uri }}">new {{ talk.event }}</a> has been proposed by <a href="https://{{ proposer_uri }}">{{ proposer.profile }}</a>!<br />
|
|
||||||
<br />
|
|
||||||
Title: {{ talk.title }}<br />
|
|
||||||
<br />
|
|
||||||
Description:<br />
|
|
||||||
<p>{{ talk.description|linebreaksbr }}</p>
|
|
||||||
{% if answering %}
|
|
||||||
<hr>
|
|
||||||
Reply to this email directly to comment this talk.{% endif %}
|
|
|
@ -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 %}
|
|
|
@ -1,18 +0,0 @@
|
||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block inboxtab %} class="active"{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<h1>{% trans "Messaging" %}</h1>
|
|
||||||
<p>{% trans "You can use this page to communicate with the staff." %}</p>
|
|
||||||
|
|
||||||
{% for message in message_list %}
|
|
||||||
{% include 'conversations/_message_detail.html' %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% include 'conversations/_message_form.html' %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -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()))
|
|
|
@ -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<username>[\w.@+-]+)/$', views.user_conversation, name='user-conversation'),
|
|
||||||
url(r'^about/(?P<talk>[\w.@+-]+)/$', views.talk_conversation, name='talk-conversation'),
|
|
||||||
url(r'^subscribe/(?P<username>[\w.@+-]+)/$', views.subscribe, name='subscribe-conversation'),
|
|
||||||
url(r'^unsubscribe/(?P<username>[\w.@+-]+)/$', views.unsubscribe, name='unsubscribe-conversation'),
|
|
||||||
]
|
|
|
@ -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)
|
|
|
@ -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)
|
|
|
@ -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()))
|
Loading…
Reference in New Issue