From 84440ceb2adce9671ab7ef287c5cbadc66c1cdf0 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 12 Apr 2018 12:17:37 +0200 Subject: [PATCH] Add view to send EDE student convocation --- common/urls.py | 4 ++ stages/admin.py | 47 ++++++++++--- stages/models.py | 28 ++++++++ stages/tests.py | 51 +++++++++++++- stages/views.py | 78 ++++++++++++++++++++- templates/email/student_convocation_EDE.txt | 21 ++++++ 6 files changed, 214 insertions(+), 15 deletions(-) create mode 100644 templates/email/student_convocation_EDE.txt diff --git a/common/urls.py b/common/urls.py index 504fe21..97d736a 100644 --- a/common/urls.py +++ b/common/urls.py @@ -34,6 +34,10 @@ urlpatterns = [ name='candidate-validation'), path('candidate//summary/', candidats_views.inscription_summary, name='candidate-summary'), + # Qualification EDE + path('student_ede//send_convocation', views.StudentConvocationExaminationView.as_view(), + name='student-ede-convocation'), + path('imputations/export/', views.imputations_export, name='imputations_export'), path('print/update_form/', views.print_update_form, name='print_update_form'), path('general_export/', views.general_export, name='general-export'), diff --git a/stages/admin.py b/stages/admin.py index f8bc4a9..30b80c2 100644 --- a/stages/admin.py +++ b/stages/admin.py @@ -7,6 +7,8 @@ from django.contrib import admin from django.db import models from django.db.models import Case, Count, When from django.http import HttpResponse +from django.urls import reverse +from django.utils.html import format_html from .models import ( Teacher, Option, Student, Section, Level, Klass, Corporation, @@ -110,17 +112,32 @@ class StudentAdmin(admin.ModelAdmin): list_filter = (('archived', ArchivedListFilter), ('klass', KlassRelatedListFilter)) search_fields = ('last_name', 'first_name', 'pcode', 'city', 'klass__name') autocomplete_fields = ('corporation', 'instructor', 'supervisor', 'mentor', 'expert') - readonly_fields = ('report_sem1_sent', 'report_sem2_sent') - fields = (('last_name', 'first_name', 'ext_id'), ('street', 'pcode', 'city', 'district'), - ('email', 'tel', 'mobile'), ('gender', 'avs', 'birth_date'), - ('archived', 'dispense_ecg', 'dispense_eps', 'soutien_dys'), - ('klass', 'option_ase'), - ('report_sem1', 'report_sem1_sent'), - ('report_sem2', 'report_sem2_sent'), - ('corporation', 'instructor',), - ('supervisor', 'mentor', 'expert')) + readonly_fields = ('report_sem1_sent', 'report_sem2_sent', 'examination_actions') + fieldsets = ( + (None, { + 'fields': (('last_name', 'first_name', 'ext_id'), ('street', 'pcode', 'city', 'district'), + ('email', 'tel', 'mobile'), ('gender', 'avs', 'birth_date'), + ('archived', 'dispense_ecg', 'dispense_eps', 'soutien_dys'), + ('klass', 'option_ase'), + ('report_sem1', 'report_sem1_sent'), + ('report_sem2', 'report_sem2_sent'), + ('corporation', 'instructor',) + ) + } + ), + ("Examen Qualification EDE", { + 'classes': ('collapse',), + 'fields': ( + ('supervisor', ), + ('subject', 'title'), + ('training_referent', 'referent', 'mentor'), + ('internal_expert', 'expert'), + ('session', 'date_exam', 'room', 'mark'), + ('examination_actions',) + ) + }), + ) actions = ['archive'] - def archive(self, request, queryset): for student in queryset: # Save each item individually to allow for custom save() logic. @@ -128,6 +145,16 @@ class StudentAdmin(admin.ModelAdmin): student.save() archive.short_description = "Marquer les étudiants sélectionnés comme archivés" + def examination_actions(self, obj): + if obj.klass.section.name == 'EDE' and obj.klass.level.name == "3": + return format_html( + 'Mail convocation soutenance', + reverse('student-ede-convocation', args=[obj.pk]) + ) + else: + return '' + examination_actions.short_description = 'Actions pour les examens EDE' + class CorpContactAdmin(admin.ModelAdmin): list_display = ('__str__', 'corporation', 'role') diff --git a/stages/models.py b/stages/models.py index 473f6be..e626a5b 100644 --- a/stages/models.py +++ b/stages/models.py @@ -77,6 +77,14 @@ class Teacher(models.Model): def __str__(self): return '{0} {1}'.format(self.last_name, self.first_name) + @property + def full_name(self): + return '{0} {1}'.format(self.first_name, self.last_name) + + @property + def civility_full_name(self): + return '{0} {1} {2}'.format(self.civility, self.first_name, self.last_name) + def calc_activity(self): """ Return a dictionary of calculations relative to teacher courses. @@ -266,10 +274,18 @@ class Student(models.Model): def full_name(self): return '{0} {1}'.format(self.first_name, self.last_name) + @property + def civility_full_name(self): + return '{0} {1} {2}'.format(self.civility, self.first_name, self.last_name) + @property def pcode_city(self): return '{0} {1}'.format(self.pcode, self.city) + @property + def is_examination_valid(self): + return (self.date_exam and self.room and self.expert and self.internal_expert) + def save(self, **kwargs): if self.archived and not self.archived_text: # Fill archived_text with training data, JSON-formatted @@ -370,6 +386,18 @@ class CorpContact(models.Model): def __str__(self): return '{0} {1}, {2}'.format(self.last_name, self.first_name, self.corporation or '-') + @property + def full_name(self): + return '{0} {1}'.format(self.first_name, self.last_name) + + @property + def civility_full_name(self): + return '{0} {1} {2}'.format(self.title, self.first_name, self.last_name) + + @property + def pcode_city(self): + return '{0} {1}'.format(self.pcode, self.city) + class Domain(models.Model): name = models.CharField(max_length=50, verbose_name='Nom') diff --git a/stages/tests.py b/stages/tests.py index 9f1d80d..bde2ecd 100644 --- a/stages/tests.py +++ b/stages/tests.py @@ -1,6 +1,6 @@ import json import os -from datetime import date +from datetime import date, datetime from django.conf import settings from django.contrib.auth.models import User @@ -50,7 +50,10 @@ class StagesTest(TestCase): Student(first_name="Gil", last_name="Schmid", birth_date="1996-02-14", pcode="2000", city="Neuchâtel", klass=klass3, corporation=corp), ]) - ref1 = Teacher.objects.create(first_name="Julie", last_name="Caux", abrev="JCA") + ref1 = Teacher.objects.create( + first_name="Julie", last_name="Caux", abrev="JCA", email="julie@eample.org", + civility="Madame", + ) cls.p1 = Period.objects.create( title="Stage de pré-sensibilisation", start_date="2012-11-26", end_date="2012-12-07", section=sect_ase, level=lev1, @@ -77,7 +80,9 @@ class StagesTest(TestCase): Training.objects.create( availability=av3, student=Student.objects.get(first_name="André"), referent=ref1, ) - cls.admin = User.objects.create_user('me', 'me@example.org', 'mepassword') + cls.admin = User.objects.create_user( + 'me', 'me@example.org', 'mepassword', first_name='Jean', last_name='Valjean', + ) def setUp(self): self.client.login(username='me', password='mepassword') @@ -147,6 +152,46 @@ class StagesTest(TestCase): ) self.assertGreater(int(response['Content-Length']), 10) + def test_send_ede_convocation(self): + st = Student.objects.get(first_name="Albin") + self.client.login(username='me', password='mepassword') + url = reverse('student-ede-convocation', args=[st.pk]) + response = self.client.get(url, follow=True) + self.assertContains(response, "Toutes les informations ne sont pas disponibles pour la convocation!") + st.date_exam = datetime(2018, 6, 28, 12, 00) + st.room = "B123" + st.expert = CorpContact.objects.get(last_name="Horner") + st.internal_expert = Teacher.objects.get(last_name="Caux") + st.save() + response = self.client.get(url, follow=True) + self.assertContains(response, "L’expert externe n’a pas de courriel valide !") + st.expert.email = "horner@example.org" + st.expert.save() + response = self.client.get(url) + expected_message = """ Albin Dupond, +Madame Julie Caux, +Monsieur Jean Horner, + + +Nous vous informons que la soutenance du travail de diplôme de Albin Dupond aura lieu dans les locaux de l’Ecole Santé-social Pierre-Coullery, rue de la Prévoyance 82, 2300 La Chaux-de-Fonds en date du: + + - jeudi 28 juin 2018 à 12h00 en salle B123 + + +Nous informons également Monsieur Horner que le mémoire lui est adressé ce jour par courrier postal. + + +Nous vous remercions de nous confirmer par retour de courriel que vous avez bien reçu ce message et dans l’attente du plaisir de vous rencontrer prochainement, nous vous prions d’agréer, Madame, Messieurs, nos salutations les meilleures. + + + +Secrétariat de la filière Education de l’enfance, dipl. ES +Jean Valjean +me@example.org +tél. 032 886 33 00 +""" + self.assertEqual(response.context['form'].initial['message'], expected_message) + class PeriodTest(TestCase): def setUp(self): diff --git a/stages/views.py b/stages/views.py index 2aba326..18553f7 100644 --- a/stages/views.py +++ b/stages/views.py @@ -18,14 +18,16 @@ from django.db import transaction from django.db.models import Case, Count, Value, When, Q from django.db.models.functions import Concat from django.http import HttpResponse, HttpResponseNotAllowed, HttpResponseRedirect -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, redirect from django.template import loader -from django.urls import reverse +from django.urls import reverse, reverse_lazy from django.utils import timezone +from django.utils.dateformat import format as django_format from django.utils.translation import ugettext as _ from django.utils.text import slugify from django.views.generic import DetailView, FormView, TemplateView, ListView +from .base_views import EmailConfirmationBaseView from .exports import OpenXMLExport from .forms import EmailBaseForm, PeriodForm, StudentImportForm, UploadHPFileForm, UploadReportForm from .models import ( @@ -657,6 +659,78 @@ class SendStudentReportsView(FormView): return context +class EmailConfirmationView(EmailConfirmationBaseView): + person_model = Student + success_url = reverse_lazy('admin:stages_student_changelist') + error_message = "Échec d’envoi pour l’étudiant {person} ({err})" + + +class StudentConvocationExaminationView(EmailConfirmationView): + success_message = "Le message de convocation a été envoyé pour l’étudiant {person}" + title = "Convocation à la soutenance du travail de diplôme" + candidate_date_field = 'convocation_date' + + def get(self, request, *args, **kwargs): + self.student = Student.objects.get(pk=self.kwargs['pk']) + error = '' + if not self.student.is_examination_valid: + error = "Toutes les informations ne sont pas disponibles pour la convocation!" + elif not self.student.expert.email: + error = "L’expert externe n’a pas de courriel valide !" + elif not self.student.internal_expert.email: + error = "L’expert interne n'a pas de courriel valide !" + if error: + messages.error(request, error) + return redirect(reverse("admin:stages_student_change", args=(self.student.pk,))) + return super().get(request, *args, **kwargs) + + def get_initial(self): + initial = super().get_initial() + to = [self.student.email, self.student.expert.email, self.student.internal_expert.email] + src_email = 'email/student_convocation_EDE.txt' + + # Recipients with ladies first! + recip_names = sorted([ + self.student.civility_full_name, + self.student.expert.civility_full_name, + self.student.internal_expert.civility_full_name, + ]) + titles = [ + self.student.civility, + self.student.expert.title, + self.student.internal_expert.civility, + ] + mme_count = titles.count('Madame') + # Civilities, with ladies first! + if mme_count == 0: + civilities = 'Messieurs' + elif mme_count == 1: + civilities = 'Madame, Messieurs' + elif mme_count == 2: + civilities = 'Mesdames, Monsieur' + else: + civilities = 'Mesdames' + + msg_context = { + 'recipient1': recip_names[0], + 'recipient2': recip_names[1], + 'recipient3': recip_names[2], + 'student': self.student, + 'sender': self.request.user, + 'global_civilities': civilities, + 'date_examen': django_format(self.student.date_exam, 'l j F Y à H\hi'), + 'salle': self.student.room, + } + initial.update({ + 'cci': self.request.user.email, + 'to': '; '.join(to), + 'subject': "Convocation à la soutenance de travail de diplôme", + 'message': loader.render_to_string(src_email, msg_context), + 'sender': self.request.user.email, + }) + return initial + + EXPORT_FIELDS = [ # Student fields ('ID externe', 'student__ext_id'), diff --git a/templates/email/student_convocation_EDE.txt b/templates/email/student_convocation_EDE.txt new file mode 100644 index 0000000..9f29500 --- /dev/null +++ b/templates/email/student_convocation_EDE.txt @@ -0,0 +1,21 @@ +{{ recipient1 }}, +{{ recipient2 }}, +{{ recipient3 }}, + + +Nous vous informons que la soutenance du travail de diplôme de {{ student.civility_full_name }} aura lieu dans les locaux de l’Ecole Santé-social Pierre-Coullery, rue de la Prévoyance 82, 2300 La Chaux-de-Fonds en date du: + + - {{ date_examen }} en salle {{ salle }} + + +Nous informons également {{ student.expert.title }} {{ student.expert.last_name }} que le mémoire lui est adressé ce jour par courrier postal. + + +Nous vous remercions de nous confirmer par retour de courriel que vous avez bien reçu ce message et dans l’attente du plaisir de vous rencontrer prochainement, nous vous prions d’agréer, {{ global_civilities }}, nos salutations les meilleures. + + + +Secrétariat de la filière Education de l’enfance, dipl. ES +{{ sender.first_name }} {{ sender.last_name }} +{{ sender.email }} +tél. 032 886 33 00