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 2a9270e..fe90efc 100644 Binary files a/locale/fr/LC_MESSAGES/django.mo and b/locale/fr/LC_MESSAGES/django.mo differ diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 21beb1b..572a747 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -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 d’ouverture de l’appel à bénévole" msgid "Volunteers enrollment closing date" msgstr "Date de fermeture de l’appel à 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 "" "L’adresse 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 "J’accepte 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 "J’ai 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 d’une 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] Quelqu’un 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 "L’intervenant %(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 "L’intervenant %(speaker)s a ANNULÉ sa participation." +msgid "Speaker %(speaker)s %(action)s his/her participation for %(talk)s." +msgstr "L’orateur %(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é à l’exposé avec succès." -#: cfp/views.py:485 +#: cfp/views.py:495 msgid "Co-speaker successfully removed from the talk." msgstr "Co-intervenant supprimé de l’exposé avec succès." -#: cfp/views.py:498 +#: cfp/views.py:508 msgid "The speaker confirmation have been noted." msgstr "La confirmation de l’orateur a été notée." -#: cfp/views.py:499 +#: cfp/views.py:510 msgid "The talk have been confirmed." msgstr "L’exposé a été confirmé." -#: cfp/views.py:501 +#: cfp/views.py:512 msgid "The speaker unavailability have been noted." msgstr "L’indisponibilité de l’intervenant a été notée." -#: cfp/views.py:502 -msgid "The talk have been cancelled." -msgstr "L’exposé a été annulé." +#: cfp/views.py:514 +#, python-format +msgid "The talk have been %(action)s." +msgstr "L’exposé a été %(action)s." -#: cfp/views.py:591 cfp/views.py:696 -msgid "The talk has been accepted." -msgstr "L’exposé a été accepté." +#: cfp/views.py:518 +#, python-format +msgid "[%(conference)s] The talk '%(talk)s' have been %(action)s." +msgstr "[%(conference)s] L’exposé « %(talk)s » a été %(action)s." -#: cfp/views.py:593 cfp/views.py:698 -msgid "The talk has been declined." -msgstr "L’exposé 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 "L’exposé 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] L’exposé « %(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 l’exposé 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 "L’intervenant %(speaker)s a ANNULÉ sa participation." + +#~ msgid "The talk have been cancelled." +#~ msgstr "L’exposé a été annulé." + +#~ msgid "The talk has been declined." +#~ msgstr "L’exposé a été décliné." + #~ msgid "Contact:" #~ msgstr "Contacter :" diff --git a/mailing/forms.py b/mailing/forms.py index 80eab2f..38bb35e 100644 --- a/mailing/forms.py +++ b/mailing/forms.py @@ -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']) diff --git a/mailing/migrations/0003_auto_20171129_2155.py b/mailing/migrations/0003_auto_20171129_2155.py new file mode 100644 index 0000000..9d7ae61 --- /dev/null +++ b/mailing/migrations/0003_auto_20171129_2155.py @@ -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), + ), + ] diff --git a/mailing/models.py b/mailing/models.py index 2e039d9..82a6455 100644 --- a/mailing/models.py +++ b/mailing/models.py @@ -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)} 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