templated email to a list of volunteers
This commit is contained in:
parent
41ea5bbb62
commit
73778e8e64
|
@ -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 = '<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():
|
||||
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
|
|
62
cfp/forms.py
62
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 ]
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<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 %}
|
||||
{% blocktrans %}To see available environment variables, please click on a speaker.{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
|
@ -36,7 +36,7 @@
|
|||
<ul class="list-group">
|
||||
{% for speaker in speakers.all %}
|
||||
<a class="list-group-item" onclick="preview({{ speaker.pk }});">
|
||||
<b>{{ speaker }}</b> {{ talk }}
|
||||
<b>{{ speaker }}</b>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
{% extends 'cfp/staff/base.html' %}
|
||||
{% load i18n bootstrap3 staticfiles %}
|
||||
|
||||
{% block volunteerstab %} class="active"{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>{% trans "Send an email to each volunteer" %}</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 volunteer.{% 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 volunteer:" %}</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 volunteer in volunteers.all %}
|
||||
<a class="list-group-item" onclick="preview({{ volunteer.pk }});">
|
||||
<b>{{ volunteer}}</b>
|
||||
</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 'volunteer-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(volunteer) {
|
||||
$('#preview').removeClass('hidden');
|
||||
$('#preview').html('Loading preview...');
|
||||
var body = $('#body').val();
|
||||
$.post(preview_url, {
|
||||
'volunteer': volunteer,
|
||||
'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 %}
|
|
@ -6,11 +6,21 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
{% if pending_email %}
|
||||
<div class="alert alert-warning">
|
||||
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
||||
{% url 'volunteer-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 "Volunteers" %}</h1>
|
||||
|
||||
<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>
|
||||
|
||||
<br /><br />
|
||||
<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">
|
||||
|
@ -21,11 +31,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
|
||||
<table class="table table-bordered table-hover">
|
||||
<caption>{% trans "Total:" %} {{ volunteer_list|length }} {% trans "volunteer" %}{{ volunteer_list|length|pluralize }}
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="text-center">{% trans "Name" %}</th>
|
||||
<th class="text-center">{% trans "Email" %}</th>
|
||||
<th class="text-center">{% trans "Phone" %}</th>
|
||||
|
@ -46,6 +59,7 @@
|
|||
<tbody>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td><input type="checkbox" name="volunteers" value="{{ volunteer.pk }}"></td>
|
||||
<td>
|
||||
<a href="{% url 'volunteer-details' volunteer.pk %}">{{ volunteer.name }}</a>
|
||||
{% if volunteer.notes %}<span class="glyphicon glyphicon-envelope pull-right" data-toggle="tooltip" data-placement="bottom" data-html="true" title="{{ volunteer.notes|linebreaksbr }}"></span>{% endif %}
|
||||
|
@ -64,6 +78,20 @@
|
|||
{% 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="volunteers" %}
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary">{% trans "Apply" %}</button>
|
||||
{% endbuttons %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_end %}
|
||||
|
|
|
@ -56,6 +56,8 @@ urlpatterns = [
|
|||
path('staff/rooms/<slug:slug>/edit/', views.RoomUpdate.as_view(), name='room-edit'),
|
||||
path('staff/volunteers/', views.volunteer_list, name='volunteer-list'),
|
||||
path('staff/volunteers/<int:volunteer_id>/', 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<program_format>[\w]+)/)?$', views.staff_schedule, name='staff-schedule'),
|
||||
path('staff/select2/', views.Select2View.as_view(), name='django_select2-json'),
|
||||
|
|
58
cfp/views.py
58
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():
|
||||
|
|
Loading…
Reference in New Issue