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 @@
{{ cdd_version.title }} diff --git a/templates/admission/exports/fac_decision_certificate.html b/templates/admission/exports/fac_decision_certificate.html index 4506f4ba0..1b3fe6239 100644 --- a/templates/admission/exports/fac_decision_certificate.html +++ b/templates/admission/exports/fac_decision_certificate.html @@ -59,7 +59,7 @@ {% block header %}
{% translate 'Dossier reference:' %} {{ proposition.reference }}
diff --git a/templates/admission/exports/recap/base_pdf.html b/templates/admission/exports/recap/base_pdf.html index fa4ef6c21..c9e5bb4a2 100644 --- a/templates/admission/exports/recap/base_pdf.html +++ b/templates/admission/exports/recap/base_pdf.html @@ -35,7 +35,7 @@ {% block header %}{% translate 'Candidate:' %} {{ proposition.prenom_candidat }} {{ proposition.nom_candidat }}
diff --git a/templates/admission/exports/sic_approval_annexe.html b/templates/admission/exports/sic_approval_annexe.html index 94bdbb3af..f7a181afa 100644 --- a/templates/admission/exports/sic_approval_annexe.html +++ b/templates/admission/exports/sic_approval_annexe.html @@ -100,7 +100,7 @@Formulaire standard (dit « Annexe 1 »)
diff --git a/templates/admission/exports/sic_approval_certificate.html b/templates/admission/exports/sic_approval_certificate.html index 407e260e1..d72cb6116 100644 --- a/templates/admission/exports/sic_approval_certificate.html +++ b/templates/admission/exports/sic_approval_certificate.html @@ -139,7 +139,7 @@