Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] [feature/OS-664 => DEV] En tant que candidat doctorand, je souhaite faire une demande d_admission au doctorat suite à une préadmission acceptée #2293

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@ repos:
- id: check_app_messages
name: Check messages
language: system
entry: bash -c '(cd .. && ./manage.py check_app_messages parcours_doctoral)'
entry: bash -c '(cd .. && ./manage.py check_app_messages admission)'
always_run: true
pass_filenames: false
13 changes: 6 additions & 7 deletions admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ class DoctorateAdmissionAdmin(AdmissionAdminMixin):
'thesis_language',
'prerequisite_courses',
'refusal_reasons',
'related_pre_admission',
]
list_display = ['reference', 'candidate_fmt', 'doctorate', 'type', 'status', 'view_on_portal']
list_filter = ['status', 'type']
Expand Down Expand Up @@ -667,15 +668,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_established_on__isnull=True
generaleducationadmission__financability_established_on__isnull=True,
)
| Q(
checklist__current__financabilite__status='GEST_REUSSITE',
generaleducationadmission__financability_established_by_id__isnull=True
generaleducationadmission__financability_established_by_id__isnull=True,
),
generaleducationadmission__isnull=False,
then=Value(False),
Expand Down Expand Up @@ -758,13 +759,10 @@ def get_queryset(self, request):
)
)

@admin.display(
ordering='_noma_sent_to_digit'
)
@admin.display(ordering='_noma_sent_to_digit')
def noma_sent_to_digit(self, obj):
return obj._noma_sent_to_digit


@admin.action(description='Injecter la demande dans EPC')
def injecter_dans_epc(self, request, queryset):
for demande in queryset.exclude(
Expand Down Expand Up @@ -928,6 +926,7 @@ def has_add_permission(self, request) -> bool:
def has_change_permission(self, request, obj=None) -> bool:
return False


# ##############################################################################
# Roles

Expand Down
94 changes: 94 additions & 0 deletions admission_utils/copy_documents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# ##############################################################################
#
# 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-2025 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/.
#
# ##############################################################################
import uuid


def copy_documents(objs):
"""
Create copies of the files of the specified objects and affect them to the specified objects.
:param objs: The list of objects.
"""
from osis_document.api.utils import get_several_remote_metadata, get_remote_tokens, documents_remote_duplicate
from osis_document.contrib import FileField
from osis_document.utils import generate_filename

all_document_uuids = []
all_document_upload_paths = {}
document_fields_by_obj_uuid = {}

# Get all the document fields and the uuids of the documents to duplicate
for obj in objs:
document_fields_by_obj_uuid[obj.uuid] = {}

for field in obj._meta.get_fields():
if isinstance(field, FileField):
document_uuids = getattr(obj, field.name)

if document_uuids:
document_fields_by_obj_uuid[obj.uuid][field.name] = field
all_document_uuids += [document_uuid for document_uuid in document_uuids if document_uuid]

all_tokens = get_remote_tokens(all_document_uuids)
metadata_by_token = get_several_remote_metadata(tokens=list(all_tokens.values()))

# Get the upload paths of the documents to duplicate
for obj in objs:
for field_name, field in document_fields_by_obj_uuid[obj.uuid].items():
document_uuids = getattr(obj, field_name)

for document_uuid in document_uuids:
if not document_uuid:
continue

document_uuid_str = str(document_uuid)
file_name = 'file'

if document_uuid_str in all_tokens and all_tokens[document_uuid_str] in metadata_by_token:
metadata = metadata_by_token[all_tokens[document_uuid_str]]
if metadata.get('name'):
file_name = metadata['name']

all_document_upload_paths[document_uuid_str] = generate_filename(obj, file_name, field.upload_to)

# Make a copy of the documents and return the uuids of the copied documents
duplicates_documents_uuids = documents_remote_duplicate(
uuids=all_document_uuids,
with_modified_upload=True,
upload_path_by_uuid=all_document_upload_paths,
)

# Update the uuids of the documents with the uuids of the copied documents
for obj in objs:
for field_name in document_fields_by_obj_uuid[obj.uuid]:
setattr(
obj,
field_name,
[
uuid.UUID(duplicates_documents_uuids[str(document_uuid)])
for document_uuid in getattr(obj, field_name)
if duplicates_documents_uuids.get(str(document_uuid))
],
)
2 changes: 1 addition & 1 deletion api/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from backoffice.settings.rest_framework.fields import ActionLinksField
from base.models.utils.utils import ChoiceEnum

ADMISSION_SDK_VERSION = "1.0.112"
ADMISSION_SDK_VERSION = "1.0.114"


class AdmissionSchemaGenerator(SchemaGenerator):
Expand Down
61 changes: 59 additions & 2 deletions api/serializers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
RelatedInstituteField,
)
from admission.api.serializers.mixins import IncludedFieldsMixin
from admission.models import DoctorateAdmission, GeneralEducationAdmission
from admission.ddd.admission.doctorat.preparation.commands import CompleterPropositionCommand, InitierPropositionCommand
from admission.ddd.admission.doctorat.preparation.domain.model.enums import (
ChoixCommissionProximiteCDEouCLSM,
Expand All @@ -49,11 +48,13 @@
DoctoratFormationDTO,
PropositionDTO as DoctoratPropositionDTO,
)
from admission.ddd.admission.dtos.campus import CampusDTO
from admission.ddd.admission.dtos.formation import FormationDTO
from admission.ddd.admission.formation_continue.domain.model.enums import ChoixStatutPropositionContinue
from admission.ddd.admission.formation_continue.dtos import PropositionDTO as FormationContinuePropositionDTO
from admission.ddd.admission.formation_generale.domain.model.enums import ChoixStatutPropositionGenerale
from admission.ddd.admission.formation_generale.dtos import PropositionDTO as FormationGeneralePropositionDTO
from admission.models import DoctorateAdmission, GeneralEducationAdmission
from backoffice.settings.rest_framework.fields import ActionLinksField
from base.utils.serializers import DTOSerializer

