templated emails to a list of speakers

This commit is contained in:
Élie Bouttier 2017-12-16 13:20:26 +01:00
parent 47ea352998
commit 3710c6c708
8 changed files with 288 additions and 14 deletions

View File

@ -5,7 +5,7 @@ from pprint import pformat
from textwrap import indent
from mailing.utils import send_message
from .environment import TalkEnvironment
from .environment import TalkEnvironment, SpeakerEnvironment
def talk_email_render_preview(talk, speaker, subject, body):
@ -24,6 +24,22 @@ def talk_email_render_preview(talk, speaker, subject, body):
return preview
def speaker_email_render_preview(speaker, subject, body):
env = SpeakerEnvironment(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 = {'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():
@ -34,3 +50,14 @@ def talk_email_send(talks, subject, body):
send_message(speaker.conversation, talk.site.conference, subject=s, content=c)
sent += 1
return sent
def speaker_email_send(speakers, subject, body):
sent = 0
for speaker in speakers.all():
env = SpeakerEnvironment(speaker)
s = env.from_string(subject).render()
c = env.from_string(body).render()
send_message(speaker.conversation, speaker.site.conference, subject=s, content=c)
sent += 1
return sent

View File

@ -20,11 +20,16 @@ def talk_to_dict(talk):
}
def speaker_to_dict(speaker):
return {
def speaker_to_dict(speaker, include_talks=False):
d = {
'name': speaker.name,
'email': speaker.email,
}
if include_talks:
d.update({
'talks': list(map(talk_to_dict, speaker.talk_set.all())),
})
return d
class TalkEnvironment(SandboxedEnvironment):
@ -34,3 +39,11 @@ class TalkEnvironment(SandboxedEnvironment):
'talk': talk_to_dict(talk),
'speaker': speaker_to_dict(speaker),
})
class SpeakerEnvironment(SandboxedEnvironment):
def __init__(self, speaker, **options):
super().__init__(**options)
self.globals.update({
'speaker': speaker_to_dict(speaker, include_talks=True),
})

View File

