From 484e32e4a7dc0d9319b49d00cf165ecd433e6bbd Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 19 Sep 2017 15:34:22 +0200 Subject: [PATCH 01/13] Try to workaround a Python bug with openpyxl usage of lru_cache --- stages/views.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/stages/views.py b/stages/views.py index 81ba162..0b6e1d0 100644 --- a/stages/views.py +++ b/stages/views.py @@ -632,7 +632,13 @@ def stages_export(request, scope=None): value = tr[field] if 'gender' in field: value = {'F': 'Madame', 'M': 'Monsieur', '': ''}[value] - ws.cell(row=row_idx, column=col_idx).value = value + try: + ws.cell(row=row_idx, column=col_idx).value = value + except KeyError: + # Ugly workaround for https://bugs.python.org/issue28969 + from openpyxl.utils.datetime import to_excel + to_excel.cache_clear() + ws.cell(row=row_idx, column=col_idx).value = value if tr[contact_test_field] is None: # Use default contact contact = default_contacts.get(tr[corp_name_field], {}).get(tr[export_fields['Filière']]) @@ -783,7 +789,13 @@ def general_export(request): tr[field] = ('', 'Oui')[tr[field]==1] if field == 'soutien_dys': tr[field] = ('', 'Oui')[tr[field]==1] - ws.cell(row=row_idx, column=col_idx).value = tr[field] + try: + ws.cell(row=row_idx, column=col_idx).value = tr[field] + except KeyError: + # Ugly workaround for https://bugs.python.org/issue28969 + from openpyxl.utils.datetime import to_excel + to_excel.cache_clear() + ws.cell(row=row_idx, column=col_idx).value = tr[field] response = HttpResponse(save_virtual_workbook(wb), content_type=openxml_contenttype) response['Content-Disposition'] = 'attachment; filename=%s%s.xlsx' % ( From a823a89a78c92b8c9a67d90880b1b38bac9c2242 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 18 Oct 2017 18:52:59 +0200 Subject: [PATCH 02/13] Add Candidate app/model --- candidats/__init__.py | 0 candidats/admin.py | 55 ++++++++++++++ candidats/migrations/0001_initial.py | 69 +++++++++++++++++ candidats/migrations/__init__.py | 0 candidats/models.py | 106 +++++++++++++++++++++++++++ common/settings.py | 1 + 6 files changed, 231 insertions(+) create mode 100644 candidats/__init__.py create mode 100644 candidats/admin.py create mode 100644 candidats/migrations/0001_initial.py create mode 100644 candidats/migrations/__init__.py create mode 100644 candidats/models.py diff --git a/candidats/__init__.py b/candidats/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/candidats/admin.py b/candidats/admin.py new file mode 100644 index 0000000..0485ca0 --- /dev/null +++ b/candidats/admin.py @@ -0,0 +1,55 @@ +from django import forms +from django.contrib import admin + +from .models import Candidate + + +class CandidateAdminForm(forms.ModelForm): + class Meta: + model = Candidate + widgets = { + 'comment': forms.Textarea(attrs={'cols': 100, 'rows': 1}), + 'pcode': forms.TextInput(attrs={'size': 10}), + } + fields = '__all__' + + +class CandidateAdmin(admin.ModelAdmin): + form = CandidateAdminForm + list_display = ('last_name', 'first_name', 'section', 'confirm_email') + list_filter = ('section', 'option') + readonly_fields = ('total_result_points', 'total_result_mark', 'date_confirmation_mail') + fieldsets = ( + (None, { + 'fields': (('first_name', 'last_name', 'gender'), + ('street', 'pcode', 'city', 'district'), + ('mobile', 'email'), + ('birth_date', 'avs', 'handicap'), + ('section', 'option'), + ('corporation', 'instructor'), + ('deposite_date', 'date_confirmation_mail', 'canceled_file'), + 'comment', + ), + }), + ("FE", { + 'classes': ('collapse',), + 'fields': (('exemption_ecg', 'integration_second_year', 'validation_sfpo'),), + }), + ("EDE/EDS", { + 'classes': ('collapse',), + 'fields': (('registration_form', 'certificate_of_payement', 'cv', 'certif_of_cfc', + 'police_record', 'certif_of_800h', 'reflexive_text', 'work_certificate', + 'marks_certificate', 'proc_admin_ext', 'promise', 'contract'), + 'comment', + ('interview_date', 'interview_room'), + ('examination_result', 'interview_result', 'file_result', 'total_result_points', + 'total_result_mark') + ), + }), + ) + + def confirm_email(self, obj): + return obj.date_confirmation_mail is not None + confirm_email.boolean = True + +admin.site.register(Candidate, CandidateAdmin) diff --git a/candidats/migrations/0001_initial.py b/candidats/migrations/0001_initial.py new file mode 100644 index 0000000..e834e2a --- /dev/null +++ b/candidats/migrations/0001_initial.py @@ -0,0 +1,69 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('stages', '0002_add_student_option_ase'), + ] + + operations = [ + migrations.CreateModel( + name='Candidate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=40, verbose_name='Prénom')), + ('last_name', models.CharField(max_length=40, verbose_name='Nom')), + ('gender', models.CharField(choices=[('M', 'Masculin'), ('F', 'Féminin'), ('I', 'Inconnu')], max_length=1, verbose_name='Genre')), + ('birth_date', models.DateField(blank=True, null=True, verbose_name='Date de naissance')), + ('street', models.CharField(blank=True, max_length=150, verbose_name='Rue')), + ('pcode', models.CharField(max_length=4, verbose_name='Code postal')), + ('city', models.CharField(max_length=40, verbose_name='Localité')), + ('district', models.CharField(blank=True, max_length=2, verbose_name='Canton')), + ('mobile', models.CharField(blank=True, max_length=40, verbose_name='Portable')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='Courriel')), + ('avs', models.CharField(blank=True, max_length=15, verbose_name='No AVS')), + ('handicap', models.BooleanField(default=False)), + ('section', models.CharField(choices=[('ASA', 'Aide en soin et accompagnement AFP'), ('ASE', 'Assist. socio-éducatif-ve CFC'), ('ASSC', 'Assist. en soin et santé communautaire CFC'), ('EDE', "Educ. de l'enfance, dipl. ES"), ('EDS', 'Educ. social-e, dipl. ES')], max_length=10, verbose_name='Filière')), + ('option', models.CharField(blank=True, choices=[('GEN', 'Généraliste'), ('ENF', 'Enfance'), ('PAG', 'Personnes âgées'), ('HAN', 'Handicap'), ('PE-5400h', 'Parcours Emploi 5400h.'), ('PE-3600h', 'Parcours Emploi 3600h.'), ('PS', 'Parcours stage')], max_length=20, verbose_name='Option')), + ('exemption_ecg', models.BooleanField(default=False)), + ('validation_sfpo', models.DateField(blank=True, null=True, verbose_name='Confirmation SFPO')), + ('integration_second_year', models.BooleanField(default=False, verbose_name='Intégration')), + ('date_confirmation_mail', models.DateField(blank=True, null=True, verbose_name='Mail de confirmation')), + ('canceled_file', models.BooleanField(default=False, verbose_name='Dossier retiré')), + ('has_photo', models.BooleanField(default=False)), + ('registration_form', models.BooleanField(default=False, verbose_name="Formulaire d'inscription")), + ('certificate_of_payement', models.BooleanField(default=False, verbose_name='Attest. paiement')), + ('police_record', models.BooleanField(default=False, verbose_name='Casier judic.')), + ('cv', models.BooleanField(default=False, verbose_name='CV')), + ('certif_of_cfc', models.BooleanField(default=False, verbose_name='CFC')), + ('certif_of_800h', models.BooleanField(default=False, verbose_name='Attest. 800h.')), + ('reflexive_text', models.BooleanField(default=False, verbose_name='Texte réflexif')), + ('promise', models.BooleanField(default=False, verbose_name="Promesse d'eng.")), + ('contract', models.BooleanField(default=False, verbose_name='Contrat valide')), + ('comment', models.TextField(blank=True, verbose_name='Remarques')), + ('proc_admin_ext', models.BooleanField(default=False, verbose_name='Insc. autre école')), + ('work_certificate', models.BooleanField(default=False, verbose_name='Certif. de travail')), + ('marks_certificate', models.BooleanField(default=False, verbose_name='Bull. notes')), + ('deposite_date', models.DateField(blank=True, null=True, verbose_name='Date dépôt dossier')), + ('interview_date', models.DateTimeField(blank=True, null=True, verbose_name='Date entretien prof.')), + ('interview_room', models.CharField(blank=True, max_length=50, verbose_name="Salle d'entretien prof.")), + ('examination_result', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Points examen')), + ('interview_result', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Points entretien prof.')), + ('file_result', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Points dossier')), + ('total_result_points', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Total points')), + ('total_result_mark', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Note finale')), + ('accepted', models.BooleanField(default=False, verbose_name='Admis')), + ('corporation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stages.Corporation', verbose_name='Employeur')), + ('file_resp', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='stages.Teacher', verbose_name='Exp. dossier')), + ('instructor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stages.CorpContact', verbose_name='FEE/FPP')), + ('interview_resp', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='stages.Teacher', verbose_name='Exp. entretien')), + ], + options={ + 'verbose_name': 'Candidat', + }, + ), + ] diff --git a/candidats/migrations/__init__.py b/candidats/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/candidats/models.py b/candidats/models.py new file mode 100644 index 0000000..fc3dd96 --- /dev/null +++ b/candidats/models.py @@ -0,0 +1,106 @@ +from django.db import models + + +GENDER_CHOICES = ( + ('M', 'Masculin'), + ('F', 'Féminin'), + ('I', 'Inconnu') +) + +SECTION_CHOICES = ( + ('ASA', 'Aide en soin et accompagnement AFP'), + ('ASE', 'Assist. socio-éducatif-ve CFC'), + ('ASSC', 'Assist. en soin et santé communautaire CFC'), + ('EDE', 'Educ. de l\'enfance, dipl. ES'), + ('EDS', 'Educ. social-e, dipl. ES'), +) + +OPTION_CHOICES = ( + ('GEN', 'Généraliste'), + ('ENF', 'Enfance'), + ('PAG', 'Personnes âgées'), + ('HAN', 'Handicap'), + ('PE-5400h', 'Parcours Emploi 5400h.'), + ('PE-3600h', 'Parcours Emploi 3600h.'), + ('PS', 'Parcours stage'), +) + +class Candidate(models.Model): + """ + Inscriptions for new students + """ + first_name = models.CharField('Prénom', max_length=40) + last_name = models.CharField('Nom', max_length=40) + gender = models.CharField('Genre', max_length=1, choices=GENDER_CHOICES) + birth_date = models.DateField('Date de naissance', blank=True, null=True) + street = models.CharField('Rue', max_length=150, blank=True) + pcode = models.CharField('Code postal', max_length=4) + city = models.CharField('Localité', max_length=40) + district = models.CharField('Canton', max_length=2, blank=True) + mobile = models.CharField('Portable', max_length=40, blank=True) + email = models.EmailField('Courriel', blank=True) + avs = models.CharField('No AVS', max_length=15, blank=True) + handicap = models.BooleanField(default=False) + + section = models.CharField('Filière', max_length=10, choices=SECTION_CHOICES) + option = models.CharField('Option', max_length=20, choices=OPTION_CHOICES, blank=True) + exemption_ecg = models.BooleanField(default=False) + validation_sfpo = models.DateField('Confirmation SFPO', blank=True, null=True) + integration_second_year = models.BooleanField('Intégration', default=False) + date_confirmation_mail = models.DateField('Mail de confirmation', blank=True, null=True) + canceled_file = models.BooleanField('Dossier retiré', default=False) + has_photo = models.BooleanField(default=False) + + corporation = models.ForeignKey( + 'stages.Corporation', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='Employeur' + ) + instructor = models.ForeignKey( + 'stages.CorpContact', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='FEE/FPP' + ) + + # Checking for registration file + registration_form = models.BooleanField("Formulaire d'inscription", default=False) + certificate_of_payement = models.BooleanField("Attest. paiement", default=False) + police_record = models.BooleanField("Casier judic.", default=False) + cv = models.BooleanField("CV", default=False) + certif_of_cfc = models.BooleanField("CFC", default=False) + certif_of_800h = models.BooleanField("Attest. 800h.", default=False) + reflexive_text = models.BooleanField("Texte réflexif", default=False) + promise = models.BooleanField("Promesse d'eng.", default=False) + contract = models.BooleanField("Contrat valide", default=False) + comment = models.TextField('Remarques', blank=True) + + proc_admin_ext = models.BooleanField("Insc. autre école", default=False) + work_certificate = models.BooleanField("Certif. de travail", default=False) + marks_certificate = models.BooleanField("Bull. notes", default=False) + deposite_date = models.DateField('Date dépôt dossier', blank=True, null=True) + interview_date = models.DateTimeField('Date entretien prof.', blank=True, null=True) + interview_room = models.CharField("Salle d'entretien prof.", max_length=50, blank=True) + examination_result = models.PositiveSmallIntegerField('Points examen', blank=True, null=True) + interview_result = models.PositiveSmallIntegerField('Points entretien prof.', blank=True, null=True) + file_result = models.PositiveSmallIntegerField('Points dossier', blank=True, null=True) + total_result_points = models.PositiveSmallIntegerField('Total points', blank=True, null=True) + total_result_mark = models.PositiveSmallIntegerField('Note finale', blank=True, null=True) + + accepted = models.BooleanField('Admis', default=False) + interview_resp = models.ForeignKey( + 'stages.Teacher', null=True, blank=True, related_name='+', verbose_name='Exp. entretien' + ) + file_resp = models.ForeignKey( + 'stages.Teacher', null=True, blank=True, related_name='+', verbose_name='Exp. dossier' + ) + + class Meta: + verbose_name = 'Candidat' + + def __str__(self): + return "%s %s" % (self.last_name, self.first_name) + + @property + def civility(self): + if self.gender == 'M': + return 'Monsieur' + if self.gender == 'F': + return 'Madame' + else: + return '' diff --git a/common/settings.py b/common/settings.py index 4f098d8..fb460ee 100644 --- a/common/settings.py +++ b/common/settings.py @@ -108,6 +108,7 @@ INSTALLED_APPS = ( 'tabimport', 'stages', + 'candidats', ) FILE_UPLOAD_HANDLERS = ["django.core.files.uploadhandler.TemporaryFileUploadHandler"] From c0bd7a70dacf6d72da2ee0ee2aeb3d6e98760b5b Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 19 Oct 2017 09:26:00 +0200 Subject: [PATCH 03/13] Factorized openpyxl export code to exports.py --- stages/exports.py | 41 +++++++++ stages/tests.py | 2 +- stages/views.py | 208 +++++++++++++++------------------------------- 3 files changed, 110 insertions(+), 141 deletions(-) create mode 100644 stages/exports.py diff --git a/stages/exports.py b/stages/exports.py new file mode 100644 index 0000000..672b380 --- /dev/null +++ b/stages/exports.py @@ -0,0 +1,41 @@ +from datetime import date + +from openpyxl import Workbook +from openpyxl.cell import get_column_letter +from openpyxl.styles import Font, Style +from openpyxl.writer.excel import save_virtual_workbook + +from django.http import HttpResponse + +openxml_contenttype = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + + +class OpenXMLExport: + def __init__(self, sheet_title): + self.wb = Workbook() + self.ws = self.wb.active + self.ws.title = sheet_title + self.bold = Style(font=Font(bold=True)) + self.row_idx = 1 + + def write_line(self, values, bold=False, col_widths=()): + for col_idx, value in enumerate(values, start=1): + cell = self.ws.cell(row=self.row_idx, column=col_idx) + try: + cell.value = value + except KeyError: + # Ugly workaround for https://bugs.python.org/issue28969 + from openpyxl.utils.datetime import to_excel + to_excel.cache_clear() + cell.value = value + if bold: + cell.style = self.bold + if col_widths: + self.ws.column_dimensions[get_column_letter(col_idx)].width = col_widths[col_idx - 1] + self.row_idx += 1 + + def get_http_response(self, filename_base): + response = HttpResponse(save_virtual_workbook(self.wb), content_type=openxml_contenttype) + response['Content-Disposition'] = 'attachment; filename=%s_%s.xlsx' % ( + filename_base, date.strftime(date.today(), '%Y-%m-%d')) + return response diff --git a/stages/tests.py b/stages/tests.py index 010bd97..a86e2c7 100644 --- a/stages/tests.py +++ b/stages/tests.py @@ -274,7 +274,7 @@ class TeacherTests(TestCase): response = self.client.get(reverse('imputations_export')) self.assertEqual( response['Content-Disposition'], - 'attachment; filename=Imputations_export%s.xlsx' % date.strftime(date.today(), '%Y-%m-%d') + 'attachment; filename=Imputations_export_%s.xlsx' % date.strftime(date.today(), '%Y-%m-%d') ) diff --git a/stages/views.py b/stages/views.py index 0b6e1d0..4182f6b 100644 --- a/stages/views.py +++ b/stages/views.py @@ -6,10 +6,6 @@ from collections import OrderedDict from datetime import date, datetime, timedelta from tabimport import CSVImportedFile, FileFactory -from openpyxl import Workbook -from openpyxl.cell import get_column_letter -from openpyxl.styles import Font, Style -from openpyxl.writer.excel import save_virtual_workbook from django.conf import settings from django.contrib import messages @@ -21,6 +17,7 @@ from django.urls import reverse from django.utils.translation import ugettext as _ from django.views.generic import DetailView, FormView, TemplateView, ListView +from .exports import OpenXMLExport from .forms import PeriodForm, StudentImportForm, UploadHPFileForm from .models import ( Klass, Section, Option, Student, Teacher, Corporation, CorpContact, Course, Period, @@ -29,8 +26,6 @@ from .models import ( from .pdf import UpdateDataFormPDF from .utils import is_int -openxml_contenttype = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - def school_year_start(): """ Return first official day of current school year """ @@ -97,39 +92,26 @@ class KlassView(DetailView): if self.request.GET.get('format') != 'xls': return super().render_to_response(context, **response_kwargs) - wb = Workbook() - ws = wb.active - ws.title = self.object.name - bold = Style(font=Font(bold=True)) - headers = [ + export = OpenXMLExport(self.object.name) + # Headers + export.write_line([ 'Nom', 'Prénom', 'Domicile', 'Date de naissance', 'Stage 1', 'Domaine 1', 'Stage 2', 'Domaine 2', 'Stage 3', 'Domaine 3', - ] - col_widths = [18, 15, 20, 14, 25, 12, 25, 12, 25, 12] - # Headers - for col_idx, header in enumerate(headers, start=1): - cell = ws.cell(row=1, column=col_idx) - cell.value = header - cell.style = bold - ws.column_dimensions[get_column_letter(col_idx)].width = col_widths[col_idx - 1] + ], bold=True, col_widths=[18, 15, 20, 14, 25, 12, 25, 12, 25, 12]) # Data - for row_idx, student in enumerate(context['students'], start=2): - ws.cell(row=row_idx, column=1).value = student.last_name - ws.cell(row=row_idx, column=2).value = student.first_name - ws.cell(row=row_idx, column=3).value = " ".join([student.pcode, student.city]) - ws.cell(row=row_idx, column=4).value = student.birth_date - col_idx = 5 + for student in context['students']: + values = [ + student.last_name, student.first_name, + " ".join([student.pcode, student.city]), student.birth_date, + ] for training in student.training_set.select_related( 'availability', 'availability__corporation', 'availability__domain' ).all(): - ws.cell(row=row_idx, column=col_idx).value = training.availability.corporation.name - ws.cell(row=row_idx, column=col_idx + 1).value = training.availability.domain.name - col_idx += 2 + values.append(training.availability.corporation.name) + values.append(training.availability.domain.name) + export.write_line(values) - response = HttpResponse(save_virtual_workbook(wb), content_type=openxml_contenttype) - response['Content-Disposition'] = 'attachment; filename=%s_export_%s.xlsx' % ( - self.object.name.replace(' ', '_'), date.strftime(date.today(), '%Y-%m-%d')) - return response + return export.get_http_response('%s_export' % self.object.name.replace(' ', '_')) class AttributionView(TemplateView): @@ -616,49 +598,32 @@ def stages_export(request, scope=None): if not default_contacts[contact.corporation.name][sname]: default_contacts[contact.corporation.name][sname] = contact - wb = Workbook() - ws = wb.active - ws.title = 'Stages' - bold = Style(font=Font(bold=True)) - # Headers - for col_idx, header in enumerate(export_fields.keys(), start=1): - cell = ws.cell(row=1, column=col_idx) - cell.value = header - cell.style = bold + export = OpenXMLExport('Stages') + export.write_line(export_fields.keys(), bold=True) # Headers # Data query_keys = [f for f in export_fields.values() if f is not None] - for row_idx, tr in enumerate(query.values(*query_keys), start=2): - for col_idx, field in enumerate(query_keys, start=1): - value = tr[field] + for line in query.values(*query_keys): + values = [] + for field in query_keys: + value = line[field] if 'gender' in field: value = {'F': 'Madame', 'M': 'Monsieur', '': ''}[value] - try: - ws.cell(row=row_idx, column=col_idx).value = value - except KeyError: - # Ugly workaround for https://bugs.python.org/issue28969 - from openpyxl.utils.datetime import to_excel - to_excel.cache_clear() - ws.cell(row=row_idx, column=col_idx).value = value - if tr[contact_test_field] is None: + values.append(value) + if line[contact_test_field] is None: # Use default contact - contact = default_contacts.get(tr[corp_name_field], {}).get(tr[export_fields['Filière']]) + contact = default_contacts.get(line[corp_name_field], {}).get(line[export_fields['Filière']]) if contact: - contact_col_idx = list(export_fields.keys()).index('Civilité contact') + 1 - ws.cell(row=row_idx, column=contact_col_idx).value = contact.title - ws.cell(row=row_idx, column=contact_col_idx + 1).value = contact.first_name - ws.cell(row=row_idx, column=contact_col_idx + 2).value = contact.last_name - ws.cell(row=row_idx, column=contact_col_idx + 3).value = contact.ext_id - ws.cell(row=row_idx, column=contact_col_idx + 4).value = contact.tel - ws.cell(row=row_idx, column=contact_col_idx + 5).value = contact.email - if always_ccs[tr[corp_name_field]].get(tr[export_fields['Filière']]): - ws.cell(row=row_idx, column=col_idx+1).value = "; ".join( - [c.email for c in always_ccs[tr[corp_name_field]].get(tr[export_fields['Filière']])] - ) + values = values[:-6] + [ + contact.title, contact.first_name, contact.last_name, contact.ext_id, + contact.tel, contact.email + ] + if always_ccs[line[corp_name_field]].get(line[export_fields['Filière']]): + values.append("; ".join( + [c.email for c in always_ccs[line[corp_name_field]].get(line[export_fields['Filière']])] + )) + export.write_line(values) - response = HttpResponse(save_virtual_workbook(wb), content_type=openxml_contenttype) - response['Content-Disposition'] = 'attachment; filename=%s%s.xlsx' % ( - 'stages_export_', date.strftime(date.today(), '%Y-%m-%d')) - return response + return export.get_http_response('stages_export') IMPUTATIONS_EXPORT_FIELDS = [ @@ -669,37 +634,22 @@ IMPUTATIONS_EXPORT_FIELDS = [ def imputations_export(request): - wb = Workbook() - ws = wb.active - ws.title = 'Imputations' - bold = Style(font=Font(bold=True)) - for col_idx, header in enumerate(IMPUTATIONS_EXPORT_FIELDS, start=1): - cell = ws.cell(row=1, column=col_idx) - cell.value = header - cell.style = bold + export = OpenXMLExport('Imputations') + export.write_line(IMPUTATIONS_EXPORT_FIELDS, bold=True) # Headers - for row_idx, teacher in enumerate(Teacher.objects.filter(archived=False), start=2): + for teacher in Teacher.objects.filter(archived=False): activities, imputations = teacher.calc_imputations() - ws.cell(row=row_idx, column=1).value = teacher.last_name - ws.cell(row=row_idx, column=2).value = teacher.first_name - ws.cell(row=row_idx, column=3).value = teacher.previous_report - ws.cell(row=row_idx, column=4).value = activities['tot_ens'] - ws.cell(row=row_idx, column=5).value = 'Ens. prof.' - ws.cell(row=row_idx, column=6).value = activities['tot_mandats'] + activities['tot_formation'] - ws.cell(row=row_idx, column=7).value = 'Accompagnement' - ws.cell(row=row_idx, column=8).value = activities['tot_paye'] - ws.cell(row=row_idx, column=9).value = 'Charge globale' - ws.cell(row=row_idx, column=10).value = '{0:.2f}'.format(activities['tot_paye']/21.50) - ws.cell(row=row_idx, column=11).value = teacher.next_report - col_idx = 12 - for k, v in imputations.items(): - ws.cell(row=row_idx, column=col_idx).value = v - col_idx += 1 + values = [ + teacher.last_name, teacher.first_name, teacher.previous_report, + activities['tot_ens'], 'Ens. prof.', activities['tot_mandats'] + activities['tot_formation'], + 'Accompagnement', activities['tot_paye'], 'Charge globale', + '{0:.2f}'.format(activities['tot_paye']/21.50), + teacher.next_report, + ] + values.extend(imputations.values()) + export.write_line(values) - response = HttpResponse(save_virtual_workbook(wb), content_type=openxml_contenttype) - response['Content-Disposition'] = 'attachment; filename=%s%s.xlsx' % ( - 'Imputations_export', date.strftime(date.today(), '%Y-%m-%d')) - return response + return export.get_http_response('Imputations_export') def print_update_form(request): @@ -768,39 +718,23 @@ def general_export(request): Export all current students data """ export_fields = OrderedDict(GENERAL_EXPORT_FIELDS) - wb = Workbook() - ws = wb.active - ws.title = 'Exportation' - bold = Style(font=Font(bold=True)) - for col_idx, header in enumerate(export_fields.keys(), start=1): - cell = ws.cell(row=1, column=col_idx) - cell.value = header - cell.style = bold + export = OpenXMLExport('Exportation') + export.write_line(export_fields.keys(), bold=True) # Headers # Data query_keys = [f for f in export_fields.values() if f is not None] query = Student.objects.filter(archived=False).order_by('klass__name', 'last_name', 'first_name') - for row_idx, tr in enumerate(query.values(*query_keys), start=2): - for col_idx, field in enumerate(query_keys, start=1): + for line in query.values(*query_keys): + values = [] + for field in query_keys: if field == 'gender': - tr[field] = ('Madame', 'Monsieur')[tr[field] == 'M'] - if field == 'dispense_ecg': - tr[field] = ('', 'Oui')[tr[field]==1] - if field == 'dispense_eps': - tr[field] = ('', 'Oui')[tr[field]==1] - if field == 'soutien_dys': - tr[field] = ('', 'Oui')[tr[field]==1] - try: - ws.cell(row=row_idx, column=col_idx).value = tr[field] - except KeyError: - # Ugly workaround for https://bugs.python.org/issue28969 - from openpyxl.utils.datetime import to_excel - to_excel.cache_clear() - ws.cell(row=row_idx, column=col_idx).value = tr[field] + values.append(('Madame', 'Monsieur')[line[field] == 'M']) + elif field in ('dispense_ecg', 'dispense_eps', 'soutien_dys'): + values.append('Oui' if line[field] is True else '') + else: + values.append(line[field]) + export.write_line(values) - response = HttpResponse(save_virtual_workbook(wb), content_type=openxml_contenttype) - response['Content-Disposition'] = 'attachment; filename=%s%s.xlsx' % ( - 'general_export_', date.strftime(date.today(), '%Y-%m-%d')) - return response + return export.get_http_response('general_export') ORTRA_EXPORT_FIELDS = [ @@ -841,14 +775,8 @@ def ortra_export(request): Export students data from sections ASAFE, ASEFE and ASSCFE """ export_fields = OrderedDict(ORTRA_EXPORT_FIELDS) - wb = Workbook() - ws = wb.active - ws.title = 'Exportation' - bold = Style(font=Font(bold=True)) - for col_idx, header in enumerate(export_fields.keys(), start=1): - cell = ws.cell(row=1, column=col_idx) - cell.value = header - cell.style = bold + export = OpenXMLExport('Exportation') + export.write_line(export_fields.keys(), bold=True) # Headers # Data query_keys = [f for f in export_fields.values() if f is not None] query = Student.objects.filter(Q(klass__name__contains='ASAFE') | @@ -858,13 +786,13 @@ def ortra_export(request): 'last_name', 'first_name') - for row_idx, tr in enumerate(query.values(*query_keys), start=2): - for col_idx, field in enumerate(query_keys, start=1): + for line in query.values(*query_keys): + values = [] + for field in query_keys: if field == 'gender': - tr[field] = ('Madame', 'Monsieur')[tr[field] == 'M'] - ws.cell(row=row_idx, column=col_idx).value = tr[field] + values.append(('Madame', 'Monsieur')[line[field] == 'M']) + else: + values.append(line[field]) + export.write_line(values) - response = HttpResponse(save_virtual_workbook(wb), content_type=openxml_contenttype) - response['Content-Disposition'] = 'attachment; filename=%s%s.xlsx' % ( - 'ortra_export_', date.strftime(date.today(), '%Y-%m-%d')) - return response \ No newline at end of file + return export.get_http_response('ortra_export') From a0a35a4b351917ef18346dc63b08f9a54d347014 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 19 Oct 2017 09:29:08 +0200 Subject: [PATCH 04/13] Add export admin action for candidates --- candidats/admin.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/candidats/admin.py b/candidats/admin.py index 0485ca0..4f687e2 100644 --- a/candidats/admin.py +++ b/candidats/admin.py @@ -1,7 +1,39 @@ +from collections import OrderedDict + from django import forms from django.contrib import admin +from django.db.models import BooleanField -from .models import Candidate +from stages.exports import OpenXMLExport +from .models import Candidate, GENDER_CHOICES + + +def export_candidates(modeladmin, request, queryset): + """ + Export all candidates fields. + """ + export_fields = OrderedDict( + [(f.verbose_name, f.name) for f in Candidate._meta.get_fields() if f.name != 'ID'] + ) + boolean_fields = [f.name for f in Candidate._meta.get_fields() if isinstance(f, BooleanField)] + export_fields['Employeur'] = 'corporation__name' + export_fields['Employeur_localite'] = 'corporation__city' + export_fields['FEE/FPP'] = 'instructor__last_name' + + export = OpenXMLExport('Exportation') + export.write_line(export_fields.keys(), bold=True) + for cand in queryset.values_list(*export_fields.values()): + values = [] + for value, field_name in zip(cand, export_fields.values()): + if field_name == 'gender': + value = dict(GENDER_CHOICES)[value] + if field_name in boolean_fields: + value = 'Oui' if value else '' + values.append(value) + export.write_line(values) + return export.get_http_response('candidats_export') + +export_candidates.short_description = "Exporter les candidats sélectionnés" class CandidateAdminForm(forms.ModelForm): @@ -19,6 +51,7 @@ class CandidateAdmin(admin.ModelAdmin): list_display = ('last_name', 'first_name', 'section', 'confirm_email') list_filter = ('section', 'option') readonly_fields = ('total_result_points', 'total_result_mark', 'date_confirmation_mail') + actions = [export_candidates] fieldsets = ( (None, { 'fields': (('first_name', 'last_name', 'gender'), From b8f3140fedb613d7f70f99712510503c700f929e Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 23 Oct 2017 14:37:12 +0200 Subject: [PATCH 05/13] Fixed indentation in some export views --- stages/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stages/views.py b/stages/views.py index 4182f6b..59a783c 100644 --- a/stages/views.py +++ b/stages/views.py @@ -732,7 +732,7 @@ def general_export(request): values.append('Oui' if line[field] is True else '') else: values.append(line[field]) - export.write_line(values) + export.write_line(values) return export.get_http_response('general_export') @@ -793,6 +793,6 @@ def ortra_export(request): values.append(('Madame', 'Monsieur')[line[field] == 'M']) else: values.append(line[field]) - export.write_line(values) + export.write_line(values) return export.get_http_response('ortra_export') From a08c25b89377841ec15ded7a1610791678304340 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 25 Oct 2017 08:54:22 +0200 Subject: [PATCH 06/13] Add choices to Student.gender --- .../migrations/0001_squashed_0024_course_public_length2.py | 2 +- stages/models.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/stages/migrations/0001_squashed_0024_course_public_length2.py b/stages/migrations/0001_squashed_0024_course_public_length2.py index 4ca9956..63deeb2 100644 --- a/stages/migrations/0001_squashed_0024_course_public_length2.py +++ b/stages/migrations/0001_squashed_0024_course_public_length2.py @@ -143,7 +143,7 @@ class Migration(migrations.Migration): ('ext_id', models.IntegerField(null=True, unique=True, verbose_name='ID externe')), ('first_name', models.CharField(max_length=40, verbose_name='Prénom')), ('last_name', models.CharField(max_length=40, verbose_name='Nom')), - ('gender', models.CharField(blank=True, max_length=3, verbose_name='Genre')), + ('gender', models.CharField(blank=True, choices=[('M', 'Masculin'), ('F', 'Féminin')], max_length=3, verbose_name='Genre')), ('birth_date', models.DateField(blank=True, verbose_name='Date de naissance')), ('street', models.CharField(blank=True, max_length=150, verbose_name='Rue')), ('pcode', models.CharField(max_length=4, verbose_name='Code postal')), diff --git a/stages/models.py b/stages/models.py index 15cfba3..817cabe 100644 --- a/stages/models.py +++ b/stages/models.py @@ -147,11 +147,16 @@ class Option(models.Model): return self.name +GENDER_CHOICES = ( + ('M', 'Masculin'), + ('F', 'Féminin'), +) + class Student(models.Model): ext_id = models.IntegerField(null=True, unique=True, verbose_name='ID externe') first_name = models.CharField(max_length=40, verbose_name='Prénom') last_name = models.CharField(max_length=40, verbose_name='Nom') - gender = models.CharField(max_length=3, blank=True, verbose_name='Genre') + gender = models.CharField('Genre', max_length=3, blank=True, choices=GENDER_CHOICES) birth_date = models.DateField(blank=True, verbose_name='Date de naissance') street = models.CharField(max_length=150, blank=True, verbose_name='Rue') pcode = models.CharField(max_length=4, verbose_name='Code postal') From ba0b6931875de2c3fe6cc1dfa3b87004fffc4b16 Mon Sep 17 00:00:00 2001 From: alazo Date: Tue, 21 Nov 2017 17:34:15 +0100 Subject: [PATCH 07/13] Minor display improvements --- .gitignore | 2 ++ candidats/admin.py | 3 +-- candidats/migrations/0001_initial.py | 2 +- candidats/models.py | 2 +- stages/models.py | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index cdd541d..ae5cdee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ *.pyc database.db common/local_settings.py +media/ scripts/epcstages.json +static/ diff --git a/candidats/admin.py b/candidats/admin.py index 4f687e2..dc86e0c 100644 --- a/candidats/admin.py +++ b/candidats/admin.py @@ -57,7 +57,7 @@ class CandidateAdmin(admin.ModelAdmin): 'fields': (('first_name', 'last_name', 'gender'), ('street', 'pcode', 'city', 'district'), ('mobile', 'email'), - ('birth_date', 'avs', 'handicap'), + ('birth_date', 'avs', 'handicap', 'has_photo'), ('section', 'option'), ('corporation', 'instructor'), ('deposite_date', 'date_confirmation_mail', 'canceled_file'), @@ -73,7 +73,6 @@ class CandidateAdmin(admin.ModelAdmin): 'fields': (('registration_form', 'certificate_of_payement', 'cv', 'certif_of_cfc', 'police_record', 'certif_of_800h', 'reflexive_text', 'work_certificate', 'marks_certificate', 'proc_admin_ext', 'promise', 'contract'), - 'comment', ('interview_date', 'interview_room'), ('examination_result', 'interview_result', 'file_result', 'total_result_points', 'total_result_mark') diff --git a/candidats/migrations/0001_initial.py b/candidats/migrations/0001_initial.py index e834e2a..bd3f605 100644 --- a/candidats/migrations/0001_initial.py +++ b/candidats/migrations/0001_initial.py @@ -34,7 +34,7 @@ class Migration(migrations.Migration): ('integration_second_year', models.BooleanField(default=False, verbose_name='Intégration')), ('date_confirmation_mail', models.DateField(blank=True, null=True, verbose_name='Mail de confirmation')), ('canceled_file', models.BooleanField(default=False, verbose_name='Dossier retiré')), - ('has_photo', models.BooleanField(default=False)), + ('has_photo', models.BooleanField(default=False, verbose_name='Photo')), ('registration_form', models.BooleanField(default=False, verbose_name="Formulaire d'inscription")), ('certificate_of_payement', models.BooleanField(default=False, verbose_name='Attest. paiement')), ('police_record', models.BooleanField(default=False, verbose_name='Casier judic.')), diff --git a/candidats/models.py b/candidats/models.py index fc3dd96..83ae97c 100644 --- a/candidats/models.py +++ b/candidats/models.py @@ -49,7 +49,7 @@ class Candidate(models.Model): integration_second_year = models.BooleanField('Intégration', default=False) date_confirmation_mail = models.DateField('Mail de confirmation', blank=True, null=True) canceled_file = models.BooleanField('Dossier retiré', default=False) - has_photo = models.BooleanField(default=False) + has_photo = models.BooleanField(default=False, verbose_name='Photo') corporation = models.ForeignKey( 'stages.Corporation', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='Employeur' diff --git a/stages/models.py b/stages/models.py index 817cabe..1adfd7f 100644 --- a/stages/models.py +++ b/stages/models.py @@ -283,7 +283,7 @@ class CorpContact(models.Model): verbose_name = "Contact" def __str__(self): - return '%s %s' % (self.last_name, self.first_name) + return '{0} {1}, {2}'.format(self.last_name, self.first_name, self.corporation) class Domain(models.Model): From 71165d29b83d4bd8cb6c99fb4bd320d4f0cc2ff1 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 23 Nov 2017 18:09:59 +0100 Subject: [PATCH 08/13] Renamed pseudo admin field --- candidats/admin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/candidats/admin.py b/candidats/admin.py index dc86e0c..1a53482 100644 --- a/candidats/admin.py +++ b/candidats/admin.py @@ -48,7 +48,7 @@ class CandidateAdminForm(forms.ModelForm): class CandidateAdmin(admin.ModelAdmin): form = CandidateAdminForm - list_display = ('last_name', 'first_name', 'section', 'confirm_email') + list_display = ('last_name', 'first_name', 'section', 'confirm_mail') list_filter = ('section', 'option') readonly_fields = ('total_result_points', 'total_result_mark', 'date_confirmation_mail') actions = [export_candidates] @@ -80,8 +80,8 @@ class CandidateAdmin(admin.ModelAdmin): }), ) - def confirm_email(self, obj): + def confirm_mail(self, obj): return obj.date_confirmation_mail is not None - confirm_email.boolean = True + confirm_mail.boolean = True admin.site.register(Candidate, CandidateAdmin) From 086d9283c32e99f15760b50578c45f14ab4fef16 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 24 Nov 2017 08:39:10 +0100 Subject: [PATCH 09/13] Added send_confirmation_mail admin action for candidates --- candidats/admin.py | 35 +++++++++++++++- candidats/tests.py | 59 +++++++++++++++++++++++++++ templates/email/candidate_confirm.txt | 11 +++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 candidats/tests.py create mode 100644 templates/email/candidate_confirm.txt diff --git a/candidats/admin.py b/candidats/admin.py index 1a53482..6591956 100644 --- a/candidats/admin.py +++ b/candidats/admin.py @@ -1,8 +1,11 @@ from collections import OrderedDict +from datetime import date from django import forms from django.contrib import admin +from django.core.mail import send_mail from django.db.models import BooleanField +from django.template import loader from stages.exports import OpenXMLExport from .models import Candidate, GENDER_CHOICES @@ -36,6 +39,36 @@ def export_candidates(modeladmin, request, queryset): export_candidates.short_description = "Exporter les candidats sélectionnés" +def send_confirmation_mail(modeladmin, request, queryset): + from_email = request.user.email + subject = "Confirmation de votre inscription à l'Ecole Santé-social Pierre-Coullery" + + for candidate in queryset.filter( + deposite_date__isnull=False, date_confirmation_mail__isnull=True, canceled_file=False): + to = [candidate.email] + if candidate.corporation and candidate.corporation.email: + to.append(candidate.corporation.email) + if candidate.instructor and candidate.instructor.email: + to.append(candidate.instructor.email) + + context = { + 'candidate_name': " ".join([candidate.civility, candidate.first_name, candidate.last_name]), + 'section': candidate.section, + 'sender_name': " ".join([request.user.first_name, request.user.last_name]), + 'sender_email': from_email, + } + body = loader.render_to_string('email/candidate_confirm.txt', context) + try: + send_mail(subject, body, from_email, to, fail_silently=False) + except Exception as err: + self.message_user(request, "Échec d'envoi pour le candidat {0} ({1})".format(candidate, err)) + else: + candidate.date_confirmation_mail = date.today() + candidate.save() + +send_confirmation_mail.short_description = "Envoyer email de confirmation" + + class CandidateAdminForm(forms.ModelForm): class Meta: model = Candidate @@ -51,7 +84,7 @@ class CandidateAdmin(admin.ModelAdmin): list_display = ('last_name', 'first_name', 'section', 'confirm_mail') list_filter = ('section', 'option') readonly_fields = ('total_result_points', 'total_result_mark', 'date_confirmation_mail') - actions = [export_candidates] + actions = [export_candidates, send_confirmation_mail] fieldsets = ( (None, { 'fields': (('first_name', 'last_name', 'gender'), diff --git a/candidats/tests.py b/candidats/tests.py new file mode 100644 index 0000000..150aacf --- /dev/null +++ b/candidats/tests.py @@ -0,0 +1,59 @@ +from datetime import date + +from django.contrib.auth.models import User +from django.core import mail +from django.test import TestCase +from django.urls import reverse + +from stages.models import Section +from .models import Candidate + + +class CandidateTests(TestCase): + @classmethod + def setUpTestData(cls): + User.objects.create_superuser( + 'me', 'me@example.org', 'mepassword', first_name='Hans', last_name='Schmid', + ) + + def test_send_confirmation_mail(self): + sect = Section.objects.create(name='EDE') + Candidate.objects.bulk_create([ + # A mail should NOT be sent for those first 4 + Candidate( + first_name='Sara', last_name='Hitz', gender='F', section=sect, + deposite_date=None), + Candidate( + first_name='Jill', last_name='Simth', gender='F', section=sect, + date_confirmation_mail=date.today()), + Candidate(first_name='Hervé', last_name='Bern', gender='M', section=sect, + canceled_file=True), + Candidate(first_name='Frank', last_name='Pit', gender='M', section=sect, email=''), + # Good + Candidate(first_name='Joé', last_name='Glatz', gender='F', section=sect, + email='joe@example.org', deposite_date=date.today()), + Candidate(first_name='Henri', last_name='Dupond', gender='M', section=sect, + 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': 'send_confirmation_mail', + '_selected_action': Candidate.objects.values_list('pk', flat=True) + }, follow=True) + self.assertEqual(len(mail.outbox), 2) + self.assertEqual(mail.outbox[0].recipients(), ['henri@example.org']) + self.assertEqual(mail.outbox[1].recipients(), ['joe@example.org']) + self.assertEqual(mail.outbox[0].body, """Madame, Monsieur, + +Nous vous confirmons la bonne réception de l'inscription de Monsieur Henri Dupond dans la filière EDE pour l'année scolaire à venir. + +Nous nous tenons à votre disposition pour tout renseignement complémentaire et vous prions de recevoir, Madame, Monsieur, nos salutations les plus cordiales. + +Secrétariat de l'EPC +tél. 032 886 33 00 + +Hans Schmid +me@example.org +""".format() + ) diff --git a/templates/email/candidate_confirm.txt b/templates/email/candidate_confirm.txt new file mode 100644 index 0000000..c0fd20b --- /dev/null +++ b/templates/email/candidate_confirm.txt @@ -0,0 +1,11 @@ +Madame, Monsieur, + +Nous vous confirmons la bonne réception de l'inscription de {{ candidate_name }} dans la filière {{ section }} pour l'année scolaire à venir. + +Nous nous tenons à votre disposition pour tout renseignement complémentaire 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 }} From 659f2ea250a3d34e62b259415298b3b9148b3b0e Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 24 Nov 2017 08:53:58 +0100 Subject: [PATCH 10/13] Remove some Python warnings --- candidats/migrations/0001_initial.py | 4 ++-- candidats/models.py | 6 ++++-- stages/pdf.py | 8 ++++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/candidats/migrations/0001_initial.py b/candidats/migrations/0001_initial.py index bd3f605..0b13540 100644 --- a/candidats/migrations/0001_initial.py +++ b/candidats/migrations/0001_initial.py @@ -58,9 +58,9 @@ class Migration(migrations.Migration): ('total_result_mark', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Note finale')), ('accepted', models.BooleanField(default=False, verbose_name='Admis')), ('corporation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stages.Corporation', verbose_name='Employeur')), - ('file_resp', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='stages.Teacher', verbose_name='Exp. dossier')), + ('file_resp', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='stages.Teacher', verbose_name='Exp. dossier')), ('instructor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stages.CorpContact', verbose_name='FEE/FPP')), - ('interview_resp', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='stages.Teacher', verbose_name='Exp. entretien')), + ('interview_resp', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='stages.Teacher', verbose_name='Exp. entretien')), ], options={ 'verbose_name': 'Candidat', diff --git a/candidats/models.py b/candidats/models.py index 83ae97c..fcbf7ab 100644 --- a/candidats/models.py +++ b/candidats/models.py @@ -84,10 +84,12 @@ class Candidate(models.Model): accepted = models.BooleanField('Admis', default=False) interview_resp = models.ForeignKey( - 'stages.Teacher', null=True, blank=True, related_name='+', verbose_name='Exp. entretien' + 'stages.Teacher', null=True, blank=True, related_name='+', verbose_name='Exp. entretien', + on_delete=models.SET_NULL ) file_resp = models.ForeignKey( - 'stages.Teacher', null=True, blank=True, related_name='+', verbose_name='Exp. dossier' + 'stages.Teacher', null=True, blank=True, related_name='+', verbose_name='Exp. dossier', + on_delete=models.SET_NULL ) class Meta: diff --git a/stages/pdf.py b/stages/pdf.py index 2bca1fb..95966f1 100644 --- a/stages/pdf.py +++ b/stages/pdf.py @@ -34,7 +34,8 @@ class ChargeSheetPDF(SimpleDocTemplate): def produce(self, activities): self.story = [] - self.story.append(Image(find('img/header.gif'), width=520, height=75)) + header = open(find('img/header.gif'), 'rb') + self.story.append(Image(header, width=520, height=75)) self.story.append(Spacer(0, 2*cm)) destinataire = '{0}
{1}'.format(self.teacher.civility, str(self.teacher)) self.story.append(Paragraph(destinataire, style_adress)) @@ -80,6 +81,7 @@ class ChargeSheetPDF(SimpleDocTemplate): self.story.append(Paragraph(d, style_normal)) self.story.append(PageBreak()) self.build(self.story) + header.close() class UpdateDataFormPDF(SimpleDocTemplate): @@ -100,8 +102,9 @@ class UpdateDataFormPDF(SimpleDocTemplate): def produce(self, klass): self.story = [] + header = open(find('img/header.gif'), 'rb') for student in klass.student_set.filter(archived=False): - self.story.append(Image(find('img/header.gif'), width=520, height=75)) + self.story.append(Image(header, width=520, height=75)) self.story.append(Spacer(0, 2*cm)) destinataire = '{0}
{1}
{2}'.format(student.civility, student.full_name, student.klass) self.story.append(Paragraph(destinataire, style_adress)) @@ -180,6 +183,7 @@ class UpdateDataFormPDF(SimpleDocTemplate): self.story.append(Paragraph("Pas d'élèves dans cette classe", style_normal)) self.build(self.story) + header.close() def is_corp_required(self, klass_name): return any(el in klass_name for el in ['FE', 'EDS', 'EDEpe']) From ed2e7233b37434c8e848d39a24038abd15600e4f Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 24 Nov 2017 08:57:38 +0100 Subject: [PATCH 11/13] Add extra assertion for confirmation_mail test --- candidats/tests.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/candidats/tests.py b/candidats/tests.py index 150aacf..2b56d27 100644 --- a/candidats/tests.py +++ b/candidats/tests.py @@ -57,3 +57,5 @@ Hans Schmid me@example.org """.format() ) + # One was already set, 2 new. + self.assertEqual(Candidate.objects.filter(date_confirmation_mail__isnull=False).count(), 3) From b58278fcf5777b557808e831810f30664d5e50e1 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 5 Dec 2017 15:35:48 +0100 Subject: [PATCH 12/13] Ignore .idea folder from PyCharm --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ae5cdee..576f0b2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ common/local_settings.py media/ scripts/epcstages.json static/ +.idea/ From 618cfdfebf1d346e780c795fe77b01820245ec49 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 5 Dec 2017 15:51:28 +0100 Subject: [PATCH 13/13] Added a special confirmation email for EDE section --- candidats/admin.py | 8 ++++- candidats/tests.py | 36 ++++++++++++++----- templates/email/candidate_confirm_EDE.txt | 14 ++++++++ ...e_confirm.txt => candidate_confirm_FE.txt} | 0 4 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 templates/email/candidate_confirm_EDE.txt rename templates/email/{candidate_confirm.txt => candidate_confirm_FE.txt} (100%) diff --git a/candidats/admin.py b/candidats/admin.py index 6591956..782111f 100644 --- a/candidats/admin.py +++ b/candidats/admin.py @@ -46,18 +46,24 @@ def send_confirmation_mail(modeladmin, request, queryset): for candidate in queryset.filter( deposite_date__isnull=False, date_confirmation_mail__isnull=True, canceled_file=False): to = [candidate.email] + if candidate.corporation and candidate.corporation.email: to.append(candidate.corporation.email) if candidate.instructor and candidate.instructor.email: to.append(candidate.instructor.email) context = { + 'candidate_civility': candidate.civility, 'candidate_name': " ".join([candidate.civility, candidate.first_name, candidate.last_name]), 'section': candidate.section, 'sender_name': " ".join([request.user.first_name, request.user.last_name]), 'sender_email': from_email, } - body = loader.render_to_string('email/candidate_confirm.txt', context) + + if candidate.section == 'EDE': + body = loader.render_to_string('email/candidate_confirm_EDE.txt', context) + else: + body = loader.render_to_string('email/candidate_confirm_FE.txt', context) try: send_mail(subject, body, from_email, to, fail_silently=False) except Exception as err: diff --git a/candidats/tests.py b/candidats/tests.py index 2b56d27..38ace98 100644 --- a/candidats/tests.py +++ b/candidats/tests.py @@ -17,22 +17,23 @@ class CandidateTests(TestCase): ) def test_send_confirmation_mail(self): - sect = Section.objects.create(name='EDE') + ede = Section.objects.create(name='EDE') + ase = Section.objects.create(name='ASE') Candidate.objects.bulk_create([ # A mail should NOT be sent for those first 4 Candidate( - first_name='Sara', last_name='Hitz', gender='F', section=sect, + first_name='Sara', last_name='Hitz', gender='F', section=ede, deposite_date=None), Candidate( - first_name='Jill', last_name='Simth', gender='F', section=sect, + first_name='Jill', last_name='Simth', gender='F', section=ede, date_confirmation_mail=date.today()), - Candidate(first_name='Hervé', last_name='Bern', gender='M', section=sect, + Candidate(first_name='Hervé', last_name='Bern', gender='M', section=ede, canceled_file=True), - Candidate(first_name='Frank', last_name='Pit', gender='M', section=sect, email=''), + Candidate(first_name='Frank', last_name='Pit', gender='M', section=ede, email=''), # Good - Candidate(first_name='Joé', last_name='Glatz', gender='F', section=sect, + Candidate(first_name='Joé', last_name='Glatz', gender='F', section=ase, email='joe@example.org', deposite_date=date.today()), - Candidate(first_name='Henri', last_name='Dupond', gender='M', section=sect, + Candidate(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') @@ -44,9 +45,26 @@ class CandidateTests(TestCase): self.assertEqual(len(mail.outbox), 2) self.assertEqual(mail.outbox[0].recipients(), ['henri@example.org']) self.assertEqual(mail.outbox[1].recipients(), ['joe@example.org']) - self.assertEqual(mail.outbox[0].body, """Madame, Monsieur, + # Mail content differ depending on the section + self.assertEqual(mail.outbox[0].body, """Monsieur, -Nous vous confirmons la bonne réception de l'inscription de Monsieur Henri Dupond dans la filière EDE pour l'année scolaire à venir. +Par ce courriel, nous vous confirmons la bonne réception de votre dossier de candidature à la formation ES d’Educateur-trice de l’enfance et vous remercions de l’intérêt que vous portez à notre institution. + +Celui-ci sera traité et des nouvelles vous seront communiquées par courriel durant la 2ème quinzaine du mois de février. + +Dans l’intervalle, nous vous adressons, Monsieur, nos salutations les plus cordiales. + + +Secrétariat de l'EPC +tél. 032 886 33 00 + +Hans Schmid +me@example.org +""".format() + ) + self.assertEqual(mail.outbox[1].body, """Madame, Monsieur, + +Nous vous confirmons la bonne réception de l'inscription de Madame Joé Glatz dans la filière ASE pour l'année scolaire à venir. Nous nous tenons à votre disposition pour tout renseignement complémentaire et vous prions de recevoir, Madame, Monsieur, nos salutations les plus cordiales. diff --git a/templates/email/candidate_confirm_EDE.txt b/templates/email/candidate_confirm_EDE.txt new file mode 100644 index 0000000..58bf84c --- /dev/null +++ b/templates/email/candidate_confirm_EDE.txt @@ -0,0 +1,14 @@ +{{ candidate_civility }}, + +Par ce courriel, nous vous confirmons la bonne réception de votre dossier de candidature à la formation ES d’Educateur-trice de l’enfance et vous remercions de l’intérêt que vous portez à notre institution. + +Celui-ci sera traité et des nouvelles vous seront communiquées par courriel durant la 2ème quinzaine du mois de février. + +Dans l’intervalle, nous vous adressons, {{ candidate_civility }}, nos salutations les plus cordiales. + + +Secrétariat de l'EPC +tél. 032 886 33 00 + +{{ sender_name }} +{{ sender_email }} diff --git a/templates/email/candidate_confirm.txt b/templates/email/candidate_confirm_FE.txt similarity index 100% rename from templates/email/candidate_confirm.txt rename to templates/email/candidate_confirm_FE.txt