Expand All @@ -76,6 +77,7 @@
"ContinuingEducationPropositionDTOSerializer",
"PROPOSITION_ERROR_SCHEMA",
"GeneralEducationPropositionIdentityWithStatusSerializer",
"DoctoratePreAdmissionSearchDTOSerializer",
]

from reference.api.serializers.language import RelatedLanguageField
Expand Down Expand Up @@ -245,6 +247,7 @@ class Meta:
source = DoctoratPropositionDTO
fields = [
'uuid',
'pre_admission_associee',
'reference',
'type_admission',
'doctorat',
Expand Down Expand Up @@ -465,6 +468,7 @@ class Meta:
source = DoctoratPropositionDTO
fields = [
'uuid',
'pre_admission_associee',
'type_admission',
'reference',
'justification',
Expand Down Expand Up @@ -710,7 +714,7 @@ class Meta:
)


class CompleterPropositionCommandSerializer(InitierPropositionCommandSerializer):
class CompleterPropositionCommandSerializer(DTOSerializer):
documents_projet = serializers.ListField(child=serializers.CharField())
graphe_gantt = serializers.ListField(child=serializers.CharField())
proposition_programme_doctoral = serializers.ListField(child=serializers.CharField())
Expand All @@ -722,6 +726,12 @@ class CompleterPropositionCommandSerializer(InitierPropositionCommandSerializer)
)
langue_redaction_these = RelatedLanguageField(required=False)
institut_these = RelatedInstituteField(required=False)
commission_proximite = serializers.ChoiceField(
choices=ChoixCommissionProximiteCDEouCLSM.choices()
+ ChoixCommissionProximiteCDSS.choices()
+ ChoixSousDomaineSciences.choices(),
allow_blank=True,
)
type_admission = None
matricule_auteur = None

Expand All @@ -732,3 +742,50 @@ class Meta:
class SectorDTOSerializer(serializers.Serializer):
sigle = serializers.ReadOnlyField()
intitule = serializers.ReadOnlyField()


class CampusDTOSerializer(IncludedFieldsMixin, DTOSerializer):
class Meta:
source = CampusDTO
fields = [
'uuid',
'nom',
]


class DoctoratSearchDTOSerializer(IncludedFieldsMixin, DTOSerializer):
campus = CampusDTOSerializer()
date_debut = None
intitule_fr = None
intitule_en = None
credits = None

class Meta:
source = DoctoratFormationDTO
fields = [
'sigle',
'code',
'annee',
'intitule',
'sigle_entite_gestion',
'campus',
]


class DoctoratePreAdmissionSearchDTOSerializer(IncludedFieldsMixin, DTOSerializer):
doctorat = DoctoratSearchDTOSerializer()
# This is to prevent schema from breaking on JSONField
erreurs = None
reponses_questions_specifiques = None
elements_confirmation = None
documents_demandes = None

