templated email to all speakers of a list of talks

This commit is contained in:
Élie Bouttier 2017-12-02 02:34:23 +01:00
parent 6cbbb6bd1f
commit aa8d4b3984
10 changed files with 531 additions and 147 deletions

36
cfp/emails.py Normal file
View File

@ -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 = '<b>' + _('Environment:') + '</b>\n\n' + escape(indent(pformat(context, indent='2'), ' '))
preview += '\n\n<b>' + _('Subject:') + '</b> ' + escape(subject) + '\n<b>' + _('Body:') + '</b>\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

36
cfp/environment.py Normal file
View File

@ -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),
})

View File

@ -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 ]

View File

@ -0,0 +1,114 @@
{% extends 'cfp/staff/base.html' %}
{% load i18n bootstrap3 staticfiles %}
{% block talkstab %} class="active"{% endblock %}
{% block content %}
<div class="page-header">
<h1>{% trans "Send an email to each speaker of each talk" %}</h1>
</div>
<form method="POST">
{% csrf_token %}
<h4>{% trans "Please write your email bellow:" %}</h4>
<p>
{% blocktrans %}You can use <a href="http://jinja.pocoo.org/docs/2.10/">Jinja2</a> templating language.{% endblocktrans %}
{% blocktrans %}To see available environment variables, please click on a talk and speaker combination.{% endblocktrans %}
</p>
<div class="row">
<div class="col-md-12">
{% bootstrap_form form exclude='confirm' %}
</div>
</div>
<h4>{% trans "To preview your email, click on a speaker and talk combination:" %}</h4>
<div class="row">
<div class="col-md-12">
<a href="preview"></a>
<pre id="preview" class="hidden"></pre>
</div>
<div class="col-md-12">
<ul class="list-group">
{% for talk in talks %}
{% for speaker in talk.speakers.all %}
<a class="list-group-item" onclick="preview({{ speaker.pk }}, {{ talk.pk }});">
<b>{{ speaker }}</b> {{ talk }}
</a>
{% endfor %}
{% endfor %}
</ul>
</div>
</div>
<div class="row">
<div class="col-md-12">
{% if form.confirm %}
{% bootstrap_field form.confirm %}
{% buttons %}
<button type="submit" class="btn btn-primary">{% trans "Send!" %}</button>
{% endbuttons %}
{% else %}
{% buttons %}
<button type="submit" class="btn btn-primary">{% trans "Check template validity" %}</button>
{% endbuttons %}
{% endif %}
</div>
</div>
</form>
{% endblock %}
{% block js_end %}
{{ block.super }}
{{ form.media.js }}
<script src="{% static 'jquery.cookie/jquery.cookie.js' %}"></script>
<script type="text/javascript">
var csrftoken = $.cookie('csrftoken');
var preview_url = "{% url 'talk-email-preview' %}";
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
function preview(speaker, talk) {
$('#preview').removeClass('hidden');
$('#preview').html('Loading preview...');
var body = $('#body').val();
$.post(preview_url, {
'speaker': speaker,
'talk': talk,
'subject': $('#id_subject').val(),
'body': $('#id_body').val(),
})
.done(function(data, textStatus) {
$('#preview').html(data);
})
.fail(function () {
$('#preview').html('Sorry, an error occured.');
})
.always(function () {
$(document).scrollTop($('#preview').offset().top);
});
}
</script>
{% endblock %}
{% block css %}
{{ block.super }}
{{ form.media.css }}
{% endblock %}

View File

@ -5,6 +5,14 @@
{% block content %}
{% if pending_email %}
<div class="alert alert-warning">
<span class="glyphicon glyphicon-exclamation-sign"></span>
{% url 'talk-email' as email_url %}
{% blocktrans %}You have a pending e-mail. To continue its edition, click <a href="{{ email_url }}">here</a>.{% endblocktrans %}
</div>
{% endif %}
<h1>{% trans "Talks" %}</h1>
<p><a class="btn btn-primary" role="button" data-toggle="collapse" href="#filter" aria-expanded="{{ show_filters|yesno:"true,false" }}" aria-controles="filter">{% trans "Show filtering options…" %}</a></p>

