Merge branch 'master' into qbe
authorOlivier Larchevêque <olivier.larcheveque@auf.org>
Thu, 26 Jul 2012 20:29:23 +0000 (16:29 -0400)
committerOlivier Larchevêque <olivier.larcheveque@auf.org>
Thu, 26 Jul 2012 20:29:23 +0000 (16:29 -0400)
16 files changed:
project/assets/js/dae-poste.js
project/dae/decorators.py
project/dae/forms.py
project/dae/models.py
project/dae/test/__init__.py [new file with mode: 0644]
project/dae/test/common.py [new file with mode: 0644]
project/dae/test/poste.py [new file with mode: 0644]
project/dae/tests.py
project/recrutement/admin.py
project/recrutement/admin.py_ [new file with mode: 0644]
project/recrutement/test/candidat.py [new file with mode: 0644]
project/recrutement/test/common.py
project/recrutement/tests.py
project/rh/test/common.py
project/rh/test/rapport.py
runtests [new file with mode: 0755]

index ea7b841..56e8574 100644 (file)
@@ -189,7 +189,10 @@ $(document).ready(function() {
     sélectionnée.
     Lorsque l'implantation est changée, on ajuste les valeurs de points en fonction de cette sélection */
     var implantation_id = $("#id_implantation").val();
-    if (implantation_id) {
+    var null_vp_min = $("#valeur_point_min").val() == '';
+    var null_vp_max = $("#valeur_point_max").val() == '';
+
+    if (implantation_id && null_vp_min && null_vp_max) {
         charger_postes(implantation_id);
         charger_valeurs_point(implantation_id);
     }
index 04f05cb..bb20b84 100644 (file)
@@ -53,13 +53,15 @@ def poste_dans_ma_region_ou_service(fn):
             return fn(request, *args, **kwargs)
 
         # Rechercher dans la demande, la région ou le service associé
-        try:
+        if '-' in key:
             source, id = key.split('-')
             if source == 'dae':
                 Poste = dae.Poste
-            if source == 'rh':
+            elif source == 'rh':
                 Poste = rh.Poste
-        except:
+            else:
+                raise Exception("source inconnue : dae ou rh")
+        else:
             id = key
             Poste = dae.Poste
         postes = Poste.objects.ma_region_ou_service(user).filter(id=id)
index f06683d..28de814 100644 (file)
@@ -178,9 +178,12 @@ def label_poste_display(poste):
 
     nom = poste.nom
     
-    label = u"%s %s - %s [%s]" % (
-        annee, nom, poste.type_poste.categorie_emploi.nom, poste.id
-    )
+    try:
+        label = u"%s %s - %s [%s]" % (
+            annee, nom, poste.type_poste.categorie_emploi.nom, poste.id
+        )
+    except:
+        label = unicode(poste)
     return label
 
 
index a87236f..29a87a1 100644 (file)
@@ -359,11 +359,14 @@ class Poste(PosteWorkflow, rh.Poste_):
         Cette fonction est consommatrice SQL car elle cherche les dossiers
         qui ont été liés à celui-ci.
         """
-        data = (
-            self.implantation,
-            self.type_poste.nom,
-            self.nom,
-        )
+        try:
+            data = (
+                self.implantation,
+                self.type_poste.nom,
+                self.nom,
+            )
+        except:
+            return self.nom
         return u'%s - %s (%s)' % data
 
 reversion.register(Poste, format='xml', follow=[
diff --git a/project/dae/test/__init__.py b/project/dae/test/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/project/dae/test/common.py b/project/dae/test/common.py
new file mode 100644 (file)
index 0000000..887853b
--- /dev/null
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+from project.rh.test.common import RhTest
+
+class DaeTest(RhTest):
+
+    def setUp(self):
+        super(DaeTest, self).setUp()
diff --git a/project/dae/test/poste.py b/project/dae/test/poste.py
new file mode 100644 (file)
index 0000000..0a38b13
--- /dev/null
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+
+from decimal import Decimal
+from django.core.urlresolvers import reverse
+from django.contrib.auth.models import User
+from project.dae import models as dae
+from project.dae.test.common import DaeTest
+from project.dae.forms import PosteForm
+
+class PosteAddTest(DaeTest):
+    """
+    Test l'ajout d'un poste
+    """
+    url = reverse('admin:rh_poste_add')
+
+    def setUp(self):
+        super(PosteAddTest, self).setUp()
+        key = "rh-%s" % self.poste_cnf_ngaoundere.id
+        self.url = reverse('poste', kwargs={'key': key})
+        self.post = {
+                'nom' : 'nom',
+                'regime_travail_nb_heure_semaine' : '35',
+                'indemn_expat_min' : '110',
+                'indemn_expat_max' : '210',
+                'type_intervention': dae.POSTE_ACTION[0][0],
+                'service' : self.srv_info.id,
+                'implantation' : self.IMPLANTATION_ACGL_CNF_NGAOUNDERE.id,
+                'indemn_fct_min' : '120',
+                'indemn_fct_max' : '220',
+                'appel' : dae.POSTE_APPEL_CHOICES[0][0],
+                'regime_travail': '100',
+                'type_poste': self.type_poste_tech.id,
+                'responsable' : self.poste_cnf_ngaoundere.id,
+                'devise_min' : self.devise_cad.id,
+                'devise_max' : self.devise_cad.id,
+                'classement_min' : self.classement_p_5_0.id,
+                'classement_max' : self.classement_p_5_10.id,
+                'valeur_point_min' : self.vp_cad_ngaoundere_2012.id,
+                'valeur_point_max' : self.vp_cad_ngaoundere_2012.id,
+                'salaire_min' : '130',
+                'salaire_max' : '230',
+                'autre_min' : '150',
+                'autre_max' : '250',
+                'charges_patronales_min' : '160',
+                'charges_patronales_max' : '260',
+
+                # pour tester la vue
+                'dae_financements-TOTAL_FORMS' : 0,
+                'dae_financements-INITIAL_FORMS': 0,
+                'dae_financements-MAX_NUM_FORMS': '',
+                'dae_pieces-TOTAL_FORMS' : 0,
+                'dae_pieces-INITIAL_FORMS': 0,
+                'dae_pieces-MAX_NUM_FORMS': '',
+                'dae_comparaisons_internes-TOTAL_FORMS' : 0,
+                'dae_comparaisons_internes-INITIAL_FORMS': 0,
+                'dae_comparaisons_internes-MAX_NUM_FORMS': '',
+
+                }
+        self.expect = {
+                'nom' : 'nom',
+                'regime_travail_nb_heure_semaine' : Decimal('35'),
+                'indemn_expat_min' : Decimal('110'),
+                'indemn_expat_max' : Decimal('210'),
+                'type_intervention': dae.POSTE_ACTION[0][0],
+                'service' : self.srv_info,
+                'implantation' : self.IMPLANTATION_ACGL_CNF_NGAOUNDERE,
+                'indemn_fct_min' : Decimal('120'),
+                'indemn_fct_max' : Decimal('220'),
+                'appel' : dae.POSTE_APPEL_CHOICES[0][0],
+                'regime_travail': Decimal('100'),
+                'type_poste': self.type_poste_tech,
+                'responsable' : self.poste_cnf_ngaoundere,
+                'devise_min' : self.devise_cad,
+                'devise_max' : self.devise_cad,
+                'classement_min' : self.classement_p_5_0,
+                'classement_max' : self.classement_p_5_10,
+                'valeur_point_min' : self.vp_cad_ngaoundere_2012,
+                'valeur_point_max' : self.vp_cad_ngaoundere_2012,
+                'salaire_min' : Decimal('130'),
+                'salaire_max' : Decimal('230'),
+                'autre_min' : Decimal('150'),
+                'autre_max' : Decimal('250'),
+                'charges_patronales_min' : Decimal('160'),
+                'charges_patronales_max' : Decimal('260'),
+                }
+
+    def test_poste_form(self):
+        """
+        Test PosteForm sans instance
+        """
+
+        class Request:
+            user = None
+
+        request = Request()
+        request.user = User.objects.get(email='0@test.auf')
+        form = PosteForm(self.post, request=request)
+        form.is_valid()
+        self.assertTrue(form.is_valid())
+
+        form.save()
+
+        poste = dae.Poste.objects.get(nom=self.post['nom'])
+        for k, expect_value in self.expect.items():
+            self.assertEqual(getattr(poste, k), expect_value)
+
+    def test_page_creation_poste(self):
+        """
+        Teste la page de création d'un poste avec un compte DRH
+        """
+        self._test_drh()
+        resp = self.client.post(self.url, self.post)
+        self.assertEqual(resp.status_code, 302)
+
+        poste = dae.Poste.objects.get(nom=self.post['nom'])
+        for k, expect_value in self.expect.items():
+            self.assertEqual(getattr(poste, k), expect_value)
+
+
+    def test_anonyme(self):
+        """
+        Un anonyme ne peut pas créer d'poste
+        """
+        self._test_anonyme()
+        self._test_acces_ko(self.url)
+
+    def test_correspondant_rh(self):
+        """
+        Un correspodant RH peut ajouter un poste
+        """
+        self._test_correspondant_rh()
+        self._test_acces_ok(self.url)
+
+    def test_administrateur_regional(self):
+        """
+        Un administrateur peut ajouter un poste
+        """
+        self._test_administrateur_regional()
+        self._test_acces_ok(self.url)
+
+    def test_directeur_bureau(self):
+        """
+        Un directeur de bureau peut ajouter un poste
+        """
+        self._test_directeur_bureau()
+        self._test_acces_ok(self.url)
+
+    def test_drh(self):
+        """
+        Un DRH peut ajouter un poste
+        """
+        self._test_drh()
+        self._test_acces_ok(self.url)
+
+    def test_drh2(self):
+        """
+        Un DRH (2ieme niveau) peut ajouter un poste
+        """
+        self._test_drh2()
+        self._test_acces_ok(self.url)
+
+    def _test_grp_accior(self):
+        """
+        Un membre de l'ACCIOR ne peut pas ajouter un poste
+        """
+        self._test_grp_accior()
+        self._test_acces_ko(self.url)
+
+    def _test_grp_abf(self):
+        """
+        Un membre de l'ABF ne peut pas ajouter un poste
+        """
+        self._test_grp_abf(self)
+        self._test_acces_ko(self.url)
+
+    def _test_grp_haute_direction(self):
+        """
+        Un membre de la haute direction ne peut pas ajouter un poste
+        """
+        self._test_grp_haute_direction()
+        self._test_acces_ko(self.url)
+
+    def _test_grp_service_utilisateurs(self):
+        """
+        Un membre du groupe service utilisateur ne peut pas ajouter un poste
+        """
+        self._test_grp_service_utilisateurs()
+        self._test_acces_ko(self.url)
+
+
index 2247054..fc582f0 100644 (file)
@@ -1,23 +1 @@
-"""
-This file demonstrates two different styles of tests (one doctest and one
-unittest). These will both pass when you run "manage.py test".
-
-Replace these with more appropriate tests for your application.
-"""
-
-from django.test import TestCase
-
-class SimpleTest(TestCase):
-    def test_basic_addition(self):
-        """
-        Tests that 1 + 1 always equals 2.
-        """
-        self.failUnlessEqual(1 + 1, 2)
-
-__test__ = {"doctest": """
-Another way to test that 1 + 1 is equal to 2.
-
->>> 1 + 1 == 2
-True
-"""}
-
+from project.dae.test.poste import *
index 8db7f58..4bc35f3 100644 (file)
@@ -389,7 +389,7 @@ class CandidatAdminMixin(BaseAdmin):
 
     ### Actions à afficher
     def get_actions(self, request):
-        actions = super(CandidatAdmin, self).get_actions(request)
+        actions = super(CandidatAdminMixin, self).get_actions(request)
         del actions['delete_selected']
         return actions
 
diff --git a/project/recrutement/admin.py_ b/project/recrutement/admin.py_
new file mode 100644 (file)
index 0000000..4bc35f3
--- /dev/null
@@ -0,0 +1,846 @@
+# -*- encoding: utf-8 -*-
+
+import textwrap
+
+from auf.django.emploi.models import OffreEmploi, Candidat, CandidatPiece
+from auf.django.references.models import Region, Bureau
+from django.conf import settings
+from django.contrib import admin
+from django.core.urlresolvers import reverse
+from django.db.models import Avg
+from django.forms.models import BaseInlineFormSet
+from django.http import HttpResponseRedirect
+from django.shortcuts import redirect
+from reversion.admin import VersionAdmin
+
+from project import groups
+from project.rh import models as rh
+from project.recrutement.forms import OffreEmploiForm
+from project.recrutement.models import \
+        Evaluateur, CandidatEvaluation, \
+        ProxyOffreEmploi, ProxyCandidat, MesCandidatEvaluation, \
+        CourrielTemplate
+
+### CONSTANTES
+IMPLANTATIONS_CENTRALES = [15, 19]
+
+
+class BaseAdmin(admin.ModelAdmin):
+
+    class Media:
+        css = {'screen': (
+            'css/admin_custom.css',
+            'jquery-autocomplete/jquery.autocomplete.css',
+        )}
+        js = (
+            'https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js',
+            'jquery-autocomplete/jquery.autocomplete.min.js',
+        )
+
+
+class OrderedChangeList(admin.views.main.ChangeList):
+    """
+    Surcharge pour appliquer le order_by d'un annotate
+    """
+    def get_query_set(self):
+        qs = super(OrderedChangeList, self).get_query_set()
+        qs = qs.order_by('-moyenne')
+        return qs
+
+
+class OffreEmploiAdminMixin(BaseAdmin):
+    date_hierarchy = 'date_creation'
+    list_display = (
+        'nom', 'date_limite', 'region',  'statut', 'est_affiche',
+        '_candidatsList'
+    )
+    exclude = ('actif', 'poste_nom', 'resume',)
+    list_filter = ('statut',)
+    actions = ['affecter_evaluateurs_offre_emploi', ]
+    form = OffreEmploiForm
+
+    ### Actions à afficher
+    def get_actions(self, request):
+        actions = super(OffreEmploiAdminMixin, self).get_actions(request)
+        del actions['delete_selected']
+        return actions
+
+    ### Affecter un évaluateurs à des offres d'emploi
+    def affecter_evaluateurs_offre_emploi(modeladmin, obj, candidats):
+        selected = obj.POST.getlist(admin.ACTION_CHECKBOX_NAME)
+
+        return HttpResponseRedirect(
+            reverse('affecter_evaluateurs_offre_emploi') +
+            "?ids=%s" % (",".join(selected))
+        )
+
+    affecter_evaluateurs_offre_emploi.short_description = \
+            u'Affecter évaluateur(s)'
+
+    ### Afficher la liste des candidats pour l'offre d'emploi
+    def _candidatsList(self, obj):
+        return "<a href='%s?offre_emploi__id__exact=%s'>Voir les candidats \
+            </a>" % (reverse('admin:recrutement_proxycandidat_changelist'), obj.id)
+    _candidatsList.allow_tags = True
+    _candidatsList.short_description = "Afficher la liste des candidats"
+
+    ### Formulaire
+    def get_form(self, request, obj=None, **kwargs):
+        form = super(OffreEmploiAdminMixin, self).get_form(request, obj, **kwargs)
+        employe = groups.get_employe_from_user(request.user)
+        user_groupes = [g.name for g in request.user.groups.all()]
+
+        # Region
+        
+        if 'region' in form.declared_fields:
+            region_field = form.declared_fields['region']
+            read_only = False
+        elif 'region' in form.base_fields:
+            region_field = form.base_fields['region']
+            read_only = False
+        else:
+            read_only = True
+
+        if not read_only:
+            if groups.DRH_NIVEAU_1 in user_groupes or \
+               groups.DRH_NIVEAU_2 in user_groupes or \
+               groups.HAUTE_DIRECTION in user_groupes:
+                region_field.queryset = Region.objects.all()
+            else:
+                region_field.queryset = Region.objects.\
+                                    filter(id=employe.implantation.region.id)
+
+        # Poste
+        if 'poste' in form.declared_fields:
+            poste_field = form.declared_fields['poste']
+            read_only = False
+        elif 'poste' in form.base_fields:
+            poste_field = form.base_fields['poste']
+            read_only = False
+        else:
+            read_only = True
+
+        if not read_only:
+            if groups.DRH_NIVEAU_1 in user_groupes or \
+               groups.DRH_NIVEAU_2 in user_groupes or \
+               groups.HAUTE_DIRECTION in user_groupes:
+                poste_field.queryset = rh.Poste.objects.all()
+            else:
+                poste_field.queryset = rh.Poste.objects.\
+                        filter(implantation__region=employe.implantation.region).\
+                        exclude(implantation__in=IMPLANTATIONS_CENTRALES)
+
+        # Bureau
+        if 'bureau' in form.declared_fields:
+            bureau_field = form.declared_fields['bureau']
+            read_only = False
+        elif 'bureau' in form.base_fields:
+            bureau_field = form.base_fields['bureau']
+            read_only = False
+        else:
+            read_only = True
+        if not read_only:
+            if groups.DRH_NIVEAU_1 in user_groupes or \
+                groups.DRH_NIVEAU_2 in user_groupes or \
+                groups.HAUTE_DIRECTION in user_groupes:
+                bureau_field.queryset = Bureau.objects.all()
+            else:
+                bureau_field.queryset = \
+                        Bureau.objects.filter(region=employe.implantation.region)
+
+        return form
+
+    ### Queryset
+
+    def queryset(self, request):
+        qs = self.model._default_manager.get_query_set() \
+                .select_related('offre_emploi')
+        user_groupes = [g.name for g in request.user.groups.all()]
+        if groups.DRH_NIVEAU_1 in user_groupes or \
+            groups.DRH_NIVEAU_2 in user_groupes or \
+            groups.HAUTE_DIRECTION in user_groupes:
+            return qs
+
+        if groups.DIRECTEUR_DE_BUREAU in user_groupes or \
+            groups.CORRESPONDANT_RH in user_groupes or \
+            groups.ADMINISTRATEURS in user_groupes:
+            employe = groups.get_employe_from_user(request.user)
+            return qs.filter(region=employe.implantation.region)
+
+        if  Evaluateur.objects.filter(user=request.user).exists():
+            evaluateur = Evaluateur.objects.get(user=request.user)
+            offre_ids = [
+                e.candidat.offre_emploi_id
+                for e in CandidatEvaluation.objects
+                .select_related('candidat')
+                .filter(evaluateur=evaluateur)
+            ]
+            return qs.filter(id__in=offre_ids)
+
+        return qs.none()
+
+    ### Permission add, delete, change
+    def has_add_permission(self, request):
+        user_groupes = [g.name for g in request.user.groups.all()]
+        if request.user.is_superuser is True or \
+            groups.DRH_NIVEAU_1 in user_groupes or \
+            groups.DRH_NIVEAU_2 in user_groupes or \
+            groups.DIRECTEUR_DE_BUREAU in user_groupes or \
+            groups.ADMINISTRATEURS in user_groupes or \
+            groups.HAUTE_DIRECTION in user_groupes:
+            return True
+        return False
+
+    def has_delete_permission(self, request, obj=None):
+        user_groupes = [g.name for g in request.user.groups.all()]
+        if request.user.is_superuser is True or \
+            groups.DRH_NIVEAU_1 in user_groupes or \
+            groups.DRH_NIVEAU_2 in user_groupes or \
+            groups.HAUTE_DIRECTION in user_groupes:
+            return True
+
+        if obj is not None:
+            employe = groups.get_employe_from_user(request.user)
+            if (groups.DIRECTEUR_DE_BUREAU in user_groupes or \
+                groups.ADMINISTRATEURS in user_groupes) and ( 
+                employe.implantation.region == obj.lieu_affectation.region):
+                return True
+
+        return False
+
+    def has_change_permission(self, request, obj=None):
+        user_groupes = [g.name for g in request.user.groups.all()]
+        if request.user.is_superuser is True or \
+            groups.DRH_NIVEAU_1 in user_groupes or \
+            groups.DRH_NIVEAU_2 in user_groupes or \
+            groups.HAUTE_DIRECTION in user_groupes:
+            return True
+
+        if obj is not None:
+            employe = groups.get_employe_from_user(request.user)
+            if (groups.DIRECTEUR_DE_BUREAU in user_groupes or \
+                groups.ADMINISTRATEURS in user_groupes) and ( 
+                employe.implantation.region == obj.lieu_affectation.region):
+                return True
+        else:
+            if  groups.DIRECTEUR_DE_BUREAU in user_groupes or \
+                    groups.ADMINISTRATEURS in user_groupes:
+                return True
+
+
+        return False
+
+
+class OffreEmploiAdmin(VersionAdmin, OffreEmploiAdminMixin):
+    pass
+
+
+class ProxyOffreEmploiAdmin(OffreEmploiAdminMixin):
+    list_display = (
+        'nom', 'date_limite', 'region', 'statut', 'est_affiche'
+    )
+    readonly_fields = (
+        'description', 'bureau', 'duree_affectation', 'renumeration',
+        'debut_affectation', 'lieu_affectation', 'nom', 'resume',
+        'date_limite', 'region', 'poste'
+    )
+    fieldsets = (
+        ('Nom', {
+            'fields': ('nom',)
+        }),
+        ('Description générale', {
+            'fields': ('description', 'date_limite',)
+        }),
+        ('Coordonnées', {
+            'fields': ('lieu_affectation', 'bureau', 'region', 'poste',)
+        }),
+        ('Autre', {
+            'fields': (
+                'debut_affectation', 'duree_affectation', 'renumeration',
+            )
+        }),
+    )
+    inlines = []
+
+    ### Lieu de redirection après le change
+    def response_change(self, request, obj):
+        return redirect('admin:recrutement_proxyoffreemploi_changelist')
+
+    ### Formulaire
+    def get_form(self, request, obj=None, **kwargs):
+        form = super(ProxyOffreEmploiAdmin, self).get_form(request, obj, **kwargs)
+        return form
+
+    ### Permissions add, delete, change
+    def has_add_permission(self, request):
+        return False
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+    def has_change_permission(self, request, obj=None):
+        user_groupes = [g.name for g in request.user.groups.all()]
+        if request.user.is_superuser is True or \
+            groups.CORRESPONDANT_RH in user_groupes or \
+            groups.DRH_NIVEAU_1 in user_groupes or \
+            groups.DRH_NIVEAU_2 in user_groupes or \
+            groups.DIRECTEUR_DE_BUREAU in user_groupes or \
+            groups.ADMINISTRATEURS in user_groupes or \
+            groups.HAUTE_DIRECTION in user_groupes:
+            return True
+
+        if obj is not None:
+            return True
+
+        return False
+
+
+class CandidatPieceInline(admin.TabularInline):
+    model = CandidatPiece
+    fields = ('candidat', 'nom', 'path',)
+    extra = 1
+    max_num = 3
+
+
+class ReadOnlyCandidatPieceInline(CandidatPieceInline):
+    readonly_fields = ('candidat', 'nom', 'path', )
+    cand_delete = False
+
+
+class CandidatEvaluationInlineFormSet(BaseInlineFormSet):
+    """
+    Empêche la suppression d'une évaluation pour le CandidatEvaluationInline
+    """
+    def __init__(self, *args, **kwargs):
+        super(CandidatEvaluationInlineFormSet, self).__init__(*args, **kwargs)
+        self.can_delete = False
+
+
+class CandidatEvaluationInline(admin.TabularInline):
+    model = CandidatEvaluation
+    fields = ('evaluateur', 'note', 'commentaire')
+    max_num = 0
+    extra = 0
+    formset = CandidatEvaluationInlineFormSet
+
+    ### Fields readonly
+    def get_readonly_fields(self, request, obj=None):
+        """
+        Empêche la modification des évaluations
+        """
+        if obj:
+            return self.readonly_fields + ('evaluateur', 'note', 'commentaire')
+        return self.readonly_fields
+
+
+class CandidatAdminMixin(BaseAdmin):
+    search_fields = ('nom', 'prenom')
+    exclude = ('actif', )
+    list_editable = ('statut', )
+    list_display = ('_candidat', 'offre_emploi',
+                    'voir_offre_emploi', 'calculer_moyenne',
+                    'afficher_candidat', '_date_creation', 'statut', )
+    list_filter = ('offre_emploi', 'offre_emploi__region', 'statut', )
+
+    fieldsets = (
+        ("Offre d'emploi", {
+            'fields': ('offre_emploi', )
+        }),
+        ('Informations personnelles', {
+            'fields': (
+                'prenom', 'nom', 'genre', 'nationalite',
+                'situation_famille', 'nombre_dependant'
+            )
+        }),
+        ('Coordonnées', {
+            'fields': (
+                'telephone', 'email', 'adresse', 'ville', 'etat_province',
+                'code_postal', 'pays'
+            )
+        }),
+        ('Informations professionnelles', {
+            'fields': (
+                'niveau_diplome', 'employeur_actuel', 'poste_actuel',
+                'domaine_professionnel'
+            )
+        }),
+        ('Traitement', {
+            'fields': ('statut', )
+        }),
+    )
+    inlines = [
+        CandidatPieceInline,
+        CandidatEvaluationInline,
+    ]
+    actions = ['envoyer_courriel_candidats']
+
+    def _candidat(self, obj):
+        txt = u"%s %s (%s)" % (obj.nom.upper(), obj.prenom, obj.genre)
+        txt = textwrap.wrap(txt, 30)
+        return "<br/>".join(txt)
+    _candidat.short_description = "Candidat"
+    _candidat.admin_order_field = "nom"
+    _candidat.allow_tags = True
+
+    def _date_creation(self, obj):
+        return obj.date_creation
+    _date_creation.admin_order_field = "date_creation"
+    _date_creation.short_description = "Date de réception"
+
+    ### Actions à afficher
+    def get_actions(self, request):
+        actions = super(CandidatAdminMixin, self).get_actions(request)
+        del actions['delete_selected']
+        return actions
+
+    ### Envoyer un courriel à des candidats
+    def envoyer_courriel_candidats(modeladmin, obj, candidats):
+        selected = obj.POST.getlist(admin.ACTION_CHECKBOX_NAME)
+
+        return HttpResponseRedirect(
+            reverse('selectionner_template') + "?ids=%s" % (",".join(selected))
+        )
+    envoyer_courriel_candidats.short_description = u'Envoyer courriel'
+
+    ### Évaluer un candidat
+    def evaluer_candidat(self, obj):
+        return "<a href='%s?candidat__id__exact=%s'>" \
+                "Évaluer le candidat</a>" % (
+                    reverse('admin:recrutement_candidatevaluation_changelist'),
+                    obj.id
+                )
+    evaluer_candidat.allow_tags = True
+    evaluer_candidat.short_description = 'Évaluation'
+
+    ### Afficher un candidat
+    def afficher_candidat(self, obj):
+        items = [u"<li><a href='%s%s'>%s</li>" % \
+                (settings.OE_PRIVE_MEDIA_URL, pj.path, pj.get_nom_display()) \
+                for pj in obj.pieces_jointes()]
+        html = "<a href='%s'>Voir le candidat</a>" % (
+            reverse('admin:recrutement_proxycandidat_change', args=(obj.id,))
+        )
+        return "%s<ul>%s</ul>" % (html, "\n".join(items))
+    afficher_candidat.allow_tags = True
+    afficher_candidat.short_description = u'Détails du candidat'
+
+    ### Voir l'offre d'emploi
+    def voir_offre_emploi(self, obj):
+        return "<a href='%s'>Voir l'offre d'emploi</a>" % (reverse(
+            'admin:recrutement_proxyoffreemploi_change',
+            args=(obj.offre_emploi.id,)
+        ))
+    voir_offre_emploi.allow_tags = True
+    voir_offre_emploi.short_description = "Afficher l'offre d'emploi"
+
+    ### Calculer la moyenne des notes
+    def calculer_moyenne(self, obj):
+        evaluations = CandidatEvaluation.objects.filter(candidat=obj)
+
+        notes = [evaluation.note for evaluation in evaluations \
+                    if evaluation.note is not None]
+
+        if len(notes) > 0:
+            moyenne_votes = round(float(sum(notes)) / len(notes), 2)
+        else:
+            moyenne_votes = "Non disponible"
+
+        totales = len(evaluations)
+        faites = len(notes)
+
+        if obj.statut == 'REC':
+            if totales == faites:
+                color = "green"
+            elif faites > 0 and float(totales) / float(faites) >= 2:
+                color = "orange"
+            else:
+                color = "red"
+        else:
+            color = "black"
+
+        return """<span style="color: %s;">%s (%s/%s)</span>""" % (
+            color, moyenne_votes, faites, totales
+        )
+    calculer_moyenne.allow_tags = True
+    calculer_moyenne.short_description = "Moyenne"
+    calculer_moyenne.admin_order_field = ""
+
+    ### Permissions add, delete, change
+    def has_add_permission(self, request):
+        user_groupes = [g.name for g in request.user.groups.all()]
+        if request.user.is_superuser is True or \
+            groups.CORRESPONDANT_RH in user_groupes or \
+            groups.DRH_NIVEAU_1 in user_groupes or \
+            groups.DRH_NIVEAU_2 in user_groupes or \
+            groups.DIRECTEUR_DE_BUREAU in user_groupes or \
+            groups.ADMINISTRATEURS in user_groupes or \
+            groups.HAUTE_DIRECTION in user_groupes:
+            return True
+        return False
+
+    def has_delete_permission(self, request, obj=None):
+        user_groupes = [g.name for g in request.user.groups.all()]
+        if request.user.is_superuser is True or \
+            groups.CORRESPONDANT_RH in user_groupes or \
+            groups.DRH_NIVEAU_1 in user_groupes or \
+            groups.DRH_NIVEAU_2 in user_groupes or \
+            groups.DIRECTEUR_DE_BUREAU in user_groupes or \
+            groups.ADMINISTRATEURS in user_groupes or \
+            groups.HAUTE_DIRECTION in user_groupes:
+            return True
+        return False
+
+    def has_change_permission(self, request, obj=None):
+        user_groupes = [g.name for g in request.user.groups.all()]
+        if request.user.is_superuser is True or \
+            groups.CORRESPONDANT_RH in user_groupes or \
+            groups.DRH_NIVEAU_1 in user_groupes or \
+            groups.DRH_NIVEAU_2 in user_groupes or \
+            groups.DIRECTEUR_DE_BUREAU in user_groupes or \
+            groups.ADMINISTRATEURS in user_groupes or \
+            groups.HAUTE_DIRECTION in user_groupes:
+            return True
+        return False
+
+    def get_changelist(self, request, **kwargs):
+        return OrderedChangeList
+
+    def queryset(self, request):
+        """
+        Spécifie un queryset limité, autrement Django exécute un
+        select_related() sans paramètre, ce qui a pour effet de charger tous
+        les objets FK, sans limite de profondeur. Dès qu'on arrive, dans les
+        modèles de Region, il existe plusieurs boucles, ce qui conduit à la
+        génération d'une requête infinie.
+        """
+        qs = self.model._default_manager.get_query_set() \
+                .select_related('offre_emploi') \
+                .annotate(moyenne=Avg('evaluations__note'))
+
+        user_groupes = [g.name for g in request.user.groups.all()]
+        if groups.DRH_NIVEAU_1 in user_groupes or \
+            groups.DRH_NIVEAU_2 in user_groupes or \
+            groups.HAUTE_DIRECTION in user_groupes:
+            return qs
+
+        if groups.DIRECTEUR_DE_BUREAU in user_groupes or \
+            groups.CORRESPONDANT_RH in user_groupes or \
+            groups.ADMINISTRATEURS in user_groupes:
+            employe = groups.get_employe_from_user(request.user)
+            return qs.filter(offre_emploi__region=employe.implantation.region)
+
+        if  Evaluateur.objects.filter(user=request.user).exists():
+            evaluateur = Evaluateur.objects.get(user=request.user)
+            candidat_ids = [e.candidat.id for e in
+                    CandidatEvaluation.objects.filter(evaluateur=evaluateur)]
+            return qs.filter(id__in=candidat_ids)
+        return qs.none()
+
+
+class CandidatAdmin(VersionAdmin, CandidatAdminMixin):
+    pass
+
+
+class ProxyCandidatAdmin(CandidatAdminMixin):
+    list_editable = ()
+    readonly_fields = (
+        'statut', 'offre_emploi', 'prenom', 'nom', 'genre', 'nationalite',
+        'situation_famille', 'nombre_dependant', 'telephone', 'email',
+        'adresse', 'ville', 'etat_province', 'code_postal', 'pays',
+        'niveau_diplome', 'employeur_actuel', 'poste_actuel',
+        'domaine_professionnel', 'pieces_jointes'
+    )
+    fieldsets = (
+        ("Offre d'emploi", {
+            'fields': ('offre_emploi', )
+        }),
+        ('Informations personnelles', {
+            'fields': (
+                'prenom', 'nom', 'genre', 'nationalite', 'situation_famille',
+                'nombre_dependant'
+            )
+        }),
+        ('Coordonnées', {
+            'fields': (
+                'telephone', 'email', 'adresse', 'ville', 'etat_province',
+                'code_postal', 'pays'
+            )
+        }),
+        ('Informations professionnelles', {
+            'fields': (
+                'niveau_diplome', 'employeur_actuel', 'poste_actuel',
+                'domaine_professionnel'
+            )
+        }),
+    )
+    inlines = (CandidatEvaluationInline, )
+
+    def has_add_permission(self, request):
+        return False
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+    def has_change_permission(self, request, obj=None):
+        user_groupes = [g.name for g in request.user.groups.all()]
+        if request.user.is_superuser is True or \
+            groups.CORRESPONDANT_RH in user_groupes or \
+            groups.DRH_NIVEAU_1 in user_groupes or \
+            groups.DRH_NIVEAU_2 in user_groupes or \
+            groups.DIRECTEUR_DE_BUREAU in user_groupes or \
+            groups.ADMINISTRATEURS in user_groupes or \
+            groups.HAUTE_DIRECTION in user_groupes:
+            return True
+
+        if obj is not None:
+            evaluateur = Evaluateur.objects.get(user=request.user)
+            for e in obj.evaluations.all():
+                if e.evaluateur == evaluateur:
+                    return True
+
+        return False
+
+    def get_actions(self, request):
+        return None
+
+
+class CandidatPieceAdmin(admin.ModelAdmin):
+    list_display = ('nom', 'candidat', )
+
+    ### Queryset
+    def queryset(self, request):
+        """
+        Spécifie un queryset limité, autrement Django exécute un
+        select_related() sans paramètre, ce qui a pour effet de charger tous
+        les objets FK, sans limite de profondeur. Dès qu'on arrive, dans les
+        modèles de Region, il existe plusieurs boucles, ce qui conduit à la
+        génération d'une requête infinie.  Affiche la liste de candidats que
+        si le user connecté possède un Evaluateur
+        """
+        qs = self.model._default_manager.get_query_set()
+        return qs.select_related('candidat')
+
+
+class EvaluateurAdmin(BaseAdmin, VersionAdmin):
+    fieldsets = (
+        ("Utilisateur", {
+            'fields': ('user',)
+        }),
+    )
+
+    ### Actions à afficher
+    def get_actions(self, request):
+        actions = super(EvaluateurAdmin, self).get_actions(request)
+        del actions['delete_selected']
+        return actions
+
+    ### Permissions add, delete, change
+    def has_add_permission(self, request):
+        user_groupes = [g.name for g in request.user.groups.all()]
+        if request.user.is_superuser is True or \
+                groups.DRH_NIVEAU_1 in user_groupes or \
+                groups.DRH_NIVEAU_2 in user_groupes or \
+                groups.HAUTE_DIRECTION in user_groupes:
+            return True
+        return False
+
+    def has_delete_permission(self, request, obj=None):
+        user_groupes = [g.name for g in request.user.groups.all()]
+        if request.user.is_superuser is True or \
+                groups.DRH_NIVEAU_1 in user_groupes or \
+                groups.DRH_NIVEAU_2 in user_groupes or \
+                groups.HAUTE_DIRECTION in user_groupes:
+            return True
+        return False
+
+    def has_change_permission(self, request, obj=None):
+        user_groupes = [g.name for g in request.user.groups.all()]
+        if request.user.is_superuser is True or \
+                groups.DRH_NIVEAU_1 in user_groupes or \
+                groups.DRH_NIVEAU_2 in user_groupes or \
+                groups.HAUTE_DIRECTION in user_groupes:
+            return True
+        return False
+
+
+class CandidatEvaluationAdmin(BaseAdmin):
+    search_fields = ('candidat__nom', 'candidat__prenom')
+    list_display = (
+        '_candidat', '_statut', '_offre_emploi', 'evaluateur', '_note',
+        '_commentaire'
+    )
+    readonly_fields = ('candidat', 'evaluateur')
+    list_filter = ('candidat__statut', 'candidat__offre_emploi',)
+    fieldsets = (
+        ('Évaluation du candidat', {
+            'fields': ('candidat', 'evaluateur', 'note', 'commentaire', )
+        }),
+    )
+
+    def get_actions(self, request):
+        # on stocke l'evaluateur connecté (pas forcément la meilleure place...)
+        try:
+            self.evaluateur = Evaluateur.objects.get(user=request.user)
+        except:
+            self.evaluateur = None
+
+        actions = super(CandidatEvaluationAdmin, self).get_actions(request)
+        del actions['delete_selected']
+        return actions
+
+    ### Afficher la note
+    def _note(self, obj):
+        """
+        Si l'évaluateur n'a pas encore donné de note au candidat, indiquer
+        un lien pour Évaluer le candidat.
+        Sinon afficher la note.
+        """
+        page = self.model.__name__.lower()
+        redirect_url = 'admin:recrutement_%s_change' % page
+
+        if obj.note is None:
+            label = "Candidat non évalué"
+        else:
+            label = obj.note
+
+        if self.evaluateur == obj.evaluateur:
+            return "<a href='%s'>%s</a>" % (
+                reverse(redirect_url,  args=(obj.id,)), label
+            )
+        else:
+            return label
+    _note.allow_tags = True
+    _note.short_description = "Note"
+    _note.admin_order_field = 'note'
+
+    def _statut(self, obj):
+        return obj.candidat.get_statut_display()
+    _statut.order_field = 'candidat__statut'
+    _statut.short_description = 'Statut'
+
+    ### Lien en lecture seule vers le candidat
+    def _candidat(self, obj):
+        return "<a href='%s'>%s</a>" \
+            % (reverse('admin:recrutement_proxycandidat_change',
+                        args=(obj.candidat.id,)), obj.candidat)
+    _candidat.allow_tags = True
+    _candidat.short_description = 'Candidat'
+
+    ### Afficher commentaire
+    def _commentaire(self, obj):
+        """
+        Si l'évaluateur n'a pas encore donné de note au candidat, indiquer
+        dans le champ commentaire, Aucun au lieu de (None)
+        Sinon afficher la note.
+        """
+        if obj.commentaire is None:
+            return "Aucun"
+        return obj.commentaire
+    _commentaire.allow_tags = True
+    _commentaire.short_description = "Commentaire"
+
+    ### Afficher offre d'emploi
+    def _offre_emploi(self, obj):
+        return "<a href='%s'>%s</a>" % \
+        (reverse('admin:recrutement_proxyoffreemploi_change',
+            args=(obj.candidat.offre_emploi.id,)), obj.candidat.offre_emploi)
+    _offre_emploi.allow_tags = True
+    _offre_emploi.short_description = "Voir offre d'emploi"
+
+    def has_add_permission(self, request):
+        return False
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+    def has_change_permission(self, request, obj=None):
+        """
+        Permettre la visualisation dans la changelist
+        mais interdire l'accès à modifier l'objet si l'évaluateur n'est pas
+        le request.user
+        """
+        user_groupes = [g.name for g in request.user.groups.all()]
+
+        if request.user.is_superuser or \
+            groups.CORRESPONDANT_RH in user_groupes or \
+            groups.DRH_NIVEAU_1 in user_groupes or \
+            groups.DRH_NIVEAU_2 in user_groupes or \
+            groups.DIRECTEUR_DE_BUREAU in user_groupes or \
+            groups.ADMINISTRATEURS in user_groupes or \
+            groups.HAUTE_DIRECTION in user_groupes:
+            is_recrutement = True
+        else:
+            is_recrutement = False
+
+        return is_recrutement
+
+    def queryset(self, request):
+        """
+        Afficher uniquement les évaluations de l'évaluateur, sauf si
+        l'utilisateur est dans les groupes suivants.
+        """
+        qs = self.model._default_manager.get_query_set() \
+                .select_related('offre_emploi')
+        user_groupes = request.user.groups.all()
+        user_groupes = [g.name for g in request.user.groups.all()]
+
+        if request.user.is_superuser or \
+            groups.CORRESPONDANT_RH in user_groupes or \
+            groups.DRH_NIVEAU_1 in user_groupes or \
+            groups.DRH_NIVEAU_2 in user_groupes or \
+            groups.DIRECTEUR_DE_BUREAU in user_groupes or \
+            groups.ADMINISTRATEURS in user_groupes or \
+            groups.HAUTE_DIRECTION in user_groupes:
+            return qs
+
+        evaluateur = Evaluateur.objects.get(user=request.user)
+        candidats_evaluations = \
+            CandidatEvaluation.objects.filter(evaluateur=evaluateur,
+                    candidat__statut__in=('REC', ))
+        candidats_evaluations_ids = [ce.id for ce in candidats_evaluations]
+        return qs.filter(id__in=candidats_evaluations_ids)
+
+
+class MesCandidatEvaluationAdmin(CandidatEvaluationAdmin):
+
+    def has_change_permission(self, request, obj=None):
+        try:
+            Evaluateur.objects.get(user=request.user)
+            is_evaluateur = True
+        except:
+            is_evaluateur = False
+
+        if obj is None and is_evaluateur:
+            return True
+
+        try:
+            return request.user == obj.evaluateur.user
+        except:
+            return False
+
+    def queryset(self, request):
+        qs = self.model._default_manager.get_query_set() \
+                .select_related('offre_emploi')
+        evaluateur = Evaluateur.objects.get(user=request.user)
+        candidats_evaluations = \
+            CandidatEvaluation.objects.filter(evaluateur=evaluateur,
+                    candidat__statut__in=('REC', ))
+        candidats_evaluations_ids = [ce.id for ce in candidats_evaluations]
+        return qs.filter(id__in=candidats_evaluations_ids)
+
+
+class CourrielTemplateAdmin(BaseAdmin, VersionAdmin):
+    ### Actions à afficher
+    def get_actions(self, request):
+        actions = super(CourrielTemplateAdmin, self).get_actions(request)
+        del actions['delete_selected']
+        return actions
+
+admin.site.register(OffreEmploi, OffreEmploiAdmin)
+admin.site.register(ProxyOffreEmploi, ProxyOffreEmploiAdmin)
+admin.site.register(Candidat, CandidatAdmin)
+admin.site.register(ProxyCandidat, ProxyCandidatAdmin)
+admin.site.register(CandidatEvaluation, CandidatEvaluationAdmin)
+admin.site.register(MesCandidatEvaluation, MesCandidatEvaluationAdmin)
+admin.site.register(Evaluateur, EvaluateurAdmin)
+admin.site.register(CourrielTemplate, CourrielTemplateAdmin)
diff --git a/project/recrutement/test/candidat.py b/project/recrutement/test/candidat.py
new file mode 100644 (file)
index 0000000..4c46036
--- /dev/null
@@ -0,0 +1,343 @@
+
+# -*- coding: utf-8 -*-
+
+from django.core.urlresolvers import reverse
+from project.recrutement.test.common import RecrutementTest
+
+class CandidatAddTest(RecrutementTest):
+    """
+    Test l'ajout d'un candidat
+    """
+    url = reverse('admin:emploi_candidat_add')
+
+    def test_anonyme(self):
+        """
+        Un anonyme ne peut pas ajouter un candidat
+        """
+        self._test_anonyme()
+        self._test_acces_ko(self.url)
+
+    def test_correspondant_rh(self):
+        """
+        Un correspondant RH peut ajouter un candidat
+        """
+        self._test_correspondant_rh()
+        self._test_acces_ok(self.url)
+
+    def test_administrateur_regional(self):
+        """
+        Un administrateur peut ajouter un candidat
+        """
+        self._test_administrateur_regional()
+        self._test_acces_ok(self.url)
+
+    def test_directeur_bureau(self):
+        """
+        Un directeur de bureau peut ajouter un candidat
+        """
+        self._test_directeur_bureau()
+        self._test_acces_ok(self.url)
+
+    def test_drh(self):
+        """
+        Un DRH peut ajouter un candidat
+        """
+        self._test_drh()
+        self._test_acces_ok(self.url)
+
+    def test_drh2(self):
+        """
+        Un DRH (2ieme niveau) peut ajouter un candidat
+        """
+        self._test_drh2()
+        self._test_acces_ok(self.url)
+
+    def _test_grp_accior(self):
+        """
+        Un membre de l'ACCIOR ne peut ajouter un candidat
+        """
+        self._test_grp_accior()
+        self._test_acces_ko(self.url)
+
+    def _test_grp_abf(self):
+        """
+        Un membre de l'ABF ne  peut ajouter un candidat
+        """
+        self._test_grp_abf(self)
+        self._test_acces_ko(self.url)
+
+    def _test_grp_haute_direction(self):
+        """
+        Un membre de la haute direction  peut ajouter un candidat
+        """
+        self._test_grp_haute_direction()
+        self._test_acces_ok(self.url)
+
+    def _test_grp_service_utilisateurs(self):
+        """
+        Un membre du groupe service utilisateur ne  peut ajouter un candidat
+        """
+        self._test_grp_service_utilisateurs()
+        self._test_acces_ko(self.url)
+
+
+class CandidatDeleteTest(RecrutementTest):
+    """
+    Test la suppression d'un candidat
+    """
+
+    def setUp(self):
+        super(CandidatDeleteTest, self).setUp()
+        self.url = reverse('admin:emploi_candidat_delete',
+                args=[self.candidat_cnf_ngaoundere.id])
+
+    def test_anonyme(self):
+        """
+        Un anonyme ne peut pas supprimer un candidat
+        """
+        self._test_anonyme()
+        self._test_acces_ko(self.url)
+
+    def test_correspondant_rh(self):
+        """
+        Un correspondant RH peut supprimer un candidat
+        """
+        self._test_correspondant_rh()
+        self._test_acces_ok(self.url)
+
+    def test_administrateur_regional(self):
+        """
+        Un administrateur peut supprimer un candidat
+        """
+        self._test_administrateur_regional()
+        self._test_acces_ok(self.url)
+        self._test_directeur_bureau(email="2@test.auf")
+        self._test_acces_ko(self.url)
+
+    def test_directeur_bureau(self):
+        """
+        Un directeur de bureau peut supprimer un candidat
+        """
+        self._test_directeur_bureau()
+        self._test_acces_ok(self.url)
+        self._test_directeur_bureau(email="2@test.auf")
+        self._test_acces_ko(self.url)
+
+    def test_drh(self):
+        """
+        Un DRH peut supprimer un candidat
+        """
+        self._test_drh()
+        self._test_acces_ok(self.url)
+
+    def test_drh2(self):
+        """
+        Un DRH (2ieme niveau) peut supprimer un candidat
+        """
+        self._test_drh2()
+        self._test_acces_ok(self.url)
+
+    def _test_grp_accior(self):
+        """
+        Un membre de l'ACCIOR ne peut supprimer un candidat
+        """
+        self._test_grp_accior()
+        self._test_acces_ko(self.url)
+
+    def _test_grp_abf(self):
+        """
+        Un membre de l'ABF ne  peut supprimer un candidat
+        """
+        self._test_grp_abf(self)
+        self._test_acces_ko(self.url)
+
+    def _test_grp_haute_direction(self):
+        """
+        Un membre de la haute direction  peut supprimer un candidat
+        """
+        self._test_grp_haute_direction()
+        self._test_acces_ok(self.url)
+
+    def _test_grp_service_utilisateurs(self):
+        """
+        Un membre du groupe service utilisateur ne  peut supprimer un candidat
+        """
+        self._test_grp_service_utilisateurs()
+        self._test_acces_ko(self.url)
+
+
+class CandidatChangeTest(RecrutementTest):
+    """
+    Test la modification d'un candidat
+    """
+
+    def setUp(self):
+        super(CandidatChangeTest, self).setUp()
+        self.url = reverse('admin:emploi_candidat_change',
+                args=[self.candidat_cnf_ngaoundere.id])
+
+    def test_anonyme(self):
+        """
+        Un anonyme ne peut pas modifier un candidat
+        """
+        self._test_anonyme()
+        self._test_acces_ko(self.url)
+
+    def test_correspondant_rh(self):
+        """
+        Un correspondant RH peut modifier un candidat
+        """
+        self._test_correspondant_rh()
+        self._test_acces_ok(self.url)
+
+    def test_administrateur_regional(self):
+        """
+        Un administrateur peut modifier un candidat
+        """
+        self._test_administrateur_regional()
+        self._test_acces_ok(self.url)
+        self._test_administrateur_regional(email="2@test.auf")
+        self._test_acces_ko(self.url)
+
+    def test_directeur_bureau(self):
+        """
+        Un directeur de bureau peut modifier un candidat
+        """
+        self._test_directeur_bureau()
+        self._test_acces_ok(self.url)
+        self._test_directeur_bureau(email="2@test.auf")
+        self._test_acces_ko(self.url)
+
+    def test_drh(self):
+        """
+        Un DRH peut modifier un candidat
+        """
+        self._test_drh()
+        self._test_acces_ok(self.url)
+
+    def test_drh2(self):
+        """
+        Un DRH (2ieme niveau) peut modifier un candidat
+        """
+        self._test_drh2()
+        self._test_acces_ok(self.url)
+
+    def _test_grp_accior(self):
+        """
+        Un membre de l'ACCIOR ne peut modifier un candidat
+        """
+        self._test_grp_accior()
+        self._test_acces_ko(self.url)
+
+    def _test_grp_abf(self):
+        """
+        Un membre de l'ABF ne  peut modifier un candidat
+        """
+        self._test_grp_abf(self)
+        self._test_acces_ko(self.url)
+
+    def _test_grp_haute_direction(self):
+        """
+        Un membre de la haute direction  peut modifier un candidat
+        """
+        self._test_grp_haute_direction()
+        self._test_acces_ok(self.url)
+
+    def _test_grp_service_utilisateurs(self):
+        """
+        Un membre du groupe service utilisateur ne  peut modifier un candidat
+        """
+        self._test_grp_service_utilisateurs()
+        self._test_acces_ko(self.url)
+
+
+class CandidatChangeListTest(RecrutementTest):
+    """
+    Test l'acces à la liste des candidats
+    """
+
+    def setUp(self):
+        super(CandidatChangeListTest, self).setUp()
+        self.url = reverse('admin:emploi_candidat_changelist')
+
+    def test_anonyme(self):
+        """
+        Un anonyme ne peut pas lister les candidats
+        """
+        self._test_anonyme()
+        self._test_acces_ko(self.url)
+
+    def test_correspondant_rh(self):
+        """
+        Un correspondant RH peut lister les candidats
+        """
+        self._test_correspondant_rh()
+        self._test_acces_ok(self.url)
+
+    def test_administrateur_regional(self):
+        """
+        Un administrateur peut lister les candidats
+        """
+        self._test_administrateur_regional()
+        self._test_acces_ok(self.url)
+        qs = self.client.get(self.url).context['cl'].query_set
+        self.assertEqual(len(qs), 1)
+        self._test_administrateur_regional(email="2@test.auf")
+        self._test_acces_ok(self.url)
+        qs = self.client.get(self.url).context['cl'].query_set
+        self.assertEqual(len(qs), 0)
+
+    def test_directeur_bureau(self):
+        """
+        Un directeur de bureau peut lister les candidats
+        """
+        self._test_directeur_bureau()
+        self._test_acces_ok(self.url)
+        qs = self.client.get(self.url).context['cl'].query_set
+        self.assertEqual(len(qs), 1)
+        self._test_directeur_bureau(email="2@test.auf")
+        self._test_acces_ok(self.url)
+        qs = self.client.get(self.url).context['cl'].query_set
+        self.assertEqual(len(qs), 0)
+
+    def test_drh(self):
+        """
+        Un DRH peut lister les candidats
+        """
+        self._test_drh()
+        self._test_acces_ok(self.url)
+
+    def test_drh2(self):
+        """
+        Un DRH (2ieme niveau) peut lister les candidats
+        """
+        self._test_drh2()
+        self._test_acces_ok(self.url)
+
+    def _test_grp_accior(self):
+        """
+        Un membre de l'ACCIOR ne peut lister les candidats
+        """
+        self._test_grp_accior()
+        self._test_acces_ko(self.url)
+
+    def _test_grp_abf(self):
+        """
+        Un membre de l'ABF ne  peut lister les candidats
+        """
+        self._test_grp_abf(self)
+        self._test_acces_ko(self.url)
+
+    def _test_grp_haute_direction(self):
+        """
+        Un membre de la haute direction  peut lister les candidats
+        """
+        self._test_grp_haute_direction()
+        self._test_acces_ok(self.url)
+
+    def _test_grp_service_utilisateurs(self):
+        """
+        Un membre du groupe service utilisateur ne  peut lister les candidats
+        """
+        self._test_grp_service_utilisateurs()
+        self._test_acces_ko(self.url)
index 06f8053..c4b3887 100644 (file)
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 from auf.django.references import models as ref
-from auf.django.emploi.models import OffreEmploi
+from auf.django.emploi.models import OffreEmploi, Candidat
 from project.rh.test.common import RhTest
 
 class RecrutementTest(RhTest):
@@ -11,6 +11,10 @@ class RecrutementTest(RhTest):
         OFFRE_EMPLOIS
         =============
         self.offre_cnf_ngaoundere
+
+        CANDIDATS
+        =========
+        self.canditat_cnf_ngaoundere
         """
         super(RecrutementTest, self).setUp()
     
@@ -23,3 +27,14 @@ class RecrutementTest(RhTest):
                 )
         self.offre_cnf_ngaoundere.save()
 
+        self.candidat_cnf_ngaoundere = Candidat(
+                prenom="olivier",
+                nom="larchevêque",
+                genre='M',
+                nationalite=ref.Pays.objects.get(code='FR'),
+                pays=ref.Pays.objects.get(code='CA'),
+                offre_emploi=self.offre_cnf_ngaoundere,
+                nombre_dependant=3,
+                )
+        self.candidat_cnf_ngaoundere.save()
+        
index c9bcec4..0877a46 100644 (file)
@@ -1,7 +1,2 @@
 from project.recrutement.test.offre_emploi import *
-#from project.recrutement.test.offre_emploi_visualisation import *
-#from project.recrutement.test.candidat import *
-#from project.recrutement.test.candidat_visualisation import *
-#from project.recrutement.test.candidatevaluation import *
-#from project.recrutement.test.evaluateur import *
-#from project.recrutement.test.modele_courriel import *
+from project.recrutement.test.candidat import *
index a297716..5a23741 100644 (file)
@@ -11,6 +11,35 @@ class RhTest(TestCase):
 
     def setUp(self):
         """
+        DEVISES
+        =======
+        self.devise_cad
+
+        CLASSEMENTS
+        ===========
+        self.classement_p_5_0
+        self.classement_p_5_10
+
+        VALEUR POINTS
+        =============
+        self.vp_cad_scm_2012
+
+        SERVICES
+        ========
+        self.srv_info
+
+        CATEGORIE EMPLOI
+        ================
+        self.cat_emploi_pro
+
+        FAMILLE PROFESSIONNELLE
+        =======================
+        self.famille_cadre
+
+        TYPE POSTES
+        ===========
+        self.type_poste_tech
+
         GROUPES
         =======
         self.grp_correspondants_rh
@@ -50,6 +79,50 @@ class RhTest(TestCase):
 
 
         """
+        #########
+        # DEVISES
+        #########
+        self.devise_cad = rh.Devise(code="CAD", nom="CAD")
+        self.devise_cad.save()
+
+        #############
+        # CLASSEMENTS
+        #############
+        self.classement_p_5_0 = rh.Classement(type="P", echelon=5, degre=1, coefficient=5.01)
+        self.classement_p_5_0.save()
+
+        self.classement_p_5_10 = rh.Classement(type="P", echelon=5, degre=10, coefficient=5.10)
+        self.classement_p_5_10.save()
+
+        ##########
+        # SERVICES
+        ##########
+        self.srv_info = rh.Service(nom="info")
+        self.srv_info.save()
+
+        ###################
+        # CATEGORIES EMPLOI
+        ###################
+        self.cat_emploi_pro = rh.CategorieEmploi(nom="pro")
+        self.cat_emploi_pro.save()
+
+        #################
+        # FAMILLES EMPLOI
+        #################
+        self.famille_cadre = rh.FamilleProfessionnelle(nom="cadre")
+        self.famille_cadre.save()
+
+        #############
+        # TYPES POSTE
+        #############
+        self.type_poste_tech = rh.TypePoste(nom="tech",
+                categorie_emploi=self.cat_emploi_pro,
+                famille_professionnelle=self.famille_cadre)
+        self.type_poste_tech.save()
+
+        #########
+        # GROUPES
+        #########
         self.grp_correspondants_rh = Group(name=groups.CORRESPONDANT_RH)
         self.grp_correspondants_rh.save()
 
@@ -92,6 +165,16 @@ class RhTest(TestCase):
         self.IMPLANTATION_BAP_BUREAU = ref.Implantation.objects.get(id=51)
         self.IMPLANTATION_BAP_IFI = ref.Implantation.objects.get(id=55)
 
+        #############
+        #VALEUR POINT
+        #############
+        self.vp_cad_ngaoundere_2012 = rh.ValeurPoint(valeur=1000.1,
+                devise=self.devise_cad,
+                implantation=self.IMPLANTATION_ACGL_CNF_NGAOUNDERE,
+                annee=2012)
+        self.vp_cad_ngaoundere_2012.save()
+
+
 
         ##########
         # Employés
index 50284aa..ab14bcb 100644 (file)
@@ -34,15 +34,12 @@ class RapportContratTest(RhTest):
                 )
         salaire.save()
 
-        dollar = rh.Devise(code="CAD", nom="Dollar CAD")
-        dollar.save()
-
         remun_cnf_ngaoundere = rh.Remuneration(
                 type=salaire,
                 date_debut=self.today,
                 dossier=self.dossier_cnf_ngaoundere,
                 montant="100",
-                devise=dollar,
+                devise=self.devise_cad,
                 )
         remun_cnf_ngaoundere.save()
 
@@ -51,7 +48,7 @@ class RapportContratTest(RhTest):
                 date_debut=self.today,
                 dossier=self.dossier_bap_ifi,
                 montant="200",
-                devise=dollar,
+                devise=self.devise_cad,
                 )
         remun_bap_ifi.save()
 
diff --git a/runtests b/runtests
new file mode 100755 (executable)
index 0000000..0225b28
--- /dev/null
+++ b/runtests
@@ -0,0 +1 @@
+bin/django test rh dae recrutement --settings=project.mysql_ram --noinput