diff --git a/common/settings.py b/common/settings.py index 296f095..a61cc66 100644 --- a/common/settings.py +++ b/common/settings.py @@ -140,8 +140,9 @@ MAX_ENS_FORMATION = 250 GLOBAL_CHARGE_TOTAL = 2150 GLOBAL_CHARGE_PERCENT = 21.5 -RESP_FILIERE_EDE = "Ann Schaub-Murray" +RESP_FILIERE_EDE = ("Ann Schaub-Murray", 'F') DATE_LIEU_EXAMEN_EDE = "mercredi 7 mars 2018, à 13h30, salle A405" +RESP_FILIERE_EDS = ("Brahim Ali Hemma", 'M') if 'TRAVIS' in os.environ: SECRET_KEY = 'secretkeyfortravistests' diff --git a/common/urls.py b/common/urls.py index a4e66d7..834b19f 100644 --- a/common/urls.py +++ b/common/urls.py @@ -45,9 +45,9 @@ urlpatterns = [ # Qualification EDE path('student_ede//send_convocation/', views.StudentConvocationExaminationView.as_view(), name='student-ede-convocation'), - path('student_ede//examination/expert/', views.print_expert_ede_compensation_form, + path('student_ede//examination/expert/', views.PrintExpertEDECompensationForm.as_view(), name='print-expert-compens-ede'), - path('student_ede//examination/mentor/', views.print_mentor_ede_compensation_form, + path('student_ede//examination/mentor/', views.PrintMentorEDECompensationForm.as_view(), name='print-mentor-compens-ede'), path('student_ede/export_qualif_ede/', views.export.export_qualification_ede, name='export-qualif-ede'), @@ -55,6 +55,8 @@ urlpatterns = [ # Qualification EDS path('student_eds//send_convocation/', views.StudentConvocationEDSView.as_view(), name='student-eds-convocation'), + path('student_eds//examination/expert/', views.PrintExpertEDSCompensationForm.as_view(), + name='print-expert-compens-eds'), path('imputations/export/', views.export.imputations_export, name='imputations_export'), path('export_sap/', views.export.export_sap, name='export_sap'), diff --git a/stages/admin.py b/stages/admin.py index 19b98a7..b6dfc60 100644 --- a/stages/admin.py +++ b/stages/admin.py @@ -105,7 +105,7 @@ class StudentAdmin(admin.ModelAdmin): ordering = ('last_name', 'first_name') list_filter = (('archived', ArchivedListFilter), ('klass', KlassRelatedListFilter)) search_fields = ('last_name', 'first_name', 'pcode', 'city', 'klass__name') - autocomplete_fields = ('corporation', 'instructor', 'supervisor', 'mentor', 'expert') + autocomplete_fields = ('corporation', 'instructor', 'supervisor', 'mentor', 'expert', 'expert_ep') readonly_fields = ( 'report_sem1_sent', 'report_sem2_sent', 'examination_actions', 'examination_ep_actions', @@ -180,7 +180,7 @@ class StudentAdmin(admin.ModelAdmin): return format_html( 'Courrier pour l’expert ' 'Mail convocation soutenance ', - '', #reverse('print-expert-compens-eds', args=[obj.pk]), + reverse('print-expert-compens-eds', args=[obj.pk]), reverse('student-eds-convocation', args=[obj.pk]), ) else: diff --git a/stages/pdf.py b/stages/pdf.py index 1da60ec..624a658 100644 --- a/stages/pdf.py +++ b/stages/pdf.py @@ -3,7 +3,6 @@ from datetime import date from django.conf import settings from django.contrib.staticfiles.finders import find from django.utils.dateformat import format as django_format -from django.utils.text import slugify from reportlab.lib.pagesizes import A4 from reportlab.lib.units import cm @@ -411,6 +410,29 @@ class CompensationForm: class ExpertEdeLetterPdf(CompensationForm, EpcBaseDocTemplate): + reference = 'ASH/val' + title = 'Travail de diplôme' + resp_filiere, resp_genre = settings.RESP_FILIERE_EDE + part1_text = """ + {expert_civility},

+ Vous avez accepté de fonctionner comme expert{expert_accord} pour un {title_lower} de l'un-e de nos + étudiant-e-s. Nous vous remercions très chaleureusement de votre disponibilité.

+ En annexe, nous avons l'avantage de vous remettre le travail de {student_civility_full_name}, + ainsi que la grille d'évaluation commune aux deux membres du jury.

+ La soutenance de ce travail de diplôme se déroulera le:

+ """ + part2_text = """ +
+ L'autre membre du jury sera {internal_expert_civility} {internal_expert_full_name}, {internal_expert_role} dans notre école.
+
+ Par ailleurs, nous nous permettons de vous faire parvenir en annexe le formulaire «Indemnisation d'experts aux examens» + que vous voudrez bien compléter au niveau des «données privées / coordonnées de paiement» et nous retourner dans les meilleurs délais. +

+ Restant à votre disposition pour tout complément d'information et en vous remerciant de + l'attention que vous porterez à la présente, nous vous prions d'agréer, {expert_civility}, l'asurance de notre considération distinguée.
+


+ """ + def __init__(self, out, student): self.student = student super().__init__(out) @@ -419,64 +441,69 @@ class ExpertEdeLetterPdf(CompensationForm, EpcBaseDocTemplate): PageTemplate(id='ISOPage', frames=[self.page_frame], onPage=self.header_iso), ]) - def produce(self): - self.add_address(self.student.expert) + def exam_data(self): + return { + 'expert': self.student.expert, + 'internal_expert': self.student.internal_expert, + 'date_exam': self.student.date_exam, + 'room': self.student.room, + } - ptext = """ + def produce(self): + exam_data = self.exam_data() + self.add_address(exam_data['expert']) + + header_text = """


La Chaux-de-Fonds, le {current_date}
- N/réf.:ASH/val
+ N/réf.: {ref}



- Travail de diplôme + {title}


- {expert_civility},

- Vous avez accepté de fonctionner comme expert{expert_accord} pour un travail de diplôme de l'un-e de nos - étudiant-e-s. Nous vous remercions très chaleureusement de votre disponibilité.

- En annexe, nous avons l'avantage de vous remettre le travail de {student_civility_full_name}, - ainsi que la grille d'évaluation commune aux deux membres du jury.

- La soutenance de ce travail de diplôme se déroulera le:

""" - self.story.append(Paragraph(ptext.format( + self.story.append(Paragraph(header_text.format( current_date=django_format(date.today(), 'j F Y'), - expert_civility=self.student.expert.civility, - expert_accord=self.student.expert.adjective_ending, + ref=self.reference, + title=self.title, + ), style_normal)) + + self.story.append(Paragraph(self.part1_text.format( + title_lower=self.title.lower(), + expert_civility=exam_data['expert'].civility, + expert_accord=exam_data['expert'].adjective_ending, student_civility_full_name=self.student.civility_full_name, ), style_normal)) - ptext = "
{0} à l'Ecole Santé-social Pierre-Coullery, salle {1}

" - self.story.append(Paragraph(ptext.format( - django_format(self.student.date_exam, 'l j F Y à H\hi'), - self.student.room + + date_text = "
{0} à l'Ecole Santé-social Pierre-Coullery, salle {1}

" + self.story.append(Paragraph(date_text.format( + django_format(exam_data['date_exam'], 'l j F Y à H\hi'), + exam_data['room'], ), style_bold_center)) - ptext = """ -
- L'autre membre du jury sera {internal_expert_civility} {internal_expert_full_name}, {internal_expert_role} dans notre école.
-
- Par ailleurs, nous nous permettons de vous faire parvenir en annexe le formulaire «Indemnisation d'experts aux examens» - que vous voudrez bien compléter au niveau des «données privées / coordonnées de paiement» et nous retourner dans les meilleurs délais. -

- Restant à votre disposition pour tout complément d'information et en vous remerciant de - l'attention que vous porterez à la présente, nous vous prions d'agréer, {expert_civility}, l'asurance de notre considération distinguée.
-


- La responsable de filière:
-

- {resp_filiere} -


- Annexes: ment. - """ - self.story.append(Paragraph(ptext.format( - internal_expert_civility=self.student.internal_expert.civility, - internal_expert_full_name=self.student.internal_expert.full_name, - internal_expert_role=self.student.internal_expert.role, - expert_civility=self.student.expert.civility, - resp_filiere=settings.RESP_FILIERE_EDE, + self.story.append(Paragraph(self.part2_text.format( + internal_expert_civility=exam_data['internal_expert'].civility, + internal_expert_full_name=exam_data['internal_expert'].full_name, + internal_expert_role=exam_data['internal_expert'].role, + expert_civility=exam_data['expert'].civility, + ), style_normal)) + + footer_text = """ + {lela} responsable de filière:
+

+ {resp_filiere} +


+ Annexes: ment. + """ + self.story.append(Paragraph(footer_text.format( + lela='Le' if self.resp_genre == 'M' else 'La', + resp_filiere=self.resp_filiere, ), style_normal)) # ISO page self.story.append(NextPageTemplate('ISOPage')) self.story.append(PageBreak()) - self.add_private_data(self.student.expert) + self.add_private_data(exam_data['expert']) self.story.append(Paragraph( "Mandat: Soutenance de {0} {1}, classe {2}".format( @@ -484,7 +511,7 @@ class ExpertEdeLetterPdf(CompensationForm, EpcBaseDocTemplate): ), style_normal )) self.story.append(Paragraph( - "Date de l'examen : {}".format(django_format(self.student.date_exam, 'l j F Y')), style_normal + "Date de l'examen : {}".format(django_format(exam_data['date_exam'], 'l j F Y')), style_normal )) self.story.append(Spacer(0, 2 * cm)) @@ -493,6 +520,29 @@ class ExpertEdeLetterPdf(CompensationForm, EpcBaseDocTemplate): self.build(self.story) +class ExpertEdsLetterPdf(ExpertEdeLetterPdf): + reference = 'BAH/ner' + title = 'Travail final' + resp_filiere, resp_genre = settings.RESP_FILIERE_EDS + part1_text = """ + {expert_civility},

+ Vous avez accepté de fonctionner comme expert{expert_accord} pour un {title_lower} de l'un-e de nos + étudiant-e-s. Nous vous remercions très chaleureusement de votre disponibilité.

+ En annexe, nous avons l'avantage de vous remettre le travail de {student_civility_full_name}, + ainsi que diverses informations sur le cadre de cet examen et la grille d'évaluation + commune aux deux membres du jury.

+ La soutenance de ce travail de diplôme se déroulera le:

+ """ + + def exam_data(self): + return { + 'expert': self.student.expert_ep, + 'internal_expert': self.student.internal_expert_ep, + 'date_exam': self.student.date_exam_ep, + 'room': self.student.room_ep, + } + + class MentorCompensationPdfForm(CompensationForm, EpcBaseDocTemplate): def __init__(self, out, student): self.student = student diff --git a/stages/tests.py b/stages/tests.py index 22cf92f..f876ee1 100644 --- a/stages/tests.py +++ b/stages/tests.py @@ -330,7 +330,7 @@ tél. 032 886 33 00 st = Student.objects.get(first_name="Albin") url = reverse('print-expert-compens-ede', args=[st.pk]) self.client.login(username='me', password='mepassword') - response = self.client.post(url, follow=True) + response = self.client.get(url, follow=True) self.assertContains(response, "Toutes les informations ne sont pas disponibles") st.expert = CorpContact.objects.get(last_name="Horner") @@ -339,7 +339,7 @@ tél. 032 886 33 00 st.room = "B123" st.save() self.client.login(username='me', password='mepassword') - response = self.client.post(url, follow=True) + response = self.client.get(url, follow=True) self.assertEqual( response['Content-Disposition'], 'attachment; filename="dupond_albin_Expert.pdf"' @@ -349,13 +349,13 @@ tél. 032 886 33 00 # Expert without corporation st.expert = CorpContact.objects.create(first_name='James', last_name='Bond') st.save() - response = self.client.post(url, follow=True) + response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) # Mentor form st.mentor = CorpContact.objects.get(last_name="Horner") st.save() - response = self.client.post(reverse('print-mentor-compens-ede', args=[st.pk]), follow=True) + response = self.client.get(reverse('print-mentor-compens-ede', args=[st.pk]), follow=True) self.assertEqual( response['Content-Disposition'], 'attachment; filename="dupond_albin_Indemn_mentor.pdf"' @@ -363,6 +363,38 @@ tél. 032 886 33 00 self.assertEqual(response['Content-Type'], 'application/pdf') self.assertGreater(int(response['Content-Length']), 1000) + def test_print_eds_compensation_forms(self): + klass = Klass.objects.create( + name="3EDS", section=Section.objects.get(name='EDS'), level=Level.objects.get(name='3') + ) + st = Student.objects.create( + first_name="Laurent", last_name="Hots", birth_date="1994-07-12", + pcode="2000", city="Neuchâtel", klass=klass + ) + url = reverse('print-expert-compens-eds', args=[st.pk]) + self.client.login(username='me', password='mepassword') + response = self.client.get(url, follow=True) + self.assertContains(response, "Toutes les informations ne sont pas disponibles") + + st.expert_ep = CorpContact.objects.get(last_name="Horner") + st.internal_expert_ep = Teacher.objects.get(last_name="Caux") + st.date_exam_ep = datetime(2018, 6, 28, 12, 00) + st.room_ep = "B123" + st.save() + + response = self.client.get(url, follow=True) + self.assertEqual( + response['Content-Disposition'], + 'attachment; filename="hots_laurent_Expert.pdf"' + ) + self.assertEqual(response['Content-Type'], 'application/pdf') + self.assertGreater(int(response['Content-Length']), 1000) + # Expert without corporation + st.expert_ep = CorpContact.objects.create(first_name='James', last_name='Bond') + st.save() + response = self.client.get(url, follow=True) + self.assertEqual(response.status_code, 200) + def test_EDEpe_klass(self): lev3 = Level.objects.create(name='3') klass4 = Klass.objects.create(name="3EDEp_pe", section=Section.objects.get(name='EDE'), level=lev3) diff --git a/stages/views/__init__.py b/stages/views/__init__.py index 91e9690..b199fb5 100644 --- a/stages/views/__init__.py +++ b/stages/views/__init__.py @@ -18,7 +18,7 @@ from django.utils.dateformat import format as django_format from django.utils.text import slugify from django.views.generic import DetailView, FormView, ListView, TemplateView, UpdateView -from .base import EmailConfirmationBaseView, ZippedFilesBaseView +from .base import EmailConfirmationBaseView, PDFBaseView, ZippedFilesBaseView from .export import OpenXMLExport from .imports import HPContactsImportView, HPImportView, ImportReportsView, StudentImportView from ..forms import CorporationMergeForm, EmailBaseForm, StudentCommentForm @@ -27,8 +27,8 @@ from ..models import ( Training, Availability ) from ..pdf import ( - ChargeSheetPDF, ExpertEdeLetterPdf, UpdateDataFormPDF, MentorCompensationPdfForm, - KlassListPDF, + ChargeSheetPDF, ExpertEdeLetterPdf, ExpertEdsLetterPdf, UpdateDataFormPDF, + MentorCompensationPdfForm, KlassListPDF, ) from ..utils import school_year_start @@ -543,47 +543,73 @@ class PrintUpdateForm(ZippedFilesBaseView): yield ('{0}.pdf'.format(klass.name), buff.getvalue()) -def print_expert_ede_compensation_form(request, pk): +class PrintExpertEDECompensationForm(PDFBaseView): """ Imprime le PDF à envoyer à l'expert EDE en accompagnement du travail de diplôme """ - student = Student.objects.get(pk=pk) - missing = student.missing_examination_data() - if missing: - messages.error(request, "\n".join( - ["Toutes les informations ne sont pas disponibles pour la lettre à l’expert!"] - + missing - )) - return redirect(reverse("admin:stages_student_change", args=(student.pk,))) - buff = io.BytesIO() - pdf = ExpertEdeLetterPdf(buff, student) - pdf.produce() - filename = slugify( - '{0}_{1}'.format(student.last_name, student.first_name) - ) + '_Expert.pdf' - buff.seek(0) - return FileResponse(buff, as_attachment=True, filename=filename) + pdf_class = ExpertEdeLetterPdf + + def filename(self, student): + return slugify('{0}_{1}'.format(student.last_name, student.first_name)) + '_Expert.pdf' + + def get_object(self): + return Student.objects.get(pk=self.kwargs['pk']) + + def check_object(self, student): + missing = student.missing_examination_data() + if missing: + messages.error(self.request, "\n".join( + ["Toutes les informations ne sont pas disponibles pour la lettre à l’expert!"] + + missing + )) + return redirect(reverse("admin:stages_student_change", args=(student.pk,))) + + def get(self, request, *args, **kwargs): + student = self.get_object() + response = self.check_object(student) + if response: + return response + return super().get(request, *args, **kwargs) -def print_mentor_ede_compensation_form(request, pk): +class PrintMentorEDECompensationForm(PDFBaseView): """ Imprime le PDF à envoyer au mentor EDE pour le mentoring """ - student = Student.objects.get(pk=pk) - if not student.mentor: - messages.error(request, "Aucun mentor n'est attribué à cet étudiant") - return redirect(reverse("admin:stages_student_change", args=(student.pk,))) - buff = io.BytesIO() - pdf = MentorCompensationPdfForm(buff, student) - pdf.produce() - filename = slugify( - '{0}_{1}'.format(student.last_name, student.first_name) - ) + '_Indemn_mentor.pdf' - buff.seek(0) - return FileResponse(buff, as_attachment=True, filename=filename) + pdf_class = MentorCompensationPdfForm + + def filename(self, student): + return slugify( + '{0}_{1}'.format(student.last_name, student.first_name) + ) + '_Indemn_mentor.pdf' + + def get_object(self): + return Student.objects.get(pk=self.kwargs['pk']) + + def get(self, request, *args, **kwargs): + student = self.get_object() + if not student.mentor: + messages.error(request, "Aucun mentor n'est attribué à cet étudiant") + return redirect(reverse("admin:stages_student_change", args=(student.pk,))) + return super().get(request, *args, **kwargs) +class PrintExpertEDSCompensationForm(PrintExpertEDECompensationForm): + """ + Imprime le PDF à envoyer à l'expert EDS en accompagnement du + travail final. + """ + pdf_class = ExpertEdsLetterPdf + + def check_object(self, student): + missing = student.missing_examination_ep_data() + if missing: + messages.error(self.request, "\n".join( + ["Toutes les informations ne sont pas disponibles pour la lettre à l’expert!"] + + missing + )) + return redirect(reverse("admin:stages_student_change", args=(student.pk,))) class PrintKlassList(ZippedFilesBaseView): @@ -594,7 +620,7 @@ class PrintKlassList(ZippedFilesBaseView): buff = io.BytesIO() pdf = KlassListPDF(buff, klass) pdf.produce(klass) - filename = slugify('{0}.pdf'.format(klass.name)) + filename = slugify(klass.name + '.pdf') yield (filename, buff.getvalue()) @@ -612,5 +638,5 @@ class PrintChargeSheet(ZippedFilesBaseView): buff = io.BytesIO() pdf = ChargeSheetPDF(buff, teacher) pdf.produce(activities) - filename = slugify('{0}_{1}.pdf'.format(teacher.last_name, teacher.first_name)) + filename = slugify('{0}_{1}'.format(teacher.last_name, teacher.first_name)) + '.pdf' yield (filename, buff.getvalue()) diff --git a/stages/views/base.py b/stages/views/base.py index 7977b83..7cab06f 100644 --- a/stages/views/base.py +++ b/stages/views/base.py @@ -1,10 +1,11 @@ +import io import os import tempfile import zipfile from django.contrib import messages from django.core.mail import EmailMessage -from django.http import HttpResponse +from django.http import FileResponse, HttpResponse from django.urls import reverse_lazy from django.views.generic import FormView, View @@ -51,6 +52,18 @@ class EmailConfirmationBaseView(FormView): return context +class PDFBaseView(View): + pdf_class = None + + def get(self, request, *args, **kwargs): + obj = self.get_object() + buff = io.BytesIO() + pdf = self.pdf_class(buff, obj) + pdf.produce() + buff.seek(0) + return FileResponse(buff, as_attachment=True, filename=self.filename(obj)) + + class ZippedFilesBaseView(View): """A base class to return a .zip file containing a compressed list of files.""" filename = 'to_be_defined.zip'