diff --git a/cfp/emails.py b/cfp/emails.py index 78b6c30..a4ca517 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 +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 = '' + _('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(): @@ -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 diff --git a/cfp/environment.py b/cfp/environment.py index ac8247a..0a527cf 100644 --- a/cfp/environment.py +++ b/cfp/environment.py @@ -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), + }) diff --git a/cfp/forms.py b/cfp/forms.py index 9d6478f..9164250 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 +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 ] diff --git a/cfp/templates/cfp/staff/participant_list.html b/cfp/templates/cfp/staff/participant_list.html index 773fc2e..343d01a 100644 --- a/cfp/templates/cfp/staff/participant_list.html +++ b/cfp/templates/cfp/staff/participant_list.html @@ -5,6 +5,14 @@ {% block content %} +{% if pending_email %} +
+ + {% url 'speaker-email' as email_url %} + {% blocktrans %}You have a pending e-mail. To continue its edition, click here.{% endblocktrans %} +
+{% endif %} +

{% trans "Speakers" %}

@@ -34,11 +42,15 @@ +

+ - + {% comment %}{% endcomment %} @@ -58,6 +70,7 @@ {% endif %} +
{% trans "Total:" %} {{ participant_list|length }} {% trans "speaker" %}{{ participant_list|length|pluralize }} + + {% trans "Total:" %} {{ participant_list|length }} {% trans "speaker" %}{{ participant_list|length|pluralize }}
{% trans "Name" %} {% trans "Talk count" %}
{{ participant }} {% if participant.vip %}VIP{% endif %} @@ -76,4 +89,18 @@ {% endfor %}
+
+
+

{% trans "For selected speakers:" %}

+ {% csrf_token %} + {% bootstrap_form_errors action_form %} + {% bootstrap_form action_form exclude="speakers" %} + {% buttons %} + + {% endbuttons %} +
+
+ +
+ {% endblock %} diff --git a/cfp/templates/cfp/staff/speaker_email.html b/cfp/templates/cfp/staff/speaker_email.html new file mode 100644 index 0000000..28f729d --- /dev/null +++ b/cfp/templates/cfp/staff/speaker_email.html @@ -0,0 +1,111 @@ +{% extends 'cfp/staff/base.html' %} +{% load i18n bootstrap3 staticfiles %} + +{% block speakerstab %} 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:" %}

+ +
+
+ + +
+
+ +
+
+ +
+
+ {% 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 01c95d0..5c40947 100644 --- a/cfp/templates/cfp/staff/talk_list.html +++ b/cfp/templates/cfp/staff/talk_list.html @@ -15,7 +15,11 @@

{% trans "Talks" %}

-

{% trans "Show filtering options…" %}

+

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

diff --git a/cfp/urls.py b/cfp/urls.py index 062cbde..e84d8d8 100644 --- a/cfp/urls.py +++ b/cfp/urls.py @@ -45,6 +45,8 @@ urlpatterns = [ path('staff/speakers//add-talk/', views.participant_add_talk, name='participant-add-talk'), path('staff/speakers//edit/', views.ParticipantUpdate.as_view(), name='participant-edit'), path('staff/speakers//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//edit/', views.TrackUpdate.as_view(), name='track-edit'), diff --git a/cfp/views.py b/cfp/views.py index 1c01648..05dd44d 100644 --- a/cfp/views.py +++ b/cfp/views.py @@ -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)