diff --git a/cfp/emails.py b/cfp/emails.py new file mode 100644 index 0000000..78b6c30 --- /dev/null +++ b/cfp/emails.py @@ -0,0 +1,36 @@ +from django.utils.translation import ugettext as _ +from django.utils.html import escape + +from pprint import pformat +from textwrap import indent + +from mailing.utils import send_message +from .environment import TalkEnvironment + + +def talk_email_render_preview(talk, speaker, subject, body): + env = TalkEnvironment(talk, speaker) + try: + subject = env.from_string(subject).render() + except Exception: + return _('There is an error in your subject template.') + try: + body = env.from_string(body).render() + except Exception: + return _('There is an error in your body template.') + context = {'talk': env.globals['talk'], 'speaker': env.globals['speaker']} + preview = '' + _('Environment:') + '\n\n' + escape(indent(pformat(context, indent='2'), ' ')) + preview += '\n\n' + _('Subject:') + ' ' + escape(subject) + '\n' + _('Body:') + '\n' + escape(body) + return preview + + +def talk_email_send(talks, subject, body): + sent = 0 + for talk in talks.all(): + for speaker in talk.speakers.all(): + env = TalkEnvironment(talk, speaker) + s = env.from_string(subject).render() + c = env.from_string(body).render() + send_message(speaker.conversation, talk.site.conference, subject=s, content=c) + sent += 1 + return sent diff --git a/cfp/environment.py b/cfp/environment.py new file mode 100644 index 0000000..ac8247a --- /dev/null +++ b/cfp/environment.py @@ -0,0 +1,36 @@ +from django.conf import settings + +from jinja2.sandbox import SandboxedEnvironment + +import pytz + + +def talk_to_dict(talk): + return { + 'title': talk.title, + 'description': talk.description, + 'category': str(talk.category), + 'accepted': talk.accepted, + 'confirmed': talk.confirmed, + 'start_date': talk.start_date.astimezone(tz=pytz.timezone(settings.TIME_ZONE)) if talk.start_date else None, + 'duration': talk.estimated_duration, + 'track': str(talk.track) if talk.track else '', + 'video': talk.video, + 'speakers': list(map(speaker_to_dict, talk.speakers.all())), + } + + +def speaker_to_dict(speaker): + return { + 'name': speaker.name, + 'email': speaker.email, + } + + +class TalkEnvironment(SandboxedEnvironment): + def __init__(self, talk, speaker, **options): + super().__init__(**options) + self.globals.update({ + 'talk': talk_to_dict(talk), + 'speaker': speaker_to_dict(speaker), + }) diff --git a/cfp/forms.py b/cfp/forms.py index 83ec3f7..1030e0d 100644 --- a/cfp/forms.py +++ b/cfp/forms.py @@ -11,6 +11,7 @@ from django_select2.forms import ModelSelect2MultipleWidget from .models import Participant, Talk, TalkCategory, Track, Tag, \ Conference, Room, Volunteer, Activity +from .environment import TalkEnvironment ACCEPTATION_CHOICES = [ @@ -182,6 +183,7 @@ class TalkActionForm(forms.Form): track = forms.ChoiceField(required=False, choices=[], label=_('Assign to a track')) tag = forms.ChoiceField(required=False, choices=[], label=_('Add a tag')) room = forms.ChoiceField(required=False, choices=[], label=_('Put in a room')) + email = forms.BooleanField(label=_('Send a email')) def __init__(self, *args, **kwargs): site = kwargs.pop('site') @@ -246,10 +248,45 @@ class ParticipantFilterForm(forms.Form): self.fields['track'].choices = [('none', _('Not assigned'))] + list(tracks.values_list('slug', 'name')) -class MailForm(forms.Form): +class EmailForm(forms.Form): email = forms.EmailField(required=True, label=_('Email')) +class PreviewMailForm(forms.Form): + speaker = forms.IntegerField() + talk = forms.IntegerField() + subject = forms.CharField(required=False) + body = forms.CharField(required=False) + + +class SendMailForm(forms.Form): + subject = forms.CharField() + body = forms.CharField(widget=forms.Textarea) + confirm = forms.BooleanField(required=False, label=_('I read my self twice, confirm sending')) + + def __init__(self, *args, **kwargs): + self._talks = kwargs.pop('talks') + super().__init__(*args, **kwargs) + self._env = dict() + + def clean_subject(self): + return self.clean_template('subject') + + def clean_body(self): + return self.clean_template('body') + + def clean_template(self, template): + try: + for talk in self._talks.all(): + for speaker in talk.speakers.all(): + env = self._env.get((talk, speaker), TalkEnvironment(talk, speaker)) + env.from_string(self.cleaned_data.get(template)).render() + except Exception as e: + raise forms.ValidationError(_("Your template does not compile (at least) with talk '%(talk)s' and speaker '%(speaker)s'.") % + {'talk': talk, 'speaker': speaker}) + return self.cleaned_data.get(template) + + class UsersWidget(ModelSelect2MultipleWidget): model = User search_fields = [ '%s__icontains' % field for field in UserAdmin.search_fields ] diff --git a/cfp/templates/cfp/staff/talk_email.html b/cfp/templates/cfp/staff/talk_email.html new file mode 100644 index 0000000..fb91066 --- /dev/null +++ b/cfp/templates/cfp/staff/talk_email.html @@ -0,0 +1,114 @@ +{% extends 'cfp/staff/base.html' %} +{% load i18n bootstrap3 staticfiles %} + +{% block talkstab %} class="active"{% endblock %} + +{% block content %} + + + +
+{% csrf_token %} + +

{% trans "Please write your email bellow:" %}

+ +

+ {% blocktrans %}You can use Jinja2 templating language.{% endblocktrans %} + {% blocktrans %}To see available environment variables, please click on a talk and speaker combination.{% endblocktrans %} +

+ +
+
+ {% bootstrap_form form exclude='confirm' %} +
+
+ +

{% trans "To preview your email, click on a speaker and talk combination:" %}

+ +
+
+ + +
+
+ +
+
+ +
+
+ {% if form.confirm %} + {% bootstrap_field form.confirm %} + {% buttons %} + + {% endbuttons %} + {% else %} + {% buttons %} + + {% endbuttons %} + {% endif %} +
+
+ +
+ +{% endblock %} + +{% block js_end %} +{{ block.super }} +{{ form.media.js }} + + +{% endblock %} + +{% block css %} +{{ block.super }} +{{ form.media.css }} +{% endblock %} diff --git a/cfp/templates/cfp/staff/talk_list.html b/cfp/templates/cfp/staff/talk_list.html index d639bc0..01c95d0 100644 --- a/cfp/templates/cfp/staff/talk_list.html +++ b/cfp/templates/cfp/staff/talk_list.html @@ -5,6 +5,14 @@ {% block content %} +{% if pending_email %} +
+ + {% url 'talk-email' as email_url %} + {% blocktrans %}You have a pending e-mail. To continue its edition, click here.{% endblocktrans %} +
+{% endif %} +

{% trans "Talks" %}

{% trans "Show filtering options…" %}

diff --git a/cfp/urls.py b/cfp/urls.py index c676c01..062cbde 100644 --- a/cfp/urls.py +++ b/cfp/urls.py @@ -37,6 +37,8 @@ urlpatterns = [ path('staff/talks//confirm/', views.talk_acknowledgment, {'confirm': True}, name='talk-confirm-by-staff'), path('staff/talks//desist/', views.talk_acknowledgment, {'confirm': False}, name='talk-desist-by-staff'), path('staff/talks//edit/', views.TalkUpdate.as_view(), name='talk-edit'), + path('staff/talks/email/', views.talk_email, name='talk-email'), + path('staff/talks/email/preview/', views.talk_email_preview, name='talk-email-preview'), path('staff/speakers/', views.participant_list, name='participant-list'), path('staff/speakers/add/', views.ParticipantCreate.as_view(), name='participant-add'), path('staff/speakers//', views.participant_details, name='participant-details'), diff --git a/cfp/views.py b/cfp/views.py index d1c8854..1c01648 100644 --- a/cfp/views.py +++ b/cfp/views.py @@ -5,13 +5,15 @@ from django.urls import reverse, reverse_lazy from django.utils.translation import ugettext_lazy as _ from django.views.generic import DeleteView, FormView, TemplateView from django.contrib import messages -from django.db.models import Q +from django.db.models import Q, Count, Sum from django.views.generic import CreateView, DetailView, ListView, UpdateView -from django.http import HttpResponse, Http404 +from django.http import HttpResponse, Http404, HttpResponseServerError from django.utils import timezone from django.core.exceptions import PermissionDenied from django.core.mail import send_mail from django.forms import modelform_factory +from django import forms +from django.views.decorators.http import require_http_methods from django_select2.views import AutoResponseView @@ -25,10 +27,11 @@ from .decorators import speaker_required, volunteer_required, staff_required from .mixins import StaffRequiredMixin, OnSiteMixin, OnSiteFormMixin from .utils import is_staff from .models import Participant, Talk, TalkCategory, Vote, Track, Tag, Room, Volunteer, Activity +from .emails import talk_email_send, talk_email_render_preview from .forms import TalkForm, TalkStaffForm, TalkFilterForm, TalkActionForm, get_talk_speaker_form_class, \ ParticipantForm, ParticipantFilterForm, NotifyForm, \ ConferenceForm, HomepageForm, CreateUserForm, TrackForm, RoomForm, \ - VolunteerForm, VolunteerFilterForm, MailForm, \ + VolunteerForm, VolunteerFilterForm, EmailForm, PreviewMailForm, SendMailForm, \ TagForm, TalkCategoryForm, ActivityForm, \ ACCEPTATION_VALUES, CONFIRMATION_VALUES @@ -90,7 +93,7 @@ Thanks! def volunteer_mail_token(request): - form = MailForm(request.POST or None) + form = EmailForm(request.POST or None) if request.method == 'POST' and form.is_valid(): try: volunteer = Volunteer.objects.get(site=request.conference.site, email=form.cleaned_data['email']) @@ -272,7 +275,7 @@ Thanks! def proposal_mail_token(request): - form = MailForm(request.POST or None) + form = EmailForm(request.POST or None) if request.method == 'POST' and form.is_valid(): try: speaker = Participant.objects.get(site=request.conference.site, email=form.cleaned_data['email']) @@ -642,6 +645,9 @@ def talk_list(request): if data['room']: talk.room = Room.objects.get(site=request.conference.site, slug=data['room']) talk.save() + if data['email']: + request.session['talk-email-list'] = data['talks'] + return redirect(reverse('talk-email')) return redirect(request.get_full_path()) # Sorting if request.GET.get('order') == 'desc': @@ -687,6 +693,7 @@ def talk_list(request): 'sort_urls': sort_urls, 'sort_glyphicons': sort_glyphicons, 'csv_link': csv_link, + 'pending_email': bool(request.session.get('talk-email-list', None)), }) @@ -777,6 +784,45 @@ def talk_decide(request, talk_id, accept): }) +@staff_required +def talk_email(request): + talks = Talk.objects.filter(pk__in=request.session.get('talk-email-list', [])) + count = talks.annotate(speakers_count=Count('speakers', distinct=True)).aggregate(Sum('speakers_count'))['speakers_count__sum'] + if not talks.exists(): + messages.error(request, _('Please select some talks.')) + return redirect('talk-list') + form = SendMailForm(request.POST or None, initial=request.session.get('talk-email-stored'), talks=talks) + if request.method == 'POST' and form.is_valid(): + subject = form.cleaned_data['subject'] + body = form.cleaned_data['body'] + request.session['talk-email-stored'] = {'subject': subject, 'body': body} + if form.cleaned_data['confirm']: + sent = talk_email_send(talks, subject, body) + messages.success(request, _('%(count)d mails have been sent.') % {'count': sent}) + del request.session['talk-email-list'] + return redirect('talk-list') + else: + messages.info(request, _('Your ready to send %(count)d emails.') % {'count': count}) + else: + form.fields.pop('confirm') + return render(request, 'cfp/staff/talk_email.html', { + 'talks': talks, + 'form': form, + }) + + +@require_http_methods(['POST']) +@staff_required +def talk_email_preview(request): + form = PreviewMailForm(request.POST or None) + if not form.is_valid(): + return HttpResponseServerError() + speaker = get_object_or_404(Participant, site=request.conference.site, pk=form.cleaned_data['speaker']) + talk = get_object_or_404(Talk, site=request.conference.site, pk=form.cleaned_data['talk']) + preview = talk_email_render_preview(talk, speaker, form.cleaned_data['subject'], form.cleaned_data['body']) + return HttpResponse(preview) + + @staff_required def participant_list(request): participants = Participant.objects.filter(site=request.conference.site) \ diff --git a/locale/fr/LC_MESSAGES/django.mo b/locale/fr/LC_MESSAGES/django.mo index ce4146d..b193baa 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 4c362a7..53cc2a2 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-12-01 20:44+0000\n" -"PO-Revision-Date: 2017-12-01 21:44+0100\n" +"POT-Creation-Date: 2017-12-10 15:23+0000\n" +"PO-Revision-Date: 2017-12-10 16:25+0100\n" "Last-Translator: \n" "Language-Team: \n" "Language: fr\n" @@ -100,167 +100,204 @@ msgstr "Profil modifié avec succès !" msgid "Please correct those errors." msgstr "Merci de corriger ces erreurs." -#: cfp/forms.py:17 +#: cfp/emails.py:28 +msgid "There is an error in your subject template." +msgstr "Il y a une erreur dans le gabarit du sujet." + +#: cfp/emails.py:32 +msgid "There is an error in your body template." +msgstr "Il y a une erreur dans le gabarit du corps." + +#: cfp/emails.py:34 +msgid "Environment:" +msgstr "Environnement :" + +#: cfp/emails.py:35 +msgid "Subject:" +msgstr "Sujet :" + +#: cfp/emails.py:35 +msgid "Body:" +msgstr "Corps :" + +#: cfp/forms.py:18 msgid "Pending decision" msgstr "Décision en attente" -#: cfp/forms.py:18 cfp/forms.py:125 cfp/forms.py:222 +#: cfp/forms.py:19 cfp/forms.py:126 cfp/forms.py:224 msgid "Accepted" msgstr "Accepté" -#: cfp/forms.py:19 +#: cfp/forms.py:20 msgid "Declined" msgstr "Décliné" -#: cfp/forms.py:28 +#: cfp/forms.py:29 msgid "Waiting" msgstr "En attente" -#: cfp/forms.py:29 cfp/forms.py:131 cfp/forms.py:228 cfp/models.py:379 +#: cfp/forms.py:30 cfp/forms.py:132 cfp/forms.py:230 cfp/models.py:379 msgid "Confirmed" msgstr "Confirmé" -#: cfp/forms.py:30 cfp/models.py:381 +#: cfp/forms.py:31 cfp/models.py:381 msgid "Cancelled" msgstr "Annulé" -#: cfp/forms.py:62 cfp/models.py:470 +#: cfp/forms.py:63 cfp/models.py:470 msgid "Activity" msgstr "Activité" -#: cfp/forms.py:72 +#: cfp/forms.py:73 msgctxt "activity" msgid "None" msgstr "Aucune" -#: cfp/forms.py:99 +#: cfp/forms.py:100 #, python-format msgid "Default duration: %(duration)d min" msgstr "Durée par défaut : %(duration)d min" -#: cfp/forms.py:107 cfp/forms.py:119 cfp/forms.py:216 +#: cfp/forms.py:108 cfp/forms.py:120 cfp/forms.py:218 #: cfp/templates/cfp/staff/talk_details.html:15 msgid "Category" msgstr "Catégorie" -#: cfp/forms.py:108 cfp/templates/cfp/staff/talk_list.html:43 +#: cfp/forms.py:109 cfp/templates/cfp/staff/talk_list.html:51 msgid "Title" msgstr "Titre" -#: cfp/forms.py:109 cfp/models.py:163 cfp/models.py:466 +#: cfp/forms.py:110 cfp/models.py:163 cfp/models.py:466 #: 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:117 cfp/models.py:491 +#: cfp/forms.py:111 cfp/models.py:117 cfp/models.py:491 #: cfp/templates/cfp/staff/participant_details.html:24 #: cfp/templates/cfp/staff/talk_details.html:83 #: cfp/templates/cfp/staff/volunteer_details.html:22 msgid "Notes" msgstr "Notes" -#: cfp/forms.py:113 +#: cfp/forms.py:114 msgid "Visible by speakers" msgstr "Visible par les orateurs" -#: cfp/forms.py:137 cfp/forms.py:234 cfp/models.py:335 +#: cfp/forms.py:138 cfp/forms.py:236 cfp/models.py:335 #: cfp/templates/cfp/staff/talk_details.html:21 -#: cfp/templates/cfp/staff/talk_list.html:46 +#: cfp/templates/cfp/staff/talk_list.html:54 #: cfp/templates/cfp/staff/track_form.html:14 msgid "Track" msgstr "Session" -#: cfp/forms.py:143 +#: cfp/forms.py:144 msgid "Tag" msgstr "Étiquette" -#: cfp/forms.py:149 cfp/templates/cfp/staff/talk_details.html:88 +#: cfp/forms.py:150 cfp/templates/cfp/staff/talk_details.html:88 msgid "Vote" msgstr "Vote" -#: cfp/forms.py:150 +#: cfp/forms.py:151 msgid "Filter talks you already / not yet voted for" msgstr "" "Filtrer les propositions pour lesquelles vous avez déjà voté / pas encore " "voté" -#: cfp/forms.py:153 cfp/templates/cfp/staff/room_form.html:14 +#: cfp/forms.py:154 cfp/templates/cfp/staff/room_form.html:14 #: cfp/templates/cfp/staff/talk_details.html:38 msgid "Room" msgstr "Salle" -#: cfp/forms.py:154 +#: cfp/forms.py:155 msgid "Filter talks already / not yet affected to a room" msgstr "Filtrer les exposés déjà / pas encore affectées à une salle" -#: cfp/forms.py:157 +#: cfp/forms.py:158 msgid "Scheduled" msgstr "Programmé" -#: cfp/forms.py:158 +#: cfp/forms.py:159 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:353 +#: cfp/forms.py:162 cfp/models.py:353 #: cfp/templates/cfp/proposal_talk_details.html:89 #: cfp/templates/cfp/staff/talk_details.html:54 msgid "Materials" msgstr "Supports" -#: cfp/forms.py:162 +#: cfp/forms.py:163 msgid "Filter talks with / without materials" msgstr "Filtrer les exposés avec / sans supports" -#: cfp/forms.py:165 cfp/templates/cfp/proposal_talk_details.html:93 +#: cfp/forms.py:166 cfp/templates/cfp/proposal_talk_details.html:93 #: cfp/templates/cfp/staff/talk_details.html:58 msgid "Video" msgstr "Vidéo" -#: cfp/forms.py:166 +#: cfp/forms.py:167 msgid "Filter talks with / without video" msgstr "Filtrer les exposés avec / sans vidéo" -#: cfp/forms.py:175 cfp/forms.py:246 +#: cfp/forms.py:176 cfp/forms.py:248 msgid "Not assigned" msgstr "Pas encore assignée" -#: cfp/forms.py:181 +#: cfp/forms.py:182 msgid "Accept talk?" msgstr "Accepter la proposition ?" -#: cfp/forms.py:182 +#: cfp/forms.py:183 msgid "Assign to a track" msgstr "Assigner à une session" -#: cfp/forms.py:183 cfp/templates/cfp/admin/tag_list.html:11 +#: cfp/forms.py:184 cfp/templates/cfp/admin/tag_list.html:11 msgid "Add a tag" msgstr "Ajouter une étiquette" -#: cfp/forms.py:184 +#: cfp/forms.py:185 msgid "Put in a room" msgstr "Assigner à une salle" -#: cfp/forms.py:200 +#: cfp/forms.py:186 +msgid "Send a email" +msgstr "Envoyer un e-mail" + +#: cfp/forms.py:202 msgid "Notify by mail?" msgstr "Notifier par e-mail ?" -#: cfp/forms.py:250 cfp/models.py:486 +#: cfp/forms.py:252 cfp/models.py:486 #: cfp/templates/cfp/staff/volunteer_list.html:30 msgid "Email" msgstr "E-mail" -#: cfp/forms.py:269 +#: cfp/forms.py:258 +msgid "I read my self twice, confirm sending" +msgstr "Je me suis relu 2 fois, confirmer l’envoi" + +#: cfp/forms.py:278 +#, python-format +msgid "" +"Your template does not compile (at least) with talk '%(talk)s' and speaker " +"'%(speaker)s'." +msgstr "" +"Vos gabarits ne compile pas avec (au moins) l’exposé « %(talk)s » et " +"l’intervenant « %(speaker)s »." + +#: cfp/forms.py:299 msgid "New staff members will be informed of their new position by e-mail." msgstr "" "Les nouveaux membres du staff seront informés de leur nouveau rôle par " "courrier électronique." -#: cfp/forms.py:295 +#: cfp/forms.py:325 msgid "An user with that firstname and that lastname already exists." msgstr "Un utilisateur avec ce prénom et ce nom existe déjà." -#: cfp/forms.py:300 +#: cfp/forms.py:330 msgid "A user with that email already exists." msgstr "Un utilisateur avec cet email existe déjà." @@ -383,7 +420,7 @@ msgstr "Label dans le xml du programme" #: cfp/templates/cfp/staff/base.html:10 #: cfp/templates/cfp/staff/participant_list.html:8 #: cfp/templates/cfp/staff/talk_details.html:68 -#: cfp/templates/cfp/staff/talk_list.html:45 +#: cfp/templates/cfp/staff/talk_list.html:53 msgid "Speakers" msgstr "Orateurs" @@ -462,7 +499,7 @@ msgstr "En cours, score : %(score).1f" #: cfp/templates/cfp/admin/base.html:14 #: cfp/templates/cfp/staff/volunteer_details.html:27 #: cfp/templates/cfp/staff/volunteer_list.html:32 -#: cfp/templates/cfp/volunteer.html:43 +#: cfp/templates/cfp/volunteer_dashboard.html:43 msgid "Activities" msgstr "Activités" @@ -514,7 +551,7 @@ msgstr "Catégories" #: cfp/templates/cfp/admin/base.html:13 cfp/templates/cfp/admin/tag_list.html:9 #: cfp/templates/cfp/staff/talk_details.html:28 -#: cfp/templates/cfp/staff/talk_list.html:47 +#: cfp/templates/cfp/staff/talk_list.html:55 msgid "Tags" msgstr "Étiquettes" @@ -597,6 +634,35 @@ msgstr "" "Si vous avez déjà soumis une proposition et que vous souhaitez l’éditer, " "cliquez ici." +#: cfp/templates/cfp/mails/speaker_send_token.txt:1 +msgid "" +"Hi {},\n" +"\n" +"Someone, probably you, ask to access your profile.\n" +"You can edit your talks or add new ones following this url:\n" +"\n" +" {}\n" +"\n" +"If you have any question, your can answer to this email.\n" +"\n" +"Sincerely,\n" +"\n" +"{}\n" +msgstr "" +"Bonjour {},\n" +"\n" +"Quelqu’un, sans doute vous, a demandé à accéder à votre profil.\n" +"Vous pouvez modifier vos propositions ou en soumettre de nouvelles à l’url " +"suivante :\n" +"\n" +" {}\n" +"\n" +"Si vous avez une question, vous pouvez répondre à ce mail.\n" +"\n" +"Sincèrement,\n" +"\n" +"{}\n" + #: cfp/templates/cfp/mails/volunteer_send_token.txt:1 #, python-format msgid "" @@ -641,19 +707,19 @@ msgid "Nope, Abort" msgstr "Non, annuler" #: cfp/templates/cfp/proposal_dashboard.html:11 -#: cfp/templates/cfp/volunteer.html:12 +#: cfp/templates/cfp/volunteer_dashboard.html:12 #, python-format msgid "Welcome %(name)s!" msgstr "Bienvenue %(name)s !" #: cfp/templates/cfp/proposal_dashboard.html:13 #: cfp/templates/cfp/proposal_speaker_form.html:21 -#: cfp/templates/cfp/volunteer.html:14 +#: cfp/templates/cfp/volunteer_dashboard.html:14 msgid "Edit your profile" msgstr "Éditer votre profil" #: cfp/templates/cfp/proposal_dashboard.html:18 -#: cfp/templates/cfp/volunteer.html:20 +#: cfp/templates/cfp/volunteer_dashboard.html:20 msgid "Your informations" msgstr "Vos informations" @@ -695,7 +761,7 @@ msgstr "Mastodon :" #: cfp/templates/cfp/proposal_dashboard.html:29 #: cfp/templates/cfp/staff/participant_details.html:38 #: cfp/templates/cfp/staff/volunteer_details.html:14 -#: cfp/templates/cfp/volunteer.html:27 +#: cfp/templates/cfp/volunteer_dashboard.html:27 msgid "Phone number:" msgstr "Numéro de téléphone :" @@ -715,7 +781,7 @@ msgstr "avec" #: cfp/templates/cfp/staff/participant_details.html:53 #: cfp/templates/cfp/staff/room_details.html:21 #: cfp/templates/cfp/staff/room_details.html:39 -#: cfp/templates/cfp/staff/talk_list.html:69 +#: cfp/templates/cfp/staff/talk_list.html:77 msgid "and" msgstr "et" @@ -723,13 +789,13 @@ msgstr "et" msgid "you must confirm you participation" msgstr "vous devez confirmer votre participation" -#: cfp/templates/cfp/proposal_dashboard.html:61 cfp/views.py:624 -#: cfp/views.py:743 +#: cfp/templates/cfp/proposal_dashboard.html:61 cfp/views.py:626 +#: cfp/views.py:751 msgid "accepted" msgstr "accepté" -#: cfp/templates/cfp/proposal_dashboard.html:63 cfp/views.py:378 -#: cfp/views.py:525 +#: cfp/templates/cfp/proposal_dashboard.html:63 cfp/views.py:380 +#: cfp/views.py:527 msgid "cancelled" msgstr "annulé" @@ -793,7 +859,7 @@ msgstr "Éditer cette proposition" #: cfp/templates/cfp/proposal_talk_details.html:28 #: cfp/templates/cfp/staff/talk_details.html:18 -#: cfp/templates/cfp/staff/talk_list.html:48 +#: cfp/templates/cfp/staff/talk_list.html:56 msgid "Status" msgstr "Statut" @@ -867,7 +933,7 @@ msgstr "Programme" #: cfp/templates/cfp/staff/base.html:11 #: cfp/templates/cfp/staff/participant_details.html:43 -#: cfp/templates/cfp/staff/talk_list.html:8 +#: cfp/templates/cfp/staff/talk_list.html:16 msgid "Talks" msgstr "Exposés" @@ -945,19 +1011,19 @@ msgid "Add a speaker" msgstr "Ajouter un intervenant" #: cfp/templates/cfp/staff/participant_list.html:12 -#: cfp/templates/cfp/staff/talk_list.html:10 +#: cfp/templates/cfp/staff/talk_list.html:18 #: cfp/templates/cfp/staff/volunteer_list.html:11 msgid "Show filtering options…" msgstr "Afficher les options de filtrage…" #: cfp/templates/cfp/staff/participant_list.html:32 -#: cfp/templates/cfp/staff/talk_list.html:31 +#: cfp/templates/cfp/staff/talk_list.html:39 #: cfp/templates/cfp/staff/volunteer_list.html:19 msgid "Filter" msgstr "Filtrer" #: cfp/templates/cfp/staff/participant_list.html:38 -#: cfp/templates/cfp/staff/talk_list.html:39 +#: cfp/templates/cfp/staff/talk_list.html:47 #: cfp/templates/cfp/staff/volunteer_list.html:25 msgid "Total:" msgstr "Total :" @@ -976,7 +1042,7 @@ msgid "contact by email" msgstr "contacter par e-mail" #: cfp/templates/cfp/staff/participant_list.html:53 -#: cfp/templates/cfp/staff/talk_list.html:54 +#: cfp/templates/cfp/staff/talk_list.html:62 #: cfp/templates/cfp/staff/volunteer_list.html:40 msgid "download as csv" msgstr "télécharger au format CSV" @@ -1028,7 +1094,7 @@ msgid "Some talks are not scheduled yet." msgstr "Certains exposés ne sont pas encore planifiés." #: cfp/templates/cfp/staff/room_list.html:24 -#: cfp/templates/cfp/staff/talk_list.html:39 +#: cfp/templates/cfp/staff/talk_list.html:47 #: cfp/templates/cfp/staff/track_list.html:21 msgid "talk" msgstr "exposé" @@ -1145,19 +1211,66 @@ msgstr "" "Commenter cette proposition – ce message sera reçu uniquement par " "l’équipe d’organisation" +#: cfp/templates/cfp/staff/talk_email.html:9 +msgid "Send an email to each speaker of each talk" +msgstr "Envoyer un e-mail à chaque intervenant de chaque exposé" + +#: cfp/templates/cfp/staff/talk_email.html:15 +msgid "Please write your email bellow:" +msgstr "Veuillez écrire votre e-mail ci-dessous :" + +#: cfp/templates/cfp/staff/talk_email.html:18 +msgid "" +"You can use Jinja2 " +"templating language." +msgstr "" +"Vous pouvez utiliser le langage de gabarit Jinja2." + +#: cfp/templates/cfp/staff/talk_email.html:19 +msgid "" +"To see available environment variables, please click on a talk and speaker " +"combination." +msgstr "" +"Pour voir les variables d’environnement disponibles, veuillez cliquer sur " +"une combinaison d’intervenant et exposé." + +#: cfp/templates/cfp/staff/talk_email.html:28 +msgid "To preview your email, click on a speaker and talk combination:" +msgstr "" +"Pour voir un aperçu de votre e-mail, cliquez sur une combinaison " +"d'intervenant et exposé." + +#: cfp/templates/cfp/staff/talk_email.html:52 +msgid "Send!" +msgstr "Envoyer !" + +#: cfp/templates/cfp/staff/talk_email.html:56 +msgid "Check template validity" +msgstr "Vérifier la validité des gabarits" + #: cfp/templates/cfp/staff/talk_form.html:10 msgid "Edit a talk" msgstr "Éditer un exposé" -#: cfp/templates/cfp/staff/talk_list.html:44 +#: cfp/templates/cfp/staff/talk_list.html:12 +#, python-format +msgid "" +"You have a pending e-mail. To continue its edition, click here." +msgstr "" +"Vous avez un e-mail en attente d’envoi. Pour continuer son édition, cliquez " +"ici." + +#: cfp/templates/cfp/staff/talk_list.html:52 msgid "Intervention kind" msgstr "Type d’intervention" -#: cfp/templates/cfp/staff/talk_list.html:87 +#: cfp/templates/cfp/staff/talk_list.html:95 msgid "For selected talks:" msgstr "Pour les exposés sélectionnés :" -#: cfp/templates/cfp/staff/talk_list.html:92 +#: cfp/templates/cfp/staff/talk_list.html:100 msgid "Apply" msgstr "Appliquer" @@ -1178,7 +1291,7 @@ msgid "Volunteer" msgstr "Bénévole" #: cfp/templates/cfp/staff/volunteer_details.html:11 -#: cfp/templates/cfp/volunteer.html:25 +#: cfp/templates/cfp/volunteer_dashboard.html:25 msgid "Email:" msgstr "E-mail :" @@ -1202,20 +1315,20 @@ msgstr "bénévole" msgid "Phone" msgstr "Téléphone" -#: cfp/templates/cfp/volunteer.html:32 +#: cfp/templates/cfp/volunteer_dashboard.html:32 msgctxt "phone number" msgid "not provided" msgstr "non fourni" -#: cfp/templates/cfp/volunteer.html:36 +#: cfp/templates/cfp/volunteer_dashboard.html:36 msgid "Notes:" msgstr "Notes :" -#: cfp/templates/cfp/volunteer.html:53 +#: cfp/templates/cfp/volunteer_dashboard.html:53 msgid "I will be happy to help on that!" msgstr "Je serai heureux d’aider à cela !" -#: cfp/templates/cfp/volunteer.html:55 +#: cfp/templates/cfp/volunteer_dashboard.html:55 msgid "Sorry, I have a setback" msgstr "Désolé, j’ai un contretemps" @@ -1241,7 +1354,7 @@ msgstr "" "mettre à jour vos disponibilités, cliquez ici." -#: cfp/views.py:66 +#: cfp/views.py:68 msgid "" "Hi {},\n" "\n" @@ -1269,44 +1382,44 @@ msgstr "" "{}\n" "\n" -#: cfp/views.py:82 +#: cfp/views.py:84 #, python-format msgid "[%(conference)s] Thank you for your help!" msgstr "[%(conference)s] Merci pour votre aide !" -#: cfp/views.py:85 +#: cfp/views.py:87 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:99 cfp/views.py:281 +#: cfp/views.py:101 cfp/views.py:283 msgid "Sorry, we do not know this email." msgstr "Désolé, nous ne connaissons pas cette e-mail." -#: cfp/views.py:112 cfp/views.py:303 +#: cfp/views.py:114 cfp/views.py:305 #, 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:115 cfp/views.py:308 +#: cfp/views.py:117 cfp/views.py:310 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:135 cfp/views.py:346 cfp/views.py:433 +#: cfp/views.py:137 cfp/views.py:348 cfp/views.py:435 msgid "Changes saved." msgstr "Modifications sauvegardées." -#: cfp/views.py:148 +#: cfp/views.py:150 msgid "Thank you for your participation!" msgstr "Merci pour votre participation !" -#: cfp/views.py:151 +#: cfp/views.py:153 msgid "Okay, no problem!" msgstr "Ok, pas de soucis !" -#: cfp/views.py:234 +#: cfp/views.py:236 msgid "" "Hi {},\n" "\n" @@ -1346,16 +1459,16 @@ msgstr "" "{}\n" "\n" -#: cfp/views.py:261 +#: cfp/views.py:263 #, python-format msgid "[%(conference)s] Thank you for your proposition '%(talk)s'" msgstr "[%(conference)s] Merci pour votre proposition « %(talk)s »" -#: cfp/views.py:267 cfp/views.py:350 +#: cfp/views.py:269 cfp/views.py:352 msgid "You proposition have been successfully submitted!" msgstr "Votre proposition a été transmise avec succès !" -#: cfp/views.py:286 +#: cfp/views.py:288 msgid "" "Hi {},\n" "\n" @@ -1386,37 +1499,37 @@ msgstr "" "{}\n" "\n" -#: cfp/views.py:367 +#: cfp/views.py:369 msgid "You already confirmed your participation to this talk." msgstr "Vous avez déjà confirmé votre participation à cet exposé." -#: cfp/views.py:369 +#: cfp/views.py:371 msgid "You already cancelled your participation to this talk." msgstr "Vous avez déjà annulé votre participation à cet exposé." -#: cfp/views.py:374 +#: cfp/views.py:376 msgid "Your participation has been taken into account, thank you!" msgstr "Votre participation a été prise en compte, merci !" -#: cfp/views.py:375 cfp/views.py:521 +#: cfp/views.py:377 cfp/views.py:523 msgid "confirmed" msgstr "confirmé" -#: cfp/views.py:377 +#: cfp/views.py:379 msgid "We have noted your unavailability." msgstr "Nous avons enregistré votre indisponibilité." -#: cfp/views.py:379 +#: cfp/views.py:381 #, python-format 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:387 +#: cfp/views.py:389 #, python-format msgid "[%(conference)s] %(speaker)s %(action)s his/her participation" msgstr "[%(conference)s] %(speaker)s a %(action) sa participation" -#: cfp/views.py:440 +#: cfp/views.py:442 msgid "" "Hi {},\n" "\n" @@ -1456,87 +1569,101 @@ msgstr "" "{}\n" "\n" -#: cfp/views.py:468 +#: cfp/views.py:470 #, 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:474 cfp/views.py:494 +#: cfp/views.py:476 cfp/views.py:496 msgid "Co-speaker successfully added to the talk." msgstr "Co-intervenant ajouté à l’exposé avec succès." -#: cfp/views.py:507 +#: cfp/views.py:509 msgid "Co-speaker successfully removed from the talk." msgstr "Co-intervenant supprimé de l’exposé avec succès." -#: cfp/views.py:520 +#: cfp/views.py:522 msgid "The speaker confirmation have been noted." msgstr "La confirmation de l’orateur a été notée." -#: cfp/views.py:522 +#: cfp/views.py:524 msgid "The talk have been confirmed." msgstr "L’exposé a été confirmé." -#: cfp/views.py:524 +#: cfp/views.py:526 msgid "The speaker unavailability have been noted." msgstr "L’indisponibilité de l’intervenant a été notée." -#: cfp/views.py:526 +#: cfp/views.py:528 #, python-format msgid "The talk have been %(action)s." msgstr "L’exposé a été %(action)s." -#: cfp/views.py:530 +#: cfp/views.py:532 #, 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:626 cfp/views.py:745 +#: cfp/views.py:628 cfp/views.py:753 msgid "declined" msgstr "décliné" -#: cfp/views.py:627 cfp/views.py:769 +#: cfp/views.py:629 cfp/views.py:777 #, python-format msgid "The talk has been %(action)s." msgstr "L’exposé a été %(action)s." -#: cfp/views.py:631 cfp/views.py:764 +#: cfp/views.py:633 cfp/views.py:772 #, 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:704 +#: cfp/views.py:710 #, python-format msgid "[%(conference)s] New comment about '%(talk)s'" msgstr "[%(conference)s] Nouveau commentaire sur « %(talk)s »" -#: cfp/views.py:718 cfp/views.py:841 +#: cfp/views.py:724 cfp/views.py:893 msgid "Message sent!" msgstr "Message envoyé !" -#: cfp/views.py:732 +#: cfp/views.py:740 msgid "Vote successfully created" msgstr "A voté !" -#: cfp/views.py:732 +#: cfp/views.py:740 msgid "Vote successfully updated" msgstr "Vote mis à jour" -#: cfp/views.py:753 +#: cfp/views.py:761 #, 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:771 +#: cfp/views.py:779 msgid "Decision taken in account" msgstr "Décision enregistrée" -#: cfp/views.py:911 +#: cfp/views.py:792 +msgid "Please select some talks." +msgstr "Veuillez sélectionner un ou plusieurs exposés." + +#: cfp/views.py:801 +#, python-format +msgid "%(count)d mails have been sent." +msgstr "%(count)d e-mails ont été envoyés." + +#: cfp/views.py:805 +#, python-format +msgid "Your ready to send %(count)d emails." +msgstr "Vous êtes prêt pour envoyer %(count)d e-mails." + +#: cfp/views.py:963 msgid "[{}] You have been added to the staff team" msgstr "[{}] Vous avez été ajouté aux membres du staff" -#: cfp/views.py:912 +#: cfp/views.py:964 msgid "" "Hi {},\n" "\n" @@ -1560,15 +1687,15 @@ msgstr "" "{}\n" "\n" -#: cfp/views.py:933 cfp/views.py:945 +#: cfp/views.py:985 cfp/views.py:997 msgid "Modifications successfully saved." msgstr "Modification enregistrée avec succès." -#: cfp/views.py:1109 +#: cfp/views.py:1161 msgid "User created successfully." msgstr "Utilisateur créé avec succès." -#: cfp/views.py:1130 +#: cfp/views.py:1182 #, python-format msgid "Format '%s' not available" msgstr "Format '%s' non disponible" @@ -1586,11 +1713,11 @@ msgstr "Envoyer" msgid "No messages." msgstr "Aucun message." -#: ponyconf/settings.py:142 +#: ponyconf/settings.py:141 msgid "English" msgstr "Anglais" -#: ponyconf/settings.py:143 +#: ponyconf/settings.py:142 msgid "French" msgstr "Français" @@ -1642,33 +1769,13 @@ msgstr "Mot de passe oublié ?" msgid "Password Change" msgstr "Changement de mot de passe" -#~ msgid "" -#~ "Hi {},\n" -#~ "\n" -#~ "Someone, probably you, ask to access your profile.\n" -#~ "You can edit your talks or add new ones following this url:\n" -#~ "\n" -#~ " {}\n" -#~ "\n" -#~ "If you have any question, your can answer to this email.\n" -#~ "\n" -#~ "Sincerely,\n" -#~ "\n" -#~ "{}\n" -#~ msgstr "" -#~ "Bonjour {},\n" -#~ "\n" -#~ "Quelqu’un, sans doute vous, a demandé à accéder à votre profil.\n" -#~ "Vous pouvez modifier vos propositions ou en soumettre de nouvelles à " -#~ "l’url suivante :\n" -#~ "\n" -#~ " {}\n" -#~ "\n" -#~ "Si vous avez une question, vous pouvez répondre à ce mail.\n" -#~ "\n" -#~ "Sincèrement,\n" -#~ "\n" -#~ "{}\n" +#~ msgid "There is an error in your content template." +#~ msgstr "Il y a une erreur dans le gabarit du corps." + +#, fuzzy +#~| msgid "Contact:" +#~ msgid "Content:" +#~ msgstr "Contacter :" #~ msgid "We are looking for help with the following activities:" #~ msgstr "Nous cherchons de l’aide pour les activités suivantes :" @@ -1694,9 +1801,6 @@ msgstr "Changement de mot de passe" #~ msgid "The talk has been declined." #~ msgstr "L’exposé a été décliné." -#~ msgid "Contact:" -#~ msgstr "Contacter :" - #~ msgid "link" #~ msgstr "lien" diff --git a/requirements.in b/requirements.in index a1a51ed..c63d306 100644 --- a/requirements.in +++ b/requirements.in @@ -11,3 +11,4 @@ markdown bleach chardet icalendar +jinja2