Skip to content

Commit

Permalink
[OS-1178] IUFC : prevent to update some profile tabs if the candidate…
Browse files Browse the repository at this point in the history
… has other admissions and restrict the views to the specific role scope
  • Loading branch information
jcougnaud committed Oct 18, 2024
1 parent 556e475 commit 0e708c9
Show file tree
Hide file tree
Showing 40 changed files with 1,611 additions and 142 deletions.
8 changes: 4 additions & 4 deletions admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion auth/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': '',
Expand Down
78 changes: 62 additions & 16 deletions auth/predicates/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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):
Expand All @@ -65,26 +88,26 @@ def _build_queryset_cache_key_from_role_qs(role_qs, suffix):
return f'{role_qs.model.__module__}_{role_qs.model.__name__}_{suffix}'.replace('.', '_')


def user_has_scope(context, user, scopes):
cache_key = _build_queryset_cache_key_from_role_qs(context['role_qs'], 'admission_scopes')

if not hasattr(user, cache_key):
setattr(
user,
cache_key,
set(scope for scope_list in context['role_qs'].values_list('scopes', flat=True) for scope in scope_list),
)
return set([s.name for s in scopes]) <= getattr(user, cache_key)


def has_scope(*scopes):
assert len(scopes) > 0, 'You must provide at least one scope name'

name = 'has_scope:%s' % ','.join(s.name for s in scopes)

@predicate(name, bind=True)
def fn(self, user):
cache_key = _build_queryset_cache_key_from_role_qs(self.context['role_qs'], 'admission_scopes')

if not hasattr(user, cache_key):
setattr(
user,
cache_key,
set(
scope
for scope_list in self.context['role_qs'].values_list('scopes', flat=True)
for scope in scope_list
),
)
return set([s.name for s in scopes]) <= getattr(user, cache_key)
return user_has_scope(self.context, user, scopes)

return fn

Expand All @@ -99,12 +122,35 @@ def is_part_of_education_group(self, user: User, obj: BaseAdmission):
return obj.training.education_group_id in getattr(user, cache_key)


def user_is_entity_manager(context, user: User, obj: BaseAdmission):
cache_key = _build_queryset_cache_key_from_role_qs(context['role_qs'], 'entities_ids')

if not hasattr(user, cache_key):
setattr(user, cache_key, context['role_qs'].get_entities_ids())

return obj.training.management_entity_id in getattr(user, cache_key)


@predicate(bind=True)
def is_entity_manager(self, user: User, obj: BaseAdmission):
cache_key = _build_queryset_cache_key_from_role_qs(self.context['role_qs'], 'entities_ids')
return user_is_entity_manager(self.context, user, obj)


@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'].get_entities_ids())
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)

Expand Down
4 changes: 3 additions & 1 deletion auth/predicates/continuing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion auth/predicates/doctorate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion auth/predicates/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
56 changes: 46 additions & 10 deletions auth/roles/central_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand Down
18 changes: 14 additions & 4 deletions auth/roles/program_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion auth/roles/sic_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
32 changes: 32 additions & 0 deletions auth/scope.py
Original file line number Diff line number Diff line change
@@ -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'
Loading

0 comments on commit 0e708c9

Please sign in to comment.