diff --git a/candidats/pdf.py b/candidats/pdf.py index 7dc45de..7a31d16 100644 --- a/candidats/pdf.py +++ b/candidats/pdf.py @@ -27,7 +27,7 @@ class InscriptionSummaryPDF(EpcBaseDocTemplate): filename = slugify('{0}_{1}'.format(candidate.last_name, candidate.first_name)) + '.pdf' path = os.path.join(tempfile.gettempdir(), filename) super().__init__(path, title="Dossier d'inscription", **kwargs) - self.setNormalTemplatePage() + self.set_normal_template_page() def produce(self, candidate): # personal data diff --git a/common/urls.py b/common/urls.py index 97d736a..0292121 100644 --- a/common/urls.py +++ b/common/urls.py @@ -36,7 +36,9 @@ urlpatterns = [ # Qualification EDE path('student_ede//send_convocation', views.StudentConvocationExaminationView.as_view(), - name='student-ede-convocation'), + name='student-ede-convocation'), + path('student_ede//pdf_to_expert', views.print_pdf_to_expert_ede, + name='print-pdf-to-expert-ede'), path('imputations/export/', views.imputations_export, name='imputations_export'), path('print/update_form/', views.print_update_form, name='print_update_form'), diff --git a/stages/admin.py b/stages/admin.py index 30b80c2..bda577a 100644 --- a/stages/admin.py +++ b/stages/admin.py @@ -148,7 +148,9 @@ class StudentAdmin(admin.ModelAdmin): def examination_actions(self, obj): if obj.klass.section.name == 'EDE' and obj.klass.level.name == "3": return format_html( + 'Courrier pour l’expert ' 'Mail convocation soutenance', + reverse('print-pdf-to-expert-ede', args=[obj.pk]), reverse('student-ede-convocation', args=[obj.pk]) ) else: diff --git a/stages/models.py b/stages/models.py index e626a5b..650b9e6 100644 --- a/stages/models.py +++ b/stages/models.py @@ -85,6 +85,10 @@ class Teacher(models.Model): def civility_full_name(self): return '{0} {1} {2}'.format(self.civility, self.first_name, self.last_name) + @property + def role(self): + return {'Monsieur': 'enseignant-formateur', 'Madame': 'enseignante-formatrice'}.get(self.civility, '') + def calc_activity(self): """ Return a dictionary of calculations relative to teacher courses. @@ -282,6 +286,13 @@ class Student(models.Model): def pcode_city(self): return '{0} {1}'.format(self.pcode, self.city) + @property + def role(self): + if self.klass.section.is_fe(): + return {'M': 'apprenti', 'F': 'apprentie'}.get(self.gender, '') + else: + return {'M': 'étudiant', 'F': 'étudiante'}.get(self.gender, '') + @property def is_examination_valid(self): return (self.date_exam and self.room and self.expert and self.internal_expert) @@ -398,6 +409,10 @@ class CorpContact(models.Model): def pcode_city(self): return '{0} {1}'.format(self.pcode, self.city) + @property + def adjective_ending(self): + return 'e' if self.title == 'Madame' else '' + class Domain(models.Model): name = models.CharField(max_length=50, verbose_name='Nom') diff --git a/stages/pdf.py b/stages/pdf.py index e512788..b385964 100644 --- a/stages/pdf.py +++ b/stages/pdf.py @@ -4,9 +4,10 @@ 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, landscape +from reportlab.lib.pagesizes import A4 from reportlab.lib.units import cm from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT from reportlab.lib import colors @@ -21,6 +22,8 @@ style_bold = PS(name='CORPS', fontName='Helvetica-Bold', fontSize=10, alignment style_title = PS(name='CORPS', fontName='Helvetica-Bold', fontSize=12, alignment = TA_LEFT, spaceBefore=1*cm) style_adress = PS(name='CORPS', fontName='Helvetica', fontSize=10, alignment = TA_LEFT, leftIndent=280) style_normal_right = PS(name='CORPS', fontName='Helvetica', fontSize=8, alignment = TA_RIGHT) +style_bold_center = PS(name="CORPS", fontName="Helvetica-Bold", fontSize=9, alignment=TA_CENTER) +style_footer = PS(name='CORPS', fontName='Helvetica', fontSize=7, alignment=TA_CENTER) LOGO_EPC = find('img/logo_EPC.png') LOGO_ESNE = find('img/logo_ESNE.png') @@ -59,7 +62,7 @@ class EpcBaseDocTemplate(SimpleDocTemplate): canvas.line(doc.leftMargin, doc.height + 0.2 * cm, doc.width + doc.leftMargin, doc.height + 0.2 * cm) canvas.restoreState() - def setNormalTemplatePage(self): + def set_normal_template_page(self): first_page_table_frame = Frame( self.leftMargin, self.bottomMargin, self.width + 1 * cm, self.height - 4 * cm, id='first_table', showBoundary=0, leftPadding=0 * cm @@ -75,6 +78,30 @@ class EpcBaseDocTemplate(SimpleDocTemplate): self.story = [NextPageTemplate(['*', 'LaterPages'])] +class EpcBaseLetterTemplate(EpcBaseDocTemplate): + def __init__(self, filename, title=''): + super().__init__(filename) + self.story = [] + self.title = title + + def header(self, canvas, doc): + canvas.saveState() + canvas.drawImage( + LOGO_EPC, doc.leftMargin, doc.height - 0.5 * cm, 5 * cm, 3 * cm, preserveAspectRatio=True + ) + canvas.drawImage( + LOGO_ESNE, doc.width - 2 * cm, doc.height - 0.5 * cm, 5 * cm, 3 * cm, preserveAspectRatio=True + ) + # Footer + canvas.line(doc.leftMargin, 1 * cm, doc.width + doc.leftMargin, 1 * cm) + footer = Paragraph('Ecole Santé-social Pierre-Coullery | Prévoyance 82 - 2300 La Chaux-de-Fonds | ' + '032 886 33 00 | cifom-epc@rpn.ch', style_footer) + w, h = footer.wrap(doc.width, doc.bottomMargin) + footer.drawOn(canvas, doc.leftMargin, h) + + canvas.restoreState() + + class ChargeSheetPDF(SimpleDocTemplate): """ Génération des feuilles de charges en pdf. @@ -85,9 +112,9 @@ class ChargeSheetPDF(SimpleDocTemplate): filename = slugify('{0}_{1}'.format(teacher.last_name, teacher.first_name)) + '.pdf' path = os.path.join(tempfile.gettempdir(), filename) super().__init__(path, pagesize=A4, topMargin=0*cm, leftMargin=2*cm) + self.story = [] def produce(self, activities): - self.story = [] header = open(find('img/header.gif'), 'rb') self.story.append(Image(header, width=520, height=75)) self.story.append(Spacer(0, 2*cm)) @@ -121,7 +148,7 @@ class ChargeSheetPDF(SimpleDocTemplate): t.hAlign = TA_CENTER self.story.append(t) self.story.append(Spacer(0, 2*cm)) - d = 'La Chaux-de-Fonds, le {0}'.format(date.today().strftime('%d.%m.%y')) + d = 'La Chaux-de-Fonds, le {0}'.format(django_format(date.today(), 'j F Y')) self.story.append(Paragraph(d, style_normal)) self.story.append(Spacer(0, 0.5*cm)) self.story.append(Paragraph('la direction', style_normal)) @@ -244,3 +271,81 @@ class UpdateDataFormPDF(SimpleDocTemplate): def is_instr_required(self, klass_name): return any(el in klass_name for el in ['FE', 'EDS']) + + +class ExpertEDEPDF(EpcBaseLetterTemplate): + """ + PDF letter to expert EDE + """ + + def __init__(self, student, **kwargs): + filename = slugify('{0}_{1}'.format(student.last_name, student.first_name)) + '.pdf' + path = os.path.join(tempfile.gettempdir(), filename) + super().__init__(path, title="", **kwargs) + self.set_normal_template_page() + + def produce(self, student): + # Expert adresse + self.story.append(Paragraph(student.expert.title, style_adress)) + self.story.append(Paragraph(student.expert.full_name, style_adress)) + self.story.append(Paragraph(student.expert.street, style_adress)) + self.story.append((Paragraph(student.expert.pcode_city, style_adress))) + ptext = """ +


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