class Meta:
source = DoctoratPropositionDTO
fields = [
'uuid',
'reference',
'doctorat',
'code_secteur_formation',
'intitule_secteur_formation',
]
1 change: 1 addition & 0 deletions api/url_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def path(pattern, view, name=None):
path('curriculum', views.PersonCurriculumView),
# Admission-related
path('propositions/doctorate', views.DoctorateTrainingChoiceAPIView),
path('propositions/doctorate/pre-admission-list', views.DoctoratePreAdmissionList),
path('propositions/doctorate/<uuid:uuid>', views.DoctoratePropositionView),
_path('propositions/doctorate/<uuid:uuid>/', include(person_tabs)),
path('propositions/doctorate/<uuid:uuid>/project', views.ProjectViewSet),
Expand Down
40 changes: 39 additions & 1 deletion api/views/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,23 @@
from admission.api import serializers
from admission.api.permissions import IsListingOrHasNotAlreadyCreatedPermission, IsSupervisionMember
from admission.api.schema import ResponseSpecificSchema
from admission.models import DoctorateAdmission
from admission.ddd.admission.doctorat.preparation.commands import (
CompleterPropositionCommand,
ListerPropositionsCandidatQuery as ListerPropositionsDoctoralesCandidatQuery,
ListerPropositionsSuperviseesQuery,
)
from admission.ddd.admission.doctorat.preparation.domain.model.enums import (
ChoixTypeAdmission,
ChoixStatutPropositionDoctorale,
)
from admission.ddd.admission.doctorat.preparation.domain.validator.exceptions import JustificationRequiseException
from admission.ddd.admission.formation_continue.commands import (
ListerPropositionsCandidatQuery as ListerPropositionsFormationContinueCandidatQuery,
)
from admission.ddd.admission.formation_generale.commands import (
ListerPropositionsCandidatQuery as ListerPropositionsFormationGeneraleCandidatQuery,
)
from admission.models import DoctorateAdmission
from admission.utils import get_cached_admission_perm_obj
from backoffice.settings.rest_framework.common_views import DisplayExceptionsByFieldNameAPIMixin
from infrastructure.messages_bus import message_bus_instance
Expand All @@ -56,6 +60,7 @@
"PropositionListView",
"SupervisedPropositionListView",
"ProjectViewSet",
"DoctoratePreAdmissionList",
]


Expand Down Expand Up @@ -229,3 +234,36 @@ def put(self, request, *args, **kwargs):
self.get_permission_object().update_detailed_status(request.user.person)
serializer = serializers.PropositionIdentityDTOSerializer(instance=result)
return Response(serializer.data, status=status.HTTP_200_OK)


class DoctoratePreAdmissionListSchema(ResponseSpecificSchema):
operation_id_base = '_doctorate_pre_admission'
serializer_mapping = {
'GET': serializers.DoctoratePreAdmissionSearchDTOSerializer,
}


class DoctoratePreAdmissionList(APIPermissionRequiredMixin, DisplayExceptionsByFieldNameAPIMixin, ListAPIView):
name = "doctorate_pre_admission_list"
schema = DoctoratePreAdmissionListSchema()
pagination_class = None
filter_backends = []
permission_classes = [IsListingOrHasNotAlreadyCreatedPermission]

def list(self, request, **kwargs):
"""List the propositions of the logged in user"""
doctorate_list = message_bus_instance.invoke(
ListerPropositionsDoctoralesCandidatQuery(
matricule_candidat=request.user.person.global_id,
type_admission=ChoixTypeAdmission.PRE_ADMISSION.name,
statut=ChoixStatutPropositionDoctorale.INSCRIPTION_AUTORISEE.name,
est_pre_admission_d_une_admission_en_cours=False,
),
)

serializer = serializers.DoctoratePreAdmissionSearchDTOSerializer(
instance=doctorate_list,
many=True,
)

return Response(serializer.data)
7 changes: 7 additions & 0 deletions auth/predicates/doctorate.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def signing_in_progress(self, user: User, obj: DoctorateAdmission):
def is_invited_to_complete(self, user: User, obj: DoctorateAdmission):
return obj.status in STATUTS_PROPOSITION_DOCTORALE_SOUMISE_POUR_CANDIDAT


@predicate(bind=True)
@predicate_failed_msg(message=_("The proposition has already been confirmed or is cancelled"))
def unconfirmed_proposition(self, user: User, obj: DoctorateAdmission):
Expand Down Expand Up @@ -223,3 +224,9 @@ def can_send_to_fac_faculty_decision(self, user: User, obj: DoctorateAdmission):
isinstance(obj, DoctorateAdmission)
and obj.status in STATUTS_PROPOSITION_DOCTORALE_ENVOYABLE_EN_CDD_POUR_DECISION
)


@predicate(bind=True)
@predicate_failed_msg(message=_("The admission must not follow a pre-admission"))
def must_not_follow_a_pre_admission(self, user: User, obj: DoctorateAdmission):
return not bool(obj.related_pre_admission_id)
4 changes: 3 additions & 1 deletion auth/roles/candidate.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@
'change_admission_languages': common.is_admission_request_author & doctorate.unconfirmed_proposition,
'change_admission_accounting': common.is_admission_request_author & doctorate.unconfirmed_proposition,
# Project tabs and supervision group edition are accessible as long as signing has not begun
'change_admission_training_choice': common.is_admission_request_author & doctorate.in_progress,
'change_admission_training_choice': common.is_admission_request_author
& doctorate.in_progress
& doctorate.must_not_follow_a_pre_admission,
'change_admission_project': common.is_admission_request_author & doctorate.in_progress,
'change_admission_cotutelle': common.is_admission_request_author & doctorate.in_progress & doctorate.is_admission,
'change_admission_supervision': common.is_admission_request_author
Expand Down
Loading