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)