+ Travail de diplôme +


+ {expert_title},

+ 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( + current_date=django_format(date.today(), 'j F Y'), + expert_title=student.expert.title, + expert_accord=student.expert.adjective_ending, + student_civility_full_name=student.civility_full_name, + ), style_normal)) + ptext = "
{0} à l'Ecole Santé-social Pierre-Coullery, salle {1}

" + self.story.append(Paragraph(ptext.format( + django_format(student.date_exam, 'l j F Y à H\hi'), + student.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_title}, l'asurance de notre considération distinguée.
+


+ La responsable de filière:
+

+ Ann Schaub-Murray +


+ Annexes: ment. +

+ Copies pour information:
+ - {student_civility} {student_full_name}, {student_role}
+ - {internal_expert_civility2} {internal_expert_full_name2}, {internal_expert_role2} + """ + self.story.append(Paragraph(ptext.format( + internal_expert_civility=student.internal_expert.civility, + internal_expert_full_name=student.internal_expert.full_name, + internal_expert_role=student.internal_expert.role, + expert_title=student.expert.title, + student_civility=student.civility, + student_full_name=student.full_name, + student_role=student.role, + internal_expert_civility2=student.internal_expert.civility, + internal_expert_full_name2=student.internal_expert.full_name, + internal_expert_role2=student.internal_expert.role + ), style_normal)) + self.build(self.story) diff --git a/stages/tests.py b/stages/tests.py index bde2ecd..2a3b1d5 100644 --- a/stages/tests.py +++ b/stages/tests.py @@ -192,6 +192,25 @@ tél. 032 886 33 00 """ self.assertEqual(response.context['form'].initial['message'], expected_message) + def test_print_letter_ede_expert(self): + st = Student.objects.get(first_name="Albin") + self.client.login(username='me', password='mepassword') + url = reverse('print-pdf-to-expert-ede', args=[st.pk]) + response = self.client.post(url, follow=True) + self.assertContains(response, "Toutes les informations ne sont pas disponibles pour la lettre à l’expert!") + 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.post(url, follow=True) + self.assertEqual( + response['Content-Disposition'], + 'attachment; filename="dupond_albin.pdf"' + ) + self.assertEqual(response['Content-Type'], 'application/pdf') + self.assertGreater(len(response.content), 200) + class PeriodTest(TestCase): def setUp(self): diff --git a/stages/views.py b/stages/views.py index 18553f7..d74b02e 100644 --- a/stages/views.py +++ b/stages/views.py @@ -34,7 +34,7 @@ from .models import ( Klass, Section, Option, Student, Teacher, Corporation, CorpContact, Course, Period, Training, Availability, ) -from .pdf import UpdateDataFormPDF +from .pdf import ExpertEDEPDF, UpdateDataFormPDF from .utils import is_int @@ -909,6 +909,24 @@ def print_update_form(request): return response +def print_pdf_to_expert_ede(request, pk): + """ + Imprime le PDF à envoyer à l'expert EDE en accompagnement du + travail de diplôme + """ + student = get_object_or_404(Student, pk=pk) + if not student.is_examination_valid: + messages.error(request, "Toutes les informations ne sont pas disponibles pour la lettre à l’expert!") + return redirect(reverse("admin:stages_student_change", args=(student.pk,))) + pdf = ExpertEDEPDF(student) + pdf.produce(student) + + 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 + + GENERAL_EXPORT_FIELDS = [ ('Num_Ele', 'ext_id'), ('Nom_Ele', 'last_name'),