View File

@ -37,6 +37,8 @@ urlpatterns = [
path('staff/talks/<int:talk_id>/confirm/', views.talk_acknowledgment, {'confirm': True}, name='talk-confirm-by-staff'),
path('staff/talks/<int:talk_id>/desist/', views.talk_acknowledgment, {'confirm': False}, name='talk-desist-by-staff'),
path('staff/talks/<int:talk_id>/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/<int:participant_id>/', views.participant_details, name='participant-details'),

View File

@ -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) \

Binary file not shown.

View File

@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-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 lenvoi"
#: 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) lexposé « %(talk)s » et "
"lintervenant « %(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 <a href=\"%(mail_token_url)s\">ici</a>."
#: 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"
"Quelquun, sans doute vous, a demandé à accéder à votre profil.\n"
"Vous pouvez modifier vos propositions ou en soumettre de nouvelles à lurl "
"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 <b>%(name)s</b>!"
msgstr "Bienvenue <b>%(name)s</b> !"
#: 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 <em>ce message sera reçu uniquement par "
"léquipe dorganisation</em>"
#: 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 <a href=\"http://jinja.pocoo.org/docs/2.10/\">Jinja2</a> "
"templating language."
msgstr ""
"Vous pouvez utiliser le langage de gabarit <a href=\"http://jinja.pocoo.org/"
"docs/2.10/\">Jinja2</a>."
#: 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 denvironnement disponibles, veuillez cliquer sur "
"une combinaison dintervenant 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 <a href="
"\"%(email_url)s\">here</a>."
msgstr ""
"Vous avez un e-mail en attente denvoi. Pour continuer son édition, cliquez "
"<a href=\"%(email_url)s\">ici</a>."
#: cfp/templates/cfp/staff/talk_list.html:52
msgid "Intervention kind"
msgstr "Type dintervention"
#: 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 daider à cela !"
#: cfp/templates/cfp/volunteer.html:55
#: cfp/templates/cfp/volunteer_dashboard.html:55
msgid "Sorry, I have a setback"
msgstr "Désolé, jai un contretemps"
@ -1241,7 +1354,7 @@ msgstr ""
"mettre à jour vos disponibilités, cliquez <a href=\"%(mail_token_url)s"
"\">ici</a>."
#: 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] Quelquun 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 "Lorateur %(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é à lexposé avec succès."
#: cfp/views.py:507
#: cfp/views.py:509
msgid "Co-speaker successfully removed from the talk."
msgstr "Co-intervenant supprimé de lexposé avec succès."
#: cfp/views.py:520
#: cfp/views.py:522
msgid "The speaker confirmation have been noted."
msgstr "La confirmation de lorateur a été notée."
#: cfp/views.py:522
#: cfp/views.py:524
msgid "The talk have been confirmed."
msgstr "Lexposé a été confirmé."
#: cfp/views.py:524
#: cfp/views.py:526
msgid "The speaker unavailability have been noted."
msgstr "Lindisponibilité de lintervenant a été notée."
#: cfp/views.py:526
#: cfp/views.py:528
#, python-format
msgid "The talk have been %(action)s."
msgstr "Lexposé 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] Lexposé « %(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 "Lexposé 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] Lexposé « %(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"
#~ "Quelquun, sans doute vous, a demandé à accéder à votre profil.\n"
#~ "Vous pouvez modifier vos propositions ou en soumettre de nouvelles à "
#~ "lurl 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 laide pour les activités suivantes :"
@ -1694,9 +1801,6 @@ msgstr "Changement de mot de passe"
#~ msgid "The talk has been declined."
#~ msgstr "Lexposé a été décliné."
#~ msgid "Contact:"
#~ msgstr "Contacter :"
#~ msgid "link"
#~ msgstr "lien"

View File

@ -11,3 +11,4 @@ markdown
bleach
chardet
icalendar
jinja2