From 73778e8e646d38a34cfd2cabe97577db58e11d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89lie=20Bouttier?= Date: Sun, 17 Dec 2017 13:04:17 +0100 Subject: [PATCH] templated email to a list of volunteers --- cfp/emails.py | 29 ++++- cfp/environment.py | 18 +++ cfp/forms.py | 62 ++++++++--- cfp/templates/cfp/staff/speaker_email.html | 4 +- cfp/templates/cfp/staff/volunteer_email.html | 111 +++++++++++++++++++ cfp/templates/cfp/staff/volunteer_list.html | 34 +++++- cfp/urls.py | 2 + cfp/views.py | 58 +++++++++- 8 files changed, 291 insertions(+), 27 deletions(-) create mode 100644 cfp/templates/cfp/staff/volunteer_email.html diff --git a/cfp/emails.py b/cfp/emails.py index a4ca517..2d65430 100644 --- a/cfp/emails.py +++ b/cfp/emails.py @@ -5,7 +5,7 @@ from pprint import pformat from textwrap import indent from mailing.utils import send_message -from .environment import TalkEnvironment, SpeakerEnvironment +from .environment import TalkEnvironment, SpeakerEnvironment, VolunteerEnvironment def talk_email_render_preview(talk, speaker, subject, body): @@ -40,6 +40,22 @@ def speaker_email_render_preview(speaker, subject, body): return preview +def volunteer_email_render_preview(volunteer, subject, body): + env = VolunteerEnvironment(volunteer) + 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 = {'volunteer': env.globals['volunteer']} + 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(): @@ -61,3 +77,14 @@ def speaker_email_send(speakers, subject, body): send_message(speaker.conversation, speaker.site.conference, subject=s, content=c) sent += 1 return sent + + +def volunteer_email_send(volunteers, subject, body): + sent = 0 + for volunteer in volunteers.all(): + env = VolunteerEnvironment(volunteer) + s = env.from_string(subject).render() + c = env.from_string(body).render() + send_message(volunteer.conversation, volunteer.site.conference, subject=s, content=c) + sent += 1 + return sent diff --git a/cfp/environment.py b/cfp/environment.py index 0a527cf..1b0134f 100644 --- a/cfp/environment.py +++ b/cfp/environment.py @@ -32,6 +32,16 @@ def speaker_to_dict(speaker, include_talks=False): return d +def volunteer_to_dict(volunteer): + return { + 'name': volunteer.name, + 'email': volunteer.email, + 'phone_number': volunteer.phone_number, + 'sms_prefered': volunteer.sms_prefered, + 'activities': list(map(lambda activity: activity.name, volunteer.activities.all())), + } + + class TalkEnvironment(SandboxedEnvironment): def __init__(self, talk, speaker, **options): super().__init__(**options) @@ -47,3 +57,11 @@ class SpeakerEnvironment(SandboxedEnvironment): self.globals.update({ 'speaker': speaker_to_dict(speaker, include_talks=True), }) + + +class VolunteerEnvironment(SandboxedEnvironment): + def __init__(self, volunteer, **options): + super().__init__(**options) + self.globals.update({ + 'volunteer': volunteer_to_dict(volunteer), + }) diff --git a/cfp/forms.py b/cfp/forms.py index ead8095..c5127e1 100644 --- a/cfp/forms.py +++ b/cfp/forms.py @@ -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, SpeakerEnvironment +from .environment import TalkEnvironment, SpeakerEnvironment, VolunteerEnvironment ACCEPTATION_CHOICES = [ @@ -216,6 +216,16 @@ class SpeakerActionForm(forms.Form): self.fields['speakers'].choices = [(speaker.pk, None) for speaker in speakers.all()] +class VolunteerActionForm(forms.Form): + volunteers = forms.MultipleChoiceField(choices=[]) + email = forms.BooleanField(required=False, label=_('Send an email')) + + def __init__(self, *args, **kwargs): + volunteers = kwargs.pop('volunteers') + super().__init__(*args, **kwargs) + self.fields['volunteers'].choices = [(volunteer.pk, None) for volunteer in volunteers.all()] + + class NotifyForm(forms.Form): notify = forms.BooleanField(initial=True, required=False, label=_('Notify by mail?')) @@ -283,22 +293,31 @@ class PreviewSpeakerMailForm(forms.Form): body = forms.CharField(required=False, label=_('Body')) -class SendTalkMailForm(forms.Form): +class PreviewVolunteerMailForm(forms.Form): + volunteer = forms.IntegerField() + subject = forms.CharField(required=False, label=_('Subject')) + body = forms.CharField(required=False, label=_('Body')) + + +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') + + +class SendTalkMailForm(SendMailForm): + def __init__(self, *args, **kwargs): + self._talks = kwargs.pop('talks') + super().__init__(*args, **kwargs) + self._env = dict() + def clean_template(self, template): try: for talk in self._talks.all(): @@ -311,22 +330,12 @@ class SendTalkMailForm(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')) - +class SendSpeakerMailForm(SendMailForm): 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(): @@ -338,6 +347,23 @@ class SendSpeakerMailForm(forms.Form): return self.cleaned_data.get(template) +class SendVolunteerMailForm(SendMailForm): + def __init__(self, *args, **kwargs): + self._volunteers = kwargs.pop('volunteers') + super().__init__(*args, **kwargs) + self._env = dict() + + def clean_template(self, template): + try: + for volunteer in self._volunteers.all(): + env = self._env.get(volunteer, VolunteerEnvironment(volunteer)) + env.from_string(self.cleaned_data.get(template)).render() + except Exception as e: + raise forms.ValidationError(_("Your template does not compile (at least) with volunteer '%(volunteer)s'.") % + {'volunteer': volunteer}) + 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/speaker_email.html b/cfp/templates/cfp/staff/speaker_email.html index 28f729d..1c44d3a 100644 --- a/cfp/templates/cfp/staff/speaker_email.html +++ b/cfp/templates/cfp/staff/speaker_email.html @@ -16,7 +16,7 @@

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

