round
[auf_rh_dae.git] / project / recrutement / admin.py
1 # -*- encoding: utf-8 -*-
2
3 from django.core.urlresolvers import reverse
4 from django.http import HttpResponseRedirect
5 from django.contrib import admin
6 from django.forms.models import BaseInlineFormSet
7
8 from reversion.admin import VersionAdmin
9 from datamaster_modeles.models import Region, Bureau
10 from project.rh import models as rh
11
12 from project.dae.utils import get_employe_from_user as get_emp
13 from recrutement.models import *
14 from recrutement.workflow import grp_drh_recrutement, grp_directeurs_bureau_recrutement, \
15 grp_administrateurs_recrutement, \
16 grp_correspondants_rh_recrutement
17
18 from recrutement.forms import *
19
20 ### CONSTANTES
21 IMPLANTATIONS_CENTRALES = [15, 19]
22
23 class OffreEmploiAdmin(VersionAdmin):
24 date_hierarchy = 'date_creation'
25 list_display = ('nom', 'date_limite', 'region', 'statut',
26 'est_affiche', '_candidatsList', )
27 exclude = ('actif', 'poste_nom', 'resume',)
28 list_filter = ('statut',)
29 actions = ['affecter_evaluateurs_offre_emploi', ]
30 form = OffreEmploiForm
31
32 ### Actions à afficher
33 def get_actions(self, request):
34 actions = super(OffreEmploiAdmin, self).get_actions(request)
35 del actions['delete_selected']
36 return actions
37
38 ### Affecter un évaluateurs à des offres d'emploi
39 def affecter_evaluateurs_offre_emploi(modeladmin, obj, candidats):
40 selected = obj.POST.getlist(admin.ACTION_CHECKBOX_NAME)
41
42 return HttpResponseRedirect(reverse('affecter_evaluateurs_offre_emploi')+
43 "?ids=%s" % (",".join(selected)))
44 affecter_evaluateurs_offre_emploi.short_description = u'Affecter évaluateur(s)'
45
46 ### Afficher la liste des candidats pour l'offre d'emploi
47 def _candidatsList(self, obj):
48 return "<a href='%s?offre_emploi__id__exact=%s'>Voir les candidats \
49 </a>" % (reverse('admin:recrutement_candidat_changelist'), obj.id)
50 _candidatsList.allow_tags = True
51 _candidatsList.short_description = "Afficher la liste des candidats"
52
53 ### Formulaire
54 def get_form(self, request, obj=None, **kwargs):
55 form = super(OffreEmploiAdmin, self).get_form(request, obj, **kwargs)
56 employe = get_emp(request.user)
57 user_groupes = request.user.groups.all()
58
59
60 # Region
61 if form.declared_fields.has_key('region'):
62 region_field = form.declared_fields['region']
63 else:
64 region_field = form.base_fields['region']
65
66 if grp_drh_recrutement in user_groupes:
67 region_field.queryset = Region.objects.all()
68 else:
69 region_field.queryset = Region.objects.\
70 filter(id=employe.implantation.region.id)
71
72 # Poste
73 if form.declared_fields.has_key('poste'):
74 poste_field = form.declared_fields['poste']
75 else:
76 poste_field = form.base_fields['poste']
77
78 if grp_drh_recrutement in user_groupes:
79 poste_field.queryset = rh.Poste.objects.all()
80 else:
81 poste_field.queryset = rh.Poste.objects.\
82 filter(implantation__region=employe.implantation.region).\
83 exclude(implantation__in=IMPLANTATIONS_CENTRALES)
84
85 # Bureau
86 if form.declared_fields.has_key('bureau'):
87 bureau_field = form.declared_fields['bureau']
88 else:
89 bureau_field = form.base_fields['bureau']
90
91 if grp_drh_recrutement in user_groupes:
92 bureau_field.queryset = Bureau.objects.all()
93 else:
94 bureau_field.queryset = Bureau.objects.\
95 filter(region=employe.implantation.region)
96
97 return form
98
99 ### Queryset
100 def queryset(self, request):
101 qs = self.model._default_manager.get_query_set().select_related('offre_emploi')
102 user_groupes = request.user.groups.all()
103 if grp_drh_recrutement in user_groupes:
104 return qs
105
106 if grp_directeurs_bureau_recrutement in user_groupes or \
107 grp_correspondants_rh_recrutement in user_groupes or \
108 grp_administrateurs_recrutement in user_groupes:
109 employe = get_emp(request.user)
110 return qs.filter(region=employe.implantation.region)
111
112 if Evaluateur.objects.filter(user=request.user).exists():
113 evaluateur = Evaluateur.objects.get(user=request.user)
114 offre_ids = [e.candidat.offre_emploi_id for e in
115 CandidatEvaluation.objects.select_related('candidat').filter(evaluateur=evaluateur)]
116 return qs.filter(id__in=offre_ids)
117
118 return qs.none()
119
120 ### Permission add, delete, change
121 def has_add_permission(self, request):
122 user_groupes = request.user.groups.all()
123 if request.user.is_superuser is True or \
124 grp_drh_recrutement in user_groupes or \
125 grp_directeurs_bureau_recrutement in user_groupes or \
126 grp_administrateurs_recrutement in user_groupes:
127 return True
128 return False
129
130 def has_delete_permission(self, request, obj=None):
131 user_groupes = request.user.groups.all()
132 if request.user.is_superuser is True or \
133 grp_drh_recrutement in user_groupes or \
134 grp_directeurs_bureau_recrutement in user_groupes or \
135 grp_administrateurs_recrutement in user_groupes:
136 return True
137 return False
138
139 def has_change_permission(self, request, obj=None):
140 user_groupes = request.user.groups.all()
141 if request.user.is_superuser is True or \
142 grp_drh_recrutement in user_groupes or \
143 grp_directeurs_bureau_recrutement in user_groupes or \
144 grp_administrateurs_recrutement in user_groupes:
145 return True
146 return False
147
148 class ProxyOffreEmploiAdmin(OffreEmploiAdmin):
149 list_display = ('nom', 'date_limite', 'region', 'statut',
150 'est_affiche')
151 readonly_fields = ('description', 'bureau', 'duree_affectation',
152 'renumeration', 'debut_affectation', 'lieu_affectation',
153 'nom', 'resume', 'date_limite', 'region', 'poste')
154 fieldsets = (
155 ('Nom', {
156 'fields': ('nom', )
157 }),
158 ('Description générale', {
159 'fields': ('description', 'date_limite', )
160 }),
161 ('Coordonnées', {
162 'fields': ('lieu_affectation', 'bureau', 'region', 'poste',)
163 }),
164 ('Autre', {
165 'fields': ('debut_affectation', 'duree_affectation',
166 'renumeration', )
167 }),
168 )
169 inlines = []
170
171
172 ### Lieu de redirection après le change
173 def response_change(self, request, obj):
174 return HttpResponseRedirect(reverse\
175 ('admin:recrutement_proxyoffreemploi_changelist'))
176
177 ### Formulaire
178 def get_form(self, request, obj=None, **kwargs):
179 form = super(OffreEmploiAdmin, self).get_form(request, obj, **kwargs)
180 return form
181
182 ### Permissions add, delete, change
183 def has_add_permission(self, request):
184 return False
185
186 def has_delete_permission(self, request, obj=None):
187 return False
188
189 def has_change_permission(self, request, obj=None):
190 return True
191
192 class CandidatPieceInline(admin.TabularInline):
193 model = CandidatPiece
194 fields = ('candidat', 'nom', 'path',)
195 extra = 1
196 max_num = 3
197
198 class ReadOnlyCandidatPieceInline(CandidatPieceInline):
199 readonly_fields = ('candidat', 'nom', 'path', )
200 cand_delete = False
201
202
203 class CandidatEvaluationInlineFormSet(BaseInlineFormSet):
204 """
205 Empêche la suppression d'une évaluation pour le CandidatEvaluationInline
206 """
207 def __init__(self, *args, **kwargs):
208 super(CandidatEvaluationInlineFormSet, self).__init__(*args, **kwargs)
209 self.can_delete = False
210
211 class CandidatEvaluationInline(admin.TabularInline):
212 model = CandidatEvaluation
213 fields = ('evaluateur', 'note', 'commentaire')
214 max_num = 0
215 extra = 0
216 formset = CandidatEvaluationInlineFormSet
217
218 ### Fields readonly
219 def get_readonly_fields(self, request, obj=None):
220 """
221 Empêche la modification des évaluations
222 """
223 if obj:
224 return self.readonly_fields+('evaluateur', 'note', 'commentaire')
225 return self.readonly_fields
226
227 class CandidatAdmin(VersionAdmin):
228 search_fields = ('nom', 'prenom' )
229 exclude = ('actif', )
230 list_editable = ('statut', )
231 list_display = ('nom', 'prenom', 'offre_emploi',
232 'voir_offre_emploi', 'calculer_moyenne',
233 'afficher_candidat', '_date_creation', 'statut', )
234 list_filter = ('offre_emploi', 'offre_emploi__region', 'statut', )
235
236 fieldsets = (
237 ("Offre d'emploi", {
238 'fields': ('offre_emploi', )
239 }),
240 ('Informations personnelles', {
241 'fields': ('prenom','nom','genre', 'nationalite',
242 'situation_famille', 'nombre_dependant',)
243 }),
244 ('Coordonnées', {
245 'fields': ('telephone', 'email', 'adresse', 'ville',
246 'etat_province', 'code_postal', 'pays', )
247 }),
248 ('Informations professionnelles', {
249 'fields': ('niveau_diplome','employeur_actuel',
250 'poste_actuel', 'domaine_professionnel',)
251 }),
252 ('Traitement', {
253 'fields': ('statut', )
254 }),
255 )
256 inlines = [
257 CandidatPieceInline,
258 CandidatEvaluationInline,
259 ]
260
261 actions = ['envoyer_courriel_candidats']
262
263 def _date_creation(self, obj):
264 return obj.date_creation
265 _date_creation.order_field = "date_creation"
266 _date_creation.short_description = "Date de création"
267
268 ### Actions à afficher
269 def get_actions(self, request):
270 actions = super(CandidatAdmin, self).get_actions(request)
271 del actions['delete_selected']
272 return actions
273
274 ### Envoyer un courriel à des candidats
275 def envoyer_courriel_candidats(modeladmin, obj, candidats):
276 selected = obj.POST.getlist(admin.ACTION_CHECKBOX_NAME)
277
278 return HttpResponseRedirect(reverse('selectionner_template')+
279 "?ids=%s" % (",".join(selected)))
280 envoyer_courriel_candidats.short_description = u'Envoyer courriel'
281
282 ### Évaluer un candidat
283 def evaluer_candidat(self, obj):
284 return "<a href='%s?candidat__id__exact=%s'>Évaluer le candidat</a>" % \
285 (reverse('admin:recrutement_candidatevaluation_changelist'),
286 obj.id)
287 evaluer_candidat.allow_tags = True
288 evaluer_candidat.short_description = 'Évaluation'
289
290 ### Afficher un candidat
291 def afficher_candidat(self, obj):
292 return "<a href='%s'>Voir le candidat</a>" % \
293 (reverse('admin:recrutement_proxycandidat_change', args=(obj.id,)))
294 afficher_candidat.allow_tags = True
295 afficher_candidat.short_description = u'Détails du candidat'
296
297 ### Voir l'offre d'emploi
298 def voir_offre_emploi(self, obj):
299 return "<a href='%s'>Voir l'offre d'emploi</a>" % \
300 (reverse('admin:recrutement_proxyoffreemploi_change',
301 args=(obj.offre_emploi.id,)))
302 voir_offre_emploi.allow_tags = True
303 voir_offre_emploi.short_description = "Afficher l'offre d'emploi"
304
305 ### Calculer la moyenne des notes
306 def calculer_moyenne(self, obj):
307 evaluations = CandidatEvaluation.objects.filter(candidat=obj)
308
309 notes = [evaluation.note for evaluation in evaluations \
310 if evaluation.note is not None]
311
312 if len(notes) > 0:
313 moyenne_votes = round(float(sum(notes)) / len(notes), 2)
314 else:
315 moyenne_votes = "Non disponible"
316
317 totales = len(evaluations)
318 faites = len(notes)
319
320 if obj.statut == 'REC':
321 if totales == faites:
322 color = "green"
323 elif faites > 0 and float(totales) / float(faites) >= 2:
324 color = "orange"
325 else:
326 color = "red"
327 else:
328 color = "black"
329
330 return """<span style="color: %s;">%s (%s/%s)</span>""" % (color, moyenne_votes, faites, totales)
331 calculer_moyenne.allow_tags = True
332 calculer_moyenne.short_description = "Notation"
333 calculer_moyenne.admin_order_field = ""
334
335 ### Permissions add, delete, change
336 def has_add_permission(self, request):
337 user_groupes = request.user.groups.all()
338 if request.user.is_superuser is True or \
339 grp_drh_recrutement in user_groupes or \
340 grp_directeurs_bureau_recrutement in user_groupes or \
341 grp_administrateurs_recrutement in user_groupes:
342 return True
343 return False
344
345 def has_delete_permission(self, request, obj=None):
346 user_groupes = request.user.groups.all()
347 if request.user.is_superuser is True or \
348 grp_drh_recrutement in user_groupes or \
349 grp_directeurs_bureau_recrutement in user_groupes or \
350 grp_administrateurs_recrutement in user_groupes:
351 return True
352 return False
353
354 def has_change_permission(self, request, obj=None):
355 user_groupes = request.user.groups.all()
356 if request.user.is_superuser is True or \
357 grp_correspondants_rh_recrutement in user_groupes or \
358 grp_drh_recrutement in user_groupes or \
359 grp_directeurs_bureau_recrutement in user_groupes or \
360 grp_administrateurs_recrutement in user_groupes:
361 return True
362 return False
363
364 ### Queryset
365 def queryset(self, request):
366 """
367 Spécifie un queryset limité, autrement Django exécute un
368 select_related() sans paramètre, ce qui a pour effet de charger tous
369 les objets FK, sans limite de profondeur. Dès qu'on arrive, dans les
370 modèles de Region, il existe plusieurs boucles, ce qui conduit à la
371 génération d'une requête infinie.
372
373 """
374 qs = self.model._default_manager.get_query_set().select_related('offre_emploi')
375 user_groupes = request.user.groups.all()
376 if grp_drh_recrutement in user_groupes:
377 return qs
378
379 if grp_directeurs_bureau_recrutement in user_groupes or \
380 grp_correspondants_rh_recrutement in user_groupes or \
381 grp_administrateurs_recrutement in user_groupes:
382 employe = get_emp(request.user)
383 return qs.filter(offre_emploi__region=employe.implantation.region)
384
385 if Evaluateur.objects.filter(user=request.user).exists():
386 evaluateur = Evaluateur.objects.get(user=request.user)
387 candidat_ids = [e.candidat.id for e in
388 CandidatEvaluation.objects.filter(evaluateur=evaluateur)]
389 return qs.filter(id__in=candidat_ids)
390 return qs.none()
391
392
393 class ProxyCandidatAdmin(CandidatAdmin):
394 list_editable = ()
395 readonly_fields = ('statut', 'offre_emploi', 'prenom', 'nom',
396 'genre', 'nationalite', 'situation_famille',
397 'nombre_dependant', 'telephone', 'email', 'adresse',
398 'ville', 'etat_province', 'code_postal', 'pays',
399 'niveau_diplome', 'employeur_actuel', 'poste_actuel',
400 'domaine_professionnel', 'pieces_jointes',)
401 fieldsets = (
402 ("Offre d'emploi", {
403 'fields': ('offre_emploi', )
404 }),
405 ('Informations personnelles', {
406 'fields': ('prenom','nom','genre', 'nationalite',
407 'situation_famille', 'nombre_dependant',)
408 }),
409 ('Coordonnées', {
410 'fields': ('telephone', 'email', 'adresse', 'ville',
411 'etat_province', 'code_postal', 'pays', )
412 }),
413 ('Informations professionnelles', {
414 'fields': ('niveau_diplome','employeur_actuel',
415 'poste_actuel', 'domaine_professionnel',)
416 }),
417 )
418 inlines = (CandidatEvaluationInline, )
419
420 def has_add_permission(self, request):
421 return False
422
423 def has_delete_permission(self, request, obj=None):
424 return False
425
426 def has_change_permission(self, request, obj=None):
427 return True
428
429 class CandidatPieceAdmin(admin.ModelAdmin):
430 list_display = ('nom', 'candidat', )
431
432 ### Queryset
433 def queryset(self, request):
434 """
435 Spécifie un queryset limité, autrement Django exécute un
436 select_related() sans paramètre, ce qui a pour effet de charger tous
437 les objets FK, sans limite de profondeur. Dès qu'on arrive, dans les
438 modèles de Region, il existe plusieurs boucles, ce qui conduit à la
439 génération d'une requête infinie.
440 Affiche la liste de candidats que si le user connecté
441 possède un Evaluateur
442 """
443 qs = self.model._default_manager.get_query_set()
444 return qs.select_related('candidat')
445
446 class EvaluateurAdmin(VersionAdmin):
447 fieldsets = (
448 ("Utilisateur", {
449 'fields': ('user',)
450 }),
451 )
452
453 ### Actions à afficher
454 def get_actions(self, request):
455 actions = super(EvaluateurAdmin, self).get_actions(request)
456 del actions['delete_selected']
457 return actions
458
459 ### Permissions add, delete, change
460 def has_add_permission(self, request):
461 user_groupes = request.user.groups.all()
462 if request.user.is_superuser is True or \
463 grp_drh_recrutement in user_groupes or \
464 grp_directeurs_bureau_recrutement in user_groupes or \
465 grp_administrateurs_recrutement in user_groupes:
466 return True
467 return False
468
469 def has_delete_permission(self, request, obj=None):
470 user_groupes = request.user.groups.all()
471 if request.user.is_superuser is True or \
472 grp_drh_recrutement in user_groupes or \
473 grp_directeurs_bureau_recrutement in user_groupes or \
474 grp_administrateurs_recrutement in user_groupes:
475 return True
476 return False
477
478 def has_change_permission(self, request, obj=None):
479 user_groupes = request.user.groups.all()
480 if request.user.is_superuser is True or \
481 grp_drh_recrutement in user_groupes or \
482 grp_directeurs_bureau_recrutement in user_groupes or \
483 grp_administrateurs_recrutement in user_groupes:
484 return True
485 return False
486
487 class CandidatEvaluationAdmin(admin.ModelAdmin):
488 search_fields = ('candidat__nom', 'candidat__prenom' )
489 list_display = ('_candidat', '_statut', '_offre_emploi', 'evaluateur', '_note',
490 '_commentaire', )
491 readonly_fields = ('candidat', 'evaluateur')
492 list_filter = ('candidat__statut', 'candidat__offre_emploi',)
493 fieldsets = (
494 ('Évaluation du candidat', {
495 'fields': ('candidat', 'evaluateur', 'note', 'commentaire', )
496 }),
497 )
498
499 def get_actions(self, request):
500 # on stocke l'evaluateur connecté (pas forcément la meilleure place...)
501 try:
502 self.evaluateur = Evaluateur.objects.get(user=request.user)
503 except:
504 self.evaluateur = None
505
506 actions = super(CandidatEvaluationAdmin, self).get_actions(request)
507 del actions['delete_selected']
508 return actions
509
510 ### Afficher la note
511 def _note(self, obj):
512 """
513 Si l'évaluateur n'a pas encore donné de note au candidat, indiquer
514 un lien pour Évaluer le candidat.
515 Sinon afficher la note.
516 """
517 page = self.model.__name__.lower()
518 redirect_url = 'admin:recrutement_%s_change' % page
519
520 if obj.note is None:
521 label = "Candidat non évalué"
522 else:
523 label = obj.note
524
525 if self.evaluateur == obj.evaluateur:
526 return "<a href='%s'>%s</a>" % (reverse(redirect_url, args=(obj.id,)), label)
527 else:
528 return label
529 _note.allow_tags = True
530 _note.short_description = "Note"
531 _note.admin_order_field = 'note'
532
533 def _statut(self, obj):
534 return obj.candidat.get_statut_display()
535 _statut.order_field = 'candidat__statut'
536 _statut.short_description = 'Statut'
537
538
539 ### Lien en lecture seule vers le candidat
540 def _candidat(self, obj):
541 return "<a href='%s'>%s</a>" \
542 % (reverse('admin:recrutement_proxycandidat_change',
543 args=(obj.candidat.id,)), obj.candidat)
544 _candidat.allow_tags = True
545 _candidat.short_description = 'Candidat'
546
547 ### Afficher commentaire
548 def _commentaire(self, obj):
549 """
550 Si l'évaluateur n'a pas encore donné de note au candidat, indiquer
551 dans le champ commentaire, Aucun au lieu de (None)
552 Sinon afficher la note.
553 """
554 if obj.commentaire is None:
555 return "Aucun"
556 return obj.commentaire
557 _commentaire.allow_tags = True
558 _commentaire.short_description = "Commentaire"
559
560 ### Afficher offre d'emploi
561 def _offre_emploi(self, obj):
562 return "<a href='%s'>%s</a>" % \
563 (reverse('admin:recrutement_proxyoffreemploi_change',
564 args=(obj.candidat.offre_emploi.id,)), obj.candidat.offre_emploi)
565 _offre_emploi.allow_tags = True
566 _offre_emploi.short_description = "Voir offre d'emploi"
567
568 def has_add_permission(self, request):
569 return False
570
571 def has_delete_permission(self, request, obj=None):
572 return False
573
574 def has_change_permission(self, request, obj=None):
575 """
576 Permettre la visualisation dans la changelist
577 mais interdire l'accès à modifier l'objet si l'évaluateur n'est pas
578 le request.user
579 """
580 user_groupes = request.user.groups.all()
581
582 if request.user.is_superuser or \
583 grp_drh_recrutement in user_groupes or \
584 grp_correspondants_rh_recrutement in user_groupes or \
585 grp_directeurs_bureau_recrutement in user_groupes or \
586 grp_administrateurs_recrutement in user_groupes:
587 is_recrutement = True
588 else:
589 is_recrutement = False
590
591 try:
592 Evaluateur.objects.get(user=request.user)
593 is_evaluateur = True
594 except:
595 is_evaluateur = False
596
597 if obj is None and (is_recrutement or is_evaluateur):
598 return True
599
600 if request.user.is_superuser is True:
601 return True
602
603 try:
604 return request.user == obj.evaluateur.user
605 except:
606 return False
607
608 ### Queryset
609 def queryset(self, request):
610 """
611 Afficher uniquement les évaluations de l'évaluateur, sauf si
612 l'utilisateur est dans les groupes suivants.
613 """
614 qs = self.model._default_manager.get_query_set().select_related('offre_emploi')
615 user_groupes = request.user.groups.all()
616
617 if grp_drh_recrutement in user_groupes or \
618 grp_correspondants_rh_recrutement in user_groupes or \
619 grp_directeurs_bureau_recrutement in user_groupes or \
620 grp_administrateurs_recrutement in user_groupes:
621 return qs
622
623 evaluateur = Evaluateur.objects.get(user=request.user)
624 candidats_evaluations = \
625 CandidatEvaluation.objects.filter(evaluateur=evaluateur,
626 candidat__statut__in=('REC', ))
627 candidats_evaluations_ids = [ce.id for ce in candidats_evaluations]
628 return qs.filter(id__in=candidats_evaluations_ids)
629
630
631 class MesCandidatEvaluationAdmin(CandidatEvaluationAdmin):
632
633 def has_change_permission(self, request, obj=None):
634 try:
635 Evaluateur.objects.get(user=request.user)
636 is_evaluateur = True
637 except:
638 is_evaluateur = False
639
640 if obj is None and is_evaluateur:
641 return True
642
643 try:
644 return request.user == obj.evaluateur.user
645 except:
646 return False
647
648 def queryset(self, request):
649 qs = self.model._default_manager.get_query_set().select_related('offre_emploi')
650 evaluateur = Evaluateur.objects.get(user=request.user)
651 candidats_evaluations = \
652 CandidatEvaluation.objects.filter(evaluateur=evaluateur,
653 candidat__statut__in=('REC', ))
654 candidats_evaluations_ids = [ce.id for ce in candidats_evaluations]
655 return qs.filter(id__in=candidats_evaluations_ids)
656
657
658 class CourrielTemplateAdmin(VersionAdmin):
659 ### Actions à afficher
660 def get_actions(self, request):
661 actions = super(CourrielTemplateAdmin, self).get_actions(request)
662 del actions['delete_selected']
663 return actions
664
665 admin.site.register(OffreEmploi, OffreEmploiAdmin)
666 admin.site.register(ProxyOffreEmploi, ProxyOffreEmploiAdmin)
667 admin.site.register(Candidat, CandidatAdmin)
668 admin.site.register(ProxyCandidat, ProxyCandidatAdmin)
669 admin.site.register(CandidatEvaluation, CandidatEvaluationAdmin)
670 admin.site.register(MesCandidatEvaluation, MesCandidatEvaluationAdmin)
671 admin.site.register(Evaluateur, EvaluateurAdmin)
672 admin.site.register(CourrielTemplate, CourrielTemplateAdmin)