improve mailing system

This commit is contained in:
Élie Bouttier 2017-11-30 20:34:12 +01:00
parent 92fda40e9c
commit 8c7660a240
11 changed files with 497 additions and 263 deletions

View File

@ -86,7 +86,7 @@ class Conference(models.Model):
})
def __str__(self):
return str(self.site)
return self.name
class ParticipantManager(models.Manager):

View File

@ -5,9 +5,11 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.contrib.auth import get_user_model
from ponyconf.decorators import disable_for_loaddata
from mailing.models import MessageThread, Message
from mailing.utils import send_message
from .models import Participant, Talk, Conference, Volunteer
@ -25,80 +27,47 @@ pre_save.connect(create_conversation, sender=Talk)
pre_save.connect(create_conversation, sender=Volunteer)
@receiver(pre_save, sender=Message, dispatch_uid="Set message author")
def set_message_author(sender, instance, **kwargs):
message = instance
if message.author is None:
# Try users
try:
instance.author = User.objects.get(email=message.from_email)
except User.DoesNotExist:
pass
else:
return
# Try participants
try:
instance.author = Participant.objects.get(email=message.from_email)
except User.DoesNotExist:
pass
else:
return
# Try conferences
try:
instance.author = Conference.objects.get(contact_email=message.from_email)
except Conference.DoesNotExist:
pass
else:
return
@receiver(post_save, sender=Message, dispatch_uid="Send message notifications")
def send_message_notifications(sender, instance, **kwargs):
message = instance
author = message.author.author
thread = message.thread
first_message = thread.message_set.first()
if message == first_message:
reference = None
if message.in_reply_to:
reference = message.in_reply_to.token
else:
reference = first_message.token
subject_prefix = 'Re: ' if reference else ''
reference = None
if hasattr(thread, 'participant'):
conf = thread.participant.site.conference
elif hasattr(thread, 'talk'):
conf = thread.talk.site.conference
elif hasattr(thread, 'volunteer'):
conf = thread.volunteer.site.conference
message_id = '<{id}@%s>' % conf.site.domain
if conf.reply_email:
reply_to = (conf.name, conf.reply_email)
reply_to = (str(conf), conf.reply_email)
else:
reply_to = None
sender = (message.author_display, 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}
proto = 'https' if conf.secure_domain else 'http'
footer = '\n\n--\n%s://' % proto + conf.site.domain + reverse('participant-details', args=[participant.pk])
if message.from_email == conf.contact_email: # this is a talk notification message
# send it only to the participant
message.send_notification(subject=subject_prefix+participant_subject, sender=sender, dests=participant_dests,
reply_to=reply_to, message_id=message_id, reference=reference)
if type(author) == get_user_model():
sender = author.get_full_name()
else:
sender = str(author)
sender = (sender, conf.contact_email)
staff_dests = [ (user, user.get_full_name(), user.email) for user in conf.staff.all() ]
if hasattr(thread, 'participant') or hasattr(thread, 'volunteer'):
if hasattr(thread, 'participant'):
user = thread.participant
else:
# this is a message between the staff and the participant
message.send_notification(subject=subject_prefix+staff_subject, sender=sender, dests=staff_dests,
reply_to=reply_to, message_id=message_id, reference=reference, footer=footer)
if message.from_email != thread.participant.email: # message from staff: sent it to the participant too
message.send_notification(subject=subject_prefix+participant_subject, sender=sender, dests=participant_dests,
reply_to=reply_to, message_id=message_id, reference=reference)
user = thread.volunteer
dests = [ (user, user.name, user.email) ]
if author == user: # message from the user, notify the staff
message.send_notification(sender=sender, dests=staff_dests, reply_to=reply_to, message_id=message_id, reference=reference)
else: # message to the user, notify the user, and the staff if the message is not a conference notification
message.send_notification(sender=sender, dests=dests, reply_to=reply_to, message_id=message_id, reference=reference)
if author != conf:
message.send_notification(sender=sender, dests=staff_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}
proto = 'https' if conf.secure_domain else 'http'
footer = '\n\n--\n%s://' % proto + conf.site.domain + reverse('talk-details', args=[thread.talk.pk])
message.send_notification(subject=subject_prefix+subject, sender=sender, dests=staff_dests,
reply_to=reply_to, message_id=message_id, reference=reference, footer=footer)
message.send_notification(sender=sender, dests=staff_dests,
reply_to=reply_to, message_id=message_id, reference=reference)
# connected in apps.py

View File

@ -69,11 +69,6 @@
<span class="text-danger">{% blocktrans count refused=participant.refused_talk_count %}refused: {{ refused }}{% plural %}refused: {{ refused }}{% endblocktrans %}</span>
</td>
{% comment %}
<td>
<a class="btn btn-{% if speaker.conversation.messages.last.author == speaker.user %}primary{% else %}default{% endif %}" href="{% url 'user-conversation' speaker.user.username %}">{% trans "Contact" %}</a>
</td>
{% endcomment %}
</tr>
{% if forloop.last %}
</tbody>

View File

