diff --git a/candidats/admin.py b/candidats/admin.py index e369da6..439ea6f 100644 --- a/candidats/admin.py +++ b/candidats/admin.py @@ -1,3 +1,6 @@ +import os +import tempfile +import zipfile from collections import OrderedDict from datetime import date @@ -5,11 +8,13 @@ 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 stages.exports import OpenXMLExport from .models import Candidate, Interview, GENDER_CHOICES +from .pdf import InscriptionSummaryPDF def export_candidates(modeladmin, request, queryset): @@ -84,10 +89,29 @@ def send_confirmation_mail(modeladmin, request, queryset): 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 CandidateAdminForm(forms.ModelForm): interview = forms.ModelChoiceField(queryset=Interview.objects.all(), required=False) @@ -123,7 +147,7 @@ class CandidateAdmin(admin.ModelAdmin): list_display = ('last_name', 'first_name', 'section', 'confirm_mail', 'convocation') list_filter = ('section', 'option') readonly_fields = ('total_result_points', 'total_result_mark', 'date_confirmation_mail') - actions = [export_candidates, send_confirmation_mail] + actions = [export_candidates, send_confirmation_mail, print_summary] fieldsets = ( (None, { 'fields': (('first_name', 'last_name', 'gender'), diff --git a/candidats/models.py b/candidats/models.py index 6a29d25..c776d60 100644 --- a/candidats/models.py +++ b/candidats/models.py @@ -138,6 +138,9 @@ class Candidate(models.Model): else: return '' + def get_ok(self, fieldname): + return 'OK' if getattr(self, fieldname) is True else 'NON' + INTERVIEW_CHOICES = ( ('N', 'Normal'), diff --git a/candidats/pdf.py b/candidats/pdf.py new file mode 100644 index 0000000..9908731 --- /dev/null +++ b/candidats/pdf.py @@ -0,0 +1,113 @@ +import os +import tempfile + +from reportlab.lib.enums import TA_LEFT +from reportlab.lib.styles import ParagraphStyle as PS +from reportlab.lib.units import cm +from reportlab.platypus import Paragraph, Preformatted, Table, TableStyle + +from django.utils.text import slugify + +from stages.pdf import EpcBaseDocTemplate +from .models import ( + AES_ACCORDS_CHOICES, DIPLOMA_CHOICES, DIPLOMA_STATUS_CHOICES, + OPTION_CHOICES, RESIDENCE_PERMITS_CHOICES, +) + +style_normal = PS(name='CORPS', fontName='Helvetica', fontSize=9, alignment=TA_LEFT) +style_normal_bold = PS(name='CORPS', fontName='Helvetica-Bold', fontSize=9, alignment=TA_LEFT, spaceBefore=0.5 * cm) + + +class InscriptionSummaryPDF(EpcBaseDocTemplate): + """ + PDF for summary of inscription + """ + def __init__(self, candidate, **kwargs): + 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() + + def produce(self, candidate): + # personal data + options = dict(OPTION_CHOICES) + diploma = dict(DIPLOMA_CHOICES) + diploma_status = dict(DIPLOMA_STATUS_CHOICES) + aes_accords = dict(AES_ACCORDS_CHOICES) + residence_permits = dict(RESIDENCE_PERMITS_CHOICES) + + myTableStyle = TableStyle([ + ('ALIGN', (0, 0), (-1, -1), 'LEFT'), + ('FONT', (0, 0), (-1, -1), 'Helvetica'), + ('SIZE', (0, 0), (-1, -1), 8) + ]) + + self.story.append(Paragraph("Données personnelles", style_normal_bold)) + data = [ + ['Nom: ', candidate.last_name, 'Date de naissance:', candidate.birth_date], + ['Prénom:', candidate.first_name, 'Canton:', candidate.district], + ['N° de tél.:', candidate.mobile, '',''], + ] + t = Table(data, colWidths=[2 * cm, 6 * cm, 4 * cm, 5 * cm], hAlign=TA_LEFT) + t.setStyle(myTableStyle) + self.story.append(t) + + # Chosen Option + data = [] + self.story.append(Paragraph("Option choisie", style_normal_bold)) + data.append([options.get(candidate.option, '')]) + t = Table(data, colWidths=[17 * cm], hAlign=TA_LEFT) + t.setStyle(myTableStyle) + self.story.append(t) + + # Diploma + data = [] + self.story.append(Paragraph("Titres / diplômes /attest. prof.", style_normal_bold)) + data.append([ + diploma[candidate.diploma], + '{0} {1}'.format(candidate.diploma_detail, diploma_status[candidate.diploma_status]) + ]) + + if candidate.diploma == 1: # CFC ASE + data.append(['Evaluation du dernier stage', candidate.get_ok('work_certificate')]) + elif candidate.diploma == 2: # CFC autre domaine + data.append(['Attestation de 800h. min. domaine Enfance',candidate.get_ok('certif_of_800_childhood')]) + data.append(["Bilan de l'activité professionnelle", candidate.get_ok('work_certificate')]) + elif candidate.diploma == 3 or candidate.diploma == 4: # Matu. aca ou ECG ou Portfolio + data.append(['Attestation de 800h. min. domaine Général', candidate.get_ok('certif_800_general')]) + data.append(['Attestation de 800h. min. domaine Enfance', candidate.get_ok('certif_of_800_childhood')]) + data.append(["Bilan de l'activité professionnelle", candidate.get_ok('work_certificate')]) + + if candidate.option != 'PS': + data.append(["Contrat de travail", candidate.get_ok('contract')]) + data.append(["Promesse d'engagement", candidate.get_ok('promise')]) + data.append(["Taux d'activité", candidate.activity_rate]) + t = Table(data, colWidths=[12 * cm, 5 * cm], hAlign=TA_LEFT) + t.setStyle(myTableStyle) + self.story.append(t) + + # Others documents + data = [] + self.story.append(Paragraph("Autres documents", style_normal_bold)) + docs = [ + 'registration_form', 'certificate_of_payement', 'police_record', 'cv', 'has_photo', + 'reflexive_text', 'marks_certificate', 'aes_accords', 'residence_permits' + ] + for doc in docs: + data.append([candidate._meta.get_field(doc).verbose_name, candidate.get_ok(doc)]) + data.append(['Validation des accords AES', aes_accords[candidate.aes_accords]]) + data.append(['Autorisation de séjour (pour les personnes étrangères)', residence_permits[candidate.residence_permits]]) + + t = Table(data, colWidths=[12 * cm, 5 * cm], hAlign=TA_LEFT) + t.setStyle(myTableStyle) + self.story.append(t) + + # Remarks + data = [] + self.story.append(Paragraph("Remarques", style_normal_bold)) + data.append([Preformatted(candidate.comment, style_normal, maxLineLength=100)]) + t = Table(data, colWidths=[17 * cm], hAlign=TA_LEFT) + t.setStyle(myTableStyle) + self.story.append(t) + + self.build(self.story) diff --git a/candidats/tests.py b/candidats/tests.py index 81700df..292d1ae 100644 --- a/candidats/tests.py +++ b/candidats/tests.py @@ -163,3 +163,22 @@ tél. 032 886 33 00""" self.assertEqual(mail.outbox[0].subject, "Procédure de qualification") henri.refresh_from_db() self.assertIsNotNone(henri.convocation_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') + 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) + self.assertEqual( + response['Content-Disposition'], + 'attachment; filename="archive_InscriptionResumes.zip"' + ) + self.assertEqual(response['Content-Type'], 'application/zip') + self.assertGreater(len(response.content), 200) diff --git a/stages/pdf.py b/stages/pdf.py index 95966f1..e512788 100644 --- a/stages/pdf.py +++ b/stages/pdf.py @@ -6,20 +6,74 @@ from django.conf import settings from django.contrib.staticfiles.finders import find from django.utils.text import slugify -from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, - PageBreak, Table, TableStyle, Image) from reportlab.lib.pagesizes import A4, landscape from reportlab.lib.units import cm from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT from reportlab.lib import colors from reportlab.lib.styles import ParagraphStyle as PS +from reportlab.platypus import ( + Frame, Image, NextPageTemplate, PageBreak, PageTemplate, Paragraph, + SimpleDocTemplate, Spacer, Table, TableStyle, +) style_normal = PS(name='CORPS', fontName='Helvetica', fontSize=8, alignment = TA_LEFT) style_bold = PS(name='CORPS', fontName='Helvetica-Bold', fontSize=10, alignment = TA_LEFT) -style_title = PS(name='CORPS', fontName='Helvetica-Bold', fontSize=12, alignment = TA_LEFT, spaceBefore=2*cm) +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) +LOGO_EPC = find('img/logo_EPC.png') +LOGO_ESNE = find('img/logo_ESNE.png') + + +class EpcBaseDocTemplate(SimpleDocTemplate): + filiere = 'Formation EDE' + + def __init__(self, filename, title='', pagesize=A4): + super().__init__( + filename, pagesize=pagesize, _pageBreakQuick=0, + lefMargin=1.5 * cm, bottomMargin=1.5 * cm, topMargin=1.5 * cm, rightMargin=1.5 * cm + ) + 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 + ) + canvas.line(doc.leftMargin, doc.height - 0.5 * cm, doc.width + doc.leftMargin, doc.height - 0.5 * cm) + canvas.drawString(doc.leftMargin, doc.height - 1.1 * cm, self.filiere) + canvas.drawRightString(doc.width + doc.leftMargin, doc.height - 1.1 * cm, self.title) + canvas.line(doc.leftMargin, doc.height - 1.3 * cm, doc.width + doc.leftMargin, doc.height - 1.3 * cm) + canvas.restoreState() + + def later_header(self, canvas, doc): + canvas.saveState() + canvas.line(doc.leftMargin, doc.height + 1 * cm, doc.width + doc.leftMargin, doc.height + 1 * cm) + canvas.drawString(doc.leftMargin, doc.height + 0.5 * cm, self.filiere) + canvas.drawRightString(doc.width + doc.leftMargin, doc.height + 0.5 * cm, self.title) + canvas.line(doc.leftMargin, doc.height + 0.2 * cm, doc.width + doc.leftMargin, doc.height + 0.2 * cm) + canvas.restoreState() + + def setNormalTemplatePage(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 + ) + later_pages_table_frame = Frame( + self.leftMargin, self.bottomMargin, self.width + 1 * cm, self.height - 2 * cm, + id='later_table', showBoundary=0, leftPadding=0 * cm + ) + # Page template + first_page = PageTemplate(id='FirstPage', frames=[first_page_table_frame], onPage=self.header) + later_pages = PageTemplate(id='LaterPages', frames=[later_pages_table_frame], onPage=self.later_header) + self.addPageTemplates([first_page, later_pages]) + self.story = [NextPageTemplate(['*', 'LaterPages'])] + class ChargeSheetPDF(SimpleDocTemplate): """