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