Add confirmation/validation/convocation views

This commit is contained in:
Claude Paroz 2018-02-02 09:55:09 +01:00
parent 5a2a5d769f
commit e62284d55c
8 changed files with 312 additions and 159 deletions

View file

@ -1,21 +1,13 @@
import os
import tempfile
import zipfile
from collections import OrderedDict
from datetime import date
from django import forms
from django.contrib import admin
from django.core.mail import EmailMessage
from django.db.models import BooleanField
from django.http import HttpResponse
from django.template import loader
from django.urls import reverse
from django.utils.html import format_html
from stages.exports import OpenXMLExport
from .forms import CandidateForm
from .models import Candidate, Interview, GENDER_CHOICES
from .pdf import InscriptionSummaryPDF
def export_candidates(modeladmin, request, queryset):
@ -46,85 +38,21 @@ def export_candidates(modeladmin, request, queryset):
export_candidates.short_description = "Exporter les candidats sélectionnés"
def send_confirmation_mail(modeladmin, request, queryset):
from_email = request.user.email
subject = "Confirmation de votre inscription à l'Ecole Santé-social Pierre-Coullery"
email_sent = 0
for candidate in queryset.filter(
deposite_date__isnull=False, date_confirmation_mail__isnull=True, canceled_file=False):
to = [candidate.email]
if candidate.corporation and candidate.corporation.email:
to.append(candidate.corporation.email)
if candidate.instructor and candidate.instructor.email:
to.append(candidate.instructor.email)
context = {
'candidate_civility': candidate.civility,
'candidate_name': " ".join([candidate.civility, candidate.first_name, candidate.last_name]),
'section': candidate.section,
'sender_name': " ".join([request.user.first_name, request.user.last_name]),
'sender_email': from_email,
}
if candidate.section == 'EDE':
body = loader.render_to_string('email/candidate_confirm_EDE.txt', context)
else:
body = loader.render_to_string('email/candidate_confirm_FE.txt', context)
email = EmailMessage(
subject=subject,
body=body,
from_email=from_email,
to=to,
bcc=[from_email]
)
try:
email.send()
email_sent += 1
except Exception as err:
modeladmin.message_user(request, "Échec denvoi pour le candidat {0} ({1})".format(candidate, err))
else:
candidate.date_confirmation_mail = date.today()
candidate.save()
modeladmin.message_user(request, "%d messages de confirmation ont été envoyés." % email_sent)
send_confirmation_mail.short_description = "Envoyer email de confirmation"
def print_summary(modeladmin, request, queryset):
"""
Print a summary of inscription
"""
filename = 'archive_InscriptionResumes.zip'
path = os.path.join(tempfile.gettempdir(), filename)
with zipfile.ZipFile(path, mode='w', compression=zipfile.ZIP_DEFLATED) as filezip:
for candidate in queryset:
pdf = InscriptionSummaryPDF(candidate)
pdf.produce(candidate)
filezip.write(pdf.filename)
with open(filezip.filename, mode='rb') as fh:
response = HttpResponse(fh.read(), content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="{0}"'.format(filename)
return response
print_summary.short_description = 'Imprimer résumé'
class CandidateAdmin(admin.ModelAdmin):
form = CandidateForm
list_display = ('last_name', 'first_name', 'section', 'confirm_mail', 'convocation')
list_filter = ('section', 'option')
readonly_fields = ('total_result_points', 'total_result_mark', 'confirmation_date')
actions = [export_candidates, send_confirmation_mail, print_summary]
readonly_fields = (
'total_result_points', 'total_result_mark', 'confirmation_date', 'validation_date',
'convocation_date', 'candidate_actions',
)
actions = [export_candidates]
fieldsets = (
(None, {
'fields': (('first_name', 'last_name', 'gender'),
('street', 'pcode', 'city', 'district'),
('mobile', 'email'),
('birth_date', 'avs', 'handicap', 'has_photo'),
('birth_date', 'avs', 'handicap'),
('section', 'option'),
('corporation', 'instructor'),
('deposite_date', 'confirmation_date', 'canceled_file'),
@ -138,15 +66,19 @@ class CandidateAdmin(admin.ModelAdmin):
("EDE/EDS", {
'classes': ('collapse',),
'fields': (('diploma', 'diploma_detail', 'diploma_status'),
('registration_form', 'certificate_of_payement', 'cv', 'police_record', 'reflexive_text',
('registration_form', 'has_photo', 'certificate_of_payement', 'cv', 'police_record', 'reflexive_text',
'marks_certificate', 'residence_permits', 'aes_accords'),
('certif_of_800_childhood', 'certif_of_800_general', 'work_certificate'),
('promise', 'contract', 'activity_rate'),
('interview',),
('examination_result', 'interview_result', 'file_result', 'total_result_points',
'total_result_mark')
'total_result_mark'),
('confirmation_date', 'validation_date', 'convocation_date'),
),
}),
(None, {
'fields': (('candidate_actions',)),
}),
)
def confirm_mail(self, obj):
@ -164,6 +96,20 @@ class CandidateAdmin(admin.ModelAdmin):
convocation.short_description = 'Convoc. aux examens'
convocation.allow_tags = True
def candidate_actions(self, obj):
return format_html(
'<a class="button" href="{}">Confirmation de réception</a>&nbsp;'
'<a class="button" href="{}">Validation enseignants EDE</a>&nbsp;'
'<a class="button" href="{}">Convocation aux examens EDE</a>&nbsp;'
'<a class="button" href="{}">Impression du résumé du dossier EDE</a>',
reverse('candidate-confirmation', args=[obj.pk]),
reverse('candidate-validation', args=[obj.pk]),
reverse('candidate-convocation', args=[obj.pk]),
reverse('candidate-summary', args=[obj.pk]),
)
candidate_actions.short_description = 'Actions pour candidats'
candidate_actions.allow_tags = True
class InterviewAdmin(admin.ModelAdmin):
pass

View file

@ -139,6 +139,13 @@ class Candidate(models.Model):
else:
return ''
@property
def section_option(self):
if not self.option:
return self.get_section_display()
else:
return '{0}, option «{1}»'.format(self.get_section_display(), self.get_option_display())
def get_ok(self, fieldname):
return 'OK' if getattr(self, fieldname) is True else 'NON'

View file

@ -86,6 +86,8 @@ class CandidateTests(TestCase):
self.assertEqual(response.status_code, 302)
def test_send_confirmation_mail(self):
self.maxDiff = None
ede = Section.objects.create(name='EDE')
ase = Section.objects.create(name='ASE')
Candidate.objects.bulk_create([
@ -96,50 +98,67 @@ class CandidateTests(TestCase):
Candidate(first_name='Hervé', last_name='Bern', gender='M', section=ede,
deposite_date=date.today(), canceled_file=True),
# Good
Candidate(first_name='Henri', last_name='Dupond', gender='M', section=ede, option='ENF',
email='henri@example.org', deposite_date=date.today()),
Candidate(first_name='Joé', last_name='Glatz', gender='F', section=ase,
email='joe@example.org', deposite_date=date.today()),
Candidate(first_name='Henri', last_name='Dupond', gender='M', section=ede,
email='henri@example.org', deposite_date=date.today()),
])
change_url = reverse('admin:candidats_candidate_changelist')
self.client.login(username='me', password='mepassword')
response = self.client.post(change_url, {
'action': 'send_confirmation_mail',
'_selected_action': Candidate.objects.values_list('pk', flat=True)
}, follow=True)
self.assertEqual(len(mail.outbox), 2)
cand1 = Candidate.objects.get(last_name='Simth')
response = self.client.get(reverse('candidate-confirmation', args=[cand1.pk]), follow=True)
msg = "\n".join(str(m) for m in response.context['messages'])
self.assertEqual(msg, "Une confirmation a déjà été envoyée!")
cand2 = Candidate.objects.get(last_name='Bern')
response = self.client.get(reverse('candidate-confirmation', args=[cand2.pk]), follow=True)
msg = "\n".join(str(m) for m in response.context['messages'])
self.assertEqual(msg, "Ce dossier a été annulé!")
cand3 = Candidate.objects.get(last_name='Dupond')
response = self.client.get(reverse('candidate-confirmation', args=[cand3.pk]))
data = response.context['form'].initial
response = self.client.post(
reverse('candidate-confirmation', args=[cand3.pk]), data=data, follow=True
)
self.assertEqual(len(mail.outbox), 1)
# Logged-in user also receives as Bcc
self.assertEqual(mail.outbox[0].recipients(), ['henri@example.org', 'me@example.org'])
self.assertEqual(mail.outbox[1].recipients(), ['joe@example.org', 'me@example.org'])
# Mail content differ depending on the section
self.assertEqual(mail.outbox[0].body, """Monsieur,
self.assertEqual(mail.outbox[0].body, """Monsieur Henri Dupond,
Par ce courriel, nous vous confirmons la bonne réception de votre dossier de candidature à la formation ES dEducateur-trice de lenfance et vous remercions de lintérêt que vous portez à notre institution.
Par ce courriel, nous vous confirmons la bonne réception de vos documents de candidature à la formation Education de l&#39;enfance, dipl. ES, option «Enfance» et vous remercions de lintérêt que vous portez à notre institution.
Celui-ci sera traité et des nouvelles vous seront communiquées par courriel durant la 2ème quinzaine du mois de février.
Votre dossier sera traité dans les jours à venir et des nouvelles vous seront communiquées par courriel durant la 2ème quinzaine du mois de février.
Dans lintervalle, nous vous adressons, Monsieur, nos salutations les plus cordiales.
Secrétariat de l'EPC
tél. 032 886 33 00
Secrétariat de la filière Education de lenfance, dipl. ES
Hans Schmid
me@example.org
""".format()
tél. 032 886 33 00"""
)
self.assertEqual(mail.outbox[1].body, """Madame, Monsieur,
Nous vous confirmons la bonne réception de l'inscription de Madame Joé Glatz dans la filière ASE pour l'année scolaire à venir.
mail.outbox = []
cand4 = Candidate.objects.get(last_name='Glatz')
response = self.client.get(reverse('candidate-confirmation', args=[cand4.pk]))
data = response.context['form'].initial
response = self.client.post(
reverse('candidate-confirmation', args=[cand4.pk]), data=data, follow=True
)
self.assertEqual(len(mail.outbox), 1)
# Logged-in user also receives as Bcc
self.assertEqual(mail.outbox[0].recipients(), ['joe@example.org', 'me@example.org'])
self.assertEqual(mail.outbox[0].body, """Madame, Monsieur,
Nous vous confirmons la bonne réception de linscription de Madame Joé Glatz dans la filière Assist. socio-éducatif-ve CFC pour lannée scolaire à venir.
Nous nous tenons à votre disposition pour tout renseignement complémentaire et vous prions de recevoir, Madame, Monsieur, nos salutations les plus cordiales.
Secrétariat de l'EPC
tél. 032 886 33 00
Secrétariat de lEPC
Hans Schmid
me@example.org
""".format()
tél. 032 886 33 00"""
)
# One was already set, 2 new.
self.assertEqual(Candidate.objects.filter(confirmation_date__isnull=False).count(), 3)
@ -150,14 +169,14 @@ me@example.org
first_name='Henri', last_name='Dupond', gender='M', section=ede,
email='henri@example.org', deposite_date=date.today()
)
change_url = reverse('admin:candidats_candidate_changelist')
self.client.login(username='me', password='mepassword')
response = self.client.get(reverse('candidate-confirmation', args=[henri.pk]))
data = response.context['form'].initial
with mock.patch('django.core.mail.EmailMessage.send') as mocked:
mocked.side_effect = Exception("Error sending mail")
response = self.client.post(change_url, {
'action': 'send_confirmation_mail',
'_selected_action': Candidate.objects.values_list('pk', flat=True)
}, follow=True)
response = self.client.post(
reverse('candidate-confirmation', args=[henri.pk]), data=data, follow=True
)
self.assertContains(response, "Échec denvoi pour le candidat Dupond Henri (Error sending mail)")
henri.refresh_from_db()
self.assertIsNone(henri.confirmation_date)
@ -217,21 +236,69 @@ tél. 032 886 33 00
henri.refresh_from_db()
self.assertIsNotNone(henri.convocation_date)
def test_validation_enseignant_ede(self):
self.maxDiff = None
ede = Section.objects.create(name='EDE')
henri = Candidate.objects.create(
first_name='Henri', last_name='Dupond', gender='M', birth_date=date(2000, 5, 15),
street="Rue Neuve 3", pcode='2222', city='Petaouchnok',
section=ede, option='ENF',
email='henri@example.org', deposite_date=date.today()
)
t1 = Teacher.objects.create(
first_name="Julie", last_name="Caux", abrev="JCA", email="julie@example.org"
)
t2 = Teacher.objects.create(
first_name='Jeanne', last_name='Dubois', abrev="JDU", email="jeanne@example.org"
)
inter = Interview.objects.create(
date=datetime(2018, 3, 10, 10, 30), room='B103', candidat=henri,
teacher_int=t1, teacher_file=t2,
)
self.client.login(username='me', password='mepassword')
response = self.client.get(reverse('candidate-validation', args=[henri.pk]))
expected_message = """Bonjour,
Par ce courriel, je vous informe qu'un entretien d'admission a été planifié selon les modalités suivantes:
- samedi 10 mars 2018 à 10h30, en salle B103
Candidat :
Monsieur Henri Dupond
Rue Neuve 3
2222 Petaouchnok
Date de naiss.: 15 mai 2000
Sans nouvelle de votre part dans les 48 heures, une convocation définitive sera envoyée au candidat.
Avec mes meilleurs messages.
Secrétariat de la filière Education de lenfance, dipl. ES
Hans Schmid
me@example.org
tél. 032 886 33 00
"""
self.assertEqual(response.context['form'].initial['message'], expected_message)
data = response.context['form'].initial
response = self.client.post(reverse('candidate-validation', args=[henri.pk]), data=data)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].recipients(), ["julie@example.org", "jeanne@example.org", 'me@example.org'])
self.assertEqual(mail.outbox[0].subject, "Validation de l'entretien d'admission")
henri.refresh_from_db()
self.assertIsNotNone(henri.validation_date)
def test_summary_pdf(self):
ede = Section.objects.create(name='EDE')
cand = Candidate.objects.create(
first_name='Henri', last_name='Dupond', gender='M', section=ede,
email='henri@example.org', deposite_date=date.today()
)
change_url = reverse('admin:candidats_candidate_changelist')
summary_url = reverse('candidate-summary', args=[cand.pk])
self.client.login(username='me', password='mepassword')
response = self.client.post(change_url, {
'action': 'print_summary',
'_selected_action': Candidate.objects.values_list('pk', flat=True)
}, follow=True)
response = self.client.post(summary_url, follow=True)
self.assertEqual(
response['Content-Disposition'],
'attachment; filename="archive_InscriptionResumes.zip"'
'attachment; filename="dupond_henri.pdf"'
)
self.assertEqual(response['Content-Type'], 'application/zip')
self.assertEqual(response['Content-Type'], 'application/pdf')
self.assertGreater(len(response.content), 200)

View file

@ -1,19 +1,145 @@
import os
from django.conf import settings
from django.contrib import messages
from django.core.mail import EmailMessage
from django.http import HttpResponse
from django.shortcuts import redirect
from django.template import loader
from django.urls import reverse_lazy
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.views.generic import FormView
from candidats.forms import EmailBaseForm
from candidats.models import Candidate
from candidats.models import Candidate, Interview
from .pdf import InscriptionSummaryPDF
class SendConvocationView(FormView):
class EmailConfirmationBaseView(FormView):
template_name = 'email_base.html'
form_class = EmailBaseForm
success_url = reverse_lazy('admin:candidats_candidate_changelist')
success_message = "Le message a été envoyé pour le candidat {candidate}"
candidate_date_field = None
def form_valid(self, form):
email = EmailMessage(
subject=form.cleaned_data['subject'],
body=form.cleaned_data['message'],
from_email=form.cleaned_data['sender'],
to=form.cleaned_data['to'].split(';'),
bcc=form.cleaned_data['cci'].split(';'),
)
candidate = Candidate.objects.get(pk=self.kwargs['pk'])
try:
email.send()
except Exception as err:
messages.error(self.request, "Échec denvoi pour le candidat {0} ({1})".format(candidate, err))
else:
setattr(candidate, self.candidate_date_field, timezone.now())
candidate.save()
messages.success(self.request, self.success_message.format(candidate=candidate))
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'candidat': Candidate.objects.get(pk=self.kwargs['pk']),
'title': self.title,
})
return context
class ConfirmationView(EmailConfirmationBaseView):
success_message = "Le message de confirmation a été envoyé pour le candidat {candidate}"
candidate_date_field = 'confirmation_date'
title = "Confirmation de réception de dossier"
def get(self, request, *args, **kwargs):
candidate = Candidate.objects.get(pk=self.kwargs['pk'])
if candidate.confirmation_date:
messages.error(request, 'Une confirmation a déjà été envoyée!')
return redirect(reverse("admin:candidats_candidate_change", args=(candidate.pk,)))
elif candidate.canceled_file:
messages.error(request, 'Ce dossier a été annulé!')
return redirect(reverse("admin:candidats_candidate_change", args=(candidate.pk,)))
return super().get(request, *args, **kwargs)
def get_initial(self):
initial = super().get_initial()
candidate = Candidate.objects.get(pk=self.kwargs['pk'])
to = [candidate.email]
if candidate.section == 'EDE':
src_email = 'email/candidate_confirm_EDE.txt'
else:
src_email = 'email/candidate_confirm_FE.txt'
if candidate.corporation and candidate.corporation.email:
to.append(candidate.corporation.email)
if candidate.instructor and candidate.instructor.email:
to.append(candidate.instructor.email)
msg_context = {
'candidate': candidate,
'sender': self.request.user,
}
initial.update({
'id_candidate': candidate.pk,
'cci': self.request.user.email,
'to': '; '.join(to),
'subject': "Inscription à la formation {0}".format(candidate.section_option),
'message': loader.render_to_string(src_email, msg_context),
'sender': self.request.user.email,
})
return initial
class ValidationView(EmailConfirmationBaseView):
success_message = "Le message de validation a été envoyé pour le candidat {candidate}"
candidate_date_field = 'validation_date'
title = "Validation des examens par les enseignant-e-s EDE"
def get(self, request, *args, **kwargs):
candidate = Candidate.objects.get(pk=self.kwargs['pk'])
if candidate.validation_date:
messages.error(request, 'Une validation a déjà été envoyée!')
return redirect(reverse("admin:candidats_candidate_change", args=(candidate.pk,)))
return super().get(request, *args, **kwargs)
def get_initial(self):
initial = super().get_initial()
candidate = Candidate.objects.get(pk=self.kwargs['pk'])
msg_context = {
'candidate': candidate,
'sender': self.request.user,
}
initial.update({
'id_candidate': candidate.pk,
'cci': self.request.user.email,
'to': ';'.join([
candidate.interview.teacher_int.email, candidate.interview.teacher_file.email
]),
'subject': "Validation de l'entretien d'admission",
'message': loader.render_to_string('email/validation_enseignant_EDE.txt', msg_context),
'sender': self.request.user.email,
})
return initial
class ConvocationView(EmailConfirmationBaseView):
success_message = "Le message de convocation a été envoyé pour le candidat {candidate}"
candidate_date_field = 'convocation_date'
title = "Convocation aux examens d'admission EDE"
def get(self, request, *args, **kwargs):
candidate = Candidate.objects.get(pk=self.kwargs['pk'])
try:
candidate.interview
except Interview.DoesNotExist:
messages.error(request, "Impossible de convoquer sans d'abord définir un interview!")
return redirect(reverse("admin:candidats_candidate_change", args=(candidate.pk,)))
return super().get(request, *args, **kwargs)
def get_initial(self):
initial = super().get_initial()
@ -59,31 +185,16 @@ class SendConvocationView(FormView):
})
return initial
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'candidat': Candidate.objects.get(pk=self.kwargs['pk']),
'title': "Convocation aux examens d'admission EDE",
})
return context
def form_valid(self, form):
email = EmailMessage(
subject=form.cleaned_data['subject'],
body=form.cleaned_data['message'],
from_email=form.cleaned_data['sender'],
to=form.cleaned_data['to'].split(';'),
bcc=form.cleaned_data['cci'].split(';'),
)
candidate = Candidate.objects.get(pk=self.kwargs['pk'])
try:
email.send()
except Exception as err:
messages.error(self.request, "Échec denvoi pour le candidat {0} ({1})".format(candidate, err))
else:
candidate.convocation_date = timezone.now()
candidate.save()
messages.success(self.request,
"Le message de convocation a été envoyé pour le candidat {0}".format(candidate)
)
return super().form_valid(form)
def inscription_summary(request, pk):
"""
Print a PDF summary of inscription
"""
candidat = Candidate.objects.get(pk=pk)
pdf = InscriptionSummaryPDF(candidat)
pdf.produce(candidat)
with open(pdf.filename, mode='rb') as fh:
response = HttpResponse(fh.read(), content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="{0}"'.format(os.path.basename(pdf.filename))
return response