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): def __str__(self):
return str(self.site) return self.name
class ParticipantManager(models.Manager): 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.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth import get_user_model
from ponyconf.decorators import disable_for_loaddata from ponyconf.decorators import disable_for_loaddata
from mailing.models import MessageThread, Message from mailing.models import MessageThread, Message
from mailing.utils import send_message
from .models import Participant, Talk, Conference, Volunteer 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) 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") @receiver(post_save, sender=Message, dispatch_uid="Send message notifications")
def send_message_notifications(sender, instance, **kwargs): def send_message_notifications(sender, instance, **kwargs):
message = instance message = instance
author = message.author.author
thread = message.thread thread = message.thread
first_message = thread.message_set.first() if message.in_reply_to:
if message == first_message: reference = message.in_reply_to.token
reference = None
else: else:
reference = first_message.token reference = None
subject_prefix = 'Re: ' if reference else ''
if hasattr(thread, 'participant'): if hasattr(thread, 'participant'):
conf = thread.participant.site.conference conf = thread.participant.site.conference
elif hasattr(thread, 'talk'): elif hasattr(thread, 'talk'):
conf = thread.talk.site.conference conf = thread.talk.site.conference
elif hasattr(thread, 'volunteer'):
conf = thread.volunteer.site.conference
message_id = '<{id}@%s>' % conf.site.domain message_id = '<{id}@%s>' % conf.site.domain
if conf.reply_email: if conf.reply_email:
reply_to = (conf.name, conf.reply_email) reply_to = (str(conf), conf.reply_email)
else: else:
reply_to = None reply_to = None
sender = (message.author_display, conf.contact_email) if type(author) == get_user_model():
staff_dests = [ (user.get_full_name(), user.email) for user in conf.staff.all() ] sender = author.get_full_name()
if hasattr(thread, 'participant'): else:
conf = thread.participant.site.conference sender = str(author)
participant = thread.participant sender = (sender, conf.contact_email)
participant_dests = [ (participant.name, participant.email) ] staff_dests = [ (user, user.get_full_name(), user.email) for user in conf.staff.all() ]
participant_subject = _('[%(prefix)s] Message from the staff') % {'prefix': conf.name} if hasattr(thread, 'participant') or hasattr(thread, 'volunteer'):
staff_subject = _('[%(prefix)s] Conversation with %(dest)s') % {'prefix': conf.name, 'dest': participant.name} if hasattr(thread, 'participant'):
proto = 'https' if conf.secure_domain else 'http' user = thread.participant
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)
else: else:
# this is a message between the staff and the participant user = thread.volunteer
message.send_notification(subject=subject_prefix+staff_subject, sender=sender, dests=staff_dests, dests = [ (user, user.name, user.email) ]
reply_to=reply_to, message_id=message_id, reference=reference, footer=footer) if author == user: # message from the user, notify the staff
if message.from_email != thread.participant.email: # message from staff: sent it to the participant too message.send_notification(sender=sender, dests=staff_dests, reply_to=reply_to, message_id=message_id, reference=reference)
message.send_notification(subject=subject_prefix+participant_subject, sender=sender, dests=participant_dests, else: # message to the user, notify the user, and the staff if the message is not a conference notification
reply_to=reply_to, message_id=message_id, reference=reference) 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'): elif hasattr(thread, 'talk'):
conf = thread.talk.site.conference message.send_notification(sender=sender, dests=staff_dests,
subject = _('[%(prefix)s] Talk: %(talk)s') % {'prefix': conf.name, 'talk': thread.talk.title} reply_to=reply_to, message_id=message_id, reference=reference)
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)
# connected in apps.py # 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> <span class="text-danger">{% blocktrans count refused=participant.refused_talk_count %}refused: {{ refused }}{% plural %}refused: {{ refused }}{% endblocktrans %}</span>
</td> </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> </tr>
{% if forloop.last %} {% if forloop.last %}
</tbody> </tbody>

View File

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

Binary file not shown.

View File

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

View File