@@ -36,7 +36,7 @@ diff --git a/cfp/templates/cfp/staff/volunteer_email.html b/cfp/templates/cfp/staff/volunteer_email.html new file mode 100644 index 0000000..99fd2e2 --- /dev/null +++ b/cfp/templates/cfp/staff/volunteer_email.html @@ -0,0 +1,111 @@ +{% extends 'cfp/staff/base.html' %} +{% load i18n bootstrap3 staticfiles %} + +{% block volunteerstab %} 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 volunteer.{% endblocktrans %} +

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

{% trans "To preview your email, click on a volunteer:" %}

+ +
+
+ + +
+
+ +
+
+ +
+
+ {% 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/volunteer_list.html b/cfp/templates/cfp/staff/volunteer_list.html index 359a405..0286431 100644 --- a/cfp/templates/cfp/staff/volunteer_list.html +++ b/cfp/templates/cfp/staff/volunteer_list.html @@ -6,11 +6,21 @@ {% block content %} +{% if pending_email %} +
+ + {% url 'volunteer-email' as email_url %} + {% blocktrans %}You have a pending e-mail. To continue its edition, click here.{% endblocktrans %} +
+{% endif %} +

{% trans "Volunteers" %}

-{% trans "Show filtering options…" %} - -

+

+ + {% trans "Show filtering options…" %} + +

@@ -21,11 +31,14 @@
+
+ + @@ -46,6 +59,7 @@ {% endif %} +
{% trans "Total:" %} {{ volunteer_list|length }} {% trans "volunteer" %}{{ volunteer_list|length|pluralize }}
{% trans "Name" %} {% trans "Email" %} {% trans "Phone" %}
{{ volunteer.name }} {% if volunteer.notes %}{% endif %} @@ -64,6 +78,20 @@ {% endfor %}
+
+
+

{% trans "For selected speakers:" %}

+ {% csrf_token %} + {% bootstrap_form_errors action_form %} + {% bootstrap_form action_form exclude="volunteers" %} + {% buttons %} + + {% endbuttons %} +
+
+ +
+ {% endblock %} {% block js_end %} diff --git a/cfp/urls.py b/cfp/urls.py index e84d8d8..70b0e10 100644 --- a/cfp/urls.py +++ b/cfp/urls.py @@ -56,6 +56,8 @@ urlpatterns = [ path('staff/rooms//edit/', views.RoomUpdate.as_view(), name='room-edit'), path('staff/volunteers/', views.volunteer_list, name='volunteer-list'), path('staff/volunteers//', views.volunteer_details, name='volunteer-details'), + path('staff/volunteers/email/', views.volunteer_email, name='volunteer-email'), + path('staff/volunteers/email/preview/', views.volunteer_email_preview, name='volunteer-email-preview'), path('staff/add-user/', views.create_user, name='create-user'), re_path(r'^staff/schedule/((?P[\w]+)/)?$', views.staff_schedule, name='staff-schedule'), path('staff/select2/', views.Select2View.as_view(), name='django_select2-json'), diff --git a/cfp/views.py b/cfp/views.py index 25e0244..061f40a 100644 --- a/cfp/views.py +++ b/cfp/views.py @@ -27,11 +27,16 @@ 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, speaker_email_send, speaker_email_render_preview -from .forms import TalkForm, TalkStaffForm, TalkFilterForm, TalkActionForm, SpeakerActionForm, get_talk_speaker_form_class, \ +from .emails import talk_email_send, talk_email_render_preview, \ + speaker_email_send, speaker_email_render_preview, \ + volunteer_email_send, volunteer_email_render_preview +from .forms import TalkForm, TalkStaffForm, TalkFilterForm, get_talk_speaker_form_class, \ + TalkActionForm, SpeakerActionForm, VolunteerActionForm, \ ParticipantForm, ParticipantFilterForm, NotifyForm, \ ConferenceForm, HomepageForm, CreateUserForm, TrackForm, RoomForm, \ - VolunteerForm, VolunteerFilterForm, EmailForm, PreviewTalkMailForm, PreviewSpeakerMailForm, SendTalkMailForm, SendSpeakerMailForm, \ + VolunteerForm, VolunteerFilterForm, EmailForm, \ + PreviewTalkMailForm, PreviewSpeakerMailForm, PreviewVolunteerMailForm, \ + SendTalkMailForm, SendSpeakerMailForm, SendVolunteerMailForm, \ TagForm, TalkCategoryForm, ActivityForm, \ ACCEPTATION_VALUES, CONFIRMATION_VALUES @@ -172,6 +177,14 @@ def volunteer_list(request): if len(data['activity']): q |= Q(activities__slug__in=data['activity']) volunteers = volunteers.filter(q) + # Action + action_form = VolunteerActionForm(request.POST or None, volunteers=volunteers) + if request.method == 'POST' and action_form.is_valid(): + data = action_form.cleaned_data + if data['email']: + request.session['volunteer-email-list'] = data['volunteers'] + return redirect(reverse('volunteer-email')) + return redirect(request.get_full_path()) if request.GET.get('format') == 'csv': response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="volunteers.csv"' @@ -187,9 +200,11 @@ def volunteer_list(request): return render(request, 'cfp/staff/volunteer_list.html', { 'volunteer_list': volunteers, 'filter_form': filter_form, + 'action_form': action_form, 'show_filters': show_filters, 'contact_link': contact_link, 'csv_link': csv_link, + 'pending_email': bool(request.session.get('volunteer-email-list', None)), }) @@ -201,6 +216,43 @@ def volunteer_details(request, volunteer_id): }) +@staff_required +def volunteer_email(request): + volunteers = Volunteer.objects.filter(pk__in=request.session.get('volunteer-email-list', [])) + if not volunteers.exists(): + messages.error(request, _('Please select some volunteers.')) + return redirect('volunteer-list') + form = SendVolunteerMailForm(request.POST or None, initial=request.session.get('volunteer-email-stored'), volunteers=volunteers) + if request.method == 'POST' and form.is_valid(): + subject = form.cleaned_data['subject'] + body = form.cleaned_data['body'] + request.session['volunteer-email-stored'] = {'subject': subject, 'body': body} + if form.cleaned_data['confirm']: + sent = volunteer_email_send(volunteers, subject, body) + messages.success(request, _('%(count)d mails have been sent.') % {'count': sent}) + del request.session['volunteer-email-list'] + return redirect('volunteer-list') + else: + messages.info(request, _('Your ready to send %(count)d emails.') % {'count': volunteers.count()}) + else: + form.fields.pop('confirm') + return render(request, 'cfp/staff/volunteer_email.html', { + 'volunteers': volunteers, + 'form': form, + }) + + +@require_http_methods(['POST']) +@staff_required +def volunteer_email_preview(request): + form = PreviewVolunteerMailForm(request.POST or None) + if not form.is_valid(): + return HttpResponseServerError() + volunteer = get_object_or_404(Volunteer, site=request.conference.site, pk=form.cleaned_data['volunteer']) + preview = volunteer_email_render_preview(volunteer, form.cleaned_data['subject'], form.cleaned_data['body']) + return HttpResponse(preview) + + def proposal_home(request): categories = request.conference.opened_categories if not categories.exists():