From 8c7660a2405c0d25c091b505fc13bf115ba912e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89lie=20Bouttier?= Date: Thu, 30 Nov 2017 20:34:12 +0100 Subject: [PATCH] improve mailing system --- cfp/models.py | 2 +- cfp/signals.py | 87 ++---- cfp/templates/cfp/staff/participant_list.html | 5 - cfp/views.py | 159 +++++++--- locale/fr/LC_MESSAGES/django.mo | Bin 25508 -> 26413 bytes locale/fr/LC_MESSAGES/django.po | 290 +++++++++++------- mailing/forms.py | 2 +- mailing/migrations/0003_auto_20171129_2155.py | 97 ++++++ mailing/models.py | 47 +-- mailing/templates/mailing/_message_list.html | 4 +- mailing/utils.py | 67 +++- 11 files changed, 497 insertions(+), 263 deletions(-) create mode 100644 mailing/migrations/0003_auto_20171129_2155.py diff --git a/cfp/models.py b/cfp/models.py index c7ed501..72c6284 100644 --- a/cfp/models.py +++ b/cfp/models.py @@ -86,7 +86,7 @@ class Conference(models.Model): }) def __str__(self): - return str(self.site) + return self.name class ParticipantManager(models.Manager): diff --git a/cfp/signals.py b/cfp/signals.py index 6fbc901..b582901 100644 --- a/cfp/signals.py +++ b/cfp/signals.py @@ -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 diff --git a/cfp/templates/cfp/staff/participant_list.html b/cfp/templates/cfp/staff/participant_list.html index bcd09fa..773fc2e 100644 --- a/cfp/templates/cfp/staff/participant_list.html +++ b/cfp/templates/cfp/staff/participant_list.html @@ -69,11 +69,6 @@ — {% blocktrans count refused=participant.refused_talk_count %}refused: {{ refused }}{% plural %}refused: {{ refused }}{% endblocktrans %} - {% comment %} - - {% trans "Contact" %} - - {% endcomment %} {% if forloop.last %} diff --git a/cfp/views.py b/cfp/views.py index 3b4ee2e..5f969b2 100644 --- a/cfp/views.py +++ b/cfp/views.py @@ -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', { diff --git a/locale/fr/LC_MESSAGES/django.mo b/locale/fr/LC_MESSAGES/django.mo index 2a9270e7bb4e89007431a2d76006d1eabb890e83..fe90efcbb6cb7624acdc14785dcb8900848da674 100644 GIT binary patch delta 8046 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' % (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)} diff --git a/mailing/templates/mailing/_message_list.html b/mailing/templates/mailing/_message_list.html index f79d128..8bc49a9 100644 --- a/mailing/templates/mailing/_message_list.html +++ b/mailing/templates/mailing/_message_list.html @@ -1,9 +1,9 @@ {% load i18n %} {% for message in messages %} -
+
- {{ message.created }} | {{ message.author_display }} + {{ message.created }} | {{ message.author }} | {{ message.subject }}
{{ message.content|linebreaksbr }} diff --git a/mailing/utils.py b/mailing/utils.py index 4317e89..683ae95 100644 --- a/mailing/utils.py +++ b/mailing/utils.py @@ -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