mailing with participants & about talks
This commit is contained in:
parent
54e4205ea7
commit
b1667592ba
|
@ -81,7 +81,7 @@ class UsersWidget(ModelSelect2MultipleWidget):
|
|||
class ConferenceForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Conference
|
||||
fields = ['name', 'home', 'venue', 'city', 'contact_email', 'staff',]
|
||||
fields = ['name', 'home', 'venue', 'city', 'contact_email', 'reply_email', 'staff',]
|
||||
widgets = {
|
||||
'staff': UsersWidget(),
|
||||
}
|
||||
|
|
46
cfp/migrations/0003_auto_20170801_1400.py
Normal file
46
cfp/migrations/0003_auto_20170801_1400.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.3 on 2017-08-01 14:00
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cfp', '0002_conference_staff'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='conference',
|
||||
name='city',
|
||||
field=models.CharField(blank=True, default='', max_length=64, verbose_name='City'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='conference',
|
||||
name='contact_email',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='Contact email'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='conference',
|
||||
name='home',
|
||||
field=models.TextField(blank=True, default='', verbose_name='Homepage (markdown)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='conference',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='Conference name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='conference',
|
||||
name='staff',
|
||||
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Staff members'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='conference',
|
||||
name='venue',
|
||||
field=models.TextField(blank=True, default='', verbose_name='Venue information'),
|
||||
),
|
||||
]
|
59
cfp/migrations/0004_auto_20170801_1408.py
Normal file
59
cfp/migrations/0004_auto_20170801_1408.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.3 on 2017-08-01 14:08
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def generate_participant_conversation(apps, schema_editor):
|
||||
MessageThread = apps.get_model("mailing", "MessageThread")
|
||||
Participant = apps.get_model("cfp", "Participant")
|
||||
db_alias = schema_editor.connection.alias
|
||||
for participant in Participant.objects.using(db_alias).filter(conversation=None):
|
||||
participant.conversation = MessageThread.objects.create()
|
||||
participant.save()
|
||||
|
||||
|
||||
def generate_talk_conversation(apps, schema_editor):
|
||||
MessageThread = apps.get_model("mailing", "MessageThread")
|
||||
Talk = apps.get_model("cfp", "Talk")
|
||||
db_alias = schema_editor.connection.alias
|
||||
for talk in Talk.objects.using(db_alias).filter(conversation=None):
|
||||
talk.conversation = MessageThread.objects.create()
|
||||
talk.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mailing', '0001_initial'),
|
||||
('cfp', '0003_auto_20170801_1400'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='participant',
|
||||
name='conversation',
|
||||
field=models.OneToOneField(null=True, default=None, on_delete=django.db.models.deletion.CASCADE, to='mailing.MessageThread'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.RunPython(generate_participant_conversation),
|
||||
migrations.AlterField(
|
||||
model_name='participant',
|
||||
name='conversation',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='mailing.MessageThread'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='talk',
|
||||
name='conversation',
|
||||
field=models.OneToOneField(null=True, default=None, on_delete=django.db.models.deletion.CASCADE, to='mailing.MessageThread'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.RunPython(generate_talk_conversation),
|
||||
migrations.AlterField(
|
||||
model_name='talk',
|
||||
name='conversation',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='mailing.MessageThread'),
|
||||
),
|
||||
]
|
20
cfp/migrations/0005_conference_reply_email.py
Normal file
20
cfp/migrations/0005_conference_reply_email.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.3 on 2017-08-01 16:40
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cfp', '0004_auto_20170801_1408'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='conference',
|
||||
name='reply_email',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='Reply email'),
|
||||
),
|
||||
]
|
|
@ -1,32 +1,21 @@
|
|||
|
||||
import uuid
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Q, Avg, Case, When
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext
|
||||
from django.utils import timezone
|
||||
|
||||
from ponyconf.utils import PonyConfModel
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
|
||||
from autoslug import AutoSlugField
|
||||
from colorful.fields import RGBColorField
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
#from ponyconf.utils import PonyConfModel, enum_to_choices
|
||||
from ponyconf.utils import PonyConfModel
|
||||
from mailing.models import MessageThread
|
||||
|
||||
|
||||
|
||||
|
@ -38,6 +27,7 @@ class Conference(models.Model):
|
|||
venue = models.TextField(blank=True, default="", verbose_name=_('Venue information'))
|
||||
city = models.CharField(max_length=64, blank=True, default="", verbose_name=_('City'))
|
||||
contact_email = models.CharField(max_length=100, blank=True, verbose_name=_('Contact email'))
|
||||
reply_email = models.CharField(max_length=100, blank=True, verbose_name=_('Reply email'))
|
||||
staff = models.ManyToManyField(User, blank=True, verbose_name=_('Staff members'))
|
||||
|
||||
custom_css = models.TextField(blank=True)
|
||||
|
@ -59,6 +49,16 @@ class Conference(models.Model):
|
|||
def from_email(self):
|
||||
return self.name+' <'+self.contact_email+'>'
|
||||
|
||||
def clean_fields(self, exclude=None):
|
||||
super().clean_fields(exclude)
|
||||
if self.reply_email is not None:
|
||||
try:
|
||||
self.reply_email.format(token='a' * 80)
|
||||
except Exception:
|
||||
raise ValidationError({
|
||||
'reply_email': _('The reply email should be a formatable string accepting a token argument (e.g. ponyconf+{token}@exemple.com).'),
|
||||
})
|
||||
|
||||
def __str__(self):
|
||||
return str(self.site)
|
||||
|
||||
|
@ -88,6 +88,8 @@ class Participant(PonyConfModel):
|
|||
|
||||
vip = models.BooleanField(default=False)
|
||||
|
||||
conversation = models.OneToOneField(MessageThread)
|
||||
|
||||
class Meta:
|
||||
# A User can participe only once to a Conference (= Site)
|
||||
unique_together = ('site', 'email')
|
||||
|
@ -254,6 +256,8 @@ class Talk(PonyConfModel):
|
|||
|
||||
token = models.UUIDField(default=uuid.uuid4, editable=False)
|
||||
|
||||
conversation = models.OneToOneField(MessageThread)
|
||||
|
||||
|
||||
objects = TalkManager()
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
from django.db.models.signals import post_save
|
||||
from django.db.models.signals import pre_save, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.sites.models import Site
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ponyconf.decorators import disable_for_loaddata
|
||||
from .models import Conference
|
||||
from mailing.models import MessageThread, Message
|
||||
from .models import Participant, Talk, Conference
|
||||
|
||||
|
||||
@receiver(post_save, sender=Site, dispatch_uid="Create Conference for Site")
|
||||
|
@ -13,6 +15,54 @@ def create_conference(sender, instance, **kwargs):
|
|||
conference, created = Conference.objects.get_or_create(site=instance)
|
||||
|
||||
|
||||
def create_conversation(sender, instance, **kwargs):
|
||||
if not hasattr(instance, 'conversation'):
|
||||
instance.conversation = MessageThread.objects.create()
|
||||
pre_save.connect(create_conversation, sender=Participant)
|
||||
pre_save.connect(create_conversation, sender=Talk)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Message, dispatch_uid="Send message notifications")
|
||||
def send_message_notification(sender, instance, **kwargs):
|
||||
message = instance
|
||||
thread = message.thread
|
||||
first_message = thread.message_set.first()
|
||||
if message == first_message:
|
||||
reference = None
|
||||
else:
|
||||
reference = first_message.token
|
||||
subject_prefix = 'Re: ' if reference else ''
|
||||
if hasattr(thread, 'participant'):
|
||||
conf = thread.participant.site.conference
|
||||
elif hasattr(thread, 'talk'):
|
||||
conf = thread.talk.site.conference
|
||||
message_id = '<{id}@%s>' % conf.site.domain
|
||||
if conf.reply_email:
|
||||
reply_to = (conf.name, conf.reply_email)
|
||||
else:
|
||||
reply_to = None
|
||||
sender = (conf.name, conf.contact_email)
|
||||
staff_dests = [ (user.get_full_name(), user.email) for user in conf.staff.all() ]
|
||||
if hasattr(thread, 'participant'):
|
||||
conf = thread.participant.site.conference
|
||||
participant = thread.participant
|
||||
participant_dests = [ (participant.name, participant.email) ]
|
||||
participant_subject = _('[%(prefix)s] Message from the staff') % {'prefix': conf.name}
|
||||
staff_subject = _('[%(prefix)s] Conversation with %(dest)s') % {'prefix': conf.name, 'dest': participant.name}
|
||||
if message.author == conf.contact_email: # this is a talk notification message
|
||||
# sent it only the participant
|
||||
message.send_notification(subject=participant_subject, sender=sender, dests=participant_dests, reply_to=reply_to, message_id=message_id, reference=reference)
|
||||
else:
|
||||
# this is a message between the staff and the participant
|
||||
message.send_notification(subject=staff_subject, sender=sender, dests=staff_dests, reply_to=reply_to, message_id=message_id, reference=reference)
|
||||
if message.author != thread.participant.email: # message from staff: sent it to the participant too
|
||||
message.send_notification(subject=participant_subject, sender=sender, dests=participant_dests, reply_to=reply_to, message_id=message_id, reference=reference)
|
||||
elif hasattr(thread, 'talk'):
|
||||
conf = thread.talk.site.conference
|
||||
subject = _('[%(prefix)s] Talk: %(talk)s') % {'prefix': conf.name, 'talk': thread.talk.title}
|
||||
message.send_notification(subject=subject, sender=sender, dests=staff_dests, reply_to=reply_to, message_id=message_id, reference=reference)
|
||||
|
||||
|
||||
# connected in apps.py
|
||||
def call_first_site_post_save(apps, **kwargs):
|
||||
try:
|
||||
|
|
|
@ -50,4 +50,11 @@
|
|||
{% empty %}{% trans "No talks" %}
|
||||
{% endfor %}
|
||||
|
||||
<h2>{% trans "Messaging" %}</h2>
|
||||
|
||||
{% include 'mailing/_message_list.html' with messages=participant.conversation.message_set.all %}
|
||||
|
||||
{% trans "Send a message – <em>this message will be received by this participant and all the staff team</em>" as message_form_title %}
|
||||
{% include 'mailing/_message_form.html' %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -134,14 +134,11 @@
|
|||
{% endif %}
|
||||
{% endcomment %}
|
||||
|
||||
{% comment %}
|
||||
<h3>{% trans "Messages" %}</h3>
|
||||
{% trans "These messages are for organization team only." %}<br /><br />
|
||||
{% for message in talk.conversation.messages.all %}
|
||||
{% include 'conversations/_message_detail.html' %}
|
||||
{% endfor %}
|
||||
<h3>{% trans "Messaging" %}</h3>
|
||||
|
||||
{% include 'conversations/_message_form.html' %}
|
||||
{% endcomment %}
|
||||
{% include 'mailing/_message_list.html' with messages=talk.conversation.message_set.all %}
|
||||
|
||||
{% trans "Send a message – <em>this message will be received by the staff team only</em>" as message_form_title %}
|
||||
{% include 'mailing/_message_form.html' %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
42
cfp/views.py
42
cfp/views.py
|
@ -11,7 +11,9 @@ from django_select2.views import AutoResponseView
|
|||
|
||||
from functools import reduce
|
||||
|
||||
from cfp.decorators import staff_required
|
||||
from mailing.models import Message
|
||||
from mailing.forms import MessageForm
|
||||
from .decorators import staff_required
|
||||
from .mixins import StaffRequiredMixin
|
||||
from .utils import is_staff
|
||||
from .models import Participant, Talk, TalkCategory, Vote
|
||||
|
@ -62,8 +64,7 @@ def talk_proposal(request, conference, talk_id=None, participant_id=None):
|
|||
url_talk_proposal_edit = base_url + reverse('talk-proposal-edit', args=[talk.token, participant.token])
|
||||
url_talk_proposal_speaker_add = base_url + reverse('talk-proposal-speaker-add', args=[talk.token])
|
||||
url_talk_proposal_speaker_edit = base_url + reverse('talk-proposal-speaker-edit', args=[talk.token, participant.token])
|
||||
msg_title = _('Your talk "{}" has been submitted for {}').format(talk.title, conference.name)
|
||||
msg_body = _("""Hi {},
|
||||
body = _("""Hi {},
|
||||
|
||||
Your talk has been submitted for {}.
|
||||
|
||||
|
@ -84,12 +85,10 @@ Thanks!
|
|||
|
||||
""").format(participant.name, conference.name, talk.title, talk.description, url_talk_proposal_edit, url_talk_proposal_speaker_add, url_talk_proposal_speaker_edit, conference.name)
|
||||
|
||||
send_mail(
|
||||
msg_title,
|
||||
msg_body,
|
||||
conference.from_email(),
|
||||
[participant.email],
|
||||
fail_silently=False,
|
||||
Message.objects.create(
|
||||
thread=participant.conversation,
|
||||
author=conference.contact_email,
|
||||
content=body,
|
||||
)
|
||||
|
||||
return render(request, 'cfp/complete.html', {'talk': talk, 'participant': participant})
|
||||
|
@ -163,9 +162,9 @@ def talk_list(request, conference):
|
|||
talks = talks.exclude(vote__user=request.user)
|
||||
# Sorting
|
||||
if request.GET.get('order') == 'desc':
|
||||
reverse = True
|
||||
sort_reverse = True
|
||||
else:
|
||||
reverse = False
|
||||
sort_reverse = False
|
||||
SORT_MAPPING = {
|
||||
'title': 'title',
|
||||
'category': 'category',
|
||||
|
@ -173,7 +172,7 @@ def talk_list(request, conference):
|
|||
}
|
||||
sort = request.GET.get('sort')
|
||||
if sort in SORT_MAPPING.keys():
|
||||
if reverse:
|
||||
if sort_reverse:
|
||||
talks = talks.order_by('-' + SORT_MAPPING[sort])
|
||||
else:
|
||||
talks = talks.order_by(SORT_MAPPING[sort])
|
||||
|
@ -184,7 +183,7 @@ def talk_list(request, conference):
|
|||
url = request.GET.copy()
|
||||
url['sort'] = c
|
||||
if c == sort:
|
||||
if reverse:
|
||||
if sort_reverse:
|
||||
del url['order']
|
||||
glyphicon = 'sort-by-attributes-alt'
|
||||
else:
|
||||
|
@ -207,6 +206,14 @@ def talk_list(request, conference):
|
|||
@staff_required
|
||||
def talk_details(request, conference, talk_id):
|
||||
talk = get_object_or_404(Talk, token=talk_id, site=conference.site)
|
||||
message_form = MessageForm(request.POST or None)
|
||||
if request.method == 'POST' and message_form.is_valid():
|
||||
message = message_form.save(commit=False)
|
||||
message.author = request.user.email
|
||||
message.thread = talk.conversation
|
||||
message.save()
|
||||
messages.success(request, _('Message sent!'))
|
||||
return redirect(reverse('talk-details', args=[talk.token]))
|
||||
return render(request, 'cfp/staff/talk_details.html', {
|
||||
'talk': talk,
|
||||
})
|
||||
|
@ -260,6 +267,14 @@ def participant_list(request, conference):
|
|||
@staff_required
|
||||
def participant_details(request, conference, participant_id):
|
||||
participant = get_object_or_404(Participant, token=participant_id, site=conference.site)
|
||||
message_form = MessageForm(request.POST or None)
|
||||
if request.method == 'POST' and message_form.is_valid():
|
||||
message = message_form.save(commit=False)
|
||||
message.author = request.user.email
|
||||
message.thread = participant.conversation
|
||||
message.save()
|
||||
messages.success(request, _('Message sent!'))
|
||||
return redirect(reverse('participant-details', args=[participant.token]))
|
||||
return render(request, 'cfp/staff/participant_details.html', {
|
||||
'participant': participant,
|
||||
})
|
||||
|
@ -290,6 +305,7 @@ You can now:
|
|||
{}
|
||||
|
||||
""")
|
||||
# TODO: send bulk emails
|
||||
for user in added_staff:
|
||||
msg_body = msg_body_template.format(user.get_full_name(), url_login, url_password_reset, conference.name)
|
||||
send_mail(
|
||||
|
|
Binary file not shown.
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-08-01 12:23+0000\n"
|
||||
"POT-Creation-Date: 2017-08-02 00:42+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -313,7 +313,7 @@ msgstr "Catégorie"
|
|||
msgid "Status"
|
||||
msgstr "Statut"
|
||||
|
||||
#: cfp/forms.py:54 cfp/models.py:234
|
||||
#: cfp/forms.py:54 cfp/models.py:243
|
||||
#: cfp/templates/cfp/staff/talk_details.html:89
|
||||
#: cfp/templates/cfp/staff/talk_list.html:41 proposals/models.py:160
|
||||
#: proposals/templates/proposals/talk_detail.html:33
|
||||
|
@ -352,30 +352,41 @@ msgstr "Un utilisateur avec ce prénom et ce nom existe déjà."
|
|||
msgid "A user with that email already exists."
|
||||
msgstr "Un utilisateur avec cet email existe déjà."
|
||||
|
||||
#: cfp/models.py:36
|
||||
#: cfp/models.py:25
|
||||
msgid "Conference name"
|
||||
msgstr "Nom de la conférence"
|
||||
|
||||
#: cfp/models.py:37
|
||||
#: cfp/models.py:26
|
||||
msgid "Homepage (markdown)"
|
||||
msgstr "Page d’accueil (markdown)"
|
||||
|
||||
#: cfp/models.py:38
|
||||
#: cfp/models.py:27
|
||||
msgid "Venue information"
|
||||
msgstr "Informations sur le lieu"
|
||||
|
||||
#: cfp/models.py:39
|
||||
#: cfp/models.py:28
|
||||
msgid "City"
|
||||
msgstr "Ville"
|
||||
|
||||
#: cfp/models.py:40
|
||||
#: cfp/models.py:29
|
||||
msgid "Contact email"
|
||||
msgstr "Email de contact"
|
||||
|
||||
#: cfp/models.py:41
|
||||
#: cfp/models.py:30
|
||||
msgid "Reply email"
|
||||
msgstr ""
|
||||
|
||||
#: cfp/models.py:31
|
||||
msgid "Staff members"
|
||||
msgstr "Membres du staff"
|
||||
|
||||
#: cfp/models.py:59
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"The reply email should be a formatable string accepting a token argument (e."
|
||||
"g. ponyconf+{token}@exemple.com)."
|
||||
msgstr ""
|
||||
|
||||
#: cfp/models.py:70
|
||||
msgid "Your Name"
|
||||
msgstr "Votre Nom"
|
||||
|
@ -384,31 +395,31 @@ msgstr "Votre Nom"
|
|||
msgid "This field is only visible by organizers."
|
||||
msgstr "Ce champs est uniquement visible par les organisateurs."
|
||||
|
||||
#: cfp/models.py:137 cfp/templates/cfp/staff/participant_list.html:49
|
||||
#: cfp/models.py:139 cfp/templates/cfp/staff/participant_list.html:49
|
||||
#: proposals/models.py:52 proposals/models.py:75 proposals/models.py:132
|
||||
#: volunteers/models.py:12
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: cfp/models.py:139 cfp/templates/cfp/staff/talk_details.html:75
|
||||
#: cfp/models.py:141 cfp/templates/cfp/staff/talk_details.html:75
|
||||
#: proposals/models.py:54 proposals/models.py:77 proposals/models.py:158
|
||||
#: proposals/templates/proposals/talk_detail.html:72 volunteers/models.py:14
|
||||
msgid "Description"
|
||||
msgstr "Description"
|
||||
|
||||
#: cfp/models.py:160 proposals/models.py:96
|
||||
#: cfp/models.py:162 proposals/models.py:96
|
||||
msgid "Default duration (min)"
|
||||
msgstr "Durée par défaut (min)"
|
||||
|
||||
#: cfp/models.py:161 proposals/models.py:97
|
||||
#: cfp/models.py:163 proposals/models.py:97
|
||||
msgid "Color on program"
|
||||
msgstr "Couleur sur le programme"
|
||||
|
||||
#: cfp/models.py:162 proposals/models.py:98
|
||||
#: cfp/models.py:164 proposals/models.py:98
|
||||
msgid "Label on program"
|
||||
msgstr "Label dans le xml du programme"
|
||||
|
||||
#: cfp/models.py:229 cfp/templates/cfp/staff/base.html:20
|
||||
#: cfp/models.py:238 cfp/templates/cfp/staff/base.html:20
|
||||
#: cfp/templates/cfp/staff/participant_list.html:8
|
||||
#: cfp/templates/cfp/staff/talk_details.html:79
|
||||
#: cfp/templates/cfp/staff/talk_list.html:40 proposals/models.py:154
|
||||
|
@ -418,19 +429,19 @@ msgstr "Label dans le xml du programme"
|
|||
msgid "Speakers"
|
||||
msgstr "Orateurs"
|
||||
|
||||
#: cfp/models.py:230
|
||||
#: cfp/models.py:239
|
||||
msgid "Talk Title"
|
||||
msgstr "Titre de votre proposition:"
|
||||
|
||||
#: cfp/models.py:233
|
||||
#: cfp/models.py:242
|
||||
msgid "Description of your talk"
|
||||
msgstr "Description de votre proposition"
|
||||
|
||||
#: cfp/models.py:235
|
||||
#: cfp/models.py:244
|
||||
msgid "Message to organizers"
|
||||
msgstr "Message aux organisateurs"
|
||||
|
||||
#: cfp/models.py:235
|
||||
#: cfp/models.py:244
|
||||
msgid ""
|
||||
"If you have any constraint or if you have anything that may help you to "
|
||||
"select your talk, like a video or slides of your talk, please write it down "
|
||||
|
@ -440,26 +451,40 @@ msgstr ""
|
|||
"votre proposition, comme une vidéo, des slides, n'hésitez pas à les ajouter "
|
||||
"ici."
|
||||
|
||||
#: cfp/models.py:236
|
||||
#: cfp/models.py:245
|
||||
msgid "Talk Category"
|
||||
msgstr "Catégorie de proposition"
|
||||
|
||||
#: cfp/models.py:237
|
||||
#: cfp/models.py:246
|
||||
msgid "I'm ok to be recorded on video"
|
||||
msgstr "J’accepte d’être enregistré en vidéo"
|
||||
|
||||
#: cfp/models.py:238
|
||||
#: cfp/models.py:247
|
||||
msgid "Video licence"
|
||||
msgstr "Licence vidéo"
|
||||
|
||||
#: cfp/models.py:239
|
||||
#: cfp/models.py:248
|
||||
msgid "I need sound"
|
||||
msgstr "J’ai besoin de son"
|
||||
|
||||
#: cfp/models.py:242 proposals/models.py:165
|
||||
#: cfp/models.py:251 proposals/models.py:165
|
||||
msgid "Duration (min)"
|
||||
msgstr "Durée (min)"
|
||||
|
||||
#: cfp/signals.py:50
|
||||
#, python-format
|
||||
msgid "[%(prefix)s] Message from the staff"
|
||||
msgstr "[%(prefix)s] Message du staff"
|
||||
|
||||
#: cfp/signals.py:51
|
||||
msgid "[%(prefix)s] Conversation with %(dest)s"
|
||||
msgstr "[%(prefix)s] Conversation avec %(dest)s"
|
||||
|
||||
#: cfp/signals.py:62
|
||||
#, python-format
|
||||
msgid "[%(prefix)s] Talk: %(talk)s"
|
||||
msgstr "[%(prefix)s] Talk: %(talk)s"
|
||||
|
||||
#: cfp/templates/cfp/closed.html:9 cfp/templates/cfp/propose.html:11
|
||||
#: cfp/templates/cfp/speaker.html:11
|
||||
#: proposals/templates/proposals/participate.html:9
|
||||
|
@ -581,6 +606,20 @@ msgstr "dans la session"
|
|||
msgid "No talks"
|
||||
msgstr "Aucun exposé"
|
||||
|
||||
#: cfp/templates/cfp/staff/participant_details.html:53
|
||||
#: cfp/templates/cfp/staff/talk_details.html:137
|
||||
#: conversations/templates/conversations/inbox.html:9
|
||||
msgid "Messaging"
|
||||
msgstr "Messagerie"
|
||||
|
||||
#: cfp/templates/cfp/staff/participant_details.html:57
|
||||
msgid ""
|
||||
"Send a message – <em>this message will be received by this participant and "
|
||||
"all the staff team</em>"
|
||||
msgstr ""
|
||||
"Envoyer un message – <em>ce message sera reçu par le participant et l’équipe "
|
||||
"d’organisation</em>"
|
||||
|
||||
#: cfp/templates/cfp/staff/participant_list.html:45
|
||||
#: cfp/templates/cfp/staff/talk_list.html:33
|
||||
#: proposals/templates/proposals/speaker_list.html:44
|
||||
|
@ -695,6 +734,14 @@ msgstr "vote"
|
|||
msgid "average:"
|
||||
msgstr "moyenne :"
|
||||
|
||||
#: cfp/templates/cfp/staff/talk_details.html:141
|
||||
msgid ""
|
||||
"Send a message – <em>this message will be received by the staff team only</"
|
||||
"em>"
|
||||
msgstr ""
|
||||
"Envoyer un message – <em>ce message sera reçu uniquement par l’équipe "
|
||||
"d’organisation</em>"
|
||||
|
||||
#: cfp/templates/cfp/staff/talk_list.html:10
|
||||
#: proposals/templates/proposals/speaker_list.html:11
|
||||
#: proposals/templates/proposals/talk_list.html:11
|
||||
|
@ -733,11 +780,7 @@ msgstr "Type d’intervention"
|
|||
msgid "Pending, score: %(score)s"
|
||||
msgstr "En cours, score : %(score)s"
|
||||
|
||||
#: cfp/views.py:65
|
||||
msgid "Your talk \"{}\" has been submitted for {}"
|
||||
msgstr "Votre proposition \"{}\" a été transmise à {}"
|
||||
|
||||
#: cfp/views.py:66
|
||||
#: cfp/views.py:67
|
||||
msgid ""
|
||||
"Hi {},\n"
|
||||
"\n"
|
||||
|
@ -777,23 +820,27 @@ msgstr ""
|
|||
"{}\n"
|
||||
"\n"
|
||||
|
||||
#: cfp/views.py:220 proposals/views.py:321
|
||||
#: cfp/views.py:215 cfp/views.py:276 conversations/views.py:40
|
||||
msgid "Message sent!"
|
||||
msgstr "Message envoyé !"
|
||||
|
||||
#: cfp/views.py:228 proposals/views.py:321
|
||||
msgid "Vote successfully created"
|
||||
msgstr "A voté !"
|
||||
|
||||
#: cfp/views.py:220 proposals/views.py:321
|
||||
#: cfp/views.py:228 proposals/views.py:321
|
||||
msgid "Vote successfully updated"
|
||||
msgstr "Vote mis à jour"
|
||||
|
||||
#: cfp/views.py:243 proposals/views.py:347
|
||||
#: cfp/views.py:251 proposals/views.py:347
|
||||
msgid "Decision taken in account"
|
||||
msgstr "Décision enregistrée"
|
||||
|
||||
#: cfp/views.py:280
|
||||
#: cfp/views.py:296
|
||||
msgid "[{}] You have been added to the staff team"
|
||||
msgstr "[{}] Vous avez été ajouté aux membres du staff"
|
||||
|
||||
#: cfp/views.py:281
|
||||
#: cfp/views.py:297
|
||||
msgid ""
|
||||
"Hi {},\n"
|
||||
"\n"
|
||||
|
@ -817,11 +864,11 @@ msgstr ""
|
|||
"{}\n"
|
||||
"\n"
|
||||
|
||||
#: cfp/views.py:301
|
||||
#: cfp/views.py:318
|
||||
msgid "Modifications successfully saved."
|
||||
msgstr "Modification enregistrée avec succès."
|
||||
|
||||
#: cfp/views.py:315
|
||||
#: cfp/views.py:332
|
||||
msgid "User created successfully."
|
||||
msgstr "Utilisateur créé avec succès."
|
||||
|
||||
|
@ -830,6 +877,7 @@ msgid "Send a message"
|
|||
msgstr "Envoyer un message"
|
||||
|
||||
#: conversations/templates/conversations/_message_form.html:12
|
||||
#: mailing/templates/mailing/_message_form.html:13
|
||||
msgid "Send"
|
||||
msgstr "Envoyer"
|
||||
|
||||
|
@ -846,18 +894,14 @@ msgstr "Correspondants"
|
|||
msgid "This is the list of participants that you follow."
|
||||
msgstr "Ceci est la liste des participants que vous suivez."
|
||||
|
||||
#: conversations/templates/conversations/inbox.html:9
|
||||
msgid "Messaging"
|
||||
msgstr "Messagerie"
|
||||
|
||||
#: conversations/templates/conversations/inbox.html:10
|
||||
msgid "You can use this page to communicate with the staff."
|
||||
msgstr ""
|
||||
"Vous pouvez utiliser cette page pour communiquer avec l’équipe organisatrice."
|
||||
|
||||
#: conversations/views.py:40
|
||||
msgid "Message sent!"
|
||||
msgstr "Message envoyé !"
|
||||
#: mailing/templates/mailing/_message_list.html:13
|
||||
msgid "No messages."
|
||||
msgstr "Aucun message."
|
||||
|
||||
#: planning/templates/planning/public-program.html:8
|
||||
#: planning/templates/planning/schedule.html:9
|
||||
|
@ -1348,3 +1392,6 @@ msgstr "Bénévoles"
|
|||
#: volunteers/templates/volunteers/volunteer_list.html:25
|
||||
msgid "volunteer"
|
||||
msgstr "bénévole"
|
||||
|
||||
#~ msgid "Your talk \"{}\" has been submitted for {}"
|
||||
#~ msgstr "Votre proposition \"{}\" a été transmise à {}"
|
||||
|
|
0
mailing/__init__.py
Normal file
0
mailing/__init__.py
Normal file
6
mailing/forms.py
Normal file
6
mailing/forms.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.forms.models import modelform_factory
|
||||
|
||||
from .models import Message
|
||||
|
||||
|
||||
MessageForm = modelform_factory(Message, fields=['content'])
|
0
mailing/management/__init__.py
Normal file
0
mailing/management/__init__.py
Normal file
0
mailing/management/commands/__init__.py
Normal file
0
mailing/management/commands/__init__.py
Normal file
34
mailing/management/commands/fetchmail.py
Normal file
34
mailing/management/commands/fetchmail.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
from mailing.utils import fetch_imap_box
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Fetch emails from IMAP inbox'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--host', required=True)
|
||||
parser.add_argument('--port', type=int)
|
||||
parser.add_argument('--user', required=True)
|
||||
parser.add_argument('--password', required=True)
|
||||
parser.add_argument('--inbox')
|
||||
grp = parser.add_mutually_exclusive_group()
|
||||
grp.add_argument('--trash')
|
||||
grp.add_argument('--no-trash', action='store_true')
|
||||
|
||||
|
||||
def handle(self, *args, **options):
|
||||
params = {
|
||||
'host': options['host'],
|
||||
'user': options['user'],
|
||||
'password': options['password'],
|
||||
}
|
||||
if options['port']:
|
||||
params['port'] = options['port']
|
||||
if options['inbox']:
|
||||
params['inbox'] = options['inbox']
|
||||
if options['trash']:
|
||||
params['trash'] = options['trash']
|
||||
elif options['no_trash']:
|
||||
params['trash'] = None
|
||||
fetch_imap_box(**params)
|
52
mailing/migrations/0001_initial.py
Normal file
52
mailing/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.3 on 2017-08-01 15:48
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import mailing.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
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)),
|
||||
('author', models.EmailField(blank=True, max_length=254)),
|
||||
('content', models.TextField(blank=True)),
|
||||
('token', models.CharField(default=mailing.models.generate_message_token, max_length=64, unique=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['created'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MessageCorrespondent',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('token', models.CharField(default=mailing.models.generate_message_token, max_length=64, unique=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MessageThread',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('token', models.CharField(default=mailing.models.generate_message_token, max_length=64, unique=True)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='message',
|
||||
name='thread',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mailing.MessageThread'),
|
||||
),
|
||||
]
|
0
mailing/migrations/__init__.py
Normal file
0
mailing/migrations/__init__.py
Normal file
73
mailing/models.py
Normal file
73
mailing/models.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
from django.db import models
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.core.mail import EmailMessage, get_connection
|
||||
from django.conf import settings
|
||||
|
||||
import hashlib
|
||||
|
||||
|
||||
def generate_message_token():
|
||||
# /!\ birthday problem
|
||||
return get_random_string(length=32)
|
||||
|
||||
|
||||
def hexdigest_sha256(*args):
|
||||
r = hashlib.sha256()
|
||||
for arg in args:
|
||||
r.update(str(arg).encode('utf-8'))
|
||||
return r.hexdigest()
|
||||
|
||||
|
||||
class MessageCorrespondent(models.Model):
|
||||
email = models.EmailField()
|
||||
token = models.CharField(max_length=64, default=generate_message_token, unique=True)
|
||||
|
||||
|
||||
class MessageThread(models.Model):
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
token = models.CharField(max_length=64, default=generate_message_token, unique=True)
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
thread = models.ForeignKey(MessageThread)
|
||||
author = models.EmailField(blank=True)
|
||||
content = models.TextField(blank=True)
|
||||
token = models.CharField(max_length=64, default=generate_message_token, unique=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['created']
|
||||
|
||||
def send_notification(self, subject, sender, dests, reply_to=None, message_id=None, reference=None):
|
||||
messages = []
|
||||
for dest_name, dest_email in dests:
|
||||
correspondent, created = MessageCorrespondent.objects.get_or_create(email=dest_email)
|
||||
token = self.thread.token + correspondent.token + hexdigest_sha256(settings.SECRET_KEY, self.thread.token, correspondent.token)[:16]
|
||||
sender_name, sender_email = sender
|
||||
if reply_to:
|
||||
reply_to_name, reply_to_email = reply_to
|
||||
reply_to_list = ['%s <%s>' % (reply_to_name, reply_to_email.format(token=token))]
|
||||
else:
|
||||
reply_to_list = []
|
||||
headers = dict()
|
||||
if message_id:
|
||||
headers.update({
|
||||
'Message-ID': message_id.format(id=self.token),
|
||||
})
|
||||
if message_id and reference:
|
||||
headers.update({
|
||||
'References': message_id.format(id=reference),
|
||||
})
|
||||
messages.append(EmailMessage(
|
||||
subject=subject,
|
||||
body=self.content,
|
||||
from_email='%s <%s>' % (sender_name, sender_email),
|
||||
to=['%s <%s>' % (dest_name, dest_email)],
|
||||
reply_to=reply_to_list,
|
||||
headers=headers,
|
||||
))
|
||||
connection = get_connection()
|
||||
connection.send_messages(messages)
|
||||
|
||||
def __str__(self):
|
||||
return "Message from %s" % self.author
|
16
mailing/templates/mailing/_message_form.html
Normal file
16
mailing/templates/mailing/_message_form.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% load i18n %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{{ message_form_title }}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form action="{{ message_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>
|
14
mailing/templates/mailing/_message_list.html
Normal file
14
mailing/templates/mailing/_message_list.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% for message in messages %}
|
||||
<div class="panel panel-{% if message.author == participant.email %}info{% else %}default{% endif %}">
|
||||
<div class="panel-heading">
|
||||
{{ message.created }} | {{ message.author }}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{ message.content|linebreaksbr }}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p><em>{% trans "No messages." %}</em></p>
|
||||
{% endfor %}
|
121
mailing/utils.py
Normal file
121
mailing/utils.py
Normal file
|
@ -0,0 +1,121 @@
|
|||
from django.conf import settings
|
||||
|
||||
import imaplib
|
||||
import ssl
|
||||
import logging
|
||||
from email import policy
|
||||
from email.parser import BytesParser
|
||||
import chardet
|
||||
import re
|
||||
|
||||
from .models import MessageThread, MessageCorrespondent, Message, hexdigest_sha256
|
||||
|
||||
|
||||
class NoTokenFoundException(Exception):
|
||||
pass
|
||||
|
||||
class InvalidTokenException(Exception):
|
||||
pass
|
||||
|
||||
class InvalidKeyException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def fetch_imap_box(user, password, host, port=993, inbox='INBOX', trash='Trash'):
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
context = ssl.create_default_context()
|
||||
success, failure = 0, 0
|
||||
with imaplib.IMAP4_SSL(host=host, port=port, ssl_context=context) as M:
|
||||
typ, data = M.login(user, password)
|
||||
if typ != 'OK':
|
||||
raise Exception(data[0].decode('utf-8'))
|
||||
typ, data = M.enable('UTF8=ACCEPT')
|
||||
if typ != 'OK':
|
||||
raise Exception(data[0].decode('utf-8'))
|
||||
if trash is not None:
|
||||
# Vérification de l’existence de la poubelle
|
||||
typ, data = M.select(mailbox=trash)
|
||||
if typ != 'OK':
|
||||
raise Exception(data[0].decode('utf-8'))
|
||||
typ, data = M.select(mailbox=inbox)
|
||||
if typ != 'OK':
|
||||
raise Exception(data[0].decode('utf-8'))
|
||||
typ, data = M.uid('search', None, 'UNSEEN')
|
||||
if typ != 'OK':
|
||||
raise Exception(data[0].decode('utf-8'))
|
||||
logging.info("Fetching %d messages" % len(data[0].split()))
|
||||
for num in data[0].split():
|
||||
typ, data = M.uid('fetch', num, '(RFC822)')
|
||||
if typ != 'OK':
|
||||
failure += 1
|
||||
logging.warning(data[0].decode('utf-8'))
|
||||
continue
|
||||
raw_email = data[0][1]
|
||||
try:
|
||||
process_email(raw_email)
|
||||
except Exception as e:
|
||||
failure += 1
|
||||
logging.exception("An error occured during mail processing")
|
||||
if type(e) == NoTokenFoundException:
|
||||
tag = 'NoTokenFound'
|
||||
if type(e) == InvalidTokenException:
|
||||
tag = 'InvalidToken'
|
||||
if type(e) == InvalidKeyException:
|
||||
tag = 'InvalidKey'
|
||||
else:
|
||||
tag = 'UnknowError'
|
||||
typ, data = M.uid('store', num, '+FLAGS', tag)
|
||||
if typ != 'OK':
|
||||
logging.warning(data[0].decode('utf-8'))
|
||||
continue
|
||||
if trash is not None:
|
||||
typ, data = M.uid('copy', num, trash)
|
||||
if typ != 'OK':
|
||||
failure += 1
|
||||
logging.warning(data[0].decode('utf-8'))
|
||||
continue
|
||||
typ, data = M.uid('store', num, '+FLAGS', '\Deleted')
|
||||
if typ != 'OK':
|
||||
failure += 1
|
||||
logging.warning(data[0].decode('utf-8'))
|
||||
continue
|
||||
success += 1
|
||||
typ, data = M.expunge()
|
||||
if typ != 'OK':
|
||||
failure += 1
|
||||
raise Exception(data[0].decode('utf-8'))
|
||||
logging.info("Finished, success: %d, failure: %d" % (success, failure))
|
||||
|
||||
|
||||
def process_email(raw_email):
|
||||
msg = BytesParser(policy=policy.default).parsebytes(raw_email)
|
||||
body = msg.get_body(preferencelist=['plain'])
|
||||
content = body.get_payload(decode=True)
|
||||
|
||||
charset = body.get_content_charset()
|
||||
if not charset:
|
||||
charset = chardet.detect(content)['encoding']
|
||||
content = content.decode(charset)
|
||||
|
||||
regex = re.compile('^[^+@]+\+(?P<token>[a-zA-Z0-9]{80})@[^@]+$')
|
||||
|
||||
for addr in msg.get('To', '').split(','):
|
||||
m = regex.match(addr.strip())
|
||||
if m:
|
||||
break
|
||||
|
||||
if not m:
|
||||
raise NoTokenFoundException
|
||||
|
||||
token = m.group('token')
|
||||
key = token[64:]
|
||||
try:
|
||||
thread = MessageThread.objects.get(token=token[:32])
|
||||
sender = MessageCorrespondent.objects.get(token=token[32:64])
|
||||
except models.DoesNotExist:
|
||||
raise InvalidTokenException
|
||||
|
||||
if key != hexdigest_sha256(settings.SECRET_KEY, thread.token, sender.token)[:16]:
|
||||
raise InvalidKeyException
|
||||
|
||||
Message.objects.create(thread=thread, author=sender.email, content=content)
|
|
@ -40,6 +40,7 @@ INSTALLED_APPS = [
|
|||
#'accounts',
|
||||
'ponyconf',
|
||||
'cfp',
|
||||
'mailing',
|
||||
#'proposals',
|
||||
#'conversations',
|
||||
#'planning',
|
||||
|
@ -237,5 +238,5 @@ SELECT2_CACHE_BACKEND = 'select2'
|
|||
SERVER_EMAIL = 'ponyconf@example.com'
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = 'localhost'
|
||||
EMAIL_PORT = 1025
|
||||
EMAIL_PORT = 25
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user