diff --git a/admin.py b/admin.py index 885dbd277..be5f625eb 100644 --- a/admin.py +++ b/admin.py @@ -105,7 +105,7 @@ from base.models.enums.education_group_categories import Categories from base.models.person import Person from base.models.person_merge_proposal import PersonMergeStatus -from education_group.auth.scope import Scope +from admission.auth.scope import Scope from education_group.contrib.admin import EducationGroupRoleModelAdmin from epc.models.inscription_programme_cycle import InscriptionProgrammeCycle from osis_profile.models import EducationalExperience, ProfessionalExperience @@ -684,15 +684,15 @@ def queryset(self, request, queryset): | Q( checklist__current__financabilite__status='GEST_REUSSITE', checklist__current__financanbilite__extra__reussite='financable', - generaleducationadmission__financability_rule='' + generaleducationadmission__financability_rule='', ) | Q( checklist__current__financabilite__status='GEST_REUSSITE', - generaleducationadmission__financability_rule_established_on__isnull=True + generaleducationadmission__financability_rule_established_on__isnull=True, ) | Q( checklist__current__financabilite__status='GEST_REUSSITE', - generaleducationadmission__financability_rule_established_by_id__isnull=True + generaleducationadmission__financability_rule_established_by_id__isnull=True, ), generaleducationadmission__isnull=False, then=Value(False), diff --git a/auth/constants.py b/auth/constants.py index d9e16d0e3..ed1568b11 100644 --- a/auth/constants.py +++ b/auth/constants.py @@ -80,7 +80,7 @@ # Training choice 'training-choice': 'admission.change_admission_training_choice', # Previous experience - 'curriculum': 'admission.change_admission_curriculum', + 'curriculum': 'admission.change_admission_global_curriculum', 'educational': '', 'educational_create': '', 'non_educational': '', diff --git a/auth/predicates/common.py b/auth/predicates/common.py index 52a2c67d1..f707d966f 100644 --- a/auth/predicates/common.py +++ b/auth/predicates/common.py @@ -31,9 +31,10 @@ from waffle import switch_is_active from admission.contrib.models import DoctorateAdmission, GeneralEducationAdmission +from admission.constants import CONTEXT_GENERAL, CONTEXT_DOCTORATE, CONTEXT_CONTINUING from admission.contrib.models.base import BaseAdmission -from admission.contrib.models.epc_injection import EPCInjectionStatus from base.models.person_creation_ticket import PersonTicketCreation, PersonTicketCreationStatus +from admission.auth.scope import Scope from osis_role.errors import predicate_failed_msg @@ -43,6 +44,28 @@ def is_admission_request_author(self, user: User, obj: BaseAdmission): return obj.candidate == user.person +@predicate(bind=True) +@predicate_failed_msg( + message=_( + "This action cannot be performed as an admission or an internal experience is related to a general education." + ), +) +def candidate_has_other_general_admissions(self, user: User, obj: BaseAdmission): + return bool(obj.other_candidate_trainings[CONTEXT_GENERAL]) + + +@predicate(bind=True) +@predicate_failed_msg( + message=_( + "This action cannot be performed as an admission or an internal experience is related to a general " + "or a doctorate education." + ), +) +def candidate_has_other_doctorate_or_general_admissions(self, user: User, obj: BaseAdmission): + other_admissions = obj.other_candidate_trainings + return bool(other_admissions[CONTEXT_GENERAL]) or bool(other_admissions[CONTEXT_DOCTORATE]) + + @predicate(bind=True) @predicate_failed_msg(message=_("Another admission has been submitted.")) def does_not_have_a_submitted_admission(self, user: User, obj: DoctorateAdmission): @@ -109,6 +132,25 @@ def is_entity_manager(self, user: User, obj: BaseAdmission): return obj.training.management_entity_id in getattr(user, cache_key) +@predicate(bind=True) +def is_scoped_entity_manager(self, user: User, obj: BaseAdmission): + """ + Check that the user is a manager of the admission training management entity with the correct scope. + """ + scope = { + CONTEXT_GENERAL: Scope.GENERAL, + CONTEXT_DOCTORATE: Scope.DOCTORAT, + CONTEXT_CONTINUING: Scope.IUFC, + }[obj.admission_context] + + cache_key = _build_queryset_cache_key_from_role_qs(self.context['role_qs'], f'entities_ids_by_scope_{scope.name}') + + if not hasattr(user, cache_key): + setattr(user, cache_key, self.context['role_qs'].filter(scopes__contains=[scope.name]).get_entities_ids()) + + return obj.training.management_entity_id in getattr(user, cache_key) + + def has_education_group_of_types(*education_group_types): name = 'has_education_group_of_types:%s' % ','.join(education_group_types) diff --git a/auth/predicates/continuing.py b/auth/predicates/continuing.py index c79f06ebe..af269dccc 100644 --- a/auth/predicates/continuing.py +++ b/auth/predicates/continuing.py @@ -50,7 +50,9 @@ def is_continuing(self, user: User, obj: ContinuingEducationAdmission): @predicate(bind=True) @predicate_failed_msg(message=_('The proposition must be in draft form to realize this action.')) def in_progress(self, user: User, obj: ContinuingEducationAdmission): - return obj.status == ChoixStatutPropositionContinue.EN_BROUILLON.name + return ( + isinstance(obj, ContinuingEducationAdmission) and obj.status == ChoixStatutPropositionContinue.EN_BROUILLON.name + ) @predicate(bind=True) diff --git a/auth/predicates/doctorate.py b/auth/predicates/doctorate.py index 7ee351f7f..84eae485d 100644 --- a/auth/predicates/doctorate.py +++ b/auth/predicates/doctorate.py @@ -55,7 +55,7 @@ @predicate(bind=True) @predicate_failed_msg(message=_("Invitations must have been sent")) def in_progress(self, user: User, obj: DoctorateAdmission): - return obj.status == ChoixStatutPropositionDoctorale.EN_BROUILLON.name + return isinstance(obj, DoctorateAdmission) and obj.status == ChoixStatutPropositionDoctorale.EN_BROUILLON.name @predicate(bind=True) diff --git a/auth/predicates/general.py b/auth/predicates/general.py index e3d3558d4..01f24853f 100644 --- a/auth/predicates/general.py +++ b/auth/predicates/general.py @@ -47,7 +47,7 @@ @predicate(bind=True) @predicate_failed_msg(message=_('The proposition must be in draft form to realize this action.')) def in_progress(self, user: User, obj: GeneralEducationAdmission): - return obj.status == ChoixStatutPropositionGenerale.EN_BROUILLON.name + return isinstance(obj, GeneralEducationAdmission) and obj.status == ChoixStatutPropositionGenerale.EN_BROUILLON.name @predicate(bind=True) diff --git a/auth/roles/central_manager.py b/auth/roles/central_manager.py index 1858ec07e..2e7665958 100644 --- a/auth/roles/central_manager.py +++ b/auth/roles/central_manager.py @@ -33,12 +33,15 @@ from admission.auth.predicates.common import ( has_scope, is_debug, - is_entity_manager, + is_entity_manager as is_entity_manager_without_scope, + is_scoped_entity_manager, is_sent_to_epc, pending_digit_ticket_response, past_experiences_checklist_tab_is_not_sufficient, + candidate_has_other_doctorate_or_general_admissions, + candidate_has_other_general_admissions, ) -from education_group.auth.scope import Scope +from admission.auth.scope import Scope from osis_role.contrib.models import EntityRoleModel @@ -59,11 +62,19 @@ class Meta: verbose_name_plural = _("Role: Central managers") group_name = "admission_central_managers" + @classmethod + def rule_set_without_scope(cls): + return cls.common_rule_set(is_entity_manager_without_scope) + @classmethod def rule_set(cls): + return cls.common_rule_set(is_scoped_entity_manager) + + @classmethod + def common_rule_set(cls, is_entity_manager: callable): ruleset = { # Listings - 'admission.view_enrolment_applications': has_scope(Scope.ALL), + 'admission.view_enrolment_applications': has_scope(Scope.GENERAL), 'admission.view_doctorate_enrolment_applications': has_scope(Scope.DOCTORAT), 'admission.view_continuing_enrolment_applications': has_scope(Scope.IUFC), # Access a single application @@ -74,14 +85,26 @@ def rule_set(cls): 'admission.appose_sic_notice': is_entity_manager, 'admission.view_admission_person': is_entity_manager, 'admission.change_admission_person': is_entity_manager - & (general.in_sic_status | continuing.in_manager_status | doctorate.in_sic_status - | general.in_progress | continuing.in_progress | doctorate.in_progress) + & ( + (general.in_sic_status | general.in_progress) + | ( + (continuing.in_manager_status | continuing.in_progress) + & ~candidate_has_other_doctorate_or_general_admissions + ) + | ((doctorate.in_sic_status | doctorate.in_progress) & ~candidate_has_other_general_admissions) + ) & ~is_sent_to_epc & ~pending_digit_ticket_response, 'admission.view_admission_coordinates': is_entity_manager, 'admission.change_admission_coordinates': is_entity_manager - & (general.in_sic_status | continuing.in_manager_status | doctorate.in_sic_status - | general.in_progress | continuing.in_progress | doctorate.in_progress) + & ( + general.in_sic_status + | continuing.in_manager_status + | doctorate.in_sic_status + | general.in_progress + | continuing.in_progress + | doctorate.in_progress + ) & ~is_sent_to_epc & ~pending_digit_ticket_response, 'admission.view_admission_training_choice': is_entity_manager, @@ -93,14 +116,27 @@ def rule_set(cls): 'admission.change_admission_languages': is_entity_manager & doctorate.in_sic_status & ~is_sent_to_epc, 'admission.view_admission_secondary_studies': is_entity_manager, 'admission.change_admission_secondary_studies': is_entity_manager - & (general.in_sic_status | continuing.in_manager_status) + & ( + general.in_sic_status + | (continuing.in_manager_status & ~candidate_has_other_doctorate_or_general_admissions) + ) & ~is_sent_to_epc, 'admission.view_admission_curriculum': is_entity_manager, - 'admission.change_admission_curriculum': is_entity_manager + 'admission.change_admission_global_curriculum': is_entity_manager & (general.in_sic_status | continuing.in_manager_status | doctorate.in_sic_status) & ~is_sent_to_epc, + 'admission.change_admission_curriculum': is_entity_manager + & ( + general.in_sic_status + | (continuing.in_manager_status & ~candidate_has_other_doctorate_or_general_admissions) + | doctorate.in_sic_status + ) + & ~is_sent_to_epc, 'admission.delete_admission_curriculum': is_entity_manager - & (general.in_sic_status | continuing.in_manager_status | doctorate.in_sic_status) + & ( + general.in_sic_status + | (continuing.in_manager_status & ~candidate_has_other_doctorate_or_general_admissions) + ) & ~is_sent_to_epc, 'admission.view_admission_project': is_entity_manager, 'admission.change_admission_project': is_entity_manager & doctorate.in_sic_status & ~is_sent_to_epc, diff --git a/auth/roles/program_manager.py b/auth/roles/program_manager.py index 342631c2e..f8204f0dd 100644 --- a/auth/roles/program_manager.py +++ b/auth/roles/program_manager.py @@ -35,6 +35,7 @@ is_sent_to_epc, pending_digit_ticket_response, past_experiences_checklist_tab_is_not_sufficient, + candidate_has_other_doctorate_or_general_admissions, ) from admission.auth.predicates import general, continuing, doctorate from admission.infrastructure.admission.domain.service.annee_inscription_formation import ( @@ -86,7 +87,8 @@ def rule_set(cls): 'admission.change_admission_person': is_part_of_education_group & continuing.in_manager_status & ~is_sent_to_epc - & ~pending_digit_ticket_response, + & ~pending_digit_ticket_response + & ~candidate_has_other_doctorate_or_general_admissions, 'admission.view_admission_coordinates': is_part_of_education_group, 'admission.change_admission_coordinates': is_part_of_education_group & continuing.in_manager_status @@ -95,18 +97,26 @@ def rule_set(cls): 'admission.view_admission_secondary_studies': is_part_of_education_group, 'admission.change_admission_secondary_studies': is_part_of_education_group & continuing.in_manager_status - & ~is_sent_to_epc, + & ~is_sent_to_epc + & ~candidate_has_other_doctorate_or_general_admissions, 'admission.view_admission_languages': is_part_of_education_group, 'admission.change_admission_languages': is_part_of_education_group & doctorate.in_fac_status & ~is_sent_to_epc, 'admission.view_admission_curriculum': is_part_of_education_group, - 'admission.change_admission_curriculum': is_part_of_education_group + 'admission.change_admission_global_curriculum': is_part_of_education_group & (continuing.in_manager_status | doctorate.in_fac_status) & ~is_sent_to_epc, + 'admission.change_admission_curriculum': is_part_of_education_group + & ( + (continuing.in_manager_status & ~candidate_has_other_doctorate_or_general_admissions) + | doctorate.in_fac_status + ) + & ~is_sent_to_epc, 'admission.delete_admission_curriculum': is_part_of_education_group & continuing.in_manager_status - & ~is_sent_to_epc, + & ~is_sent_to_epc + & ~candidate_has_other_doctorate_or_general_admissions, # Project 'admission.view_admission_project': is_part_of_education_group, 'admission.view_admission_cotutelle': is_part_of_education_group, diff --git a/auth/roles/sic_management.py b/auth/roles/sic_management.py index 315bb41fa..fa145b2d8 100644 --- a/auth/roles/sic_management.py +++ b/auth/roles/sic_management.py @@ -49,7 +49,7 @@ class Meta: @classmethod def rule_set(cls): ruleset = { - **CentralManager.rule_set(), + **CentralManager.rule_set_without_scope(), # Listings 'admission.checklist_change_sic_decision': rules.always_allow & ~is_sent_to_epc, 'admission.view_enrolment_applications': rules.always_allow, diff --git a/auth/scope.py b/auth/scope.py new file mode 100644 index 000000000..f451ab8f9 --- /dev/null +++ b/auth/scope.py @@ -0,0 +1,32 @@ +############################################################################## +# +# OSIS stands for Open Student Information System. It's an application +# designed to manage the core business of higher education institutions, +# such as universities, faculties, institutes and professional schools. +# The core business involves the administration of students, teachers, +# courses, programs and so on. +# +# Copyright (C) 2015-2024 Université catholique de Louvain (http://www.uclouvain.be) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# A copy of this license - GNU General Public License - is available +# at the root of the source code of this program. If not, +# see http://www.gnu.org/licenses/. +# +############################################################################## +from base.models.utils.utils import ChoiceEnum + + +class Scope(ChoiceEnum): + GENERAL = 'GENERAL' + IUFC = 'IUFC' + DOCTORAT = 'DOCTORAT' diff --git a/contrib/models/base.py b/contrib/models/base.py index 06928d092..1a1fe8e08 100644 --- a/contrib/models/base.py +++ b/contrib/models/base.py @@ -23,7 +23,9 @@ # see http://www.gnu.org/licenses/. # ############################################################################## +import itertools import uuid +from typing import Dict, Set from django.conf import settings from django.contrib.auth.models import User @@ -41,7 +43,6 @@ from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _, get_language, pgettext_lazy from osis_comment.models import CommentDeleteMixin -from osis_document.contrib import FileField from osis_history.models import HistoryEntry from admission.constants import ( @@ -55,6 +56,7 @@ from admission.contrib.models.functions import ToChar from admission.ddd.admission.doctorat.preparation.domain.model.enums import ( STATUTS_PROPOSITION_DOCTORALE_NON_SOUMISE, + STATUTS_PROPOSITION_DOCTORALE_PEU_AVANCEE, ) from admission.ddd.admission.enums.type_demande import TypeDemande from admission.ddd.admission.formation_continue.domain.model.enums import ( @@ -67,6 +69,7 @@ from admission.ddd.admission.repository.i_proposition import CAMPUS_LETTRE_DOSSIER from admission.infrastructure.admission.domain.service.annee_inscription_formation import ( AnneeInscriptionFormationTranslator, + ADMISSION_CONTEXT_BY_ALL_OSIS_EDUCATION_TYPE, ) from base.models.academic_calendar import AcademicCalendar from base.models.education_group_year import EducationGroupYear @@ -80,6 +83,9 @@ from base.models.student import Student from base.utils.cte import CTESubquery from education_group.contrib.models import EducationGroupRoleModel +from epc.models.enums.etat_inscription import EtatInscriptionFormation +from epc.models.inscription_programme_annuel import InscriptionProgrammeAnnuel +from osis_document.contrib import FileField from osis_role.contrib.models import EntityRoleModel from osis_role.contrib.permissions import _get_relevant_roles from program_management.models.education_group_version import EducationGroupVersion @@ -622,6 +628,54 @@ def sent_to_epc(self): def is_in_quarantine(self): return BaseAdmission.objects.filter(pk=self.pk).filter_in_quarantine().exists() + @cached_property + def other_candidate_trainings(self) -> Dict[str, Set[str]]: + # Retrieve the education group types from the admissions + admission_training_types = ( + BaseAdmission.objects.filter( + candidate_id=self.candidate_id, + ) + .exclude( + Q(pk=self.pk) + | Q(generaleducationadmission__status__in=STATUTS_PROPOSITION_GENERALE_NON_SOUMISE) + | Q(continuingeducationadmission__status__in=STATUTS_PROPOSITION_CONTINUE_NON_SOUMISE) + | Q(doctorateadmission__status__in=STATUTS_PROPOSITION_DOCTORALE_PEU_AVANCEE), + ) + .values_list( + 'training__education_group_type__name', + flat=True, + ) + ) + + # Retrieve the education group types from the internal trainings + internal_training_types = ( + InscriptionProgrammeAnnuel.objects.filter( + programme_cycle__etudiant__person_id=self.candidate_id, + programme__isnull=False, + ) + .exclude( + etat_inscription__in=[ + EtatInscriptionFormation.ERREUR.name, + EtatInscriptionFormation.ERREUR_PROCEDURE.name, + ] + ) + .values_list( + 'programme__root_group__education_group_type__name', + flat=True, + ) + ) + + other_admissions = { + CONTEXT_GENERAL: set(), + CONTEXT_DOCTORATE: set(), + CONTEXT_CONTINUING: set(), + } + + for training in itertools.chain(internal_training_types, admission_training_types): + other_admissions[ADMISSION_CONTEXT_BY_ALL_OSIS_EDUCATION_TYPE[training]].add(training) + + return other_admissions + class AdmissionEducationalValuatedExperiences(models.Model): baseadmission = models.ForeignKey( diff --git a/ddd/admission/doctorat/preparation/domain/model/enums/projet.py b/ddd/admission/doctorat/preparation/domain/model/enums/projet.py index e230a5308..10f809449 100644 --- a/ddd/admission/doctorat/preparation/domain/model/enums/projet.py +++ b/ddd/admission/doctorat/preparation/domain/model/enums/projet.py @@ -79,6 +79,11 @@ def get_specific_values(cls, keys: Iterable[str]): set(ChoixStatutPropositionDoctorale.get_names()) - STATUTS_PROPOSITION_DOCTORALE_NON_SOUMISE ) +STATUTS_PROPOSITION_DOCTORALE_PEU_AVANCEE = { + ChoixStatutPropositionDoctorale.ANNULEE.name, + ChoixStatutPropositionDoctorale.EN_BROUILLON.name, +} + # Le gestionnaire FAC a la main STATUTS_PROPOSITION_DOCTORALE_SOUMISE_POUR_FAC = { ChoixStatutPropositionDoctorale.COMPLETEE_POUR_FAC.name, diff --git a/infrastructure/admission/domain/service/annee_inscription_formation.py b/infrastructure/admission/domain/service/annee_inscription_formation.py index 3a6f10641..a37f4c5de 100644 --- a/infrastructure/admission/domain/service/annee_inscription_formation.py +++ b/infrastructure/admission/domain/service/annee_inscription_formation.py @@ -24,15 +24,15 @@ # ############################################################################## import datetime -from typing import Optional +from typing import Optional, Dict +from admission.constants import CONTEXT_GENERAL, CONTEXT_DOCTORATE, CONTEXT_CONTINUING from admission.ddd.admission.domain.enums import TypeFormation from admission.ddd.admission.domain.service.i_annee_inscription_formation import IAnneeInscriptionFormationTranslator from base.models.academic_calendar import AcademicCalendar from base.models.enums.academic_calendar_type import AcademicCalendarTypes -from base.models.enums.education_group_types import TrainingType - +from base.models.enums.education_group_types import TrainingType, ALL_TYPES, AllTypes ADMISSION_EDUCATION_TYPE_BY_ADMISSION_CONTEXT = { 'general-education': { @@ -145,3 +145,16 @@ def recuperer_annee_selon_type_formation(cls, type_formation: TrainingType) -> O for admission_type in admission_types for osis_type in AnneeInscriptionFormationTranslator.OSIS_ADMISSION_EDUCATION_TYPES_MAPPING[admission_type] } + + +doctorate_types_as_set = set(TrainingType.doctorate_types()) +continuing_education_types_as_set = set(TrainingType.continuing_education_types()) + +ADMISSION_CONTEXT_BY_ALL_OSIS_EDUCATION_TYPE: Dict[str, str] = { + osis_type: CONTEXT_DOCTORATE + if osis_type in doctorate_types_as_set + else CONTEXT_CONTINUING + if osis_type in continuing_education_types_as_set + else CONTEXT_GENERAL + for osis_type in AllTypes.get_names() +} diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 9711828e4..67cb20556 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -7317,6 +7317,16 @@ msgstr "" msgid "This account number has been verified" msgstr "" +msgid "" +"This action cannot be performed as an admission or an internal experience is " +"related to a general education." +msgstr "" + +msgid "" +"This action cannot be performed as an admission or an internal experience is " +"related to a general or a doctorate education." +msgstr "" + msgid "This action is limited to a specific admission context." msgstr "" diff --git a/locale/fr_BE/LC_MESSAGES/django.po b/locale/fr_BE/LC_MESSAGES/django.po index 25fa731e7..62c0a3ae9 100644 --- a/locale/fr_BE/LC_MESSAGES/django.po +++ b/locale/fr_BE/LC_MESSAGES/django.po @@ -8111,6 +8111,21 @@ msgstr "Données relatives à la thèse" msgid "This account number has been verified" msgstr "Ce numéro a été vérifié" +msgid "" +"This action cannot be performed as an admission or an internal experience is " +"related to a general education." +msgstr "" +"Cette action ne peut être réalisée car il existe, pour ce candidat, une " +"autre admission ou expérience interne relative à une formation générale." + +msgid "" +"This action cannot be performed as an admission or an internal experience is " +"related to a general or a doctorate education." +msgstr "" +"Cette action ne peut être réalisée car il existe, pour ce candidat, une " +"autre admission ou expérience interne relative à une formation générale ou " +"doctorale." + msgid "This action is limited to a specific admission context." msgstr "Cette action est limitée à un contexte d'admission particulier." diff --git a/migrations/0227_alter_centralmanager_scopes.py b/migrations/0227_alter_centralmanager_scopes.py new file mode 100644 index 000000000..d3864672d --- /dev/null +++ b/migrations/0227_alter_centralmanager_scopes.py @@ -0,0 +1,48 @@ +# Generated by Django 3.2.25 on 2024-10-18 17:40 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +def _update_central_manager_scopes(apps, old_value, new_value): + CentralManager = apps.get_model('admission', 'CentralManager') + central_managers_with_all_scope = CentralManager.objects.filter(scopes__contains=[old_value]) + + for central_manager in central_managers_with_all_scope: + central_manager.scopes[central_manager.scopes.index(old_value)] = new_value + + CentralManager.objects.bulk_update(central_managers_with_all_scope, ['scopes']) + + +def update_central_manager_scopes_forward(apps, schema_editor): + _update_central_manager_scopes(apps, 'ALL', 'GENERAL') + + +def update_central_manager_scopes_reverse(apps, schema_editor): + _update_central_manager_scopes(apps, 'GENERAL', 'ALL') + + +class Migration(migrations.Migration): + + dependencies = [ + ('admission', '0226_initialize_iufc_specific_questions'), + ] + + operations = [ + migrations.AlterField( + model_name='centralmanager', + name='scopes', + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[('GENERAL', 'GENERAL'), ('IUFC', 'IUFC'), ('DOCTORAT', 'DOCTORAT')], + max_length=200, + ), + blank=True, + size=None, + ), + ), + migrations.RunPython( + code=update_central_manager_scopes_forward, + reverse_code=update_central_manager_scopes_reverse, + ), + ] diff --git a/templates/admission/doctorate/checklist.html b/templates/admission/doctorate/checklist.html index 74a672e84..501e5ede8 100644 --- a/templates/admission/doctorate/checklist.html +++ b/templates/admission/doctorate/checklist.html @@ -63,8 +63,9 @@ {% can_update_tab 'languages' as can_update_languages_tab %} {% can_update_tab 'accounting' as can_update_accounting_tab %} {% can_update_tab 'training-choice' as can_update_training_choice_tab %} - {% can_update_tab 'curriculum' as can_update_curriculum %} + {% can_update_tab 'curriculum' as can_update_global_curriculum %} {% can_update_tab 'education' as can_update_education %} + {% has_perm 'admission.change_admission_curriculum' as can_update_curriculum %} {% has_perm 'admission.delete_admission_curriculum' as can_delete_curriculum %} {% if can_update_person_tab %} @@ -268,7 +269,7 @@
- {% if can_update_curriculum %} + {% if can_update_global_curriculum %} {% url 'admission:doctorate:update:curriculum' view.kwargs.uuid as global_curriculum_update_url %} {% endif %} {% multiple_field_data specific_questions_by_tab|get_item:'CURRICULUM' edit_link_button=global_curriculum_update_url|add:next_url %} diff --git a/templates/admission/general_education/checklist.html b/templates/admission/general_education/checklist.html index a9d16d3e6..9dac3283a 100644 --- a/templates/admission/general_education/checklist.html +++ b/templates/admission/general_education/checklist.html @@ -61,8 +61,9 @@ {% can_update_tab 'accounting' as can_update_accounting_tab %} {% can_update_tab 'training-choice' as can_update_training_choice_tab %} {% can_update_tab 'specific-questions' as can_update_specific_questions_tab %} - {% can_update_tab 'curriculum' as can_update_curriculum %} + {% can_update_tab 'curriculum' as can_update_global_curriculum %} {% can_update_tab 'education' as can_update_education %} + {% has_perm 'admission.change_admission_curriculum' as can_update_curriculum %} {% has_perm 'admission.delete_admission_curriculum' as can_delete_curriculum %} {% if can_update_person_tab %} @@ -319,7 +320,7 @@ {% include 'admission/general_education/includes/checklist/financeabilty_info.html' with current=original_admission.checklist.current.financabilite %}
- {% if can_update_curriculum %} + {% if can_update_global_curriculum %} {% url 'admission:general-education:update:curriculum' view.kwargs.uuid as global_curriculum_update_url %} {% endif %} {% multiple_field_data specific_questions_by_tab|get_item:'CURRICULUM' edit_link_button=global_curriculum_update_url|add:next_url %} diff --git a/templates/admission/includes/common_curriculum.html b/templates/admission/includes/common_curriculum.html index dc1e659b4..996f9ac39 100644 --- a/templates/admission/includes/common_curriculum.html +++ b/templates/admission/includes/common_curriculum.html @@ -26,7 +26,7 @@ {% endcomment %} -{% can_update_tab 'curriculum' as can_update_curriculum%} +{% has_perm 'admission.change_admission_curriculum' as can_update_curriculum %} {% url base_namespace|add:':update:curriculum:educational_create' view.kwargs.uuid as curriculum_educational_create_url %} {% url base_namespace|add:':update:curriculum:non_educational_create' view.kwargs.uuid as curriculum_professional_create_url %} diff --git a/tests/auth/predicates/test_common.py b/tests/auth/predicates/test_common.py index 2aa54a09b..1a8135739 100644 --- a/tests/auth/predicates/test_common.py +++ b/tests/auth/predicates/test_common.py @@ -24,11 +24,20 @@ # # ############################################################################## from unittest import mock + +from django.contrib.auth.models import User from django.test import TestCase +from pytz.reference import Central from admission.auth.predicates import common +from admission.auth.predicates.common import is_scoped_entity_manager +from admission.auth.roles.central_manager import CentralManager from admission.tests.factories import DoctorateAdmissionFactory -from admission.tests.factories.roles import CandidateFactory +from admission.tests.factories.continuing_education import ContinuingEducationAdmissionFactory +from admission.tests.factories.general_education import GeneralEducationAdmissionFactory +from admission.tests.factories.roles import CandidateFactory, CentralManagerRoleFactory +from base.tests.factories.entity_version import EntityVersionFactory +from admission.auth.scope import Scope class PredicatesTestCase(TestCase): @@ -47,3 +56,158 @@ def test_is_admission_request_author(self): request = DoctorateAdmissionFactory(candidate=candidate1) self.assertTrue(common.is_admission_request_author(candidate1.user, request)) self.assertFalse(common.is_admission_request_author(candidate2.user, request)) + + +class TestIsEntityManager(TestCase): + @classmethod + def setUpTestData(cls): + cls.sector_entity = EntityVersionFactory(acronym='SSH') + + cls.faculty_entity_1 = EntityVersionFactory(acronym='LSM', parent=cls.sector_entity.entity) + cls.faculty_entity_2 = EntityVersionFactory(acronym='AGRO', parent=cls.sector_entity.entity) + cls.faculty_entity_3 = EntityVersionFactory(acronym='LOCI', parent=cls.sector_entity.entity) + + cls.school_entity_1 = EntityVersionFactory(acronym='CLSM', parent=cls.faculty_entity_1.entity) + + def setUp(self): + self.predicate_context_mock = mock.patch( + "rules.Predicate.context", + new_callable=mock.PropertyMock, + return_value={ + 'perm_name': 'dummy_perm', + 'role_qs': CentralManager.objects.none(), + }, + ) + self.mock = self.predicate_context_mock.start() + self.addCleanup(self.predicate_context_mock.stop) + + def test_is_entity_manager_depending_on_the_training_management_entity(self): + entity_manager = CentralManagerRoleFactory( + entity=self.faculty_entity_1.entity, + scopes=[Scope.GENERAL.name, Scope.DOCTORAT.name, Scope.IUFC.name], + with_child=False, + ) + + entity_manager_user = entity_manager.person.user + + self.mock.return_value['role_qs'] = CentralManager.objects.filter(person=entity_manager.person) + + # Correct with direct entity + admission = GeneralEducationAdmissionFactory( + training__management_entity=self.faculty_entity_1.entity, + ) + + self.assertTrue(is_scoped_entity_manager(entity_manager_user, admission)) + + # Wrong with direct entity + admission.training.management_entity = self.faculty_entity_2.entity + admission.training.save(update_fields=['management_entity']) + self.assertFalse(is_scoped_entity_manager(entity_manager_user, admission)) + + # Wrong with child entity + admission.training.management_entity = self.school_entity_1.entity + admission.training.save(update_fields=['management_entity']) + self.assertFalse(is_scoped_entity_manager(entity_manager_user, admission)) + + # Correct with child entity + self.mock.return_value['role_qs'] = CentralManager.objects.filter(person=entity_manager.person) + entity_manager_user = User.objects.get(pk=entity_manager_user.pk) + + entity_manager.with_child = True + entity_manager.save(update_fields=['with_child']) + self.assertTrue(is_scoped_entity_manager(entity_manager_user, admission)) + + def test_is_entity_manager_depending_on_the_scope(self): + entity_manager = CentralManagerRoleFactory( + entity=self.faculty_entity_1.entity, + scopes=[], + with_child=False, + ) + + other_role = CentralManagerRoleFactory( + person=entity_manager.person, + scopes=[Scope.GENERAL.name, Scope.DOCTORAT.name, Scope.IUFC.name], + with_child=False, + entity=self.school_entity_1.entity, + ) + + entity_manager_user = entity_manager.person.user + + self.mock.return_value['role_qs'] = CentralManager.objects.filter(person=entity_manager.person) + + # General education admission + admission = GeneralEducationAdmissionFactory( + training__management_entity=self.faculty_entity_1.entity, + ) + + entity_manager_user = User.objects.get(pk=entity_manager_user.pk) + self.assertFalse(is_scoped_entity_manager(entity_manager_user, admission)) + + entity_manager.scopes = [Scope.GENERAL.name] + entity_manager.save(update_fields=['scopes']) + entity_manager_user = User.objects.get(pk=entity_manager_user.pk) + self.assertTrue(is_scoped_entity_manager(entity_manager_user, admission)) + + entity_manager.scopes = [Scope.DOCTORAT.name] + entity_manager.save(update_fields=['scopes']) + entity_manager_user = User.objects.get(pk=entity_manager_user.pk) + self.assertFalse(is_scoped_entity_manager(entity_manager_user, admission)) + + entity_manager.scopes = [Scope.IUFC.name] + entity_manager.save(update_fields=['scopes']) + entity_manager_user = User.objects.get(pk=entity_manager_user.pk) + self.assertFalse(is_scoped_entity_manager(entity_manager_user, admission)) + + # Doctorate education admission + entity_manager.scopes = [] + entity_manager.save(update_fields=['scopes']) + entity_manager_user = User.objects.get(pk=entity_manager_user.pk) + + admission = DoctorateAdmissionFactory( + training__management_entity=self.faculty_entity_1.entity, + ) + + entity_manager_user = User.objects.get(pk=entity_manager_user.pk) + self.assertFalse(is_scoped_entity_manager(entity_manager_user, admission)) + + entity_manager.scopes = [Scope.GENERAL.name] + entity_manager.save(update_fields=['scopes']) + entity_manager_user = User.objects.get(pk=entity_manager_user.pk) + self.assertFalse(is_scoped_entity_manager(entity_manager_user, admission)) + + entity_manager.scopes = [Scope.DOCTORAT.name] + entity_manager.save(update_fields=['scopes']) + entity_manager_user = User.objects.get(pk=entity_manager_user.pk) + self.assertTrue(is_scoped_entity_manager(entity_manager_user, admission)) + + entity_manager.scopes = [Scope.IUFC.name] + entity_manager.save(update_fields=['scopes']) + entity_manager_user = User.objects.get(pk=entity_manager_user.pk) + self.assertFalse(is_scoped_entity_manager(entity_manager_user, admission)) + + # Continuing education admission + entity_manager.scopes = [] + entity_manager.save(update_fields=['scopes']) + entity_manager_user = User.objects.get(pk=entity_manager_user.pk) + + admission = ContinuingEducationAdmissionFactory( + training__management_entity=self.faculty_entity_1.entity, + ) + + entity_manager_user = User.objects.get(pk=entity_manager_user.pk) + self.assertFalse(is_scoped_entity_manager(entity_manager_user, admission)) + + entity_manager.scopes = [Scope.GENERAL.name] + entity_manager.save(update_fields=['scopes']) + entity_manager_user = User.objects.get(pk=entity_manager_user.pk) + self.assertFalse(is_scoped_entity_manager(entity_manager_user, admission)) + + entity_manager.scopes = [Scope.DOCTORAT.name] + entity_manager.save(update_fields=['scopes']) + entity_manager_user = User.objects.get(pk=entity_manager_user.pk) + self.assertFalse(is_scoped_entity_manager(entity_manager_user, admission)) + + entity_manager.scopes = [Scope.IUFC.name] + entity_manager.save(update_fields=['scopes']) + entity_manager_user = User.objects.get(pk=entity_manager_user.pk) + self.assertTrue(is_scoped_entity_manager(entity_manager_user, admission)) diff --git a/tests/contrib/models/test_base.py b/tests/contrib/models/test_base.py index c5ae880ec..4c3230b14 100644 --- a/tests/contrib/models/test_base.py +++ b/tests/contrib/models/test_base.py @@ -29,19 +29,29 @@ from django.db import IntegrityError from django.test import TestCase +from admission.constants import CONTEXT_GENERAL, CONTEXT_CONTINUING, CONTEXT_DOCTORATE from admission.contrib.models import AdmissionViewer, ContinuingEducationAdmissionProxy from admission.contrib.models.base import admission_directory_path, BaseAdmission +from admission.ddd.admission.doctorat.preparation.domain.model.enums import ChoixStatutPropositionDoctorale +from admission.ddd.admission.formation_continue.domain.model.enums import ChoixStatutPropositionContinue from admission.ddd.admission.formation_generale.domain.model.enums import ChoixStatutPropositionGenerale +from admission.infrastructure.admission.domain.service.annee_inscription_formation import ( + continuing_education_types_as_set, + doctorate_types_as_set, +) from admission.tests.factories import DoctorateAdmissionFactory from admission.tests.factories.admission_viewer import AdmissionViewerFactory from admission.tests.factories.continuing_education import ContinuingEducationAdmissionFactory from admission.tests.factories.general_education import GeneralEducationAdmissionFactory from base.models.entity_version import EntityVersion +from base.models.enums.education_group_types import TrainingType, AllTypes from base.models.enums.entity_type import EntityType from base.models.person_merge_proposal import PersonMergeProposal, PersonMergeStatus from base.tests.factories.academic_year import AcademicYearFactory +from base.tests.factories.education_group_type import EducationGroupTypeFactory from base.tests.factories.entity_version import MainEntityVersionFactory, EntityVersionFactory from base.tests.factories.person import PersonFactory +from epc.tests.factories.inscription_programme_annuel import InscriptionProgrammeAnnuelFactory class BaseTestCase(TestCase): @@ -264,3 +274,152 @@ def test_get_formatted_reference_depending_on_academic_year(self): admission = BaseAdmission.objects.with_training_management_and_reference().get(uuid=created_admission.uuid) self.assertEqual(admission.formatted_reference, reference % {'year': '21'}) + + +class OtherCandidateTrainingsTestCase(TestCase): + @classmethod + def setUpTestData(cls): + cls.admission = GeneralEducationAdmissionFactory( + training__education_group_type__name=TrainingType.BACHELOR.name, + ) + + def setUp(self): + try: + delattr(self.admission, 'other_candidate_trainings') + except AttributeError: + pass + + def test_by_excluding_the_current_admission(self): + # Don't use the current admission + other_contexts = self.admission.other_candidate_trainings + + self.assertEqual(other_contexts[CONTEXT_GENERAL], set()) + self.assertEqual(other_contexts[CONTEXT_CONTINUING], set()) + self.assertEqual(other_contexts[CONTEXT_DOCTORATE], set()) + + def test_with_general_admission(self): + excluding_statuses = { + ChoixStatutPropositionGenerale.EN_BROUILLON, + ChoixStatutPropositionGenerale.ANNULEE, + } + + other_admission = GeneralEducationAdmissionFactory( + training__education_group_type__name=TrainingType.MASTER_MC.name, + candidate=self.admission.candidate, + ) + + for status in ChoixStatutPropositionGenerale: + other_admission.status = status.name + other_admission.save(update_fields=['status']) + + other_contexts = self.admission.other_candidate_trainings + + self.assertEqual( + other_contexts[CONTEXT_GENERAL], + {TrainingType.MASTER_MC.name} if status not in excluding_statuses else set(), + ) + self.assertEqual(other_contexts[CONTEXT_CONTINUING], set()) + self.assertEqual(other_contexts[CONTEXT_DOCTORATE], set()) + + delattr(self.admission, 'other_candidate_trainings') + + def test_with_doctorate_admission(self): + excluding_statuses = { + ChoixStatutPropositionDoctorale.EN_BROUILLON, + ChoixStatutPropositionDoctorale.ANNULEE, + } + + other_admission = DoctorateAdmissionFactory( + training__education_group_type__name=TrainingType.PHD.name, + candidate=self.admission.candidate, + ) + + for status in ChoixStatutPropositionDoctorale: + other_admission.status = status.name + other_admission.save(update_fields=['status']) + + other_contexts = self.admission.other_candidate_trainings + + self.assertEqual( + other_contexts[CONTEXT_DOCTORATE], + {TrainingType.PHD.name} if status not in excluding_statuses else set(), + ) + self.assertEqual(other_contexts[CONTEXT_GENERAL], set()) + self.assertEqual(other_contexts[CONTEXT_CONTINUING], set()) + + delattr(self.admission, 'other_candidate_trainings') + + def test_with_continuing_admission(self): + excluding_statuses = { + ChoixStatutPropositionContinue.EN_BROUILLON, + ChoixStatutPropositionContinue.ANNULEE, + } + + other_admission = ContinuingEducationAdmissionFactory( + training__education_group_type__name=TrainingType.CERTIFICATE_OF_SUCCESS.name, + candidate=self.admission.candidate, + ) + + for status in ChoixStatutPropositionContinue: + other_admission.status = status.name + other_admission.save(update_fields=['status']) + + other_contexts = self.admission.other_candidate_trainings + + self.assertEqual( + other_contexts[CONTEXT_CONTINUING], + {TrainingType.CERTIFICATE_OF_SUCCESS.name} if status not in excluding_statuses else set(), + ) + self.assertEqual(other_contexts[CONTEXT_GENERAL], set()) + self.assertEqual(other_contexts[CONTEXT_DOCTORATE], set()) + + delattr(self.admission, 'other_candidate_trainings') + + def test_with_internal_experiences(self): + internal_experience = InscriptionProgrammeAnnuelFactory( + programme_cycle__etudiant__person=self.admission.candidate, + programme__root_group__education_group_type__name=TrainingType.MASTER_M1.name, + ) + + other_contexts = self.admission.other_candidate_trainings + self.assertEqual(other_contexts[CONTEXT_CONTINUING], set()) + self.assertEqual(other_contexts[CONTEXT_GENERAL], {TrainingType.MASTER_M1.name}) + self.assertEqual(other_contexts[CONTEXT_DOCTORATE], set()) + + delattr(self.admission, 'other_candidate_trainings') + + internal_experience.programme = None + internal_experience.save(update_fields=['programme']) + + other_contexts = self.admission.other_candidate_trainings + self.assertEqual(other_contexts[CONTEXT_CONTINUING], set()) + self.assertEqual(other_contexts[CONTEXT_GENERAL], set()) + self.assertEqual(other_contexts[CONTEXT_DOCTORATE], set()) + + def test_right_context_by_training_type(self): + internal_experience = InscriptionProgrammeAnnuelFactory( + programme_cycle__etudiant__person=self.admission.candidate, + ) + + root_group = internal_experience.programme.root_group + + for training_type in AllTypes.get_names(): + root_group.education_group_type = EducationGroupTypeFactory(name=training_type) + root_group.save(update_fields=['education_group_type']) + + other_contexts = self.admission.other_candidate_trainings + + if training_type in continuing_education_types_as_set: + self.assertEqual(other_contexts[CONTEXT_CONTINUING], {training_type}) + self.assertEqual(other_contexts[CONTEXT_GENERAL], set()) + self.assertEqual(other_contexts[CONTEXT_DOCTORATE], set()) + elif training_type in doctorate_types_as_set: + self.assertEqual(other_contexts[CONTEXT_DOCTORATE], {training_type}) + self.assertEqual(other_contexts[CONTEXT_GENERAL], set()) + self.assertEqual(other_contexts[CONTEXT_CONTINUING], set()) + else: + self.assertEqual(other_contexts[CONTEXT_GENERAL], {training_type}) + self.assertEqual(other_contexts[CONTEXT_CONTINUING], set()) + self.assertEqual(other_contexts[CONTEXT_DOCTORATE], set()) + + delattr(self.admission, 'other_candidate_trainings') diff --git a/tests/factories/roles.py b/tests/factories/roles.py index 66d0e80cf..83eec8790 100644 --- a/tests/factories/roles.py +++ b/tests/factories/roles.py @@ -6,7 +6,7 @@ # The core business involves the administration of students, teachers, # courses, programs and so on. # -# Copyright (C) 2015-2023 Université catholique de Louvain (http://www.uclouvain.be) +# Copyright (C) 2015-2024 Université catholique de Louvain (http://www.uclouvain.be) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -35,7 +35,7 @@ from admission.auth.roles.program_manager import ProgramManager from admission.auth.roles.promoter import Promoter from admission.auth.roles.sic_management import SicManagement -from education_group.auth.scope import Scope +from admission.auth.scope import Scope from osis_role.contrib.tests.factories import EducationGroupRoleModelFactory @@ -92,7 +92,7 @@ class Meta: 'base.tests.factories.entity.EntityWithVersionFactory', organization=None, ) - scopes = [Scope.ALL.name, Scope.DOCTORAT.name, Scope.IUFC.name] + scopes = [Scope.GENERAL.name, Scope.DOCTORAT.name, Scope.IUFC.name] with_child = True diff --git a/tests/views/autocomplete/test_trainings.py b/tests/views/autocomplete/test_trainings.py index 48ee76dce..870da9b40 100644 --- a/tests/views/autocomplete/test_trainings.py +++ b/tests/views/autocomplete/test_trainings.py @@ -37,7 +37,7 @@ from base.tests.factories.academic_year import AcademicYearFactory from base.tests.factories.entity import EntityWithVersionFactory from base.tests.factories.user import UserFactory -from education_group.auth.scope import Scope +from admission.auth.scope import Scope from program_management.models.education_group_version import EducationGroupVersion diff --git a/tests/views/common/detail_tabs/curriculum/test_educational_experience.py b/tests/views/common/detail_tabs/curriculum/test_educational_experience.py index c80c6a562..6635243d6 100644 --- a/tests/views/common/detail_tabs/curriculum/test_educational_experience.py +++ b/tests/views/common/detail_tabs/curriculum/test_educational_experience.py @@ -67,33 +67,43 @@ def setUpTestData(cls): cls.sic_manager_user = SicManagementRoleFactory(entity=cls.entity).person.user cls.candidate = CandidateFactory().person + cls.other_candidate = CandidateFactory().person cls.educational_experience: EducationalExperience = EducationalExperienceFactory( person=cls.candidate, linguistic_regime=cls.linguistic_regime, country=cls.be_country, ) + cls.other_educational_experience: EducationalExperience = EducationalExperienceFactory( + person=cls.other_candidate, + linguistic_regime=cls.linguistic_regime, + country=cls.be_country, + ) cls.educational_experience_year: EducationalExperienceYear = EducationalExperienceYearFactory( educational_experience=cls.educational_experience, academic_year=academic_years[0], ) + cls.other_educational_experience_year: EducationalExperienceYear = EducationalExperienceYearFactory( + educational_experience=cls.other_educational_experience, + academic_year=academic_years[0], + ) - cls.continuing_admission: ContinuingEducationAdmission = ContinuingEducationAdmissionFactory( + cls.other_continuing_admission: ContinuingEducationAdmission = ContinuingEducationAdmissionFactory( training__management_entity=cls.entity, training__academic_year=academic_years[0], status=ChoixStatutPropositionContinue.CONFIRMEE.name, - candidate=cls.candidate, + candidate=cls.other_candidate, ) - cls.continuing_program_manager_user = ProgramManagerRoleFactory( - education_group=cls.continuing_admission.training.education_group, + cls.other_continuing_program_manager_user = ProgramManagerRoleFactory( + education_group=cls.other_continuing_admission.training.education_group, ).person.user - cls.continuing_url = resolve_url( + cls.other_continuing_url = resolve_url( 'admission:continuing-education:curriculum:educational', - uuid=cls.continuing_admission.uuid, - experience_uuid=cls.educational_experience.uuid, + uuid=cls.other_continuing_admission.uuid, + experience_uuid=cls.other_educational_experience.uuid, ) cls.general_admission: GeneralEducationAdmission = GeneralEducationAdmissionFactory( @@ -154,35 +164,74 @@ def test_general_with_sic_manager(self): self.assertEqual(response.status_code, 200) def test_continuing_with_program_manager(self): - self.client.force_login(user=self.continuing_program_manager_user) - response = self.client.get(self.continuing_url) + self.client.force_login(user=self.other_continuing_program_manager_user) + response = self.client.get(self.other_continuing_url) self.assertEqual(response.status_code, 200) + self.assertEqual( + response.context['edit_url'], + f'/admissions/continuing-education/{self.other_continuing_admission.uuid}/update/curriculum/educational/' + f'{self.other_educational_experience.uuid}', + ) def test_continuing_with_sic_manager(self): self.client.force_login(user=self.sic_manager_user) - response = self.client.get(self.continuing_url) + response = self.client.get(self.other_continuing_url) self.assertEqual(response.status_code, 200) experience = response.context['experience'] self.assertIsInstance(experience, ExperienceAcademiqueDTO) - self.assertEqual(experience.uuid, self.educational_experience.uuid) + self.assertEqual(experience.uuid, self.other_educational_experience.uuid) self.assertFalse(response.context['translation_required']) self.assertFalse(response.context['is_foreign_experience']) self.assertFalse(response.context['evaluation_system_with_credits']) self.assertTrue(response.context['is_belgian_experience']) self.assertEqual( response.context['edit_url'], - f'/admissions/continuing-education/{self.continuing_admission.uuid}/update/curriculum/educational/' - f'{self.educational_experience.uuid}', + f'/admissions/continuing-education/{self.other_continuing_admission.uuid}/update/curriculum/educational/' + f'{self.other_educational_experience.uuid}', + ) + + def test_continuing_with_blocking_admissions(self): + self.client.force_login(user=self.sic_manager_user) + + doctorate_admission = DoctorateAdmissionFactory( + candidate=self.other_candidate, + submitted=True, ) + response = self.client.get(self.other_continuing_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['edit_url'], '') + + self.client.force_login(user=self.other_continuing_program_manager_user) + response = self.client.get(self.other_continuing_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['edit_url'], '') + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=self.other_candidate, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, + ) + + response = self.client.get(self.other_continuing_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['edit_url'], '') + + self.client.force_login(user=self.sic_manager_user) + + response = self.client.get(self.other_continuing_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['edit_url'], '') + def test_continuing_with_unknown_experience(self): self.client.force_login(user=self.sic_manager_user) response = self.client.get( resolve_url( 'admission:continuing-education:curriculum:educational', - uuid=self.continuing_admission.uuid, + uuid=self.other_continuing_admission.uuid, experience_uuid=uuid.uuid4(), ) ) diff --git a/tests/views/common/detail_tabs/curriculum/test_non_educational_experience.py b/tests/views/common/detail_tabs/curriculum/test_non_educational_experience.py index 7096adce4..5bf58d9aa 100644 --- a/tests/views/common/detail_tabs/curriculum/test_non_educational_experience.py +++ b/tests/views/common/detail_tabs/curriculum/test_non_educational_experience.py @@ -63,26 +63,30 @@ def setUpTestData(cls): cls.sic_manager_user = SicManagementRoleFactory(entity=cls.entity).person.user cls.candidate = CandidateFactory().person + cls.other_candidate = CandidateFactory().person cls.experience: ProfessionalExperience = ProfessionalExperienceFactory( person=cls.candidate, ) + cls.other_experience: ProfessionalExperience = ProfessionalExperienceFactory( + person=cls.other_candidate, + ) - cls.continuing_admission: ContinuingEducationAdmission = ContinuingEducationAdmissionFactory( + cls.other_continuing_admission: ContinuingEducationAdmission = ContinuingEducationAdmissionFactory( training__management_entity=cls.entity, training__academic_year=academic_years[0], status=ChoixStatutPropositionContinue.CONFIRMEE.name, - candidate=cls.candidate, + candidate=cls.other_candidate, ) - cls.continuing_program_manager_user = ProgramManagerRoleFactory( - education_group=cls.continuing_admission.training.education_group, + cls.other_continuing_program_manager_user = ProgramManagerRoleFactory( + education_group=cls.other_continuing_admission.training.education_group, ).person.user - cls.continuing_url = resolve_url( + cls.other_continuing_url = resolve_url( 'admission:continuing-education:curriculum:non_educational', - uuid=cls.continuing_admission.uuid, - experience_uuid=cls.experience.uuid, + uuid=cls.other_continuing_admission.uuid, + experience_uuid=cls.other_experience.uuid, ) cls.doctorate_admission: DoctorateAdmission = DoctorateAdmissionFactory( @@ -143,33 +147,74 @@ def test_general_with_sic_manager(self): self.assertEqual(response.status_code, 200) def test_continuing_with_program_manager(self): - self.client.force_login(user=self.continuing_program_manager_user) - response = self.client.get(self.continuing_url) + self.client.force_login(user=self.other_continuing_program_manager_user) + response = self.client.get(self.other_continuing_url) self.assertEqual(response.status_code, 200) + self.assertEqual( + response.context['edit_url'], + f'/admissions/continuing-education/{self.other_continuing_admission.uuid}/update/curriculum/' + f'non_educational/' + f'{self.other_experience.uuid}', + ) def test_continuing_with_sic_manager(self): self.client.force_login(user=self.sic_manager_user) - response = self.client.get(self.continuing_url) + response = self.client.get(self.other_continuing_url) self.assertEqual(response.status_code, 200) experience = response.context['experience'] self.assertIsInstance(experience, ExperienceNonAcademiqueDTO) - self.assertEqual(experience.uuid, self.experience.uuid) + self.assertEqual(experience.uuid, self.other_experience.uuid) self.assertEqual( response.context['edit_url'], - f'/admissions/continuing-education/{self.continuing_admission.uuid}/update/curriculum/non_educational/' - f'{self.experience.uuid}', + f'/admissions/continuing-education/{self.other_continuing_admission.uuid}/update/curriculum/' + f'non_educational/' + f'{self.other_experience.uuid}', + ) + + def test_continuing_with_blocking_admissions(self): + self.client.force_login(user=self.sic_manager_user) + + doctorate_admission = DoctorateAdmissionFactory( + candidate=self.other_candidate, + submitted=True, + ) + + response = self.client.get(self.other_continuing_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['edit_url'], '') + + self.client.force_login(user=self.other_continuing_program_manager_user) + response = self.client.get(self.other_continuing_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['edit_url'], '') + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=self.other_candidate, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, ) + response = self.client.get(self.other_continuing_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['edit_url'], '') + + self.client.force_login(user=self.sic_manager_user) + + response = self.client.get(self.other_continuing_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['edit_url'], '') + def test_continuing_with_unknown_experience(self): self.client.force_login(user=self.sic_manager_user) response = self.client.get( resolve_url( 'admission:continuing-education:curriculum:non_educational', - uuid=self.continuing_admission.uuid, + uuid=self.other_continuing_admission.uuid, experience_uuid=uuid.uuid4(), ) ) diff --git a/tests/views/common/detail_tabs/test_person.py b/tests/views/common/detail_tabs/test_person.py index 24de24b61..10f04ed14 100644 --- a/tests/views/common/detail_tabs/test_person.py +++ b/tests/views/common/detail_tabs/test_person.py @@ -29,6 +29,7 @@ from django.test import TestCase from django.utils.translation import gettext_lazy as _ +from admission.auth.scope import Scope from admission.contrib.models import ContinuingEducationAdmission, DoctorateAdmission, GeneralEducationAdmission from admission.ddd.admission.doctorat.preparation.domain.model.doctorat import ENTITY_CDE from admission.ddd.admission.dtos.profil_candidat import ProfilCandidatDTO @@ -139,6 +140,38 @@ def test_continuing_person_detail_sic_manager(self): self.assertEqual(response.context['person'], self.continuing_admission.candidate) self.assertEqual(response.context['contact_language'], _('French')) + def test_person_detail_depending_on_the_sic_manager_scope(self): + entity_manager = CentralManagerRoleFactory( + entity=self.continuing_admission.training.management_entity, + scopes=[], + ) + + self.client.force_login(user=entity_manager.person.user) + + for invalid_scope in [ + [], + [Scope.GENERAL.name], + [Scope.DOCTORAT.name], + [Scope.GENERAL.name, Scope.DOCTORAT.name], + ]: + entity_manager.scopes = invalid_scope + entity_manager.save(update_fields=['scopes']) + + response = self.client.get(self.continuing_url) + self.assertEqual(response.status_code, 403) + + for valid_scope in [ + [Scope.IUFC.name], + [Scope.IUFC.name, Scope.GENERAL.name], + [Scope.IUFC.name, Scope.DOCTORAT.name], + [Scope.IUFC.name, Scope.GENERAL.name, Scope.DOCTORAT.name], + ]: + entity_manager.scopes = valid_scope + entity_manager.save(update_fields=['scopes']) + + response = self.client.get(self.continuing_url) + self.assertEqual(response.status_code, 200) + def test_general_person_detail_program_manager(self): self.client.force_login(user=self.general_program_manager_user) diff --git a/tests/views/common/form_tabs/curriculum/educational_experience_update/test_continuing.py b/tests/views/common/form_tabs/curriculum/educational_experience_update/test_continuing.py index 7c3b63852..0806ebca4 100644 --- a/tests/views/common/form_tabs/curriculum/educational_experience_update/test_continuing.py +++ b/tests/views/common/form_tabs/curriculum/educational_experience_update/test_continuing.py @@ -35,11 +35,13 @@ from admission.constants import CONTEXT_CONTINUING from admission.contrib.models import ContinuingEducationAdmission from admission.ddd.admission.formation_continue.domain.model.enums import ChoixStatutPropositionContinue +from admission.tests.factories import DoctorateAdmissionFactory from admission.tests.factories.continuing_education import ContinuingEducationAdmissionFactory from admission.tests.factories.curriculum import ( EducationalExperienceFactory, EducationalExperienceYearFactory, ) +from admission.tests.factories.general_education import GeneralEducationAdmissionFactory from admission.tests.factories.roles import SicManagementRoleFactory, ProgramManagerRoleFactory from base.forms.utils.file_field import PDF_MIME_TYPE from base.models.campus import Campus @@ -206,16 +208,50 @@ def setUp(self): uuid=self.continuing_admission.uuid, ) - def test_update_curriculum_is_allowed_for_fac_users(self): + def test_update_curriculum_for_fac_users(self): self.client.force_login(self.program_manager_user) + response = self.client.get(self.form_url) self.assertEqual(response.status_code, status.HTTP_200_OK) + doctorate_admission = DoctorateAdmissionFactory( + candidate=self.continuing_admission.candidate, + submitted=True, + ) + response = self.client.get(self.form_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=self.continuing_admission.candidate, + status=ChoixStatutPropositionContinue.CONFIRMEE.name, + ) + response = self.client.get(self.form_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_update_curriculum_is_allowed_for_sic_users(self): self.client.force_login(self.sic_manager_user) + response = self.client.get(self.form_url) self.assertEqual(response.status_code, status.HTTP_200_OK) + doctorate_admission = DoctorateAdmissionFactory( + candidate=self.continuing_admission.candidate, + submitted=True, + ) + response = self.client.get(self.form_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=self.continuing_admission.candidate, + status=ChoixStatutPropositionContinue.CONFIRMEE.name, + ) + response = self.client.get(self.form_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_form_initialization(self): self.client.force_login(self.sic_manager_user) diff --git a/tests/views/common/form_tabs/curriculum/global/test_continuing.py b/tests/views/common/form_tabs/curriculum/global/test_continuing.py index 3afb426cd..b0051b7ca 100644 --- a/tests/views/common/form_tabs/curriculum/global/test_continuing.py +++ b/tests/views/common/form_tabs/curriculum/global/test_continuing.py @@ -36,6 +36,7 @@ from admission.ddd.admission.enums import Onglets from admission.ddd.admission.formation_continue.domain.model.enums import ChoixStatutPropositionContinue from admission.forms import REQUIRED_FIELD_CLASS +from admission.tests.factories import DoctorateAdmissionFactory from admission.tests.factories.continuing_education import ( ContinuingEducationTrainingFactory, ContinuingEducationAdmissionFactory, @@ -45,6 +46,7 @@ ) from admission.tests.factories.curriculum import EducationalExperienceFactory, EducationalExperienceYearFactory from admission.tests.factories.form_item import TextAdmissionFormItemFactory, AdmissionFormItemInstantiationFactory +from admission.tests.factories.general_education import GeneralEducationAdmissionFactory from admission.tests.factories.roles import SicManagementRoleFactory, ProgramManagerRoleFactory, CandidateFactory from base.forms.utils.file_field import PDF_MIME_TYPE from base.models.enums.education_group_types import TrainingType @@ -192,6 +194,16 @@ def test_update_global_curriculum_is_allowed_for_fac_users(self): self.assertEqual(response.status_code, status.HTTP_200_OK) + GeneralEducationAdmissionFactory(candidate=self.continuing_admission_with_attachments.candidate) + + response = self.client.get(self.form_url_with_attachments) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + DoctorateAdmissionFactory(candidate=self.continuing_admission_with_attachments.candidate) + + response = self.client.get(self.form_url_with_attachments) + self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_update_education_is_allowed_for_sic_users(self): self.client.force_login(self.sic_manager_user) response = self.client.get(self.form_url_with_attachments) diff --git a/tests/views/common/form_tabs/curriculum/test_educational_experience_delete.py b/tests/views/common/form_tabs/curriculum/test_educational_experience_delete.py index 437f0979f..e529403f4 100644 --- a/tests/views/common/form_tabs/curriculum/test_educational_experience_delete.py +++ b/tests/views/common/form_tabs/curriculum/test_educational_experience_delete.py @@ -35,18 +35,24 @@ from django.utils.translation import gettext from rest_framework import status -from admission.contrib.models import EPCInjection as AdmissionEPCInjection, DoctorateAdmission +from admission.contrib.models import ( + EPCInjection as AdmissionEPCInjection, + DoctorateAdmission, + ContinuingEducationAdmission, +) from admission.contrib.models.base import AdmissionEducationalValuatedExperiences from admission.contrib.models.epc_injection import EPCInjectionType, EPCInjectionStatus as AdmissionEPCInjectionStatus from admission.contrib.models.general_education import GeneralEducationAdmission from admission.ddd.admission.doctorat.preparation.domain.model.doctorat import ENTITY_CDE from admission.ddd.admission.doctorat.preparation.domain.model.enums import ChoixStatutPropositionDoctorale from admission.ddd.admission.enums.emplacement_document import OngletsDemande +from admission.ddd.admission.formation_continue.domain.model.enums import ChoixStatutPropositionContinue from admission.ddd.admission.formation_generale.domain.model.enums import ( ChoixStatutPropositionGenerale, ) from admission.ddd.admission.formation_generale.domain.service.checklist import Checklist from admission.tests.factories import DoctorateAdmissionFactory +from admission.tests.factories.continuing_education import ContinuingEducationAdmissionFactory from admission.tests.factories.curriculum import ( EducationalExperienceFactory, EducationalExperienceYearFactory, @@ -104,6 +110,12 @@ def setUpTestData(cls): submitted=True, ) + cls.other_continuing_admission: ContinuingEducationAdmission = ContinuingEducationAdmissionFactory( + training__management_entity=first_doctoral_commission, + training__academic_year=cls.academic_years[0], + status=ChoixStatutPropositionContinue.CONFIRMEE.name, + ) + # Create users cls.sic_manager_user = SicManagementRoleFactory(entity=first_doctoral_commission).person.user cls.program_manager_user = ProgramManagerRoleFactory( @@ -112,6 +124,9 @@ def setUpTestData(cls): cls.doctorate_program_manager_user = ProgramManagerRoleFactory( education_group=cls.doctorate_admission.training.education_group, ).person.user + cls.other_continuing_program_manager_user = ProgramManagerRoleFactory( + education_group=cls.other_continuing_admission.training.education_group, + ).person.user cls.first_cycle_diploma = DiplomaTitleFactory( cycle=Cycle.FIRST_CYCLE.name, ) @@ -364,12 +379,103 @@ def test_delete_experience_from_doctorate_curriculum_is_not_allowed_for_fac_user ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_delete_experience_from_doctorate_curriculum_is_allowed_for_sic_users(self): + def test_delete_experience_from_doctorate_curriculum_is_not_allowed_for_sic_users(self): self.client.force_login(self.sic_manager_user) response = self.client.delete(self.doctorate_delete_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_experience_from_continuing_curriculum_for_fac_users(self): + self.client.force_login(self.other_continuing_program_manager_user) + + experience = EducationalExperienceFactory( + person=self.other_continuing_admission.candidate, + ) + + url = resolve_url( + 'admission:continuing-education:update:curriculum:educational_delete', + uuid=self.other_continuing_admission.uuid, + experience_uuid=experience.uuid, + ) + + doctorate_admission = DoctorateAdmissionFactory( + candidate=experience.person, + submitted=True, + ) + + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=experience.person, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, + ) + + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + general_admission.delete() + + response = self.client.delete(url) + + base_curriculum_url = resolve_url( + 'admission:continuing-education:curriculum', + uuid=self.other_continuing_admission.uuid, + ) self.assertRedirects( response=response, fetch_redirect_response=False, - expected_url=resolve_url('admission:doctorate:checklist', uuid=self.doctorate_admission.uuid), + expected_url=base_curriculum_url, ) + + self.assertFalse(EducationalExperience.objects.filter(uuid=experience.uuid).exists()) + + def test_delete_experience_from_continuing_curriculum_for_sic_users(self): + self.client.force_login(self.sic_manager_user) + + experience = EducationalExperienceFactory( + person=self.other_continuing_admission.candidate, + ) + + url = resolve_url( + 'admission:continuing-education:update:curriculum:educational_delete', + uuid=self.other_continuing_admission.uuid, + experience_uuid=experience.uuid, + ) + + doctorate_admission = DoctorateAdmissionFactory( + candidate=experience.person, + submitted=True, + ) + + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=experience.person, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, + ) + + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + general_admission.delete() + + response = self.client.delete(url) + + base_curriculum_url = resolve_url( + 'admission:continuing-education:curriculum', + uuid=self.other_continuing_admission.uuid, + ) + + self.assertRedirects( + response=response, + fetch_redirect_response=False, + expected_url=base_curriculum_url, + ) + + self.assertFalse(EducationalExperience.objects.filter(uuid=experience.uuid).exists()) diff --git a/tests/views/common/form_tabs/curriculum/test_educational_experience_duplicate.py b/tests/views/common/form_tabs/curriculum/test_educational_experience_duplicate.py index 97bc76c5b..04bf9426b 100644 --- a/tests/views/common/form_tabs/curriculum/test_educational_experience_duplicate.py +++ b/tests/views/common/form_tabs/curriculum/test_educational_experience_duplicate.py @@ -35,16 +35,18 @@ from django.test import TestCase from rest_framework import status -from admission.contrib.models import DoctorateAdmission +from admission.contrib.models import DoctorateAdmission, ContinuingEducationAdmission from admission.contrib.models.base import AdmissionEducationalValuatedExperiences from admission.contrib.models.general_education import GeneralEducationAdmission from admission.ddd.admission.doctorat.preparation.domain.model.doctorat import ENTITY_CDE from admission.ddd.admission.doctorat.preparation.domain.model.enums import ChoixStatutPropositionDoctorale +from admission.ddd.admission.formation_continue.domain.model.enums import ChoixStatutPropositionContinue from admission.ddd.admission.formation_generale.domain.model.enums import ( ChoixStatutPropositionGenerale, ) from admission.ddd.admission.formation_generale.domain.service.checklist import Checklist from admission.tests.factories import DoctorateAdmissionFactory +from admission.tests.factories.continuing_education import ContinuingEducationAdmissionFactory from admission.tests.factories.curriculum import ( EducationalExperienceFactory, EducationalExperienceYearFactory, @@ -93,6 +95,12 @@ def setUpTestData(cls): submitted=True, ) + cls.other_continuing_admission: ContinuingEducationAdmission = ContinuingEducationAdmissionFactory( + training__management_entity=first_doctoral_commission, + training__academic_year=cls.academic_years[0], + status=ChoixStatutPropositionContinue.CONFIRMEE.name, + ) + cls.be_country = CountryFactory(iso_code='BE', name='Belgique', name_en='Belgium') cls.fr_country = CountryFactory(iso_code='FR', name='France', name_en='France') @@ -120,6 +128,9 @@ def setUpTestData(cls): cls.doctorate_program_manager_user = ProgramManagerRoleFactory( education_group=cls.doctorate_admission.training.education_group, ).person.user + cls.other_continuing_program_manager_user = ProgramManagerRoleFactory( + education_group=cls.other_continuing_admission.training.education_group, + ).person.user cls.files_uuids = [uuid.uuid4() for _ in range(9)] cls.files_uuids_str = [str(current_uuid) for current_uuid in cls.files_uuids] @@ -537,3 +548,95 @@ def test_duplicate_experience_from_doctorate_curriculum_is_allowed_for_sic_users fetch_redirect_response=False, expected_url=resolve_url('admission:doctorate:checklist', uuid=self.doctorate_admission.uuid), ) + + def test_duplicate_experience_from_continuing_curriculum_for_fac_users(self): + self.client.force_login(self.other_continuing_program_manager_user) + + experience = EducationalExperienceFactory( + person=self.other_continuing_admission.candidate, + ) + + url = resolve_url( + 'admission:continuing-education:update:curriculum:educational_duplicate', + uuid=self.other_continuing_admission.uuid, + experience_uuid=experience.uuid, + ) + + doctorate_admission = DoctorateAdmissionFactory( + candidate=experience.person, + submitted=True, + ) + + response = self.client.post(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=experience.person, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, + ) + + response = self.client.post(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + general_admission.delete() + + response = self.client.post(url) + + base_curriculum_url = resolve_url( + 'admission:continuing-education:curriculum', + uuid=self.other_continuing_admission.uuid, + ) + + self.assertRedirects( + response=response, + fetch_redirect_response=False, + expected_url=base_curriculum_url, + ) + + def test_duplicate_experience_from_continuing_curriculum_for_sic_users(self): + self.client.force_login(self.sic_manager_user) + + experience = EducationalExperienceFactory( + person=self.other_continuing_admission.candidate, + ) + + url = resolve_url( + 'admission:continuing-education:update:curriculum:educational_duplicate', + uuid=self.other_continuing_admission.uuid, + experience_uuid=experience.uuid, + ) + + doctorate_admission = DoctorateAdmissionFactory( + candidate=experience.person, + submitted=True, + ) + + response = self.client.post(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=experience.person, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, + ) + + response = self.client.post(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + general_admission.delete() + + response = self.client.post(url) + + base_curriculum_url = resolve_url( + 'admission:continuing-education:curriculum', + uuid=self.other_continuing_admission.uuid, + ) + + self.assertRedirects( + response=response, + fetch_redirect_response=False, + expected_url=base_curriculum_url, + ) diff --git a/tests/views/common/form_tabs/curriculum/test_educational_experience_valuate.py b/tests/views/common/form_tabs/curriculum/test_educational_experience_valuate.py index 8be92890b..8d55c4625 100644 --- a/tests/views/common/form_tabs/curriculum/test_educational_experience_valuate.py +++ b/tests/views/common/form_tabs/curriculum/test_educational_experience_valuate.py @@ -32,15 +32,17 @@ from django.test import TestCase from rest_framework import status -from admission.contrib.models import DoctorateAdmission +from admission.contrib.models import DoctorateAdmission, ContinuingEducationAdmission from admission.contrib.models.base import AdmissionEducationalValuatedExperiences from admission.contrib.models.general_education import GeneralEducationAdmission from admission.ddd.admission.doctorat.preparation.domain.model.enums import ChoixStatutPropositionDoctorale +from admission.ddd.admission.formation_continue.domain.model.enums import ChoixStatutPropositionContinue from admission.ddd.admission.formation_generale.domain.model.enums import ( ChoixStatutPropositionGenerale, ) from admission.ddd.admission.formation_generale.domain.service.checklist import Checklist from admission.tests.factories import DoctorateAdmissionFactory +from admission.tests.factories.continuing_education import ContinuingEducationAdmissionFactory from admission.tests.factories.curriculum import ( EducationalExperienceFactory, ) @@ -71,6 +73,12 @@ def setUpTestData(cls): submitted=True, ) + cls.other_continuing_admission: ContinuingEducationAdmission = ContinuingEducationAdmissionFactory( + training__management_entity=entity, + training__academic_year=cls.academic_years[0], + status=ChoixStatutPropositionContinue.CONFIRMEE.name, + ) + # Create users cls.sic_manager_user = SicManagementRoleFactory(entity=entity).person.user cls.program_manager_user = ProgramManagerRoleFactory( @@ -79,6 +87,9 @@ def setUpTestData(cls): cls.doctorate_program_manager_user = ProgramManagerRoleFactory( education_group=cls.doctorate_admission.training.education_group, ).person.user + cls.other_continuing_program_manager_user = ProgramManagerRoleFactory( + education_group=cls.other_continuing_admission.training.education_group, + ).person.user def setUp(self): # Create data @@ -224,3 +235,95 @@ def test_valuate_experience_from_doctorate_curriculum_is_allowed_for_sic_users(s response = self.client.post(f'{self.doctorate_valuate_url}?next={admission_url}&next_hash_url=custom_hash') self.assertRedirects(response=response, fetch_redirect_response=False, expected_url=expected_url) + + def test_valuate_experience_from_continuing_curriculum_for_fac_users(self): + self.client.force_login(self.other_continuing_program_manager_user) + + experience = EducationalExperienceFactory( + person=self.other_continuing_admission.candidate, + ) + + url = resolve_url( + 'admission:continuing-education:update:curriculum:educational_valuate', + uuid=self.other_continuing_admission.uuid, + experience_uuid=experience.uuid, + ) + + doctorate_admission = DoctorateAdmissionFactory( + candidate=experience.person, + submitted=True, + ) + + response = self.client.post(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=experience.person, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, + ) + + response = self.client.post(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + general_admission.delete() + + response = self.client.post(url) + + base_curriculum_url = resolve_url( + 'admission:continuing-education:checklist', + uuid=self.other_continuing_admission.uuid, + ) + + self.assertRedirects( + response=response, + fetch_redirect_response=False, + expected_url=base_curriculum_url, + ) + + def test_valuate_experience_from_continuing_curriculum_for_sic_users(self): + self.client.force_login(self.sic_manager_user) + + experience = EducationalExperienceFactory( + person=self.other_continuing_admission.candidate, + ) + + url = resolve_url( + 'admission:continuing-education:update:curriculum:educational_valuate', + uuid=self.other_continuing_admission.uuid, + experience_uuid=experience.uuid, + ) + + doctorate_admission = DoctorateAdmissionFactory( + candidate=experience.person, + submitted=True, + ) + + response = self.client.post(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=experience.person, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, + ) + + response = self.client.post(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + general_admission.delete() + + response = self.client.post(url) + + base_curriculum_url = resolve_url( + 'admission:continuing-education:checklist', + uuid=self.other_continuing_admission.uuid, + ) + + self.assertRedirects( + response=response, + fetch_redirect_response=False, + expected_url=base_curriculum_url, + ) diff --git a/tests/views/common/form_tabs/curriculum/test_non_educational_experience_delete.py b/tests/views/common/form_tabs/curriculum/test_non_educational_experience_delete.py index 4c1b0155f..68c86ca82 100644 --- a/tests/views/common/form_tabs/curriculum/test_non_educational_experience_delete.py +++ b/tests/views/common/form_tabs/curriculum/test_non_educational_experience_delete.py @@ -33,20 +33,25 @@ from django.test import TestCase from rest_framework import status -from admission.contrib.models import EPCInjection as AdmissionEPCInjection, DoctorateAdmission +from admission.contrib.models import ( + EPCInjection as AdmissionEPCInjection, + DoctorateAdmission, + ContinuingEducationAdmission, +) from admission.contrib.models.base import ( AdmissionProfessionalValuatedExperiences, ) from admission.contrib.models.epc_injection import EPCInjectionType, EPCInjectionStatus as AdmissionEPCInjectionStatus from admission.contrib.models.general_education import GeneralEducationAdmission from admission.ddd.admission.doctorat.preparation.domain.model.doctorat import ENTITY_CDE -from admission.ddd.admission.doctorat.preparation.domain.model.enums import ChoixStatutPropositionDoctorale from admission.ddd.admission.enums.emplacement_document import OngletsDemande +from admission.ddd.admission.formation_continue.domain.model.enums import ChoixStatutPropositionContinue from admission.ddd.admission.formation_generale.domain.model.enums import ( ChoixStatutPropositionGenerale, ) from admission.ddd.admission.formation_generale.domain.service.checklist import Checklist from admission.tests.factories import DoctorateAdmissionFactory +from admission.tests.factories.continuing_education import ContinuingEducationAdmissionFactory from admission.tests.factories.curriculum import ( ProfessionalExperienceFactory, AdmissionProfessionalValuatedExperiencesFactory, @@ -93,6 +98,12 @@ def setUpTestData(cls): submitted=True, ) + cls.other_continuing_admission: ContinuingEducationAdmission = ContinuingEducationAdmissionFactory( + training__management_entity=first_doctoral_commission, + training__academic_year=cls.academic_years[0], + status=ChoixStatutPropositionContinue.CONFIRMEE.name, + ) + # Create users cls.sic_manager_user = SicManagementRoleFactory(entity=first_doctoral_commission).person.user cls.program_manager_user = ProgramManagerRoleFactory( @@ -101,6 +112,9 @@ def setUpTestData(cls): cls.doctorate_program_manager_user = ProgramManagerRoleFactory( education_group=cls.doctorate_admission.training.education_group, ).person.user + cls.other_continuing_program_manager_user = ProgramManagerRoleFactory( + education_group=cls.other_continuing_admission.training.education_group, + ).person.user cls.file_uuid = uuid.uuid4() def setUp(self): @@ -264,31 +278,111 @@ def test_delete_known_experience(self): def test_delete_experience_from_doctorate_curriculum_is_not_allowed_for_fac_users(self): self.client.force_login(self.doctorate_program_manager_user) - other_admission = DoctorateAdmissionFactory( - training=self.doctorate_admission.training, - candidate=self.doctorate_admission.candidate, - status=ChoixStatutPropositionDoctorale.TRAITEMENT_FAC.name, + response = self.client.post(self.doctorate_delete_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_experience_from_doctorate_curriculum_is_not_allowed_for_sic_users(self): + self.client.force_login(self.doctorate_program_manager_user) + response = self.client.post(self.doctorate_delete_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_experience_from_continuing_curriculum_is_not_allowed_with_blocking_admissions(self): + self.client.force_login(self.doctorate_program_manager_user) + response = self.client.post(self.doctorate_delete_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_experience_from_continuing_curriculum_for_fac_users(self): + self.client.force_login(self.other_continuing_program_manager_user) + + experience = ProfessionalExperienceFactory( + person=self.other_continuing_admission.candidate, ) - response = self.client.post( - resolve_url( - 'admission:doctorate:update:curriculum:non_educational_delete', - uuid=other_admission.uuid, - experience_uuid=self.experience.uuid, - ), + + url = resolve_url( + 'admission:continuing-education:update:curriculum:non_educational_delete', + uuid=self.other_continuing_admission.uuid, + experience_uuid=experience.uuid, + ) + + doctorate_admission = DoctorateAdmissionFactory( + candidate=experience.person, + submitted=True, ) + + response = self.client.delete(url) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_delete_experience_from_doctorate_curriculum_is_allowed_for_sic_users(self): + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=experience.person, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, + ) + + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + general_admission.delete() + + response = self.client.delete(url) + + base_curriculum_url = resolve_url( + 'admission:continuing-education:curriculum', + uuid=self.other_continuing_admission.uuid, + ) + + self.assertRedirects( + response=response, + fetch_redirect_response=False, + expected_url=base_curriculum_url, + ) + + self.assertFalse(ProfessionalExperience.objects.filter(uuid=experience.uuid).exists()) + + def test_delete_experience_from_continuing_curriculum_for_sic_users(self): self.client.force_login(self.sic_manager_user) - admission_url = resolve_url('admission') - expected_url = f'{admission_url}#custom_hash' + experience = ProfessionalExperienceFactory( + person=self.other_continuing_admission.candidate, + ) - response = self.client.delete(f'{self.doctorate_delete_url}?next={admission_url}&next_hash_url=custom_hash') + url = resolve_url( + 'admission:continuing-education:update:curriculum:non_educational_delete', + uuid=self.other_continuing_admission.uuid, + experience_uuid=experience.uuid, + ) + + doctorate_admission = DoctorateAdmissionFactory( + candidate=experience.person, + submitted=True, + ) + + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=experience.person, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, + ) + + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + general_admission.delete() + + response = self.client.delete(url) + + base_curriculum_url = resolve_url( + 'admission:continuing-education:curriculum', + uuid=self.other_continuing_admission.uuid, + ) self.assertRedirects( response=response, fetch_redirect_response=False, - expected_url=expected_url, + expected_url=base_curriculum_url, ) - self.assertEqual(response.status_code, status.HTTP_302_FOUND) + + self.assertFalse(ProfessionalExperience.objects.filter(uuid=experience.uuid).exists()) diff --git a/tests/views/common/form_tabs/curriculum/test_non_educational_experience_duplicate.py b/tests/views/common/form_tabs/curriculum/test_non_educational_experience_duplicate.py index 4e9d488ce..9849eb5b6 100644 --- a/tests/views/common/form_tabs/curriculum/test_non_educational_experience_duplicate.py +++ b/tests/views/common/form_tabs/curriculum/test_non_educational_experience_duplicate.py @@ -35,18 +35,20 @@ from django.test import TestCase from rest_framework import status -from admission.contrib.models import DoctorateAdmission +from admission.contrib.models import DoctorateAdmission, ContinuingEducationAdmission from admission.contrib.models.base import ( AdmissionProfessionalValuatedExperiences, ) from admission.contrib.models.general_education import GeneralEducationAdmission from admission.ddd.admission.doctorat.preparation.domain.model.doctorat import ENTITY_CDE from admission.ddd.admission.doctorat.preparation.domain.model.enums import ChoixStatutPropositionDoctorale +from admission.ddd.admission.formation_continue.domain.model.enums import ChoixStatutPropositionContinue from admission.ddd.admission.formation_generale.domain.model.enums import ( ChoixStatutPropositionGenerale, ) from admission.ddd.admission.formation_generale.domain.service.checklist import Checklist from admission.tests.factories import DoctorateAdmissionFactory +from admission.tests.factories.continuing_education import ContinuingEducationAdmissionFactory from admission.tests.factories.curriculum import ( ProfessionalExperienceFactory, ) @@ -87,6 +89,12 @@ def setUpTestData(cls): submitted=True, ) + cls.other_continuing_admission: ContinuingEducationAdmission = ContinuingEducationAdmissionFactory( + training__management_entity=first_doctoral_commission, + training__academic_year=cls.academic_years[0], + status=ChoixStatutPropositionContinue.CONFIRMEE.name, + ) + # Create users cls.sic_manager_user = SicManagementRoleFactory(entity=first_doctoral_commission).person.user cls.program_manager_user = ProgramManagerRoleFactory( @@ -95,6 +103,9 @@ def setUpTestData(cls): cls.doctorate_program_manager_user = ProgramManagerRoleFactory( education_group=cls.doctorate_admission.training.education_group, ).person.user + cls.other_continuing_program_manager_user = ProgramManagerRoleFactory( + education_group=cls.other_continuing_admission.training.education_group, + ).person.user cls.file_uuid = uuid.uuid4() cls.file_uuid_str = str(cls.file_uuid) cls.duplicate_uuid = uuid.uuid4() @@ -376,3 +387,95 @@ def test_duplicate_experience_from_doctorate_curriculum_is_allowed_for_sic_users fetch_redirect_response=False, expected_url=expected_url, ) + + def test_duplicate_experience_from_continuing_curriculum_for_fac_users(self): + self.client.force_login(self.other_continuing_program_manager_user) + + experience = ProfessionalExperienceFactory( + person=self.other_continuing_admission.candidate, + ) + + url = resolve_url( + 'admission:continuing-education:update:curriculum:non_educational_duplicate', + uuid=self.other_continuing_admission.uuid, + experience_uuid=experience.uuid, + ) + + doctorate_admission = DoctorateAdmissionFactory( + candidate=experience.person, + submitted=True, + ) + + response = self.client.post(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=experience.person, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, + ) + + response = self.client.post(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + general_admission.delete() + + response = self.client.post(url) + + base_curriculum_url = resolve_url( + 'admission:continuing-education:curriculum', + uuid=self.other_continuing_admission.uuid, + ) + + self.assertRedirects( + response=response, + fetch_redirect_response=False, + expected_url=base_curriculum_url, + ) + + def test_duplicate_experience_from_continuing_curriculum_for_sic_users(self): + self.client.force_login(self.sic_manager_user) + + experience = ProfessionalExperienceFactory( + person=self.other_continuing_admission.candidate, + ) + + url = resolve_url( + 'admission:continuing-education:update:curriculum:non_educational_duplicate', + uuid=self.other_continuing_admission.uuid, + experience_uuid=experience.uuid, + ) + + doctorate_admission = DoctorateAdmissionFactory( + candidate=experience.person, + submitted=True, + ) + + response = self.client.post(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=experience.person, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, + ) + + response = self.client.post(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + general_admission.delete() + + response = self.client.post(url) + + base_curriculum_url = resolve_url( + 'admission:continuing-education:curriculum', + uuid=self.other_continuing_admission.uuid, + ) + + self.assertRedirects( + response=response, + fetch_redirect_response=False, + expected_url=base_curriculum_url, + ) diff --git a/tests/views/common/form_tabs/curriculum/test_non_educational_experience_update.py b/tests/views/common/form_tabs/curriculum/test_non_educational_experience_update.py index 88dfafdc5..ff0eaa674 100644 --- a/tests/views/common/form_tabs/curriculum/test_non_educational_experience_update.py +++ b/tests/views/common/form_tabs/curriculum/test_non_educational_experience_update.py @@ -96,8 +96,7 @@ def setUpTestData(cls): status=ChoixStatutPropositionGenerale.CONFIRMEE.name, ) - cls.continuing_admission: ContinuingEducationAdmission = ContinuingEducationAdmissionFactory( - candidate=cls.general_admission.candidate, + cls.other_continuing_admission: ContinuingEducationAdmission = ContinuingEducationAdmissionFactory( training__management_entity=entity, training__academic_year=cls.academic_years[0], status=ChoixStatutPropositionContinue.CONFIRMEE.name, @@ -115,8 +114,8 @@ def setUpTestData(cls): cls.general_program_manager_user = ProgramManagerRoleFactory( education_group=cls.general_admission.training.education_group, ).person.user - cls.continuing_program_manager_user = ProgramManagerRoleFactory( - education_group=cls.continuing_admission.training.education_group, + cls.other_continuing_program_manager_user = ProgramManagerRoleFactory( + education_group=cls.other_continuing_admission.training.education_group, ).person.user cls.doctorate_program_manager_user = ProgramManagerRoleFactory( education_group=cls.doctorate_admission.training.education_group, @@ -162,15 +161,6 @@ def setUp(self): 'admission:general-education:update:curriculum:non_educational_create', uuid=self.general_admission.uuid, ) - self.continuing_form_url = resolve_url( - 'admission:continuing-education:update:curriculum:non_educational', - uuid=self.continuing_admission.uuid, - experience_uuid=self.experience.uuid, - ) - self.continuing_create_url = resolve_url( - 'admission:continuing-education:update:curriculum:non_educational_create', - uuid=self.continuing_admission.uuid, - ) self.doctorate_form_url = resolve_url( 'admission:doctorate:update:curriculum:non_educational', uuid=self.doctorate_admission.uuid, @@ -535,25 +525,93 @@ def test_general_submit_valid_form_for_create_a_new_work_activity(self): }, ) - def test_continuing_update_curriculum_is_allowed_for_fac_users(self): - self.client.force_login(self.continuing_program_manager_user) - response = self.client.get(self.continuing_form_url) + def test_continuing_update_curriculum_for_fac_users(self): + self.client.force_login(self.other_continuing_program_manager_user) + + experience = ProfessionalExperienceFactory( + person=self.other_continuing_admission.candidate, + ) + + url = resolve_url( + 'admission:continuing-education:update:curriculum:non_educational', + uuid=self.other_continuing_admission.uuid, + experience_uuid=experience.uuid, + ) + + response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_continuing_update_curriculum_is_allowed_for_sic_users(self): + doctorate_admission = DoctorateAdmissionFactory( + candidate=experience.person, + submitted=True, + ) + + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=experience.person, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, + ) + + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_continuing_update_curriculum_for_sic_users(self): self.client.force_login(self.sic_manager_user) - response = self.client.get(self.continuing_form_url) + + experience = ProfessionalExperienceFactory( + person=self.other_continuing_admission.candidate, + ) + + url = resolve_url( + 'admission:continuing-education:update:curriculum:non_educational', + uuid=self.other_continuing_admission.uuid, + experience_uuid=experience.uuid, + ) + + response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) form = response.context['form'] self.assertEqual(form.fields['certificate'].disabled, True) self.assertIsInstance(form.fields['certificate'].widget, MultipleHiddenInput) + doctorate_admission = DoctorateAdmissionFactory( + candidate=experience.person, + submitted=True, + ) + + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=experience.person, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, + ) + + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_continuing_submit_form(self): self.client.force_login(self.sic_manager_user) + experience = ProfessionalExperienceFactory( + person=self.other_continuing_admission.candidate, + ) + + url = resolve_url( + 'admission:continuing-education:update:curriculum:non_educational', + uuid=self.other_continuing_admission.uuid, + experience_uuid=experience.uuid, + ) + response = self.client.post( - self.continuing_form_url, + url, { 'start_date_month': 1, 'start_date_year': 2020, @@ -564,28 +622,28 @@ def test_continuing_submit_form(self): 'sector': ActivitySector.PRIVATE.name, 'institute_name': 'Institute', 'activity': 'Activity', - 'certificate_0': 'certificate-token', + 'certificate_0': [self.file_uuid], }, ) self.assertEqual(response.status_code, status.HTTP_302_FOUND) # Check the experience - self.experience.refresh_from_db() + experience.refresh_from_db() - self.assertEqual(self.experience.start_date, datetime.date(2020, 1, 1)) - self.assertEqual(self.experience.end_date, datetime.date(2020, 5, 31)) - self.assertEqual(self.experience.type, ActivityType.INTERNSHIP.name) - self.assertEqual(self.experience.role, '') - self.assertEqual(self.experience.sector, '') - self.assertEqual(self.experience.institute_name, '') - self.assertEqual(self.experience.activity, '') - self.assertEqual(self.experience.certificate, [self.file_uuid]) + self.assertEqual(experience.start_date, datetime.date(2020, 1, 1)) + self.assertEqual(experience.end_date, datetime.date(2020, 5, 31)) + self.assertEqual(experience.type, ActivityType.INTERNSHIP.name) + self.assertEqual(experience.role, '') + self.assertEqual(experience.sector, '') + self.assertEqual(experience.institute_name, '') + self.assertEqual(experience.activity, '') + self.assertEqual(experience.certificate, []) # Check the admission - self.continuing_admission.refresh_from_db() - self.assertEqual(self.continuing_admission.modified_at, datetime.datetime.now()) - self.assertEqual(self.continuing_admission.last_update_author, self.sic_manager_user.person) + self.other_continuing_admission.refresh_from_db() + self.assertEqual(self.other_continuing_admission.modified_at, datetime.datetime.now()) + self.assertEqual(self.other_continuing_admission.last_update_author, self.sic_manager_user.person) def test_doctorate_update_curriculum_is_allowed_for_fac_users(self): other_admission = DoctorateAdmissionFactory( diff --git a/tests/views/common/form_tabs/test_education.py b/tests/views/common/form_tabs/test_education.py index 7f3d126b9..9a162e3b7 100644 --- a/tests/views/common/form_tabs/test_education.py +++ b/tests/views/common/form_tabs/test_education.py @@ -38,10 +38,12 @@ from admission.contrib.models.epc_injection import EPCInjectionType, EPCInjectionStatus as AdmissionEPCInjectionStatus from admission.contrib.models.general_education import GeneralEducationAdmission from admission.ddd.admission.doctorat.preparation.domain.model.doctorat import ENTITY_CDE +from admission.ddd.admission.doctorat.preparation.domain.model.enums import ChoixStatutPropositionDoctorale from admission.ddd.admission.enums import Onglets from admission.ddd.admission.enums.emplacement_document import OngletsDemande from admission.ddd.admission.formation_continue.domain.model.enums import ChoixStatutPropositionContinue from admission.ddd.admission.formation_generale.domain.model.enums import ChoixStatutPropositionGenerale +from admission.tests.factories import DoctorateAdmissionFactory from admission.tests.factories.continuing_education import ContinuingEducationAdmissionFactory from admission.tests.factories.form_item import TextAdmissionFormItemFactory, AdmissionFormItemInstantiationFactory from admission.tests.factories.general_education import GeneralEducationAdmissionFactory @@ -723,18 +725,70 @@ def setUp(self): patched = patcher.start() patched.side_effect = lambda _, value, __: value - def test_update_education_is_allowed_for_fac_users(self): + def test_update_education_for_fac_users(self): self.client.force_login(self.program_manager_user) + response = self.client.get(self.form_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + continuing_admission = ContinuingEducationAdmissionFactory( + candidate=self.continuing_admission.candidate, + status=ChoixStatutPropositionContinue.CONFIRMEE.name, + ) + response = self.client.get(self.form_url) self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_update_education_is_allowed_for_sic_users(self): + doctorate_admission = DoctorateAdmissionFactory( + candidate=self.continuing_admission.candidate, + status=ChoixStatutPropositionDoctorale.CONFIRMEE.name, + ) + + response = self.client.get(self.form_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=self.continuing_admission.candidate, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, + ) + + response = self.client.get(self.form_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_education_for_sic_users(self): self.client.force_login(self.sic_manager_user) response = self.client.get(self.form_url) self.assertEqual(response.status_code, status.HTTP_200_OK) + continuing_admission = ContinuingEducationAdmissionFactory( + candidate=self.continuing_admission.candidate, + status=ChoixStatutPropositionContinue.CONFIRMEE.name, + ) + + response = self.client.get(self.form_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + doctorate_admission = DoctorateAdmissionFactory( + candidate=self.continuing_admission.candidate, + status=ChoixStatutPropositionDoctorale.CONFIRMEE.name, + ) + + response = self.client.get(self.form_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=self.continuing_admission.candidate, + status=ChoixStatutPropositionGenerale.CONFIRMEE.name, + ) + + response = self.client.get(self.form_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_submit_valid_data(self): self.client.force_login(self.sic_manager_user) diff --git a/tests/views/common/form_tabs/test_person.py b/tests/views/common/form_tabs/test_person.py index fe9c6e382..da685844a 100644 --- a/tests/views/common/form_tabs/test_person.py +++ b/tests/views/common/form_tabs/test_person.py @@ -53,7 +53,11 @@ AdmissionEducationalValuatedExperiencesFactory, ) from admission.tests.factories.general_education import GeneralEducationAdmissionFactory -from admission.tests.factories.roles import SicManagementRoleFactory, ProgramManagerRoleFactory +from admission.tests.factories.roles import ( + SicManagementRoleFactory, + ProgramManagerRoleFactory, + CentralManagerRoleFactory, +) from base.forms.utils import FIELD_REQUIRED_MESSAGE from base.models.enums.civil_state import CivilState from base.models.enums.person_address_type import PersonAddressType @@ -125,7 +129,6 @@ def setUpTestData(cls): cls.continuing_admission: ContinuingEducationAdmission = ContinuingEducationAdmissionFactory( training__management_entity=first_doctoral_commission, training__academic_year=academic_years[0], - candidate=cls.general_admission.candidate, status=ChoixStatutPropositionContinue.CONFIRMEE.name, ) @@ -141,7 +144,6 @@ def setUpTestData(cls): cls.doctorate_admission: DoctorateAdmission = DoctorateAdmissionFactory( training__management_entity=first_doctoral_commission, training__academic_year=academic_years[0], - candidate=cls.general_admission.candidate, status=ChoixStatutPropositionDoctorale.CONFIRMEE.name, ) @@ -838,6 +840,35 @@ def test_continuing_person_form_on_get_sic_manager(self): self.assertEqual(response.status_code, 200) + continuing_admission = ContinuingEducationAdmissionFactory( + candidate=self.continuing_admission.candidate, + status=ChoixStatutPropositionContinue.CONFIRMEE.name, + ) + + response = self.client.get(self.continuing_url) + + self.assertEqual(response.status_code, 200) + + doctorate_admission = DoctorateAdmissionFactory( + candidate=self.continuing_admission.candidate, + status=ChoixStatutPropositionDoctorale.CONFIRMEE.name, + ) + + response = self.client.get(self.continuing_url) + + self.assertEqual(response.status_code, 403) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=self.continuing_admission.candidate, + status=ChoixStatutPropositionDoctorale.CONFIRMEE.name, + ) + + response = self.client.get(self.continuing_url) + + self.assertEqual(response.status_code, 403) + def test_continuing_person_form_on_get_program_manager(self): self.client.force_login(user=self.continuing_program_manager_user) @@ -845,12 +876,34 @@ def test_continuing_person_form_on_get_program_manager(self): self.assertEqual(response.status_code, 200) - def test_continuing_person_form_on_post_program_manager_is_allowed(self): - self.client.force_login(user=self.continuing_program_manager_user) + continuing_admission = ContinuingEducationAdmissionFactory( + candidate=self.continuing_admission.candidate, + status=ChoixStatutPropositionContinue.CONFIRMEE.name, + ) - response = self.client.post(self.continuing_url, self.form_data) + response = self.client.get(self.continuing_url) - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 200) + + doctorate_admission = DoctorateAdmissionFactory( + candidate=self.continuing_admission.candidate, + status=ChoixStatutPropositionDoctorale.CONFIRMEE.name, + ) + + response = self.client.get(self.continuing_url) + + self.assertEqual(response.status_code, 403) + + doctorate_admission.delete() + + general_admission = GeneralEducationAdmissionFactory( + candidate=self.continuing_admission.candidate, + status=ChoixStatutPropositionDoctorale.CONFIRMEE.name, + ) + + response = self.client.get(self.continuing_url) + + self.assertEqual(response.status_code, 403) def test_continuing_person_form_on_get(self): self.client.force_login(user=self.sic_manager_user) @@ -901,14 +954,47 @@ def test_continuing_person_form_post_with_invalid_data(self): response = self.client.post(self.continuing_url, {}) self.assertEqual(response.status_code, 200) + def test_doctorate_person_form_on_get_program_manager(self): + self.client.force_login(user=self.doctorate_program_manager_user) + + response = self.client.get(self.doctorate_url) + + self.assertEqual(response.status_code, 403) + def test_doctorate_person_form_on_get_sic_manager(self): self.client.force_login(user=self.sic_manager_user) - # No residential address response = self.client.get(self.doctorate_url) self.assertEqual(response.status_code, 200) + ContinuingEducationAdmissionFactory( + candidate=self.doctorate_admission.candidate, + status=ChoixStatutPropositionContinue.CONFIRMEE.name, + ) + + response = self.client.get(self.doctorate_url) + + self.assertEqual(response.status_code, 200) + + DoctorateAdmissionFactory( + candidate=self.doctorate_admission.candidate, + status=ChoixStatutPropositionDoctorale.CONFIRMEE.name, + ) + + response = self.client.get(self.doctorate_url) + + self.assertEqual(response.status_code, 200) + + GeneralEducationAdmissionFactory( + candidate=self.doctorate_admission.candidate, + status=ChoixStatutPropositionDoctorale.CONFIRMEE.name, + ) + + response = self.client.get(self.doctorate_url) + + self.assertEqual(response.status_code, 403) + def test_doctorate_person_form_on_get(self): self.client.force_login(user=self.sic_manager_user) diff --git a/tests/views/continuing_education/checklist/test_decision.py b/tests/views/continuing_education/checklist/test_decision.py index 3e1e8f118..0d63d276c 100644 --- a/tests/views/continuing_education/checklist/test_decision.py +++ b/tests/views/continuing_education/checklist/test_decision.py @@ -65,7 +65,7 @@ from base.models.enums.education_group_types import TrainingType from base.tests.factories.academic_year import AcademicYearFactory from base.tests.factories.entity import EntityWithVersionFactory -from education_group.auth.scope import Scope +from admission.auth.scope import Scope class ChecklistViewTestCase(TestCase): diff --git a/tests/views/continuing_education/test_list.py b/tests/views/continuing_education/test_list.py index 646a34f2c..c38915a8b 100644 --- a/tests/views/continuing_education/test_list.py +++ b/tests/views/continuing_education/test_list.py @@ -58,7 +58,7 @@ from base.tests.factories.person import PersonFactory from base.tests.factories.student import StudentFactory from base.tests.factories.user import UserFactory -from education_group.auth.scope import Scope +from admission.auth.scope import Scope @freezegun.freeze_time('2023-01-01') @@ -235,7 +235,7 @@ def test_list_central_manager_scoped_iufc_not_entity(self): self.assertEqual(len(response.context['object_list']), 0) def test_list_central_manager_scoped_all_not_entity(self): - manager = CentralManagerRoleFactory(scopes=[Scope.ALL.name]) + manager = CentralManagerRoleFactory(scopes=[Scope.GENERAL.name]) self.client.force_login(user=manager.person.user) response = self._do_request(allowed_sql_surplus=1) @@ -249,7 +249,7 @@ def test_list_central_manager_scoped_doctorate_not_entity(self): self.assertEqual(response.status_code, 403) def test_list_central_manager_scoped_all_on_entity(self): - manager = CentralManagerRoleFactory(scopes=[Scope.ALL.name], entity=self.first_entity) + manager = CentralManagerRoleFactory(scopes=[Scope.GENERAL.name], entity=self.first_entity) self.client.force_login(user=manager.person.user) response = self._do_request(allowed_sql_surplus=1) diff --git a/tests/views/test_list.py b/tests/views/test_list.py index d4bd5e382..0fe97c2b0 100644 --- a/tests/views/test_list.py +++ b/tests/views/test_list.py @@ -73,7 +73,7 @@ from base.tests.factories.person import PersonFactory from base.tests.factories.student import StudentFactory from base.tests.factories.user import UserFactory -from education_group.auth.scope import Scope +from admission.auth.scope import Scope from program_management.models.education_group_version import EducationGroupVersion from reference.tests.factories.country import CountryFactory @@ -281,7 +281,7 @@ def test_list_initialization_just_after_academic_year_change(self): self.assertEqual(form['annee_academique'].initial, 2024) def test_list_central_manager_scoped_not_entity(self): - manager = CentralManagerRoleFactory(scopes=[Scope.ALL.name]) + manager = CentralManagerRoleFactory(scopes=[Scope.GENERAL.name]) self.client.force_login(user=manager.person.user) response = self._do_request(allowed_sql_surplus=1) @@ -289,7 +289,7 @@ def test_list_central_manager_scoped_not_entity(self): self.assertEqual(len(response.context['object_list']), 0) def test_list_central_manager_scoped_on_entity(self): - manager = CentralManagerRoleFactory(scopes=[Scope.ALL.name], entity=self.first_entity) + manager = CentralManagerRoleFactory(scopes=[Scope.GENERAL.name], entity=self.first_entity) self.client.force_login(user=manager.person.user) response = self._do_request(allowed_sql_surplus=1) diff --git a/views/common/form_tabs/curriculum_global.py b/views/common/form_tabs/curriculum_global.py index 5941a12e8..b5523e683 100644 --- a/views/common/form_tabs/curriculum_global.py +++ b/views/common/form_tabs/curriculum_global.py @@ -53,7 +53,7 @@ class CurriculumGlobalFormView(AdmissionFormMixin, CurriculumGlobalCommonViewMixin, FormView): urlpatterns = {'curriculum': 'curriculum'} template_name = 'admission/forms/curriculum.html' - permission_required = 'admission.change_admission_curriculum' + permission_required = 'admission.change_admission_global_curriculum' form_class = GlobalCurriculumForm extra_context = { 'force_form': True,