diff --git a/common/urls.py b/common/urls.py index e0a6b33..c190a6c 100644 --- a/common/urls.py +++ b/common/urls.py @@ -11,6 +11,7 @@ urlpatterns = [ url(r'^import_students/', views.StudentImportView.as_view(), name='import-students'), url(r'^import_hp/', views.HPImportView.as_view(), name='import-hp'), url(r'^import_hp_contacts/', views.HPContactsImportView.as_view(), name='import-hp-contacts'), + url(r'^import_bulletins/', views.ImportBulletinView.as_view(), name='import-bulletins'), url(r'^attribution/$', views.AttributionView.as_view(), name='attribution'), url(r'^stages/export/(?Pall)?/?$', views.stages_export, name='stages_export'), diff --git a/stages/forms.py b/stages/forms.py index 2fd2a0f..f7317f1 100644 --- a/stages/forms.py +++ b/stages/forms.py @@ -34,3 +34,7 @@ class PeriodForm(forms.Form): class UploadHPFileForm(forms.Form): upload = forms.FileField(label='Fichier HyperPlanning') + + +class UploadBulletinForm(forms.Form): + upload = forms.FileField(label='Bulletins CLOEE (pdf)') diff --git a/stages/models.py b/stages/models.py index b293a5a..72eace4 100644 --- a/stages/models.py +++ b/stages/models.py @@ -189,7 +189,7 @@ class Student(models.Model): @property def civility(self): - return 'Monsieur' if self.gender == 'M' else 'Madame' + return {'M': 'Monsieur', 'F': 'Madame'}.get(self.gender, '') @property def full_name(self): diff --git a/stages/test_files/1ASEFEa.pdf b/stages/test_files/1ASEFEa.pdf new file mode 100644 index 0000000..dcb581b Binary files /dev/null and b/stages/test_files/1ASEFEa.pdf differ diff --git a/stages/tests.py b/stages/tests.py index 1077c18..cd765c6 100644 --- a/stages/tests.py +++ b/stages/tests.py @@ -4,6 +4,7 @@ from datetime import date from django.conf import settings from django.contrib.auth.models import User +from django.core import mail from django.test import TestCase, override_settings from django.urls import reverse from django.utils.html import escape @@ -348,3 +349,32 @@ class ImportTests(TestCase): self.assertContains(response, "NoSIRET est vide à ligne 4. Ligne ignorée") st1.refresh_from_db() self.assertEqual(st1.instructor.last_name, 'Geiser') + + def test_import_and_send_bulletins(self): + lev1 = Level.objects.create(name='1') + klass1 = Klass.objects.create( + name='1ASEFEa', + section=Section.objects.create(name='ASE'), + level=lev1, + ) + Student.objects.bulk_create([ + Student(first_name="Albin", last_name="Dupond", birth_date="1994-05-12", gender='M', + pcode="2300", city="La Chaux-de-Fonds", email="albin@example.org", + klass=klass1), + Student(first_name="Justine", last_name="Varrin", birth_date="1994-07-12", + pcode="2000", city="Neuchâtel", email="justine@example.org", klass=klass1), + Student(first_name="Elvire", last_name="Hickx", birth_date="1994-05-20", + pcode="2053", city="Cernier", email="elvire@example.org", klass=klass1), + ]) + path = os.path.join(os.path.dirname(__file__), 'test_files', '1ASEFEa.pdf') + self.client.login(username='me', password='mepassword') + with open(path, 'rb') as fh: + response = self.client.post(reverse('import-bulletins'), {'upload': fh}, follow=True) + messages = [str(msg) for msg in response.context['messages']] + self.assertIn("Impossible de trouver un fichier PDF pour l'étudiant Hickx Elvire", messages) + self.assertIn('2 messages sur 3 élèves ont été envoyés', messages) + self.assertEqual(len(mail.outbox), 2) + # Second email as bcc + self.assertEqual(mail.outbox[0].recipients(), ['albin@example.org', 'me@example.org']) + self.assertEqual(mail.outbox[1].recipients(), ['justine@example.org', 'me@example.org']) + self.assertIn("le bulletin scolaire de Monsieur Albin Dupond", mail.outbox[0].body) diff --git a/stages/views.py b/stages/views.py index 290aee0..2d97558 100644 --- a/stages/views.py +++ b/stages/views.py @@ -1,5 +1,8 @@ import json import os +import re +from subprocess import PIPE, Popen, call + import tempfile import zipfile from collections import OrderedDict @@ -10,15 +13,18 @@ from tabimport import CSVImportedFile, FileFactory from django.conf import settings from django.contrib import messages from django.core.files import File +from django.core.mail import EmailMessage from django.db.models import Case, Count, When, Q from django.http import HttpResponse, HttpResponseNotAllowed, HttpResponseRedirect from django.shortcuts import get_object_or_404 +from django.template import loader from django.urls import reverse from django.utils.translation import ugettext as _ +from django.utils.text import slugify from django.views.generic import DetailView, FormView, TemplateView, ListView from .exports import OpenXMLExport -from .forms import PeriodForm, StudentImportForm, UploadHPFileForm +from .forms import PeriodForm, StudentImportForm, UploadHPFileForm, UploadBulletinForm from .models import ( Klass, Section, Option, Student, Teacher, Corporation, CorpContact, Course, Period, Training, Availability, @@ -497,6 +503,113 @@ class HPContactsImportView(ImportViewBase): return {'modified': obj_modified, 'errors': errors} +class ImportBulletinView(FormView): + template_name = 'file_import.html' + form_class = UploadBulletinForm + + def form_valid(self, form): + upfile = form.cleaned_data['upload'] + klass_name = upfile.name[:-4] + + try: + klass = Klass.objects.get(name=klass_name) + except Klass.DoesNotExist: + messages.error(self.request, "La classe %s n'existe pas !" % klass_name) + return HttpResponseRedirect(reverse('admin:index')) + + # Check poppler-utils presence on server + res = call(['pdftotext', '-v'], stderr=PIPE) + if res != 0: + messages.error(self.request, "Unable to find pdftotext on your system. Try to install the poppler-utils package.") + return HttpResponseRedirect(reverse('admin:index')) + + # Move the file to MEDIA directory + pdf_origin = os.path.join(settings.MEDIA_ROOT, upfile.name) + with open(pdf_origin, 'wb+') as destination: + for chunk in upfile.chunks(): + destination.write(chunk) + + try: + self.send_bulletins(klass, pdf_origin) + except Exception as err: + if settings.DEBUG: + raise + else: + messages.error(self.request, "Erreur durant l'envoi des bulletins PDF: %s" % err) + return HttpResponseRedirect(reverse('admin:index')) + + def send_bulletins(self, klass, pdf_path): + path = os.path.abspath(pdf_path) + student_regex = '[E|É]lève\s*:\s*([^\n]*)' + # Directory automatically deleted when the variable is deleted + _temp_dir = tempfile.TemporaryDirectory() + temp_dir = _temp_dir.name + + os.system("pdfseparate %s %s/%s_%%d.pdf" % (path, temp_dir, os.path.basename(path)[:-4])) + + # Look for student names in each separated PDF and rename PDF with student name + for filename in os.listdir(temp_dir): + p = Popen(['pdftotext', os.path.join(temp_dir, filename), '-'], + shell=False, stdout=PIPE, stderr=PIPE) + output, errors = p.communicate() + m = re.search(student_regex, output.decode('utf-8')) + if not m: + print("Unable to find student name in %s" % filename) + continue + student_name = m.groups()[0] + os.rename( + os.path.join(temp_dir, filename), + "%s.pdf" % (os.path.join(temp_dir, slugify(student_name))) + ) + + email_sent = 0 + pdf_file_list = os.listdir(temp_dir) + + students = klass.student_set.exclude(archived=True) + for student in students: + if not student.email: + messages.warning(self.request, "L'étudiant %s ne possède pas d'email." % student) + continue + context = { + 'student_name': " ".join([student.civility, student.first_name, student.last_name]), + 'sender_name': " ".join([self.request.user.first_name, self.request.user.last_name]), + 'sender_email': self.request.user.email, + } + student_filename = slugify('{0} {1}'.format(student.last_name, student.first_name)) + student_filename = '{0}.pdf'.format(student_filename) + try: + attach_idx = pdf_file_list.index(student_filename) + except ValueError: + messages.error(self.request, + "Impossible de trouver un fichier PDF pour l'étudiant %s" % student) + continue + + to = [student.email] + if student.instructor and student.instructor.email: + to.append(student.instructor.email) + email = EmailMessage( + subject='Bulletins scolaires', + body=loader.render_to_string('email/bulletins_scolaires.txt', context), + from_email=self.request.user.email, + to=to, + bcc=[self.request.user.email], + ) + # Attach PDF file to email + pdf_file = os.path.join(temp_dir, pdf_file_list[attach_idx]) + pdf_name = 'bulletin_scol_{0}'.format(student_filename) + with open(pdf_file, 'rb') as pdf: + email.attach(pdf_name, pdf.read(), 'application/pdf') + + try: + email.send(fail_silently=False) + email_sent += 1 + except Exception as err: + messages.error(self.request, "Échec d'envoi pour le candidat {0} ({1})".format(student, err)) + + messages.warning(self.request, '{0} messages sur {1} élèves ont été envoyés' + .format(email_sent, students.count())) + + EXPORT_FIELDS = [ # Student fields ('ID externe', 'student__ext_id'), diff --git a/templates/admin/index.html b/templates/admin/index.html index a85222f..5129e61 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -76,6 +76,7 @@
  • Importer un fichier d'étudiants
  • Importer le fichier HP
  • Importer les formateurs (fichier HP)
  • +
  • Envoyer les bulletins (par classe)
  • Exporter les données de stages (récentes)
  • Exporter les données de stages (toutes)
  • Exporter les données comptables
  • diff --git a/templates/email/bulletins_scolaires.txt b/templates/email/bulletins_scolaires.txt new file mode 100644 index 0000000..db98d9e --- /dev/null +++ b/templates/email/bulletins_scolaires.txt @@ -0,0 +1,11 @@ +Madame, Monsieur, + +Vous trouverez en annexe le bulletin scolaire de {{ student_name }} pour le semestre écoulé. + +Nous vous souhaitons bonne réception de ce document et vous prions de recevoir, Madame, Monsieur, nos salutations les plus cordiales. + +Secrétariat de l'EPC +tél. 032 886 33 00 + +{{ sender_name }} +{{ sender_email }}