@ -19,8 +19,8 @@ from django_select2.views import AutoResponseView
from functools import reduce
import csv
from mailing.models import Message
from mailing.forms import MessageForm
from mailing.utils import send_message
from .planning import Program
from .decorators import speaker_required, volunteer_required, staff_required
from .mixins import StaffRequiredMixin, OnSiteMixin, OnSiteFormMixin
@ -75,17 +75,11 @@ Thanks!
{}
""").format(volunteer.name, request.conference.name, volunteer.get_secret_url(full=True), request.conference.name)
#Message.objects.create(
# thread=volunteer.conversation,
# author=request.conference,
# from_email=request.conference.contact_email,
# content=body,
#)
send_mail(
subject=_('Thank you for your help!'),
message=body,
from_email='%s <%s>' % (request.conference.name, request.conference.contact_email),
recipient_list=['%s <%s>' % (volunteer.name, volunteer.email)],
send_message(
thread=volunteer.conversation,
author=request.conference,
subject=_('[%(conference)s] Thank you for your help!') % {'conference': request.conference},
content=body,
)
messages.success(request, _('Thank you for your participation! You can now subscribe to some activities.'))
return redirect(reverse('volunteer-dashboard', kwargs={'volunteer_token': volunteer.token}))
@ -111,17 +105,11 @@ def volunteer_mail_token(request):
'url': url,
'conf': request.conference
})
#Message.objects.create(
# thread=volunteer.conversation,
# author=request.conference,
# from_email=request.conference.contact_email,
# content=body,
#)
send_mail(
subject=_('Thank you for your help!'),
message=body,
from_email='%s <%s>' % (request.conference.name, request.conference.contact_email),
recipient_list=['%s <%s>' % (volunteer.name, volunteer.email)],
send_message(
thread=volunteer.conversation,
author=request.conference,
subject=_("[%(conference)s] Someone asked to access your profil") % {'conference': request.conference},
content=body,
)
messages.success(request, _('A email have been sent with a link to access to your profil.'))
return redirect(reverse('volunteer-mail-token'))
@ -251,14 +239,17 @@ Thanks!
{}
""").format(
speaker.name, request.conference.name,talk.title, talk.description,
speaker.name, request.conference.name, talk.title, talk.description,
url_dashboard, url_talk_details, url_speaker_add,
request.conference.name,
)
Message.objects.create(
send_message(
thread=speaker.conversation,
author=request.conference,
from_email=request.conference.contact_email,
subject=_("[%(conference)s] Thank you for your proposition '%(talk)s'") % {
'conference': request.conference.name,
'talk': talk,
},
content=body,
)
messages.success(request, _('You proposition have been successfully submitted!'))
@ -282,7 +273,7 @@ def proposal_mail_token(request):
dashboard_url = base_url + reverse('proposal-dashboard', kwargs=dict(speaker_token=speaker.token))
body = _("""Hi {},
Someone, probably you, ask to access your profile.
Someone, probably you, asked to access your profile.
You can edit your talks or add new ones following this url:
{}
@ -294,10 +285,12 @@ Sincerely,
{}
""").format(speaker.name, dashboard_url, request.conference.name)
Message.objects.create(
send_message(
thread=speaker.conversation,
author=request.conference,
from_email=request.conference.contact_email,
subject=_("[%(conference)s] Someone asked to access your profil") % {
'conference': request.conference.name,
},
content=body,
)
messages.success(request, _('A email have been sent with a link to access to your profil.'))
@ -367,11 +360,25 @@ def proposal_talk_acknowledgment(request, speaker, talk_id, confirm):
talk.save()
if confirm:
confirmation_message= _('Your participation has been taken into account, thank you!')
thread_note = _('Speaker %(speaker)s confirmed his/her participation.' % {'speaker': speaker})
action = _('confirmed')
else:
confirmation_message = _('We have noted your unavailability.')
thread_note = _('Speaker %(speaker)s CANCELLED his/her participation.' % {'speaker': speaker})
Message.objects.create(thread=talk.conversation, author=speaker, content=thread_note)
action = _('cancelled')
content = _('Speaker %(speaker)s %(action)s his/her participation for %(talk)s.') % {
'speaker': speaker,
'action': action,
'talk': talk,
}
send_message(
thread=talk.conversation,
author=speaker,
subject=_('[%(conference)s] %(speaker)s %(action)s his/her participation') % {
'conference': request.conference,
'speaker': speaker,
'action': action,
},
content=content,
)
messages.success(request, confirmation_message)
return redirect(reverse('proposal-talk-details', kwargs={'speaker_token': speaker.token, 'talk_id': talk.pk}))
@ -443,10 +450,13 @@ Thanks!
url_dashboard, url_talk_details, url_speaker_add,
request.conference.name,
)
Message.objects.create(
send_message(
thread=edited_speaker.conversation,
author=request.conference,
from_email=request.conference.contact_email,
subject=_("[%(conference)s] You have been added as co-speaker to '%(talk)s'") % {
'conference': request.conference,
'talk': talk,
},
content=body,
)
messages.success(request, _('Co-speaker successfully added to the talk.'))
@ -496,11 +506,22 @@ def talk_acknowledgment(request, talk_id, confirm):
talk.save()
if confirm:
confirmation_message= _('The speaker confirmation have been noted.')
action = _('confirmed')
thread_note = _('The talk have been confirmed.')
else:
confirmation_message = _('The speaker unavailability have been noted.')
thread_note = _('The talk have been cancelled.')
Message.objects.create(thread=talk.conversation, author=request.user, content=thread_note)
action = _('cancelled')
thread_note = _('The talk have been %(action)s.') % {'action': action}
send_message(
thread=talk.conversation,
author=request.user,
subject=_("[%(conference)s] The talk '%(talk)s' have been %(action)s.") % {
'conference': request.conference,
'talk': talk,
'action': action,
},
content=thread_note,
)
messages.success(request, confirmation_message)
return redirect(reverse('talk-details', kwargs=dict(talk_id=talk_id)))
@ -588,10 +609,20 @@ def talk_list(request):
talk = Talk.objects.get(site=request.conference.site, pk=talk_id)
if data['decision'] != None and data['decision'] != talk.accepted:
if data['decision']:
note = _("The talk has been accepted.")
action = _('accepted')
else:
note = _("The talk has been declined.")
Message.objects.create(thread=talk.conversation, author=request.user, content=note)
action = _('declined')
note = _('The talk has been %(action)s.') % {'action': action}
send_message(
thread=talk.conversation,
author=request.user,
subject=_("[%(conference)s] The talk '%(talk)s' have been %(action)s") % {
'conference': conference,
'talk': talk,
'action': action,
},
content=note,
)
talk.accepted = data['decision']
if data['track']:
talk.track = Track.objects.get(site=request.conference.site, slug=data['track'])
@ -657,11 +688,21 @@ def talk_details(request, talk_id):
vote = None
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
message.from_email = request.user.email
message.thread = talk.conversation
message.save()
in_reply_to = talk.conversation.message_set.last()
subject=_("[%(conference)s] New comment about '%(talk)s'") % {
'conference': request.conference,
'talk': talk,
}
if in_reply_to:
# Maybe use in_reply_to.subject?
subject = 'Re: ' + subject
send_message(
thread=talk.conversation,
author=request.user,
subject=subject,
content=message_form.cleaned_data['content'],
in_reply_to=in_reply_to,
)
messages.success(request, _('Message sent!'))
return redirect(reverse('talk-details', args=[talk.pk]))
return render(request, 'cfp/staff/talk_details.html', {
@ -686,17 +727,35 @@ def talk_decide(request, talk_id, accept):
if request.method == 'POST':
talk.accepted = accept
talk.save()
if accept:
action = _('accepted')
else:
action = _('declined')
# Does we need to send a notification to the proposer?
m = request.POST.get('message', '').strip()
if m:
for participant in talk.speakers.all():
Message.objects.create(thread=talk.conversation, author=request.user, content=m)
send_message(
thread=talk.conversation,
author=request.conference,
subject=_("[%(conference)s] Your talk '%(talk)s' have been %(action)s") % {
'conference': request.conference,
'talk': talk,
'action': action,
},
content=m,
)
# Save the decision in the talk's conversation
if accept:
note = _("The talk has been accepted.")
else:
note = _("The talk has been declined.")
Message.objects.create(thread=talk.conversation, author=request.user, content=note)
send_message(
thread=talk.conversation,
author=request.user,
subject=_("[%(conference)s] The talk '%(talk)s' have been %(action)s") % {
'conference': request.conference,
'talk': talk,
'action': action,
},
content=_('The talk has been %(action)s.') % {'action': action},
)
messages.success(request, _('Decision taken in account'))
return redirect(talk.get_absolute_url())
return render(request, 'cfp/staff/talk_decide.html', {

Binary file not shown.

View File

@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-11-25 22:23+0000\n"
"PO-Revision-Date: 2017-11-25 23:24+0100\n"
"POT-Creation-Date: 2017-11-30 10:05+0000\n"
"PO-Revision-Date: 2017-11-30 11:06+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: fr\n"
@ -22,41 +22,41 @@ msgstr ""
msgid "Email address"
msgstr "Adresse e-mail"
#: accounts/models.py:10 cfp/models.py:110 cfp/models.py:462
#: accounts/models.py:10 cfp/models.py:115 cfp/models.py:467
msgid "Phone number"
msgstr "Numéro de téléphone"
#: accounts/models.py:11 cfp/models.py:463
#: accounts/models.py:11 cfp/models.py:468
msgid "SMS prefered"
msgstr "SMS préférés"
#: accounts/models.py:12 cfp/models.py:102
#: accounts/models.py:12 cfp/models.py:107
#: cfp/templates/cfp/proposal_dashboard.html:33
#: cfp/templates/cfp/staff/participant_details.html:15
msgid "Biography"
msgstr "Biographie"
#: accounts/models.py:14 cfp/models.py:104
#: accounts/models.py:14 cfp/models.py:109
msgid "Twitter"
msgstr "Twitter"
#: accounts/models.py:15 cfp/models.py:105
#: accounts/models.py:15 cfp/models.py:110
msgid "LinkedIn"
msgstr "LinkedIn"
#: accounts/models.py:16 cfp/models.py:106
#: accounts/models.py:16 cfp/models.py:111
msgid "Github"
msgstr "Github"
#: accounts/models.py:17 cfp/models.py:107
#: accounts/models.py:17 cfp/models.py:112
msgid "Website"
msgstr "Site web"
#: accounts/models.py:18 cfp/models.py:108
#: accounts/models.py:18 cfp/models.py:113
msgid "Facebook"
msgstr "Facebook"
#: accounts/models.py:19 cfp/models.py:109
#: accounts/models.py:19 cfp/models.py:114
msgid "Mastodon"
msgstr "Mastodon"
@ -114,15 +114,15 @@ msgstr "Décliné"
msgid "Waiting"
msgstr "En attente"
#: cfp/forms.py:29 cfp/forms.py:131 cfp/forms.py:228 cfp/models.py:374
#: cfp/forms.py:29 cfp/forms.py:131 cfp/forms.py:228 cfp/models.py:379
msgid "Confirmed"
msgstr "Confirmé"
#: cfp/forms.py:30 cfp/models.py:376
#: cfp/forms.py:30 cfp/models.py:381
msgid "Cancelled"
msgstr "Annulé"
#: cfp/forms.py:62 cfp/models.py:505
#: cfp/forms.py:62 cfp/models.py:510
msgid "Activity"
msgstr "Activité"
@ -145,13 +145,13 @@ msgstr "Catégorie"
msgid "Title"
msgstr "Titre"
#: cfp/forms.py:109 cfp/models.py:158 cfp/models.py:500
#: cfp/forms.py:109 cfp/models.py:163 cfp/models.py:505
#: cfp/templates/cfp/proposal_talk_details.html:75
#: cfp/templates/cfp/staff/talk_details.html:64
msgid "Description"
msgstr "Description"
#: cfp/forms.py:110 cfp/models.py:112 cfp/models.py:465
#: cfp/forms.py:110 cfp/models.py:117 cfp/models.py:470
#: cfp/templates/cfp/staff/participant_details.html:19
#: cfp/templates/cfp/staff/talk_details.html:82
#: cfp/templates/cfp/staff/volunteer_details.html:22
@ -162,7 +162,7 @@ msgstr "Notes"
msgid "Visible by speakers"
msgstr "Visible par les orateurs"
#: cfp/forms.py:137 cfp/forms.py:234 cfp/models.py:330
#: cfp/forms.py:137 cfp/forms.py:234 cfp/models.py:335
#: cfp/templates/cfp/staff/talk_details.html:21
#: cfp/templates/cfp/staff/talk_list.html:46
#: cfp/templates/cfp/staff/track_form.html:14
@ -200,7 +200,7 @@ msgstr "Programmé"
msgid "Filter talks already / not yet scheduled"
msgstr "Filtrer les exposés déjà / pas encore planifiées"
#: cfp/forms.py:161 cfp/models.py:348
#: cfp/forms.py:161 cfp/models.py:353
#: cfp/templates/cfp/proposal_talk_details.html:89
#: cfp/templates/cfp/staff/talk_details.html:54
msgid "Materials"
@ -243,7 +243,7 @@ msgstr "Assigner à une salle"
msgid "Notify by mail?"
msgstr "Notifier par e-mail ?"
#: cfp/forms.py:250 cfp/models.py:460
#: cfp/forms.py:250 cfp/models.py:465
#: cfp/templates/cfp/staff/volunteer_list.html:30
msgid "Email"
msgstr "E-mail"
@ -318,7 +318,11 @@ msgstr "Date douverture de lappel à bénévole"
msgid "Volunteers enrollment closing date"
msgstr "Date de fermeture de lappel à bénévole"
#: cfp/models.py:80
#: cfp/models.py:44
msgid "Video publishing date"
msgstr "Date de publication des vidéos"
#: cfp/models.py:85
#, python-brace-format
msgid ""
"The reply email should be a formatable string accepting a token argument (e."
@ -327,53 +331,53 @@ msgstr ""
"Ladresse de réponse doit être une chaine de texte formatable avec un "
"argument « token » (e.g. ponyconf+{token}@exemple.com)."
#: cfp/models.py:100 cfp/models.py:156 cfp/models.py:178 cfp/models.py:208
#: cfp/models.py:498 cfp/templates/cfp/staff/participant_list.html:42
#: cfp/models.py:105 cfp/models.py:161 cfp/models.py:183 cfp/models.py:213
#: cfp/models.py:503 cfp/templates/cfp/staff/participant_list.html:42
#: cfp/templates/cfp/staff/volunteer_list.html:29
msgid "Name"
msgstr "Nom"
#: cfp/models.py:113
#: cfp/models.py:118
msgid "This field is only visible by organizers."
msgstr "Ce champ est uniquement visible par les organisateurs."
#: cfp/models.py:114 cfp/templates/cfp/staff/participant_details.html:25
#: cfp/models.py:119 cfp/templates/cfp/staff/participant_details.html:25
msgid "Invited speaker"
msgstr "Orateur invité"
#: cfp/models.py:180
#: cfp/models.py:185
msgid "Label"
msgstr "Étiquette"
#: cfp/models.py:181
#: cfp/models.py:186
msgid "Capacity"
msgstr "Capacité"
#: cfp/models.py:210
#: cfp/models.py:215
msgid "Color"
msgstr "Couleur"
#: cfp/models.py:212
#: cfp/models.py:217
msgid "Show the tag on the public program"
msgstr "Afficher létiquette sur le programme public"
#: cfp/models.py:213
#: cfp/models.py:218
msgid "Show the tag on the staff program"
msgstr "Afficher létiquette sur le programme organisateur"
#: cfp/models.py:249
#: cfp/models.py:254
msgid "Default duration (min)"
msgstr "Durée par défaut (min)"
#: cfp/models.py:250
#: cfp/models.py:255
msgid "Color on program"
msgstr "Couleur sur le programme"
#: cfp/models.py:251
#: cfp/models.py:256
msgid "Label on program"
msgstr "Label dans le xml du programme"
#: cfp/models.py:325 cfp/templates/cfp/proposal_talk_details.html:53
#: cfp/models.py:330 cfp/templates/cfp/proposal_talk_details.html:53
#: cfp/templates/cfp/staff/base.html:10
#: cfp/templates/cfp/staff/participant_list.html:8
#: cfp/templates/cfp/staff/talk_details.html:68
@ -381,23 +385,23 @@ msgstr "Label dans le xml du programme"
msgid "Speakers"
msgstr "Orateurs"
#: cfp/models.py:326
#: cfp/models.py:331
msgid "Talk Title"
msgstr "Titre de la proposition"
#: cfp/models.py:328
#: cfp/models.py:333
msgid "Description of your talk"
msgstr "Description de votre proposition"
#: cfp/models.py:329
#: cfp/models.py:334
msgid "This description will be visible on the program."
msgstr "Cette description sera visible sur le programme."
#: cfp/models.py:332 cfp/templates/cfp/proposal_talk_details.html:99
#: cfp/models.py:337 cfp/templates/cfp/proposal_talk_details.html:99
msgid "Message to organizers"
msgstr "Message aux organisateurs"
#: cfp/models.py:333
#: cfp/models.py:338
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 "
@ -407,84 +411,69 @@ msgstr ""
"votre proposition, comme une vidéo, des slides, n'hésitez pas à les ajouter "
"ici. Ce champ ne sera visible que par les organisateurs."
#: cfp/models.py:337
#: cfp/models.py:342
msgid "Talk Category"
msgstr "Catégorie de proposition"
#: cfp/models.py:338
#: cfp/models.py:343
msgid "I'm ok to be recorded on video"
msgstr "Jaccepte dêtre enregistré en vidéo"
#: cfp/models.py:340
#: cfp/models.py:345
msgid "Video licence"
msgstr "Licence vidéo"
#: cfp/models.py:341
#: cfp/models.py:346
msgid "I need sound"
msgstr "Jai besoin de son"
#: cfp/models.py:344
#: cfp/models.py:349
msgid "Beginning date and time"
msgstr "Date et heure de début"
#: cfp/models.py:345
#: cfp/models.py:350
msgid "Duration (min)"
msgstr "Durée (min)"
#: cfp/models.py:349
#: cfp/models.py:354
msgid ""
"You can use this field to share some materials related to your intervention."
msgstr ""
"Vous pouvez utiliser ce champ pour partager les supports de votre "
"intervention."
#: cfp/models.py:378
#: cfp/models.py:383
msgid "Waiting confirmation"
msgstr "En attente de confirmation"
#: cfp/models.py:380
#: cfp/models.py:385
msgid "Refused"
msgstr "Refusé"
#: cfp/models.py:382
#: cfp/models.py:387
#, python-format
msgid "Pending decision, score: %(score).1f"
msgstr "En cours, score : %(score).1f"
#: cfp/models.py:459
#: cfp/models.py:464
msgid "Your Name"
msgstr "Votre Nom"
#: cfp/models.py:466
#: cfp/models.py:471
msgid "If you have some constraints, you can indicate them here."
msgstr "Si vous avez des contraintes, vous pouvez les indiquer ici."
#: cfp/models.py:501 cfp/templates/cfp/staff/volunteer_details.html:8
#: cfp/models.py:506 cfp/templates/cfp/staff/volunteer_details.html:8
msgid "Volunteer"
msgstr "Bénévole"
#: cfp/models.py:506 cfp/templates/cfp/admin/activity_list.html:9
#: cfp/models.py:511 cfp/templates/cfp/admin/activity_list.html:9
#: cfp/templates/cfp/admin/base.html:14
#: cfp/templates/cfp/staff/volunteer_details.html:27
#: cfp/templates/cfp/staff/volunteer_list.html:32
msgid "Activities"
msgstr "Activités"
#: cfp/signals.py:80
#, python-format
msgid "[%(prefix)s] Message from the staff"
msgstr "[%(prefix)s] Message du staff"
#: cfp/signals.py:81
#, python-format
msgid "[%(prefix)s] Conversation with %(dest)s"
msgstr "[%(prefix)s] Conversation avec %(dest)s"
#: cfp/signals.py:97
#, python-format
msgid "[%(prefix)s] Talk: %(talk)s"
msgstr "[%(prefix)s] Talk: %(talk)s"
#: cfp/templates/cfp/admin/activity_form.html:16
msgid "Edit activity"
msgstr "Édition dune activité"
@ -746,11 +735,13 @@ msgstr "et"
msgid "you must confirm you participation"
msgstr "vous devez confirmer votre participation"
#: cfp/templates/cfp/proposal_dashboard.html:61
#: cfp/templates/cfp/proposal_dashboard.html:61 cfp/views.py:612
#: cfp/views.py:731
msgid "accepted"
msgstr "accepté"
#: cfp/templates/cfp/proposal_dashboard.html:63
#: cfp/templates/cfp/proposal_dashboard.html:63 cfp/views.py:366
#: cfp/views.py:513
msgid "cancelled"
msgstr "annulé"
@ -1271,34 +1262,40 @@ msgstr ""
"{}\n"
"\n"
#: cfp/views.py:85 cfp/views.py:121
msgid "Thank you for your help!"
msgstr "Merci pour votre aide !"
#: cfp/views.py:81
#, python-format
msgid "[%(conference)s] Thank you for your help!"
msgstr "[%(conference)s] Merci pour votre aide !"
#: cfp/views.py:90
#: cfp/views.py:84
msgid ""
"Thank you for your participation! You can now subscribe to some activities."
msgstr ""
"Merci pour votre participation ! Vous pouvez maintenant vous inscrire à une "
"ou plusieurs activités."
#: cfp/views.py:104 cfp/views.py:278
#: cfp/views.py:98 cfp/views.py:269
msgid "Sorry, we do not know this email."
msgstr "Désolé, nous ne connaissons pas cette e-mail."
#: cfp/views.py:126 cfp/views.py:303
#: cfp/views.py:111 cfp/views.py:291
#, python-format
msgid "[%(conference)s] Someone asked to access your profil"
msgstr "[%(conference)s] Quelquun a demandé à accéder à votre profil"
#: cfp/views.py:114 cfp/views.py:296
msgid "A email have been sent with a link to access to your profil."
msgstr "Un e-mail vous a été envoyé avec un lien pour accéder à votre profil."
#: cfp/views.py:147
#: cfp/views.py:135
msgid "Thank you for your participation!"
msgstr "Merci pour votre participation !"
#: cfp/views.py:151
#: cfp/views.py:139
msgid "Okay, no problem!"
msgstr "Ok, pas de soucis !"
#: cfp/views.py:234
#: cfp/views.py:222
msgid ""
"Hi {},\n"
"\n"
@ -1338,15 +1335,20 @@ msgstr ""
"{}\n"
"\n"
#: cfp/views.py:264 cfp/views.py:345
#: cfp/views.py:249
#, python-format
msgid "[%(conference)s] Thank you for your proposition '%(talk)s'"
msgstr "[%(conference)s] Merci pour votre proposition « %(talk)s »"
#: cfp/views.py:255 cfp/views.py:338
msgid "You proposition have been successfully submitted!"
msgstr "Votre proposition a été transmise avec succès !"
#: cfp/views.py:283
#: cfp/views.py:274
msgid ""
"Hi {},\n"
"\n"
"Someone, probably you, ask to access your profile.\n"
"Someone, probably you, asked to access your profile.\n"
"You can edit your talks or add new ones following this url:\n"
"\n"
" {}\n"
@ -1373,37 +1375,41 @@ msgstr ""
"{}\n"
"\n"
#: cfp/views.py:341 cfp/views.py:414
#: cfp/views.py:334 cfp/views.py:421
msgid "Changes saved."
msgstr "Modifications sauvegardées."
#: cfp/views.py:362
#: cfp/views.py:355
msgid "You already confirmed your participation to this talk."
msgstr "Vous avez déjà confirmé votre participation à cet exposé."
#: cfp/views.py:364
#: cfp/views.py:357
msgid "You already cancelled your participation to this talk."
msgstr "Vous avez déjà annulé votre participation à cet exposé."
#: cfp/views.py:369
#: cfp/views.py:362
msgid "Your participation has been taken into account, thank you!"
msgstr "Votre participation a été prise en compte, merci !"
#: cfp/views.py:370
#, python-format
msgid "Speaker %(speaker)s confirmed his/her participation."
msgstr "Lintervenant %(speaker)s a confirmé sa participation."
#: cfp/views.py:363 cfp/views.py:509
msgid "confirmed"
msgstr "confirmé"
#: cfp/views.py:372
#: cfp/views.py:365
msgid "We have noted your unavailability."
msgstr "Nous avons enregistré votre indisponibilité."
#: cfp/views.py:373
#: cfp/views.py:367
#, python-format
msgid "Speaker %(speaker)s CANCELLED his/her participation."
msgstr "Lintervenant %(speaker)s a ANNULÉ sa participation."
msgid "Speaker %(speaker)s %(action)s his/her participation for %(talk)s."
msgstr "Lorateur %(speaker)s a %(action) sa participation pour %(talk)s."
#: cfp/views.py:421
#: cfp/views.py:375
#, python-format
msgid "[%(conference)s] %(speaker)s %(action)s his/her participation"
msgstr "[%(conference)s] %(speaker)s a %(action) sa participation"
#: cfp/views.py:428
msgid ""
"Hi {},\n"
"\n"
@ -1443,63 +1449,91 @@ msgstr ""
"{}\n"
"\n"
#: cfp/views.py:452 cfp/views.py:472
#: cfp/views.py:456
#, python-format
msgid "[%(conference)s] You have been added as co-speaker to '%(talk)s'"
msgstr ""
"[%(conference)s] Vous avez été ajouté comme co-intervenant pour « %(talk)s »"
#: cfp/views.py:462 cfp/views.py:482
msgid "Co-speaker successfully added to the talk."
msgstr "Co-intervenant ajouté à lexposé avec succès."
#: cfp/views.py:485
#: cfp/views.py:495
msgid "Co-speaker successfully removed from the talk."
msgstr "Co-intervenant supprimé de lexposé avec succès."
#: cfp/views.py:498
#: cfp/views.py:508
msgid "The speaker confirmation have been noted."
msgstr "La confirmation de lorateur a été notée."
#: cfp/views.py:499
#: cfp/views.py:510
msgid "The talk have been confirmed."
msgstr "Lexposé a été confirmé."
#: cfp/views.py:501
#: cfp/views.py:512
msgid "The speaker unavailability have been noted."
msgstr "Lindisponibilité de lintervenant a été notée."
#: cfp/views.py:502
msgid "The talk have been cancelled."
msgstr "Lexposé a été annulé."
#: cfp/views.py:514
#, python-format
msgid "The talk have been %(action)s."
msgstr "Lexposé a été %(action)s."
#: cfp/views.py:591 cfp/views.py:696
msgid "The talk has been accepted."
msgstr "Lexposé a été accepté."
#: cfp/views.py:518
#, python-format
msgid "[%(conference)s] The talk '%(talk)s' have been %(action)s."
msgstr "[%(conference)s] Lexposé « %(talk)s » a été %(action)s."
#: cfp/views.py:593 cfp/views.py:698
msgid "The talk has been declined."
msgstr "Lexposé a été décliné."
#: cfp/views.py:614 cfp/views.py:733
msgid "declined"
msgstr "décliné"
#: cfp/views.py:665 cfp/views.py:784
#: cfp/views.py:615 cfp/views.py:757
#, python-format
msgid "The talk has been %(action)s."
msgstr "Lexposé a été %(action)s."
#: cfp/views.py:619 cfp/views.py:752
#, python-format
msgid "[%(conference)s] The talk '%(talk)s' have been %(action)s"
msgstr "[%(conference)s] Lexposé « %(talk)s » a été %(action)s"
#: cfp/views.py:692
#, python-format
msgid "[%(conference)s] New comment about '%(talk)s'"
msgstr "[%(conference)s] Nouveau commentaire sur « %(talk)s »"
#: cfp/views.py:706 cfp/views.py:843
msgid "Message sent!"
msgstr "Message envoyé !"
#: cfp/views.py:679
#: cfp/views.py:720
msgid "Vote successfully created"
msgstr "A voté !"
#: cfp/views.py:679
#: cfp/views.py:720
msgid "Vote successfully updated"
msgstr "Vote mis à jour"
#: cfp/views.py:700
#: cfp/views.py:741
#, python-format
msgid "[%(conference)s] Your talk '%(talk)s' have been %(action)s"
msgstr "[%(conference)s] Votre exposé « %(talk)s » a été %(action)s"
#: cfp/views.py:759
msgid "Decision taken in account"
msgstr "Décision enregistrée"
#: cfp/views.py:718
#: cfp/views.py:777
msgid "Speaker removed from this talk"
msgstr "Intervenant supprimé de lexposé avec succès"
#: cfp/views.py:843
#: cfp/views.py:904
msgid "[{}] You have been added to the staff team"
msgstr "[{}] Vous avez été ajouté aux membres du staff"
#: cfp/views.py:844
#: cfp/views.py:905
msgid ""
"Hi {},\n"
"\n"
@ -1523,20 +1557,20 @@ msgstr ""
"{}\n"
"\n"
#: cfp/views.py:865 cfp/views.py:877
#: cfp/views.py:926 cfp/views.py:938
msgid "Modifications successfully saved."
msgstr "Modification enregistrée avec succès."
#: cfp/views.py:1038
#: cfp/views.py:1101
msgid "User created successfully."
msgstr "Utilisateur créé avec succès."
#: cfp/views.py:1059
#: cfp/views.py:1122
#, python-format
msgid "Format '%s' not available"
msgstr "Format '%s' non disponible"
#: mailing/models.py:103
#: mailing/models.py:106
#, python-format
msgid "Message from %(author)s"
msgstr "Message de %(author)s"
@ -1605,6 +1639,24 @@ msgstr "Mot de passe oublié ?"
msgid "Password Change"
msgstr "Changement de mot de passe"
#~ msgid "[%(prefix)s] Message from the staff"
#~ msgstr "[%(prefix)s] Message du staff"
#~ msgid "[%(prefix)s] Conversation with %(dest)s"
#~ msgstr "[%(prefix)s] Conversation avec %(dest)s"
#~ msgid "[%(prefix)s] Talk: %(talk)s"
#~ msgstr "[%(prefix)s] Talk: %(talk)s"
#~ msgid "Speaker %(speaker)s CANCELLED his/her participation."
#~ msgstr "Lintervenant %(speaker)s a ANNULÉ sa participation."
#~ msgid "The talk have been cancelled."
#~ msgstr "Lexposé a été annulé."
#~ msgid "The talk has been declined."
#~ msgstr "Lexposé a été décliné."
#~ msgid "Contact:"
#~ msgstr "Contacter :"

View File

@ -3,4 +3,4 @@ from django.forms.models import modelform_factory
from .models import Message
MessageForm = modelform_factory(Message, fields=['content'])
MessageForm = modelform_factory(Message, fields=['subject', 'content'])

View File

@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.7 on 2017-11-29 21:55
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import mailing.models
def forward(apps, schema_editor):
db_alias = schema_editor.connection.alias
Message = apps.get_model("mailing", "Message")
MessageAuthor = apps.get_model("mailing", "MessageAuthor")
for message in Message.objects.using(db_alias).all():
message.new_author, _ = MessageAuthor.objects.using(db_alias).get_or_create(author_type=message.author_type, author_id=message.author_id)
message.save()
def backward(apps, schema_editor):
db_alias = schema_editor.connection.alias
Message = apps.get_model("mailing", "Message")
ContentType = apps.get_model("contenttypes", "ContentType")
for message in Message.objects.using(db_alias).all():
author_type = message.new_author.author_type
message.author_type = message.new_author.author_type
message.author_id = message.new_author.author_id
AuthorType = apps.get_model(author_type.app_label, author_type.model)
author = AuthorType.objects.get(pk=message.author_id)
if author_type.model == 'conference':
message.from_email = author.contact_email
else:
message.from_email = author.email
message.save()
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('mailing', '0002_message_author'),
]
operations = [
migrations.CreateModel(
name='MessageAuthor',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('author_id', models.PositiveIntegerField(blank=True, null=True)),
('token', models.CharField(default=mailing.models.generate_message_token, max_length=64, unique=True)),
('author_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
),
migrations.AddField(
model_name='message',
name='new_author',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='mailing.MessageAuthor'),
preserve_default=False,
),
migrations.AlterField(
model_name='message',
name='from_email',
field=models.EmailField(blank=True, null=True),
),
migrations.RunPython(forward, backward),
migrations.RemoveField(
model_name='message',
name='author_id',
),
migrations.RemoveField(
model_name='message',
name='author_type',
),
migrations.RemoveField(
model_name='message',
name='from_email',
),
migrations.RenameField(
model_name='message',
old_name='new_author',
new_name='author',
),
migrations.AlterField(
model_name='message',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mailing.MessageAuthor'),
),
migrations.AddField(
model_name='message',
name='in_reply_to',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='mailing.Message'),
),
migrations.AddField(
model_name='message',
name='subject',
field=models.CharField(blank=True, max_length=1000),
),
]

View File

@ -27,6 +27,20 @@ class MessageCorrespondent(models.Model):
token = models.CharField(max_length=64, default=generate_message_token, unique=True)
class MessageAuthor(models.Model):
author_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True)
author_id = models.PositiveIntegerField(null=True, blank=True)
author = GenericForeignKey('author_type', 'author_id')
token = models.CharField(max_length=64, default=generate_message_token, unique=True)
def __str__(self):
author_class = self.author_type.model_class()
if author_class == get_user_model():
return self.author.get_full_name()
else:
return str(self.author)
class MessageThread(models.Model):
created = models.DateTimeField(auto_now_add=True)
token = models.CharField(max_length=64, default=generate_message_token, unique=True)
@ -36,17 +50,16 @@ class MessageManager(models.Manager):
def get_queyset(self):
qs = super().get_queryset()
# Does not work so well as prefetch_related is limited to one content type for generic foreign keys
qs = qs.prefetch_related('author')
qs = qs.prefetch_related('author__author')
return qs
class Message(models.Model):
created = models.DateTimeField(auto_now_add=True)
thread = models.ForeignKey(MessageThread)
author_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True)
author_id = models.PositiveIntegerField(null=True, blank=True)
author = GenericForeignKey('author_type', 'author_id')
from_email = models.EmailField()
author = models.ForeignKey(MessageAuthor)
in_reply_to = models.ForeignKey('self', null=True, blank=True)
subject = models.CharField(max_length=1000, blank=True)
content = models.TextField(blank=True)
token = models.CharField(max_length=64, default=generate_message_token, unique=True)
@ -55,11 +68,12 @@ class Message(models.Model):
class Meta:
ordering = ['created']
def send_notification(self, subject, sender, dests, reply_to=None, message_id=None, reference=None, footer=None):
def send_notification(self, sender, dests, reply_to=None, message_id=None, reference=None, footer=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]
for dest, dest_name, dest_email in dests:
dest_type = ContentType.objects.get_for_model(dest)
dest, _ = MessageAuthor.objects.get_or_create(author_type=dest_type, author_id=dest.pk)
token = self.token + dest.token + hexdigest_sha256(settings.SECRET_KEY, self.token, dest.token)[:16]
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))]
@ -78,7 +92,7 @@ class Message(models.Model):
if footer is not None:
body += footer
messages.append(EmailMessage(
subject=subject,
subject=self.subject,
body=body,
from_email='%s <%s>' % sender,
to=['%s <%s>' % (dest_name, dest_email)],
@ -88,16 +102,5 @@ class Message(models.Model):
connection = get_connection()
connection.send_messages(messages)
@property
def author_display(self):
if self.author:
author_class = ContentType.objects.get_for_model(self.author).model_class()
if author_class == get_user_model():
return self.author.get_full_name()
else:
return str(self.author)
else:
return self.from_email
def __str__(self):
return _("Message from %(author)s") % {'author': self.author_display}
return _("Message from %(author)s") % {'author': str(self.author)}

View File

@ -1,9 +1,9 @@
{% load i18n %}
{% for message in messages %}
<div class="panel panel-{% if message.from_email == participant.email %}info{% else %}default{% endif %}">
<div class="panel panel-default">
<div class="panel-heading">
{{ message.created }} | {{ message.author_display }}
{{ message.created }} | {{ message.author }} | {{ message.subject }}
</div>
<div class="panel-body">
{{ message.content|linebreaksbr }}

View File

@ -1,5 +1,6 @@
from django.conf import settings
from django.db import models
from django.contrib.contenttypes.models import ContentType
import imaplib
import ssl
@ -9,7 +10,7 @@ from email.parser import BytesParser
import chardet
import re
from .models import MessageThread, MessageCorrespondent, Message, hexdigest_sha256
from .models import MessageThread, MessageAuthor, Message, hexdigest_sha256
class NoTokenFoundException(Exception):
@ -22,12 +23,24 @@ class InvalidKeyException(Exception):
pass
def fetch_imap_box(user, password, host, port=993, ssl=True, inbox='INBOX', trash='Trash'):
def send_message(thread, author, subject, content, in_reply_to=None):
author_type = ContentType.objects.get_for_model(author)
author, _ = MessageAuthor.objects.get_or_create(author_type=author_type, author_id=author.pk)
Message.objects.create(
thread=thread,
author=author,
subject=subject,
content=content,
in_reply_to=in_reply_to,
)
def fetch_imap_box(user, password, host, port=993, use_ssl=True, inbox='INBOX', trash='Trash'):
logging.basicConfig(level=logging.DEBUG)
context = ssl.create_default_context()
success, failure = 0, 0
kwargs = {'host': host, 'port': port}
if ssl:
if use_ssl:
IMAP4 = imaplib.IMAP4_SSL
kwargs.update({'ssl_context': ssl.create_default_context()})
else:
@ -116,7 +129,32 @@ def process_email(raw_email):
raise NoTokenFoundException
token = m.group('token')
try:
in_reply_to, author = process_new_token(token)
except InvalidTokenException:
in_reply_to, author = process_old_token(token)
subject = msg.get('Subject', '')
Message.objects.create(thread=in_reply_to.thread, in_reply_to=in_reply_to, author=author, subject=subject, content=content)
def process_new_token(token):
key = token[64:]
try:
in_reply_to = Message.objects.get(token__iexact=token[:32])
author = MessageAuthor.objects.get(token__iexact=token[32:64])
except models.ObjectDoesNotExist:
raise InvalidTokenException
if key.lower() != hexdigest_sha256(settings.SECRET_KEY, in_reply_to.token, author.token)[:16]:
raise InvalidKeyException
return in_reply_to, author
def process_old_token(token):
try:
thread = MessageThread.objects.get(token__iexact=token[:32])
sender = MessageCorrespondent.objects.get(token__iexact=token[32:64])
@ -126,4 +164,25 @@ def process_email(raw_email):
if key.lower() != hexdigest_sha256(settings.SECRET_KEY, thread.token, sender.token)[:16]:
raise InvalidKeyException
Message.objects.create(thread=thread, from_email=sender.email, content=content)
in_reply_to = thread.message_set.last()
author = None
if author is None:
try:
author = User.objects.get(email=sender.email)
except User.DoesNotExist:
pass
if author is None:
try:
author = Participant.objects.get(email=message.from_email)
except Participant.DoesNotExist:
pass
if author is None:
try:
author = Conference.objects.get(contact_email=message.from_email)
except Conference.DoesNotExist:
raise # this was last hope...
author = MessageAuthor.objects.get_or_create(author=author)
return in_reply_to, author