@ -3,4 +3,4 @@ from django.forms.models import modelform_factory
from .models import Message 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) 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): class MessageThread(models.Model):
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
token = models.CharField(max_length=64, default=generate_message_token, unique=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): def get_queyset(self):
qs = super().get_queryset() qs = super().get_queryset()
# Does not work so well as prefetch_related is limited to one content type for generic foreign keys # 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 return qs
class Message(models.Model): class Message(models.Model):
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
thread = models.ForeignKey(MessageThread) thread = models.ForeignKey(MessageThread)
author_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True) author = models.ForeignKey(MessageAuthor)
author_id = models.PositiveIntegerField(null=True, blank=True) in_reply_to = models.ForeignKey('self', null=True, blank=True)
author = GenericForeignKey('author_type', 'author_id') subject = models.CharField(max_length=1000, blank=True)
from_email = models.EmailField()
content = models.TextField(blank=True) content = models.TextField(blank=True)
token = models.CharField(max_length=64, default=generate_message_token, unique=True) token = models.CharField(max_length=64, default=generate_message_token, unique=True)
@ -55,11 +68,12 @@ class Message(models.Model):
class Meta: class Meta:
ordering = ['created'] 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 = [] messages = []
for dest_name, dest_email in dests: for dest, dest_name, dest_email in dests:
correspondent, created = MessageCorrespondent.objects.get_or_create(email=dest_email) dest_type = ContentType.objects.get_for_model(dest)
token = self.thread.token + correspondent.token + hexdigest_sha256(settings.SECRET_KEY, self.thread.token, correspondent.token)[:16] 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: if reply_to:
reply_to_name, reply_to_email = reply_to reply_to_name, reply_to_email = reply_to
reply_to_list = ['%s <%s>' % (reply_to_name, reply_to_email.format(token=token))] 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: if footer is not None:
body += footer body += footer
messages.append(EmailMessage( messages.append(EmailMessage(
subject=subject, subject=self.subject,
body=body, body=body,
from_email='%s <%s>' % sender, from_email='%s <%s>' % sender,
to=['%s <%s>' % (dest_name, dest_email)], to=['%s <%s>' % (dest_name, dest_email)],
@ -88,16 +102,5 @@ class Message(models.Model):
connection = get_connection() connection = get_connection()
connection.send_messages(messages) 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): 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 %} {% load i18n %}
{% for message in messages %} {% 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"> <div class="panel-heading">
{{ message.created }} | {{ message.author_display }} {{ message.created }} | {{ message.author }} | {{ message.subject }}
</div> </div>
<div class="panel-body"> <div class="panel-body">
{{ message.content|linebreaksbr }} {{ message.content|linebreaksbr }}

View File

@ -1,5 +1,6 @@
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.contrib.contenttypes.models import ContentType
import imaplib import imaplib
import ssl import ssl
@ -9,7 +10,7 @@ from email.parser import BytesParser
import chardet import chardet
import re import re
from .models import MessageThread, MessageCorrespondent, Message, hexdigest_sha256 from .models import MessageThread, MessageAuthor, Message, hexdigest_sha256
class NoTokenFoundException(Exception): class NoTokenFoundException(Exception):
@ -22,12 +23,24 @@ class InvalidKeyException(Exception):
pass 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) logging.basicConfig(level=logging.DEBUG)
context = ssl.create_default_context() context = ssl.create_default_context()
success, failure = 0, 0 success, failure = 0, 0
kwargs = {'host': host, 'port': port} kwargs = {'host': host, 'port': port}
if ssl: if use_ssl:
IMAP4 = imaplib.IMAP4_SSL IMAP4 = imaplib.IMAP4_SSL
kwargs.update({'ssl_context': ssl.create_default_context()}) kwargs.update({'ssl_context': ssl.create_default_context()})
else: else:
@ -116,7 +129,32 @@ def process_email(raw_email):
raise NoTokenFoundException raise NoTokenFoundException
token = m.group('token') 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:] 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: try:
thread = MessageThread.objects.get(token__iexact=token[:32]) thread = MessageThread.objects.get(token__iexact=token[:32])
sender = MessageCorrespondent.objects.get(token__iexact=token[32:64]) 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]: if key.lower() != hexdigest_sha256(settings.SECRET_KEY, thread.token, sender.token)[:16]:
raise InvalidKeyException 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