1 # -*- encoding: utf-8 -*-
4 from django
.core
.urlresolvers
import reverse
5 from django
.http
import HttpResponseRedirect
6 from django
.contrib
import admin
7 from django
.forms
.models
import BaseInlineFormSet
8 from django
.db
.models
import Avg
9 from django
.conf
import settings
11 from reversion
.admin
import VersionAdmin
12 from datamaster_modeles
.models
import Region
, Bureau
13 from project
.rh
import models
as rh
15 from project
.dae
.utils
import get_employe_from_user
as get_emp
16 from recrutement
.models
import *
17 from recrutement
.workflow
import grp_drh_recrutement
, grp_directeurs_bureau_recrutement
, \
18 grp_administrateurs_recrutement
, \
19 grp_correspondants_rh_recrutement
21 from recrutement
.forms
import *
24 IMPLANTATIONS_CENTRALES
= [15, 19]
26 class OrderedChangeList(admin
.views
.main
.ChangeList
):
28 Surcharge pour appliquer le order_by d'un annotate
30 def get_query_set(self
):
31 qs
= super(OrderedChangeList
, self
).get_query_set()
32 qs
= qs
.order_by('-moyenne')
35 class OffreEmploiAdmin(VersionAdmin
):
36 date_hierarchy
= 'date_creation'
37 list_display
= ('nom', 'date_limite', 'region', 'statut',
38 'est_affiche', '_candidatsList', )
39 exclude
= ('actif', 'poste_nom', 'resume',)
40 list_filter
= ('statut',)
41 actions
= ['affecter_evaluateurs_offre_emploi', ]
42 form
= OffreEmploiForm
44 ### Actions à afficher
45 def get_actions(self
, request
):
46 actions
= super(OffreEmploiAdmin
, self
).get_actions(request
)
47 del actions
['delete_selected']
50 ### Affecter un évaluateurs à des offres d'emploi
51 def affecter_evaluateurs_offre_emploi(modeladmin
, obj
, candidats
):
52 selected
= obj
.POST
.getlist(admin
.ACTION_CHECKBOX_NAME
)
54 return HttpResponseRedirect(reverse('affecter_evaluateurs_offre_emploi')+
55 "?ids=%s" % (",".join(selected
)))
56 affecter_evaluateurs_offre_emploi
.short_description
= u
'Affecter évaluateur(s)'
58 ### Afficher la liste des candidats pour l'offre d'emploi
59 def _candidatsList(self
, obj
):
60 return "<a href='%s?offre_emploi__id__exact=%s'>Voir les candidats \
61 </a>" % (reverse('admin:recrutement_candidat_changelist'), obj
.id)
62 _candidatsList
.allow_tags
= True
63 _candidatsList
.short_description
= "Afficher la liste des candidats"
66 def get_form(self
, request
, obj
=None, **kwargs
):
67 form
= super(OffreEmploiAdmin
, self
).get_form(request
, obj
, **kwargs
)
68 employe
= get_emp(request
.user
)
69 user_groupes
= request
.user
.groups
.all()
73 if form
.declared_fields
.has_key('region'):
74 region_field
= form
.declared_fields
['region']
76 region_field
= form
.base_fields
['region']
78 if grp_drh_recrutement
in user_groupes
:
79 region_field
.queryset
= Region
.objects
.all()
81 region_field
.queryset
= Region
.objects
.\
82 filter(id=employe
.implantation
.region
.id)
85 if form
.declared_fields
.has_key('poste'):
86 poste_field
= form
.declared_fields
['poste']
88 poste_field
= form
.base_fields
['poste']
90 if grp_drh_recrutement
in user_groupes
:
91 poste_field
.queryset
= rh
.Poste
.objects
.all()
93 poste_field
.queryset
= rh
.Poste
.objects
.\
94 filter(implantation__region
=employe
.implantation
.region
).\
95 exclude(implantation__in
=IMPLANTATIONS_CENTRALES
)
98 if form
.declared_fields
.has_key('bureau'):
99 bureau_field
= form
.declared_fields
['bureau']
101 bureau_field
= form
.base_fields
['bureau']
103 if grp_drh_recrutement
in user_groupes
:
104 bureau_field
.queryset
= Bureau
.objects
.all()
106 bureau_field
.queryset
= Bureau
.objects
.\
107 filter(region
=employe
.implantation
.region
)
112 def queryset(self
, request
):
113 qs
= self
.model
._default_manager
.get_query_set().select_related('offre_emploi')
114 user_groupes
= request
.user
.groups
.all()
115 if grp_drh_recrutement
in user_groupes
:
118 if grp_directeurs_bureau_recrutement
in user_groupes
or \
119 grp_correspondants_rh_recrutement
in user_groupes
or \
120 grp_administrateurs_recrutement
in user_groupes
:
121 employe
= get_emp(request
.user
)
122 return qs
.filter(region
=employe
.implantation
.region
)
124 if Evaluateur
.objects
.filter(user
=request
.user
).exists():
125 evaluateur
= Evaluateur
.objects
.get(user
=request
.user
)
126 offre_ids
= [e
.candidat
.offre_emploi_id
for e
in
127 CandidatEvaluation
.objects
.select_related('candidat').filter(evaluateur
=evaluateur
)]
128 return qs
.filter(id__in
=offre_ids
)
132 ### Permission add, delete, change
133 def has_add_permission(self
, request
):
134 user_groupes
= request
.user
.groups
.all()
135 if request
.user
.is_superuser
is True or \
136 grp_drh_recrutement
in user_groupes
or \
137 grp_directeurs_bureau_recrutement
in user_groupes
or \
138 grp_administrateurs_recrutement
in user_groupes
:
142 def has_delete_permission(self
, request
, obj
=None):
143 user_groupes
= request
.user
.groups
.all()
144 if request
.user
.is_superuser
is True or \
145 grp_drh_recrutement
in user_groupes
or \
146 grp_directeurs_bureau_recrutement
in user_groupes
or \
147 grp_administrateurs_recrutement
in user_groupes
:
151 def has_change_permission(self
, request
, obj
=None):
152 user_groupes
= request
.user
.groups
.all()
153 if request
.user
.is_superuser
is True or \
154 grp_drh_recrutement
in user_groupes
or \
155 grp_directeurs_bureau_recrutement
in user_groupes
or \
156 grp_administrateurs_recrutement
in user_groupes
:
160 class ProxyOffreEmploiAdmin(OffreEmploiAdmin
):
161 list_display
= ('nom', 'date_limite', 'region', 'statut',
163 readonly_fields
= ('description', 'bureau', 'duree_affectation',
164 'renumeration', 'debut_affectation', 'lieu_affectation',
165 'nom', 'resume', 'date_limite', 'region', 'poste')
170 ('Description générale', {
171 'fields': ('description', 'date_limite', )
174 'fields': ('lieu_affectation', 'bureau', 'region', 'poste',)
177 'fields': ('debut_affectation', 'duree_affectation',
184 ### Lieu de redirection après le change
185 def response_change(self
, request
, obj
):
186 return HttpResponseRedirect(reverse\
187 ('admin:recrutement_proxyoffreemploi_changelist'))
190 def get_form(self
, request
, obj
=None, **kwargs
):
191 form
= super(OffreEmploiAdmin
, self
).get_form(request
, obj
, **kwargs
)
194 ### Permissions add, delete, change
195 def has_add_permission(self
, request
):
198 def has_delete_permission(self
, request
, obj
=None):
201 def has_change_permission(self
, request
, obj
=None):
204 class CandidatPieceInline(admin
.TabularInline
):
205 model
= CandidatPiece
206 fields
= ('candidat', 'nom', 'path',)
210 class ReadOnlyCandidatPieceInline(CandidatPieceInline
):
211 readonly_fields
= ('candidat', 'nom', 'path', )
215 class CandidatEvaluationInlineFormSet(BaseInlineFormSet
):
217 Empêche la suppression d'une évaluation pour le CandidatEvaluationInline
219 def __init__(self
, *args
, **kwargs
):
220 super(CandidatEvaluationInlineFormSet
, self
).__init__(*args
, **kwargs
)
221 self
.can_delete
= False
223 class CandidatEvaluationInline(admin
.TabularInline
):
224 model
= CandidatEvaluation
225 fields
= ('evaluateur', 'note', 'commentaire')
228 formset
= CandidatEvaluationInlineFormSet
231 def get_readonly_fields(self
, request
, obj
=None):
233 Empêche la modification des évaluations
236 return self
.readonly_fields
+('evaluateur', 'note', 'commentaire')
237 return self
.readonly_fields
239 class CandidatAdmin(VersionAdmin
):
240 search_fields
= ('nom', 'prenom' )
241 exclude
= ('actif', )
242 list_editable
= ('statut', )
243 list_display
= ('_candidat', 'offre_emploi',
244 'voir_offre_emploi', 'calculer_moyenne',
245 'afficher_candidat', '_date_creation', 'statut', )
246 list_filter
= ('offre_emploi', 'offre_emploi__region', 'statut', )
250 'fields': ('offre_emploi', )
252 ('Informations personnelles', {
253 'fields': ('prenom','nom','genre', 'nationalite',
254 'situation_famille', 'nombre_dependant',)
257 'fields': ('telephone', 'email', 'adresse', 'ville',
258 'etat_province', 'code_postal', 'pays', )
260 ('Informations professionnelles', {
261 'fields': ('niveau_diplome','employeur_actuel',
262 'poste_actuel', 'domaine_professionnel',)
265 'fields': ('statut', )
270 CandidatEvaluationInline
,
272 actions
= ['envoyer_courriel_candidats']
274 def _candidat(self
, obj
):
275 txt
= u
"%s %s (%s)" % ( obj
.nom
.upper(), obj
.prenom
,
277 txt
= textwrap
.wrap(txt
, 30)
278 return "<br/>".join(txt
)
279 _candidat
.short_description
= "Candidat"
280 _candidat
.admin_order_field
= "nom"
281 _candidat
.allow_tags
= True
283 def _date_creation(self
, obj
):
284 return obj
.date_creation
285 _date_creation
.admin_order_field
= "date_creation"
286 _date_creation
.short_description
= "Date de réception"
288 ### Actions à afficher
289 def get_actions(self
, request
):
290 actions
= super(CandidatAdmin
, self
).get_actions(request
)
291 del actions
['delete_selected']
294 ### Envoyer un courriel à des candidats
295 def envoyer_courriel_candidats(modeladmin
, obj
, candidats
):
296 selected
= obj
.POST
.getlist(admin
.ACTION_CHECKBOX_NAME
)
298 return HttpResponseRedirect(reverse('selectionner_template')+
299 "?ids=%s" % (",".join(selected
)))
300 envoyer_courriel_candidats
.short_description
= u
'Envoyer courriel'
302 ### Évaluer un candidat
303 def evaluer_candidat(self
, obj
):
304 return "<a href='%s?candidat__id__exact=%s'>Évaluer le candidat</a>" % \
305 (reverse('admin:recrutement_candidatevaluation_changelist'),
307 evaluer_candidat
.allow_tags
= True
308 evaluer_candidat
.short_description
= 'Évaluation'
310 ### Afficher un candidat
311 def afficher_candidat(self
, obj
):
312 items
= [u
"<li><a href='%s%s'>%s</li>" % \
313 (settings
.OE_PRIVE_MEDIA_URL
, pj
.path
, pj
.get_nom_display()) \
314 for pj
in obj
.pieces_jointes()]
315 html
= "<a href='%s'>Voir le candidat</a>" % \
316 (reverse('admin:recrutement_proxycandidat_change', args
=(obj
.id,)))
317 return "%s<ul>%s</ul>" % (html
, "\n".join(items
))
318 afficher_candidat
.allow_tags
= True
319 afficher_candidat
.short_description
= u
'Détails du candidat'
321 ### Voir l'offre d'emploi
322 def voir_offre_emploi(self
, obj
):
323 return "<a href='%s'>Voir l'offre d'emploi</a>" % \
324 (reverse('admin:recrutement_proxyoffreemploi_change',
325 args
=(obj
.offre_emploi
.id,)))
326 voir_offre_emploi
.allow_tags
= True
327 voir_offre_emploi
.short_description
= "Afficher l'offre d'emploi"
329 ### Calculer la moyenne des notes
330 def calculer_moyenne(self
, obj
):
331 evaluations
= CandidatEvaluation
.objects
.filter(candidat
=obj
)
333 notes
= [evaluation
.note
for evaluation
in evaluations \
334 if evaluation
.note
is not None]
337 moyenne_votes
= round(float(sum(notes
)) / len(notes
), 2)
339 moyenne_votes
= "Non disponible"
341 totales
= len(evaluations
)
344 if obj
.statut
== 'REC':
345 if totales
== faites
:
347 elif faites
> 0 and float(totales
) / float(faites
) >= 2:
354 return """<span style="color: %s;">%s (%s/%s)</span>""" % (color
, moyenne_votes
, faites
, totales
)
355 calculer_moyenne
.allow_tags
= True
356 calculer_moyenne
.short_description
= "Moyenne"
357 calculer_moyenne
.admin_order_field
= ""
359 ### Permissions add, delete, change
360 def has_add_permission(self
, request
):
361 user_groupes
= request
.user
.groups
.all()
362 if request
.user
.is_superuser
is True or \
363 grp_correspondants_rh_recrutement
in user_groupes
or \
364 grp_drh_recrutement
in user_groupes
or \
365 grp_directeurs_bureau_recrutement
in user_groupes
or \
366 grp_administrateurs_recrutement
in user_groupes
:
370 def has_delete_permission(self
, request
, obj
=None):
371 user_groupes
= request
.user
.groups
.all()
372 if request
.user
.is_superuser
is True or \
373 grp_correspondants_rh_recrutement
in user_groupes
or \
374 grp_drh_recrutement
in user_groupes
or \
375 grp_directeurs_bureau_recrutement
in user_groupes
or \
376 grp_administrateurs_recrutement
in user_groupes
:
380 def has_change_permission(self
, request
, obj
=None):
381 user_groupes
= request
.user
.groups
.all()
382 if request
.user
.is_superuser
is True or \
383 grp_correspondants_rh_recrutement
in user_groupes
or \
384 grp_drh_recrutement
in user_groupes
or \
385 grp_directeurs_bureau_recrutement
in user_groupes
or \
386 grp_administrateurs_recrutement
in user_groupes
:
390 def get_changelist(self
, request
, **kwargs
):
391 return OrderedChangeList
393 def queryset(self
, request
):
395 Spécifie un queryset limité, autrement Django exécute un
396 select_related() sans paramètre, ce qui a pour effet de charger tous
397 les objets FK, sans limite de profondeur. Dès qu'on arrive, dans les
398 modèles de Region, il existe plusieurs boucles, ce qui conduit à la
399 génération d'une requête infinie.
403 qs
= self
.model
._default_manager
.get_query_set().select_related('offre_emploi').annotate(moyenne
=Avg('evaluations__note'))
405 user_groupes
= request
.user
.groups
.all()
406 if grp_drh_recrutement
in user_groupes
:
409 if grp_directeurs_bureau_recrutement
in user_groupes
or \
410 grp_correspondants_rh_recrutement
in user_groupes
or \
411 grp_administrateurs_recrutement
in user_groupes
:
412 employe
= get_emp(request
.user
)
413 return qs
.filter(offre_emploi__region
=employe
.implantation
.region
)
415 if Evaluateur
.objects
.filter(user
=request
.user
).exists():
416 evaluateur
= Evaluateur
.objects
.get(user
=request
.user
)
417 candidat_ids
= [e
.candidat
.id for e
in
418 CandidatEvaluation
.objects
.filter(evaluateur
=evaluateur
)]
419 return qs
.filter(id__in
=candidat_ids
)
423 class ProxyCandidatAdmin(CandidatAdmin
):
425 readonly_fields
= ('statut', 'offre_emploi', 'prenom', 'nom',
426 'genre', 'nationalite', 'situation_famille',
427 'nombre_dependant', 'telephone', 'email', 'adresse',
428 'ville', 'etat_province', 'code_postal', 'pays',
429 'niveau_diplome', 'employeur_actuel', 'poste_actuel',
430 'domaine_professionnel', 'pieces_jointes',)
433 'fields': ('offre_emploi', )
435 ('Informations personnelles', {
436 'fields': ('prenom','nom','genre', 'nationalite',
437 'situation_famille', 'nombre_dependant',)
440 'fields': ('telephone', 'email', 'adresse', 'ville',
441 'etat_province', 'code_postal', 'pays', )
443 ('Informations professionnelles', {
444 'fields': ('niveau_diplome','employeur_actuel',
445 'poste_actuel', 'domaine_professionnel',)
448 inlines
= (CandidatEvaluationInline
, )
450 def has_add_permission(self
, request
):
453 def has_delete_permission(self
, request
, obj
=None):
456 def has_change_permission(self
, request
, obj
=None):
459 def get_actions(self
, request
):
462 class CandidatPieceAdmin(admin
.ModelAdmin
):
463 list_display
= ('nom', 'candidat', )
466 def queryset(self
, request
):
468 Spécifie un queryset limité, autrement Django exécute un
469 select_related() sans paramètre, ce qui a pour effet de charger tous
470 les objets FK, sans limite de profondeur. Dès qu'on arrive, dans les
471 modèles de Region, il existe plusieurs boucles, ce qui conduit à la
472 génération d'une requête infinie.
473 Affiche la liste de candidats que si le user connecté
474 possède un Evaluateur
476 qs
= self
.model
._default_manager
.get_query_set()
477 return qs
.select_related('candidat')
479 class EvaluateurAdmin(VersionAdmin
):
486 ### Actions à afficher
487 def get_actions(self
, request
):
488 actions
= super(EvaluateurAdmin
, self
).get_actions(request
)
489 del actions
['delete_selected']
492 ### Permissions add, delete, change
493 def has_add_permission(self
, request
):
494 user_groupes
= request
.user
.groups
.all()
495 if request
.user
.is_superuser
is True or \
496 grp_drh_recrutement
in user_groupes
:
500 def has_delete_permission(self
, request
, obj
=None):
501 user_groupes
= request
.user
.groups
.all()
502 if request
.user
.is_superuser
is True or \
503 grp_drh_recrutement
in user_groupes
:
507 def has_change_permission(self
, request
, obj
=None):
508 user_groupes
= request
.user
.groups
.all()
509 if request
.user
.is_superuser
is True or \
510 grp_drh_recrutement
in user_groupes
:
514 class CandidatEvaluationAdmin(admin
.ModelAdmin
):
515 search_fields
= ('candidat__nom', 'candidat__prenom' )
516 list_display
= ('_candidat', '_statut', '_offre_emploi', 'evaluateur', '_note',
518 readonly_fields
= ('candidat', 'evaluateur')
519 list_filter
= ('candidat__statut', 'candidat__offre_emploi',)
521 ('Évaluation du candidat', {
522 'fields': ('candidat', 'evaluateur', 'note', 'commentaire', )
526 def get_actions(self
, request
):
527 # on stocke l'evaluateur connecté (pas forcément la meilleure place...)
529 self
.evaluateur
= Evaluateur
.objects
.get(user
=request
.user
)
531 self
.evaluateur
= None
533 actions
= super(CandidatEvaluationAdmin
, self
).get_actions(request
)
534 del actions
['delete_selected']
538 def _note(self
, obj
):
540 Si l'évaluateur n'a pas encore donné de note au candidat, indiquer
541 un lien pour Évaluer le candidat.
542 Sinon afficher la note.
544 page
= self
.model
.__name__
.lower()
545 redirect_url
= 'admin:recrutement_%s_change' % page
548 label
= "Candidat non évalué"
552 if self
.evaluateur
== obj
.evaluateur
:
553 return "<a href='%s'>%s</a>" % (reverse(redirect_url
, args
=(obj
.id,)), label
)
556 _note
.allow_tags
= True
557 _note
.short_description
= "Note"
558 _note
.admin_order_field
= 'note'
560 def _statut(self
, obj
):
561 return obj
.candidat
.get_statut_display()
562 _statut
.order_field
= 'candidat__statut'
563 _statut
.short_description
= 'Statut'
566 ### Lien en lecture seule vers le candidat
567 def _candidat(self
, obj
):
568 return "<a href='%s'>%s</a>" \
569 % (reverse('admin:recrutement_proxycandidat_change',
570 args
=(obj
.candidat
.id,)), obj
.candidat
)
571 _candidat
.allow_tags
= True
572 _candidat
.short_description
= 'Candidat'
574 ### Afficher commentaire
575 def _commentaire(self
, obj
):
577 Si l'évaluateur n'a pas encore donné de note au candidat, indiquer
578 dans le champ commentaire, Aucun au lieu de (None)
579 Sinon afficher la note.
581 if obj
.commentaire
is None:
583 return obj
.commentaire
584 _commentaire
.allow_tags
= True
585 _commentaire
.short_description
= "Commentaire"
587 ### Afficher offre d'emploi
588 def _offre_emploi(self
, obj
):
589 return "<a href='%s'>%s</a>" % \
590 (reverse('admin:recrutement_proxyoffreemploi_change',
591 args
=(obj
.candidat
.offre_emploi
.id,)), obj
.candidat
.offre_emploi
)
592 _offre_emploi
.allow_tags
= True
593 _offre_emploi
.short_description
= "Voir offre d'emploi"
595 def has_add_permission(self
, request
):
598 def has_delete_permission(self
, request
, obj
=None):
601 def has_change_permission(self
, request
, obj
=None):
603 Permettre la visualisation dans la changelist
604 mais interdire l'accès à modifier l'objet si l'évaluateur n'est pas
607 user_groupes
= request
.user
.groups
.all()
609 if request
.user
.is_superuser
or \
610 grp_drh_recrutement
in user_groupes
or \
611 grp_correspondants_rh_recrutement
in user_groupes
or \
612 grp_directeurs_bureau_recrutement
in user_groupes
or \
613 grp_administrateurs_recrutement
in user_groupes
:
614 is_recrutement
= True
616 is_recrutement
= False
619 Evaluateur
.objects
.get(user
=request
.user
)
622 is_evaluateur
= False
624 if obj
is None and (is_recrutement
or is_evaluateur
):
627 if request
.user
.is_superuser
is True:
631 return request
.user
== obj
.evaluateur
.user
636 def queryset(self
, request
):
638 Afficher uniquement les évaluations de l'évaluateur, sauf si
639 l'utilisateur est dans les groupes suivants.
641 qs
= self
.model
._default_manager
.get_query_set().select_related('offre_emploi')
642 user_groupes
= request
.user
.groups
.all()
644 if grp_drh_recrutement
in user_groupes
or \
645 grp_correspondants_rh_recrutement
in user_groupes
or \
646 grp_directeurs_bureau_recrutement
in user_groupes
or \
647 grp_administrateurs_recrutement
in user_groupes
:
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
)
658 class MesCandidatEvaluationAdmin(CandidatEvaluationAdmin
):
660 def has_change_permission(self
, request
, obj
=None):
662 Evaluateur
.objects
.get(user
=request
.user
)
665 is_evaluateur
= False
667 if obj
is None and is_evaluateur
:
671 return request
.user
== obj
.evaluateur
.user
675 def queryset(self
, request
):
676 qs
= self
.model
._default_manager
.get_query_set().select_related('offre_emploi')
677 evaluateur
= Evaluateur
.objects
.get(user
=request
.user
)
678 candidats_evaluations
= \
679 CandidatEvaluation
.objects
.filter(evaluateur
=evaluateur
,
680 candidat__statut__in
=('REC', ))
681 candidats_evaluations_ids
= [ce
.id for ce
in candidats_evaluations
]
682 return qs
.filter(id__in
=candidats_evaluations_ids
)
685 class CourrielTemplateAdmin(VersionAdmin
):
686 ### Actions à afficher
687 def get_actions(self
, request
):
688 actions
= super(CourrielTemplateAdmin
, self
).get_actions(request
)
689 del actions
['delete_selected']
692 admin
.site
.register(OffreEmploi
, OffreEmploiAdmin
)
693 admin
.site
.register(ProxyOffreEmploi
, ProxyOffreEmploiAdmin
)
694 admin
.site
.register(Candidat
, CandidatAdmin
)
695 admin
.site
.register(ProxyCandidat
, ProxyCandidatAdmin
)
696 admin
.site
.register(CandidatEvaluation
, CandidatEvaluationAdmin
)
697 admin
.site
.register(MesCandidatEvaluation
, MesCandidatEvaluationAdmin
)
698 admin
.site
.register(Evaluateur
, EvaluateurAdmin
)
699 admin
.site
.register(CourrielTemplate
, CourrielTemplateAdmin
)