From ad3b9bd9365722362b1253078d70bab42bcb1f42 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 13 Feb 2020 09:39:27 +0100 Subject: [PATCH] Add new examination model --- stages/admin.py | 156 ++++++++++++------------ stages/migrations/0026_examination.py | 32 +++++ stages/migrations/0027_migrate_exams.py | 35 ++++++ stages/models.py | 53 ++++++++ stages/views/export.py | 44 ++++--- 5 files changed, 223 insertions(+), 97 deletions(-) create mode 100644 stages/migrations/0026_examination.py create mode 100644 stages/migrations/0027_migrate_exams.py diff --git a/stages/admin.py b/stages/admin.py index dc06d43..35bea22 100644 --- a/stages/admin.py +++ b/stages/admin.py @@ -14,7 +14,7 @@ from django.utils.safestring import mark_safe from .models import ( Teacher, Option, Student, StudentFile, Section, Level, Klass, Corporation, CorpContact, Domain, Period, Availability, Training, Course, - LogBookReason, LogBook, ExamEDESession, SupervisionBill + LogBookReason, LogBook, ExamEDESession, Examination, SupervisionBill ) from .views.export import OpenXMLExport @@ -87,6 +87,7 @@ class LogBookInline(admin.TabularInline): class TeacherAdmin(admin.ModelAdmin): list_display = ('__str__', 'abrev', 'email', 'contract', 'rate', 'total_logbook', 'archived') list_filter = (('archived', ArchivedListFilter), 'contract') + search_fields = ('last_name', 'first_name', 'email') fields = (('civility', 'last_name', 'first_name', 'abrev'), ('birth_date', 'email', 'ext_id'), ('contract', 'rate', 'can_examinate', 'archived'), @@ -102,76 +103,105 @@ class SupervisionBillInline(admin.TabularInline): extra = 0 +class ExaminationInline(admin.StackedInline): + model = Examination + extra = 1 + verbose_name = "Procédure de qualification" + verbose_name_plural = "Procédures de qualification" + autocomplete_fields = ('internal_expert', 'external_expert') + fields = (('session', 'type_exam', 'date_exam', 'room'), + ('internal_expert', 'external_expert'), + ('mark', 'mark_acq'), + ('examination_actions'), + ('date_soutenance_mailed', 'date_confirm_received'),) + readonly_fields = ( + 'examination_actions', 'date_soutenance_mailed' + ) + + def examination_actions(self, obj): + missing_message = mark_safe( + '
Veuillez compléter les informations ' + 'd’examen (date/salle/experts) pour accéder aux boutons d’impression.
' + ) + if obj and obj.student.is_ede_3(): + if obj.missing_examination_data(): + return missing_message + else: + return format_html( + 'Courrier pour l’expert ' + 'Mail convocation soutenance ' + 'Indemnité au mentor', + reverse('print-expert-compens-ede', args=[obj.student.pk]), + reverse('student-ede-convocation', args=[obj.student.pk]), + reverse('print-mentor-compens-ede', args=[obj.student.pk]), + ) + elif obj and obj.student.is_eds_3(): + if obj.missing_examination_data(): + return missing_message + else: + return format_html( + 'Courrier pour l’expert ' + 'Mail convocation soutenance ' + 'Indemnité au mentor', + reverse('print-expert-compens-eds', args=[obj.student.pk]), + reverse('student-eds-convocation', args=[obj.student.pk]), + reverse('print-mentor-compens-ede', args=[obj.student.pk]), + ) + else: + return missing_message + examination_actions.short_description = 'Actions pour la procédure' + + class StudentAdmin(admin.ModelAdmin): list_display = ('__str__', 'pcode', 'city', 'klass', 'archived') ordering = ('last_name', 'first_name') list_filter = (('archived', ArchivedListFilter), ('klass', KlassRelatedListFilter)) search_fields = ('last_name', 'first_name', 'pcode', 'city', 'klass__name') autocomplete_fields = ('corporation', 'instructor', 'supervisor', 'mentor', 'expert', 'expert_ep') - readonly_fields = ( - 'report_sem1_sent', 'report_sem2_sent', - 'examination_actions', - 'date_soutenance_mailed', 'date_soutenance_ep_mailed' - ) + readonly_fields = ('report_sem1_sent', 'report_sem2_sent') fieldsets = [ (None, { - 'fields': (('last_name', 'first_name', 'ext_id'), ('street', 'pcode', 'city', 'district'), - ('email', 'tel', 'mobile'), ('gender', 'avs', 'birth_date'), - ('archived', 'dispense_ecg', 'dispense_eps', 'soutien_dys'), - ('klass', 'option_ase'), - ('report_sem1', 'report_sem1_sent'), - ('report_sem2', 'report_sem2_sent'), - ('corporation', 'instructor',) - ) - } - ), - ("Examen Qualification ES", { + 'fields': ( + ('last_name', 'first_name', 'ext_id'), ('street', 'pcode', 'city', 'district'), + ('email', 'tel', 'mobile'), ('gender', 'avs', 'birth_date'), + ('archived', 'dispense_ecg', 'dispense_eps', 'soutien_dys'), + ('klass', 'option_ase'), + ('report_sem1', 'report_sem1_sent'), + ('report_sem2', 'report_sem2_sent'), + ('corporation', 'instructor',) + )} + ), + ("Procédure de qualification", { 'classes': ['collapse'], 'fields': ( - ('session', 'date_exam', 'room'), ('supervisor', 'supervision_attest_received'), ('subject', 'title'), ('training_referent', 'referent', 'mentor'), - ('internal_expert', 'expert'), - ('date_soutenance_mailed', 'date_confirm_received'), - ('examination_actions',), - ('mark', 'mark_acq'), - ) - }), - ("Entretien professionnel ES", { - 'classes': ['collapse'], - 'fields': ( - ('session_ep', 'date_exam_ep', 'room_ep'), - ('internal_expert_ep', 'expert_ep'), - ('date_soutenance_ep_mailed', 'date_confirm_ep_received'), - ('mark_ep', 'mark_ep_acq'), ) }), ] actions = ['archive'] - inlines = [SupervisionBillInline] + inlines = [ExaminationInline, SupervisionBillInline] - def get_inline_instances(self, request, obj=None): - # SupervisionBillInline is only adequate for EDE students - if obj is None or not obj.klass or obj.klass.section.name != 'EDE': + def get_inlines(self, request, obj=None): + if obj is None: return [] - return super().get_inline_instances(request, obj=obj) + inlines = super().get_inlines(request, obj=obj) + # SupervisionBillInline is only adequate for EDE students + if not obj.klass or obj.klass.section.name != 'EDE': + inlines = [inl for inl in inlines if inl != SupervisionBillInline] + if not obj.is_ede_3() and not obj.is_eds_3(): + inlines = [inl for inl in inlines if inl != ExaminationInline] + return inlines def get_fieldsets(self, request, obj=None): - if not self.is_ede_3(obj) and not self.is_eds_3(obj): - # Hide "Examen Qualification ES"/"Entretien professionnel ES" + if not obj or (not obj.is_ede_3() and not obj.is_eds_3()): + # Hide group "Procédure de qualification" fieldsets = deepcopy(self.fieldsets) fieldsets[1][1]['classes'] = ['hidden'] - fieldsets[2][1]['classes'] = ['hidden'] return fieldsets return super().get_fieldsets(request, obj) - def is_ede_3(self, obj): - return obj and obj.klass and obj.klass.section.name == 'EDE' and obj.klass.level.name == '3' - - def is_eds_3(self, obj): - return obj and obj.klass and obj.klass.section.name == 'EDS' and obj.klass.level.name == '3' - def archive(self, request, queryset): for student in queryset: # Save each item individually to allow for custom save() logic. @@ -179,41 +209,6 @@ class StudentAdmin(admin.ModelAdmin): student.save() archive.short_description = "Marquer les étudiants sélectionnés comme archivés" - def examination_actions(self, obj): - if self.is_ede_3(obj): - if obj.missing_examination_data(): - return mark_safe( - '
Veuillez compléter les informations ' - 'd’examen (date/salle/experts) pour accéder aux boutons d’impression.
' - ) - else: - return format_html( - 'Courrier pour l’expert ' - 'Mail convocation soutenance ' - 'Indemnité au mentor', - reverse('print-expert-compens-ede', args=[obj.pk]), - reverse('student-ede-convocation', args=[obj.pk]), - reverse('print-mentor-compens-ede', args=[obj.pk]), - ) - elif self.is_eds_3(obj): - if obj.missing_examination_data(): - return mark_safe( - '
Veuillez compléter les informations ' - 'd’examen (date/salle/experts) pour accéder aux boutons d’impression.
' - ) - else: - return format_html( - 'Courrier pour l’expert ' - 'Mail convocation soutenance ' - 'Indemnité au mentor', - reverse('print-expert-compens-eds', args=[obj.pk]), - reverse('student-eds-convocation', args=[obj.pk]), - reverse('print-mentor-compens-ede', args=[obj.pk]), - ) - else: - return '' - examination_actions.short_description = 'Actions pour les examens' - class CorpContactAdmin(admin.ModelAdmin): list_display = ('__str__', 'corporation', 'role') @@ -412,6 +407,7 @@ admin.site.register(Training, TrainingAdmin) admin.site.register(LogBookReason) admin.site.register(LogBook) admin.site.register(ExamEDESession) +admin.site.register(Examination) admin.site.unregister(Group) admin.site.register(Group, GroupAdmin) diff --git a/stages/migrations/0026_examination.py b/stages/migrations/0026_examination.py new file mode 100644 index 0000000..a768992 --- /dev/null +++ b/stages/migrations/0026_examination.py @@ -0,0 +1,32 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stages', '0025_section_has_stages'), + ] + + operations = [ + migrations.CreateModel( + name='Examination', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_exam', models.DateTimeField(blank=True, null=True)), + ('room', models.CharField(blank=True, max_length=15, verbose_name='Salle')), + ('mark', models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True, verbose_name='Note')), + ('mark_acq', models.CharField(blank=True, choices=[('non', 'Non acquis'), ('part', 'Partiellement acquis'), ('acq', 'Acquis')], max_length=5, verbose_name='Note')), + ('date_soutenance_mailed', models.DateTimeField(blank=True, null=True, verbose_name='Convoc. env.')), + ('date_confirm_received', models.DateTimeField(blank=True, null=True, verbose_name='Récept. confirm')), + ('external_expert', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stages.CorpContact', verbose_name='Expert externe')), + ('internal_expert', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stages.Teacher', verbose_name='Expert interne')), + ('session', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stages.ExamEDESession', verbose_name='Session')), + ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stages.Student')), + ('type_exam', models.CharField(choices=[('exam', 'Examen qualification'), ('entr', 'Entretien professionnel')], default='', max_length=10, verbose_name='Type')), + ], + options={ + 'verbose_name': 'Examen', + }, + ), + ] diff --git a/stages/migrations/0027_migrate_exams.py b/stages/migrations/0027_migrate_exams.py new file mode 100644 index 0000000..e52fea9 --- /dev/null +++ b/stages/migrations/0027_migrate_exams.py @@ -0,0 +1,35 @@ +from django.db import migrations + + +def migrate_exams(apps, schema_editor): + Student = apps.get_model('stages', 'Student') + Examination = apps.get_model('stages', 'Examination') + + for student in Student.objects.all(): + if student.session or student.date_exam: + Examination.objects.create( + student=student, type_exam='exam', + session=student.session, date_exam=student.date_exam, + room=student.room, mark=student.mark, mark_acq=student.mark_acq, + internal_expert=student.internal_expert, external_expert=student.expert, + date_soutenance_mailed=student.date_soutenance_mailed, + date_confirm_received=student.date_confirm_received, + ) + if student.session_ep or student.date_exam_ep: + Examination.objects.create( + student=student, type_exam='entre', + session=student.session_ep, date_exam=student.date_exam_ep, + room=student.room_ep, mark=student.mark_ep, mark_acq=student.mark_ep_acq, + internal_expert=student.internal_expert_ep, external_expert=student.expert_ep, + date_soutenance_mailed=student.date_soutenance_ep_mailed, + date_confirm_received=student.date_confirm_ep_received, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('stages', '0026_examination'), + ] + + operations = [migrations.RunPython(migrate_exams)] diff --git a/stages/models.py b/stages/models.py index 229e21d..896d1e1 100644 --- a/stages/models.py +++ b/stages/models.py @@ -406,6 +406,12 @@ class Student(models.Model): return user.has_perm('stages.change_student') or user.teacher == self.klass.teacher return False + def is_ede_3(self): + return self.klass and self.klass.section.name == 'EDE' and self.klass.level.name == '3' + + def is_eds_3(self): + return self.klass and self.klass.section.name == 'EDS' and self.klass.level.name == '3' + def missing_examination_data(self): missing = [] if not self.date_exam: @@ -431,6 +437,53 @@ class Student(models.Model): return missing +class Examination(models.Model): + ACQ_MARK_CHOICES = ( + ('non', 'Non acquis'), + ('part', 'Partiellement acquis'), + ('acq', 'Acquis'), + ) + TYPE_EXAM_CHOICES = ( + ('exam', 'Examen qualification'), + ('entr', 'Entretien professionnel'), + ) + + student = models.ForeignKey(Student, on_delete=models.CASCADE) + session = models.ForeignKey( + ExamEDESession, null=True, blank=True, on_delete=models.SET_NULL, verbose_name='Session', + ) + type_exam = models.CharField("Type", max_length=10, choices=TYPE_EXAM_CHOICES) + date_exam = models.DateTimeField(blank=True, null=True) + room = models.CharField('Salle', max_length=15, blank=True) + mark = models.DecimalField('Note', max_digits=3, decimal_places=2, blank=True, null=True) + mark_acq = models.CharField('Note', max_length=5, choices=ACQ_MARK_CHOICES, blank=True) + internal_expert = models.ForeignKey( + Teacher, verbose_name='Expert interne', + null=True, blank=True, on_delete=models.SET_NULL + ) + external_expert = models.ForeignKey( + 'CorpContact', verbose_name='Expert externe', + null=True, blank=True, on_delete=models.SET_NULL + ) + date_soutenance_mailed = models.DateTimeField("Convoc. env.", blank=True, null=True) + date_confirm_received = models.DateTimeField("Récept. confirm", blank=True, null=True) + + class Meta: + verbose_name = "Examen" + + def missing_examination_data(self): + missing = [] + if not self.date_exam: + missing.append("La date d’examen est manquante") + if not self.room: + missing.append("La salle d’examen n’est pas définie") + if not self.external_expert: + missing.append("L’expert externe n’est pas défini") + if not self.internal_expert: + missing.append("L’expert interne n’est pas défini") + return missing + + class StudentFile(models.Model): student = models.ForeignKey(Student, on_delete=models.CASCADE) fichier = models.FileField(upload_to='etudiants') diff --git a/stages/views/export.py b/stages/views/export.py index cb16724..06ed0dc 100644 --- a/stages/views/export.py +++ b/stages/views/export.py @@ -418,11 +418,9 @@ def ortra_export(request): def export_qualification(request, section='ede'): headers = [ 'Classe', 'Etudiant-e', - 'Référent pratique', 'Résumé TD', 'Ens. référent', 'dernier RDV', + 'Référent pratique', 'Titre TD', 'Résumé TD', 'Ens. référent', 'Mentor', - 'Session', - 'Titre TD', - 'Exp_int.', + 'Session', 'Type', 'Exp_int.', 'Expert ext. Civilité', 'Expert ext. Nom', 'Expert ext. Adresse', 'Expert ext. Localité', 'Date', 'Salle', 'Note', ] @@ -432,29 +430,41 @@ def export_qualification(request, section='ede'): export.write_line(headers, bold=True) # Data + empty_values = [''] * 7 for student in Student.objects.filter(klass__name__startswith='3%s' % section.upper(), archived=False ).select_related('klass', 'referent', 'training_referent', 'mentor', 'expert', 'internal_expert', + ).prefetch_related('examination_set' ).order_by('klass__name', 'last_name'): - values = [ + stud_values = [ student.klass.name, student.full_name, student.training_referent.full_name if student.training_referent else '', + student.title, student.subject, student.referent.full_name if student.referent else '', - student.last_appointment, student.mentor.full_name if student.mentor else '', - str(student.session), - student.title, - student.internal_expert.full_name if student.internal_expert else '', - student.expert.civility if student.expert else '', - student.expert.full_name if student.expert else '', - student.expert.street if student.expert else '', - student.expert.pcode_city if student.expert else '', - student.date_exam, - student.room, - student.mark, ] - export.write_line(values) + lines_exported = 0 + for exam in student.examination_set.all(): + exam_values = [ + str(exam.session), + exam.get_type_exam_display(), + exam.internal_expert.full_name if exam.internal_expert else '', + exam.external_expert.civility if exam.external_expert else '', + exam.external_expert.full_name if exam.external_expert else '', + exam.external_expert.street if exam.external_expert else '', + exam.external_expert.pcode_city if exam.external_expert else '', + exam.date_exam, + exam.room, + exam.mark, + ] + if lines_exported == 0: + export.write_line(stud_values + exam_values) + else: + export.write_line(empty_values + exam_values) + lines_exported += 1 + if lines_exported == 0: + export.write_line(stud_values) return export.get_http_response(export_name)