@ -11,7 +11,7 @@ from django_select2.forms import ModelSelect2MultipleWidget
from .models import Participant, Talk, TalkCategory, Track, Tag, \
Conference, Room, Volunteer, Activity
from .environment import TalkEnvironment
from .environment import TalkEnvironment, SpeakerEnvironment
ACCEPTATION_CHOICES = [
@ -183,7 +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'))
email = forms.BooleanField(required=False, label=_('Send a email'))
def __init__(self, *args, **kwargs):
site = kwargs.pop('site')
@ -198,6 +198,16 @@ class TalkActionForm(forms.Form):
self.fields['room'].choices = [(None, "---------")] + list(rooms.values_list('slug', 'name'))
class SpeakerActionForm(forms.Form):
speakers = forms.MultipleChoiceField(choices=[])
email = forms.BooleanField(required=False, label=_('Send a email'))
def __init__(self, *args, **kwargs):
speakers = kwargs.pop('speakers')
super().__init__(*args, **kwargs)
self.fields['speakers'].choices = [(speaker.pk, None) for speaker in speakers.all()]
class NotifyForm(forms.Form):
notify = forms.BooleanField(initial=True, required=False, label=_('Notify by mail?'))
@ -252,14 +262,20 @@ class EmailForm(forms.Form):
email = forms.EmailField(required=True, label=_('Email'))
class PreviewMailForm(forms.Form):
class PreviewTalkMailForm(forms.Form):
speaker = forms.IntegerField()
talk = forms.IntegerField()
subject = forms.CharField(required=False, label=_('Subject'))
body = forms.CharField(required=False, label=_('Body'))
class SendMailForm(forms.Form):
class PreviewSpeakerMailForm(forms.Form):
speaker = forms.IntegerField()
subject = forms.CharField(required=False, label=_('Subject'))
body = forms.CharField(required=False, label=_('Body'))
class SendTalkMailForm(forms.Form):
subject = forms.CharField()
body = forms.CharField(widget=forms.Textarea)
confirm = forms.BooleanField(required=False, label=_('I read my self twice, confirm sending'))
@ -287,6 +303,33 @@ class SendMailForm(forms.Form):
return self.cleaned_data.get(template)
class SendSpeakerMailForm(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._speakers = kwargs.pop('speakers')
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 speaker in self._speakers.all():
env = self._env.get(speaker, SpeakerEnvironment(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 speaker '%(speaker)s'.") %
{'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

@ -5,6 +5,14 @@
{% block content %}
{% if pending_email %}
<div class="alert alert-warning">
<span class="glyphicon glyphicon-exclamation-sign"></span>
{% url 'speaker-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 "Speakers" %}</h1>
<p>
@ -34,11 +42,15 @@
</div>
</div>
<form method="post">
<table class="table table-bordered table-hover">
<caption>{% trans "Total:" %} {{ participant_list|length }} {% trans "speaker" %}{{ participant_list|length|pluralize }}
<caption>
{% trans "Total:" %} {{ participant_list|length }} {% trans "speaker" %}{{ participant_list|length|pluralize }}
</caption>
<thead>
<tr>
<th></th>
<th class="text-center">{% trans "Name" %}</th>
<th class="text-center">{% trans "Talk count" %}</th>
{% comment %}<th class="text-center"></th>{% endcomment %}
@ -58,6 +70,7 @@
<tbody>
{% endif %}
<tr>
<td><input type="checkbox" name="speakers" value="{{ participant.pk }}"></td>
<td>
<a href="{% url 'participant-details' participant.pk %}">{{ participant }}</a>
{% if participant.vip %}<span class="badge pull-right">VIP</span>{% endif %}
@ -76,4 +89,18 @@
{% endfor %}
</table>
<div id="filter">
<div class="well">
<h4>{% trans "For selected speakers:" %}</h4>
{% csrf_token %}
{% bootstrap_form_errors action_form %}
{% bootstrap_form action_form exclude="speakers" %}
{% buttons %}
<button type="submit" class="btn btn-primary">{% trans "Apply" %}</button>
{% endbuttons %}
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,111 @@
{% extends 'cfp/staff/base.html' %}
{% load i18n bootstrap3 staticfiles %}
{% block speakerstab %} class="active"{% endblock %}
{% block content %}
<div class="page-header">
<h1>{% trans "Send an email to each speaker" %}</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:" %}</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 speaker in speakers.all %}
<a class="list-group-item" onclick="preview({{ speaker.pk }});">
<b>{{ speaker }}</b> {{ talk }}
</a>
{% 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 'speaker-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) {
$('#preview').removeClass('hidden');
$('#preview').html('Loading preview...');
var body = $('#body').val();
$.post(preview_url, {
'speaker': speaker,
'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

@ -15,7 +15,11 @@
<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>
<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>
<div class="collapse{{ show_filters|yesno:" in," }}" id="filter">
<div class="well">

View File

@ -45,6 +45,8 @@ urlpatterns = [
path('staff/speakers/<int:participant_id>/add-talk/', views.participant_add_talk, name='participant-add-talk'),
path('staff/speakers/<int:participant_id>/edit/', views.ParticipantUpdate.as_view(), name='participant-edit'),
path('staff/speakers/<int:participant_id>/remove/', views.ParticipantRemove.as_view(), name='participant-remove'),
path('staff/speakers/email/', views.speaker_email, name='speaker-email'),
path('staff/speakers/email/preview/', views.speaker_email_preview, name='speaker-email-preview'),
path('staff/tracks/', views.TrackList.as_view(), name='track-list'),
path('staff/tracks/add/', views.TrackCreate.as_view(), name='track-add'),
path('staff/tracks/<slug:slug>/edit/', views.TrackUpdate.as_view(), name='track-edit'),

View File

@ -27,11 +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, \
from .emails import talk_email_send, talk_email_render_preview, speaker_email_send, speaker_email_render_preview
from .forms import TalkForm, TalkStaffForm, TalkFilterForm, TalkActionForm, SpeakerActionForm, get_talk_speaker_form_class, \
ParticipantForm, ParticipantFilterForm, NotifyForm, \
ConferenceForm, HomepageForm, CreateUserForm, TrackForm, RoomForm, \
VolunteerForm, VolunteerFilterForm, EmailForm, PreviewMailForm, SendMailForm, \
VolunteerForm, VolunteerFilterForm, EmailForm, PreviewTalkMailForm, PreviewSpeakerMailForm, SendTalkMailForm, SendSpeakerMailForm, \
TagForm, TalkCategoryForm, ActivityForm, \
ACCEPTATION_VALUES, CONFIRMATION_VALUES
@ -791,7 +791,7 @@ def talk_email(request):
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)
form = SendTalkMailForm(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']
@ -814,7 +814,7 @@ def talk_email(request):
@require_http_methods(['POST'])
@staff_required
def talk_email_preview(request):
form = PreviewMailForm(request.POST or None)
form = PreviewTalkMailForm(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'])
@ -854,6 +854,14 @@ def participant_list(request):
q |= Q(track__slug__in=data['track'])
talks = talks.filter(q)
participants = participants.filter(talk__in=talks)
# Action
action_form = SpeakerActionForm(request.POST or None, speakers=participants)
if request.method == 'POST' and action_form.is_valid():
data = action_form.cleaned_data
if data['email']:
request.session['speaker-email-list'] = data['speakers']
return redirect(reverse('speaker-email'))
return redirect(request.get_full_path())
if request.GET.get('format') == 'csv':
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="participants.csv"'
@ -868,10 +876,12 @@ def participant_list(request):
csv_link = '?' + csv_query_dict.urlencode()
return render(request, 'cfp/staff/participant_list.html', {
'filter_form': filter_form,
'action_form': action_form,
'participant_list': participants,
'show_filters': show_filters,
'contact_link': contact_link,
'csv_link': csv_link,
'pending_email': bool(request.session.get('speaker-email-list', None)),
})
@ -943,6 +953,43 @@ def participant_add_talk(request, participant_id):
})
@staff_required
def speaker_email(request):
speakers = Participant.objects.filter(pk__in=request.session.get('speaker-email-list', []))
if not speakers.exists():
messages.error(request, _('Please select some speakers.'))
return redirect('participant-list')
form = SendSpeakerMailForm(request.POST or None, initial=request.session.get('speaker-email-stored'), speakers=speakers)
if request.method == 'POST' and form.is_valid():
subject = form.cleaned_data['subject']
body = form.cleaned_data['body']
request.session['speaker-email-stored'] = {'subject': subject, 'body': body}
if form.cleaned_data['confirm']:
sent = speaker_email_send(speakers, subject, body)
messages.success(request, _('%(count)d mails have been sent.') % {'count': sent})
del request.session['speaker-email-list']
return redirect('participant-list')
else:
messages.info(request, _('Your ready to send %(count)d emails.') % {'count': speakers.count()})
else:
form.fields.pop('confirm')
return render(request, 'cfp/staff/speaker_email.html', {
'speakers': speakers,
'form': form,
})
@require_http_methods(['POST'])
@staff_required
def speaker_email_preview(request):
form = PreviewSpeakerMailForm(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'])
preview = speaker_email_render_preview(speaker, form.cleaned_data['subject'], form.cleaned_data['body'])
return HttpResponse(preview)
@staff_required
def conference_edit(request):
form = ConferenceForm(request.POST or None, instance=request.conference)