diff --git a/common/settings.py b/common/settings.py index 5ad26d1..63a7668 100644 --- a/common/settings.py +++ b/common/settings.py @@ -34,7 +34,7 @@ USE_TZ = False # Absolute filesystem path to the directory that will hold user-uploaded files. MEDIA_ROOT = os.path.join(PROJECT_PATH, 'media') -MEDIA_URL = '' +MEDIA_URL = '/media/' # Absolute path to the directory static files should be collected to. # Don't put anything in this directory yourself; store your static files diff --git a/common/urls.py b/common/urls.py index 44362a9..c1b16a5 100644 --- a/common/urls.py +++ b/common/urls.py @@ -12,7 +12,6 @@ urlpatterns = [ path('import_students/', views.StudentImportView.as_view(), name='import-students'), path('import_hp/', views.HPImportView.as_view(), name='import-hp'), path('import_hp_contacts/', views.HPContactsImportView.as_view(), name='import-hp-contacts'), - path('import_bulletins/', views.ImportBulletinView.as_view(), name='import-bulletins'), path('attribution/', views.AttributionView.as_view(), name='attribution'), re_path(r'^stages/export/(?Pall)?/?$', views.stages_export, name='stages_export'), @@ -21,6 +20,7 @@ urlpatterns = [ path('institutions//', views.CorporationView.as_view(), name='corporation'), path('classes/', views.KlassListView.as_view(), name='classes'), path('classes//', views.KlassView.as_view(), name='class'), + path('classes//import_reports/', views.ImportReportsView.as_view(), name='import-reports'), path('candidate//send_convocation/', candidats_views.ConvocationView.as_view(), name='candidate-convocation'), @@ -46,6 +46,8 @@ urlpatterns = [ path('training/by_period//', views.TrainingsByPeriodView.as_view()), path('student//summary/', views.StudentSummaryView.as_view()), + path('student//send_reports/sem//', views.SendStudentReportsView.as_view(), + name='send-student-reports'), path('availability//summary/', views.AvailabilitySummaryView.as_view()), path('corporation//contacts/', views.CorpContactJSONView.as_view()), ] diff --git a/stages/forms.py b/stages/forms.py index f7317f1..a14e93a 100644 --- a/stages/forms.py +++ b/stages/forms.py @@ -36,5 +36,15 @@ class UploadHPFileForm(forms.Form): upload = forms.FileField(label='Fichier HyperPlanning') -class UploadBulletinForm(forms.Form): +class UploadReportForm(forms.Form): + semester = forms.ChoiceField(label='Semestre', choices=(('1', '1'), ('2', '2')), required=True) upload = forms.FileField(label='Bulletins CLOEE (pdf)') + + +class EmailStudentBaseForm(forms.Form): + id_student = forms.CharField(widget=forms.HiddenInput()) + sender = forms.CharField(widget=forms.HiddenInput()) + to = forms.CharField(widget=forms.TextInput(attrs={'size': '60'})) + cci = forms.CharField(widget=forms.TextInput(attrs={'size': '60'})) + subject = forms.CharField(widget=forms.TextInput(attrs={'size': '60'})) + message = forms.CharField(widget=forms.Textarea(attrs={'rows': 20, 'cols': 120})) diff --git a/stages/static/img/envelope.png b/stages/static/img/envelope.png new file mode 100644 index 0000000..abd1284 Binary files /dev/null and b/stages/static/img/envelope.png differ diff --git a/stages/tests.py b/stages/tests.py index b0ff7d5..294ff2e 100644 --- a/stages/tests.py +++ b/stages/tests.py @@ -283,6 +283,12 @@ class ImportTests(TestCase): def setUp(self): User.objects.create_user('me', 'me@example.org', 'mepassword') + def tearDown(self): + # Clean uploaded bulletins + bulletins_dir = os.path.join(settings.MEDIA_ROOT, 'bulletins') + for f in os.listdir(bulletins_dir): + os.remove(os.path.join(bulletins_dir, f)) + def test_import_students(self): """ Import of the main students file. @@ -369,12 +375,23 @@ class ImportTests(TestCase): 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) + response = self.client.post( + reverse('import-reports', args=[klass1.pk]), + data={'upload': fh, 'semester': '1'}, + 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) + self.assertIn('2 bulletins PDF ont été importés pour la classe 1ASEFEa (sur 3 élèves)', messages) + student = Student.objects.get(last_name="Dupond") + self.assertEqual(student.report_sem1.name, 'bulletins/1ASEFEa_1.pdf') + + # Now send + send_url = reverse('send-student-reports', args=[student.pk, '1']) + response = self.client.get(send_url) + data = response.context['form'].initial + self.assertEqual(data['to'], "albin@example.org") + response = self.client.post(send_url, data=data, follow=True) + self.assertEqual(len(mail.outbox), 1) # 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 ff4bb66..469dcf0 100644 --- a/stages/views.py +++ b/stages/views.py @@ -14,7 +14,8 @@ 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.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.template import loader @@ -24,7 +25,7 @@ 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, UploadBulletinForm +from .forms import EmailStudentBaseForm, PeriodForm, StudentImportForm, UploadHPFileForm, UploadReportForm from .models import ( Klass, Section, Option, Student, Teacher, Corporation, CorpContact, Course, Period, Training, Availability, @@ -503,25 +504,31 @@ class HPContactsImportView(ImportViewBase): return {'modified': obj_modified, 'errors': errors} -class ImportBulletinView(FormView): +class ImportReportsView(FormView): template_name = 'file_import.html' - form_class = UploadBulletinForm + form_class = UploadReportForm + + def dispatch(self, request, *args, **kwargs): + self.klass = get_object_or_404(Klass, pk=kwargs['pk']) + self.title = "Importation d'un fichier PDF de moyennes pour la classe {}".format(self.klass.name) + return super().dispatch(request, *args, **kwargs) def form_valid(self, form): upfile = form.cleaned_data['upload'] klass_name = upfile.name[:-4] + redirect_url = reverse('class', args=[self.klass.pk]) - 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')) + if self.klass.name != klass_name: + messages.error(self.request, + "Le fichier téléchargé ne correspond pas à la classe {} !".format(self.klass.name) + ) + return HttpResponseRedirect(redirect_url) # 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')) + return HttpResponseRedirect(redirect_url) # Move the file to MEDIA directory pdf_origin = os.path.join(settings.MEDIA_ROOT, upfile.name) @@ -530,15 +537,16 @@ class ImportBulletinView(FormView): destination.write(chunk) try: - self.send_bulletins(klass, pdf_origin) + self.import_reports(pdf_origin, form.cleaned_data['semester']) except Exception as err: + raise if settings.DEBUG: raise else: - messages.error(self.request, "Erreur durant l'envoi des bulletins PDF: %s" % err) - return HttpResponseRedirect(reverse('admin:index')) + messages.error(self.request, "Erreur durant l'importation des bulletins PDF: %s" % err) + return HttpResponseRedirect(redirect_url) - def send_bulletins(self, klass, pdf_path): + def import_reports(self, pdf_path, semester): path = os.path.abspath(pdf_path) student_regex = '[E|É]lève\s*:\s*([^\n]*)' # Directory automatically deleted when the variable is deleted @@ -548,66 +556,101 @@ class ImportBulletinView(FormView): 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 + pdf_count = 0 + pdf_field = 'report_sem' + semester 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() + output, errs = 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).order_by('last_name', 'first_name') - 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) + # Find a student with the found student_name 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) + student = self.klass.student_set.exclude(archived=True + ).annotate(fullname=Concat('last_name', Value(' '), 'first_name')).get(fullname=student_name) + except Student.DoesNotExist: + messages.warning( + self.request, + "Impossible de trouver l'étudiant {} dans la classe {}".format(student_name, self.klass.name) + ) continue + with open(os.path.join(temp_dir, filename), 'rb') as pdf: + getattr(student, pdf_field).save(filename, File(pdf), save=True) + student.save() + pdf_count += 1 - 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], + messages.success( + self.request, + '{0} bulletins PDF ont été importés pour la classe {1} (sur {2} élèves)'.format( + pdf_count, self.klass.name, + self.klass.student_set.exclude(archived=True).count() ) - # 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())) +class SendStudentReportsView(FormView): + template_name = 'email_report.html' + form_class = EmailStudentBaseForm + + def get_initial(self): + initial = super().get_initial() + self.student = Student.objects.get(pk=self.kwargs['pk']) + self.semestre = self.kwargs['semestre'] + + to = [self.student.email] + if self.student.instructor and self.student.instructor.email: + to.append(self.student.instructor.email) + + context = { + 'student': self.student, + 'sender': self.request.user, + } + + initial.update({ + 'id_student': self.student.pk, + 'cci': self.request.user.email, + 'to': '; '.join(to), + 'subject': "Bulletin semestriel", + 'message': loader.render_to_string('email/bulletins_scolaires.txt', context), + 'sender': self.request.user.email, + }) + return initial + + def form_valid(self, form): + email = EmailMessage( + subject=form.cleaned_data['subject'], + body=form.cleaned_data['message'], + from_email=form.cleaned_data['sender'], + to=form.cleaned_data['to'].split(';'), + bcc=form.cleaned_data['cci'].split(';'), + ) + # Attach PDF file to email + student_filename = slugify('{0} {1}'.format(self.student.last_name, self.student.first_name)) + student_filename = '{0}.pdf'.format(student_filename) + #pdf_file = os.path.join(dir_klass, pdf_file_list[attach_idx]) + pdf_name = 'bulletin_scol_{0}'.format(student_filename) + with open(getattr(self.student, 'report_sem%d' % self.semestre).path, 'rb') as pdf: + email.attach(pdf_name, pdf.read(), 'application/pdf') + + try: + email.send() + except Exception as err: + messages.error(self.request, "Échec d’envoi pour l'étudiant {0} ({1})".format(self.student, err)) + else: + messages.success(self.request, "Le message a été envoyé.") + return HttpResponseRedirect(reverse('class', args=[self.student.klass.pk])) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'candidat': self.student, + 'title': 'Envoi du bulletin semestriel', + 'pdf_field': getattr(self.student, 'report_sem%d' % self.semestre), + }) + return context EXPORT_FIELDS = [ diff --git a/templates/admin/index.html b/templates/admin/index.html index 5129e61..a85222f 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -76,7 +76,6 @@
  • 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/class.html b/templates/class.html index d673c0e..40583a6 100644 --- a/templates/class.html +++ b/templates/class.html @@ -10,15 +10,30 @@ {% block content %}

    {{ klass.name }}

    +
    -

    Récapitulatif des stages

    +

    Liste des éléves

    + {% for student in students %} + {% for train in student.training_set.all %}
    Nom, prénomDate naiss.BulletinsRécapitulatif des stages
    {{ student }} {{ student.birth_date }}{% if student.report_sem1 %}Sem. 1 + {% if student.report_sem1_sent %}{% else %} + + {% endif %} + {% endif %} + {% if student.report_sem2 %}
    Sem. 2 + {% if student.report_sem2_sent %}{% else %} + + {% endif %} + {% endif %} +
    {{ train.availability.period }}
    {{ train.availability.corporation }}
    {% if train.comment %}
    {% endif %} diff --git a/templates/email/bulletins_scolaires.txt b/templates/email/bulletins_scolaires.txt index db98d9e..6752f82 100644 --- a/templates/email/bulletins_scolaires.txt +++ b/templates/email/bulletins_scolaires.txt @@ -1,11 +1,10 @@ Madame, Monsieur, -Vous trouverez en annexe le bulletin scolaire de {{ student_name }} pour le semestre écoulé. +Vous trouverez en annexe le bulletin scolaire de {{student.civility }} {{ student.full_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 +{{ sender.first_name }} {{ sender.last_name }} +{{ sender.email }} tél. 032 886 33 00 - -{{ sender_name }} -{{ sender_email }} diff --git a/templates/email_base.html b/templates/email_base.html index d0602da..3befe06 100644 --- a/templates/email_base.html +++ b/templates/email_base.html @@ -16,5 +16,6 @@
    +{% block post_form %}{% endblock %} {% endblock %} diff --git a/templates/email_report.html b/templates/email_report.html new file mode 100644 index 0000000..2a02a42 --- /dev/null +++ b/templates/email_report.html @@ -0,0 +1,5 @@ +{% extends "email_base.html" %} + +{% block post_form %} +Bulletin qui sera joint au message: {{ pdf_field.name }} +{% endblock %} diff --git a/templates/file_import.html b/templates/file_import.html index 62d6284..15659cb 100644 --- a/templates/file_import.html +++ b/templates/file_import.html @@ -8,6 +8,8 @@ {% endblock %} {% block content %} +

    {{ view.title }}

    +
    {% csrf_token %} {{ form.as_p }}