Skip to content

Commit 7ec8b9f

Browse files
committed
Fix OS-1651 OS-1653 OS-1654 OS-1655: Contingenté front-office
1 parent 288694d commit 7ec8b9f

File tree

52 files changed

+1499
-317
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1499
-317
lines changed

api/schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
# ##############################################################################
2626
from drf_spectacular.generators import SchemaGenerator
2727

28-
ADMISSION_SDK_VERSION = "1.1.16"
28+
ADMISSION_SDK_VERSION = "1.1.17.dev1651"
2929

3030

3131
class AdmissionSchemaGenerator(SchemaGenerator):

api/serializers/pool_questions.py

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@
3131
from admission.ddd.admission.shared_kernel.domain.service.i_calendrier_inscription import (
3232
ICalendrierInscription,
3333
)
34-
from admission.ddd.admission.shared_kernel.domain.validator.exceptions import (
35-
ResidenceAuSensDuDecretNonDisponiblePourInscriptionException,
36-
)
3734
from admission.models import GeneralEducationAdmission
3835

3936

@@ -42,26 +39,10 @@ class PoolQuestionsSerializer(serializers.ModelSerializer):
4239
reorientation_pool_academic_year = serializers.IntegerField(read_only=True, allow_null=True)
4340
modification_pool_end_date = serializers.DateTimeField(read_only=True, allow_null=True)
4441
modification_pool_academic_year = serializers.IntegerField(read_only=True, allow_null=True)
45-
forbid_enrolment_limited_course_for_non_resident = serializers.SerializerMethodField()
46-
47-
@extend_schema_field(OpenApiTypes.STR)
48-
def get_forbid_enrolment_limited_course_for_non_resident(self, obj):
49-
return ResidenceAuSensDuDecretNonDisponiblePourInscriptionException.get_message(
50-
nom_formation_fr=obj.training.title,
51-
nom_formation_en=obj.training.title_english,
52-
)
53-
54-
def get_field_names(self, *args, **kwargs):
55-
field_names = super().get_field_names(*args, **kwargs)
56-
57-
# Add or remove the forbid enrolment limited course for non resident message depending of what is desired
58-
if 'forbid_enrolment_limited_course_for_non_resident' in field_names:
59-
if not ICalendrierInscription.INTERDIRE_INSCRIPTION_ETUDES_CONTINGENTES_POUR_NON_RESIDENT:
60-
field_names.remove('forbid_enrolment_limited_course_for_non_resident')
61-
elif ICalendrierInscription.INTERDIRE_INSCRIPTION_ETUDES_CONTINGENTES_POUR_NON_RESIDENT:
62-
field_names.append('forbid_enrolment_limited_course_for_non_resident')
63-
64-
return field_names
42+
non_resident_quota_pool_start_date = serializers.DateField(read_only=True, allow_null=True)
43+
non_resident_quota_pool_start_time = serializers.TimeField(read_only=True, allow_null=True)
44+
non_resident_quota_pool_end_date = serializers.DateField(read_only=True, allow_null=True)
45+
non_resident_quota_pool_end_time = serializers.TimeField(read_only=True, allow_null=True)
6546

6647
class Meta:
6748
model = GeneralEducationAdmission
@@ -74,9 +55,17 @@ class Meta:
7455
'registration_change_form',
7556
'regular_registration_proof_for_registration_change',
7657
'is_non_resident',
58+
'residence_certificate',
59+
'residence_student_form',
60+
'non_resident_file',
61+
'non_resident_with_second_year_enrolment',
62+
'non_resident_with_second_year_enrolment_form',
7763
'reorientation_pool_end_date',
7864
'reorientation_pool_academic_year',
7965
'modification_pool_end_date',
8066
'modification_pool_academic_year',
81-
'forbid_enrolment_limited_course_for_non_resident',
67+
'non_resident_quota_pool_start_date',
68+
'non_resident_quota_pool_start_time',
69+
'non_resident_quota_pool_end_date',
70+
'non_resident_quota_pool_end_time',
8271
]

api/serializers/submission.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ class PropositionErrorsSerializer(serializers.Serializer):
5353
errors = PropositionErrorSerializer(many=True)
5454
pool_start_date = serializers.DateField(allow_null=True, required=False)
5555
pool_end_date = serializers.DateField(allow_null=True, required=False)
56+
pool_start_time = serializers.TimeField(allow_null=True, required=False)
57+
pool_end_time = serializers.TimeField(allow_null=True, required=False)
5658
access_conditions_url = serializers.CharField(allow_null=True, required=False)
5759
elements_confirmation = ElementConfirmationSerializer(many=True, allow_null=True, required=False)
5860

api/views/pool_questions.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,17 @@
3030
from rest_framework.generics import RetrieveAPIView
3131
from rest_framework.response import Response
3232
from rest_framework.settings import api_settings
33+
from rest_framework.views import APIView
3334

3435
from admission.api.serializers.pool_questions import PoolQuestionsSerializer
3536
from admission.calendar.admission_calendar import SIGLES_WITH_QUOTA
37+
from admission.ddd import CODE_BACHELIER_VETERINAIRE
38+
from admission.ddd.admission.formation_generale.commands import VerifierPropositionQuery
3639
from admission.ddd.admission.shared_kernel.domain.validator.exceptions import (
3740
ModificationInscriptionExterneNonConfirmeeException,
3841
ReorientationInscriptionExterneNonConfirmeeException,
3942
ResidenceAuSensDuDecretNonRenseigneeException,
4043
)
41-
from admission.ddd.admission.formation_generale.commands import VerifierPropositionQuery
4244
from admission.models import GeneralEducationAdmission
4345
from admission.utils import (
4446
gather_business_exceptions,
@@ -129,8 +131,39 @@ def get(self, request, *args, **kwargs):
129131
field_questions_to_display.append(academic_year_field_name)
130132

131133
# Build relevant field list
132-
if self.get_permission_object().training.acronym in SIGLES_WITH_QUOTA:
133-
field_questions_to_display.append('is_non_resident')
134+
if admission.training.acronym in SIGLES_WITH_QUOTA:
135+
# Get dates for non-resident
136+
calendar = (
137+
AcademicCalendar.objects.filter(
138+
end_date__gte=datetime.date.today(),
139+
reference=AcademicCalendarTypes.ADMISSION_POOL_NON_RESIDENT_QUOTA.name,
140+
)
141+
.order_by('end_date')
142+
.values('start_date', 'start_time', 'end_date', 'end_time')
143+
.first()
144+
)
145+
146+
setattr(admission, 'non_resident_quota_pool_start_date', calendar['start_date'])
147+
setattr(admission, 'non_resident_quota_pool_start_time', calendar['start_time'])
148+
setattr(admission, 'non_resident_quota_pool_end_date', calendar['end_date'])
149+
setattr(admission, 'non_resident_quota_pool_end_time', calendar['end_time'])
150+
151+
field_questions_to_display += [
152+
'is_non_resident',
153+
'residence_certificate',
154+
'residence_student_form',
155+
'non_resident_file',
156+
'non_resident_quota_pool_start_date',
157+
'non_resident_quota_pool_start_time',
158+
'non_resident_quota_pool_end_date',
159+
'non_resident_quota_pool_end_time',
160+
]
161+
if admission.training.acronym not in CODE_BACHELIER_VETERINAIRE:
162+
field_questions_to_display += [
163+
'non_resident_with_second_year_enrolment',
164+
'non_resident_with_second_year_enrolment_form',
165+
]
166+
134167
if admission.reorientation_pool_end_date is not None:
135168
field_questions_to_display += [
136169
'is_belgian_bachelor',
@@ -161,6 +194,11 @@ def put(self, request, *args, **kwargs):
161194
data = {
162195
# Reset to default if not defined
163196
'is_non_resident': None,
197+
'residence_certificate': [],
198+
'residence_student_form': [],
199+
'non_resident_file': [],
200+
'non_resident_with_second_year_enrolment': None,
201+
'non_resident_with_second_year_enrolment_form': [],
164202
'is_belgian_bachelor': None,
165203
'is_external_modification': None,
166204
'is_external_reorientation': None,

api/views/project.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
from admission.ddd.admission.formation_generale.commands import (
6161
ListerPropositionsCandidatQuery as ListerPropositionsFormationGeneraleCandidatQuery,
6262
)
63-
from admission.models import DoctorateAdmission
63+
from admission.models import DoctorateAdmission, GeneralEducationAdmission
6464
from admission.utils import get_cached_admission_perm_obj
6565
from backoffice.settings.rest_framework.common_views import (
6666
DisplayExceptionsByFieldNameAPIMixin,
@@ -127,6 +127,17 @@ def list(self, request, **kwargs):
127127
]
128128
)
129129

130+
# Set _perm_obj to handle permissions in action_links
131+
queryset = (
132+
GeneralEducationAdmission.objects.select_related(
133+
'candidate',
134+
)
135+
.filter(uuid__in=[p.uuid for p in general_education_list])
136+
.in_bulk(field_name='uuid')
137+
)
138+
for proposition in general_education_list:
139+
proposition._perm_obj = queryset[proposition.uuid]
140+
130141
serializer = serializers.PropositionSearchSerializer(
131142
instance={
132143
"doctorate_propositions": doctorate_list,

api/views/proposition.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
# see http://www.gnu.org/licenses/.
2424
#
2525
# ##############################################################################
26+
from typing import Optional
27+
28+
from django.db.models import Q
29+
from django.utils import timezone
2630
from drf_spectacular.utils import extend_schema
2731
from rest_framework import mixins, status
2832
from rest_framework.generics import GenericAPIView, RetrieveAPIView
@@ -36,11 +40,15 @@
3640
from admission.ddd.admission.formation_generale import (
3741
commands as general_education_commands,
3842
)
43+
from admission.ddd.admission.shared_kernel.domain.validator.exceptions import \
44+
PoolNonResidentContingenteNonOuvertException
3945
from admission.utils import (
4046
get_cached_admission_perm_obj,
4147
get_cached_continuing_education_admission_perm_obj,
4248
get_cached_general_education_admission_perm_obj,
4349
)
50+
from base.models.academic_calendar import AcademicCalendar
51+
from base.models.enums.academic_calendar_type import AcademicCalendarTypes
4452
from infrastructure.messages_bus import message_bus_instance
4553
from osis_role.contrib.views import APIPermissionRequiredMixin
4654

@@ -54,6 +62,20 @@ class GeneralPropositionView(APIPermissionRequiredMixin, RetrieveAPIView):
5462
'DELETE': 'admission.delete_generaleducationadmission',
5563
}
5664

65+
def check_method_permissions(self, user, method, obj=None) -> Optional[str]:
66+
""" Check calendar is open for non-resident submitted proposition with quota """
67+
error_message = super().check_method_permissions(user, method, obj)
68+
if error_message or method != 'DELETE':
69+
return error_message
70+
now = timezone.now()
71+
if not AcademicCalendar.objects.filter(
72+
Q(start_date__lt=now) | Q(start_date=now, start_time__lte=now),
73+
Q(end_date__gt=now) | Q(end_date=now, end_time__gt=now),
74+
reference=AcademicCalendarTypes.ADMISSION_POOL_NON_RESIDENT_QUOTA.name,
75+
).exists():
76+
return PoolNonResidentContingenteNonOuvertException().message
77+
return None
78+
5779
def get_permission_object(self):
5880
return get_cached_general_education_admission_perm_obj(self.kwargs['uuid'])
5981

api/views/submission.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,6 @@
4141
SoumettrePropositionCommand as SoumettrePropositionDoctoratCommand,
4242
)
4343
from admission.ddd.admission.doctorat.preparation.commands import VerifierProjetQuery
44-
from admission.ddd.admission.shared_kernel.domain.validator.exceptions import (
45-
ConditionsAccessNonRempliesException,
46-
PoolNonResidentContingenteNonOuvertException,
47-
)
4844
from admission.ddd.admission.formation_continue.commands import (
4945
RecupererElementsConfirmationQuery as RecupererElementsConfirmationContinueQuery,
5046
)
@@ -57,6 +53,10 @@
5753
from admission.ddd.admission.formation_generale.commands import (
5854
SoumettrePropositionCommand as SoumettrePropositionGeneraleCommand,
5955
)
56+
from admission.ddd.admission.shared_kernel.domain.validator.exceptions import (
57+
ConditionsAccessNonRempliesException,
58+
PoolNonResidentContingenteNonOuvertException,
59+
)
6060
from admission.models import (
6161
ContinuingEducationAdmission,
6262
DoctorateAdmission,
@@ -243,6 +243,8 @@ def get(self, request, *args, **kwargs):
243243
)
244244
data['pool_start_date'] = period.start_date
245245
data['pool_end_date'] = period.end_date
246+
data['pool_start_time'] = period.start_time
247+
data['pool_end_time'] = period.end_time
246248

247249
self.add_access_conditions_url(data)
248250
if not data['errors']:

auth/predicates/general.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from rules import predicate
3030

3131
from admission.auth.predicates import not_in_general_statuses_predicate_message
32+
from admission.calendar.admission_calendar import SIGLES_WITH_QUOTA
3233
from admission.models import GeneralEducationAdmission
3334
from admission.ddd.admission.formation_generale.domain.model.enums import (
3435
ChoixStatutPropositionGenerale,
@@ -98,6 +99,12 @@ def can_view_payment(self, user: User, obj: GeneralEducationAdmission):
9899
}
99100

100101

102+
@predicate(bind=True)
103+
@predicate_failed_msg(_("The proposition must be confirmed to realize this action."))
104+
def is_confirmed(self, user: User, obj: GeneralEducationAdmission):
105+
return obj.status == ChoixStatutPropositionGenerale.CONFIRMEE.name
106+
107+
101108
@predicate(bind=True)
102109
@predicate_failed_msg(message=_("The proposition must be submitted to realize this action."))
103110
def is_submitted(self, user: User, obj: GeneralEducationAdmission):
@@ -187,3 +194,9 @@ def is_general(self, user: User, obj: GeneralEducationAdmission):
187194
from admission.constants import CONTEXT_GENERAL
188195

189196
return obj.admission_context == CONTEXT_GENERAL
197+
198+
199+
@predicate(bind=True)
200+
@predicate_failed_msg(_("The proposition must be for a contingent training for a non-resident candidat."))
201+
def is_contingent_non_resident(self, user: User, obj: GeneralEducationAdmission):
202+
return obj.training.acronym in SIGLES_WITH_QUOTA and obj.is_non_resident

auth/roles/candidate.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,9 @@
128128
'change_generaleducationadmission_accounting': common.is_admission_request_author & general.in_progress,
129129
'change_generaleducationadmission_specific_question': common.is_admission_request_author & general.in_progress,
130130
'change_generaleducationadmission': common.is_admission_request_author & general.in_progress,
131-
'delete_generaleducationadmission': common.is_admission_request_author & general.in_progress,
131+
'delete_generaleducationadmission': common.is_admission_request_author & (
132+
general.in_progress | (general.is_confirmed & general.is_contingent_non_resident)
133+
),
132134
'submit_generaleducationadmission': common.is_admission_request_author & general.in_progress,
133135
# A candidate can edit some tabs after the proposition has been submitted
134136
'view_generaleducationadmission_documents': common.is_admission_request_author & general.is_invited_to_complete,

calendar/admission_calendar.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from admission.ddd.admission.doctorat.preparation.domain.model.doctorat_formation import (
3131
DoctoratFormation,
3232
)
33+
from admission.ddd import CODE_BACHELIER_VETERINAIRE
3334
from admission.ddd.admission.doctorat.preparation.domain.validator.exceptions import (
3435
AdresseDomicileLegalNonCompleteeException,
3536
)
@@ -60,6 +61,7 @@
6061
from base.models.enums.education_group_types import TrainingType
6162

6263
__all__ = [
64+
"AdmissionNonResidentQuotaResultPublication",
6365
"AdmissionPoolExternalEnrollmentChangeCalendar",
6466
"AdmissionPoolExternalReorientationCalendar",
6567
"AdmissionPoolHue5BelgiumResidencyCalendar",
@@ -94,7 +96,7 @@
9496
ConditionAccess.ALTERNATIVE_ETUDES_SECONDAIRES,
9597
]
9698

97-
SIGLES_WITH_QUOTA = ['KINE1BA', 'VETE1BA', 'LOGO1BA']
99+
SIGLES_WITH_QUOTA = ['KINE1BA', CODE_BACHELIER_VETERINAIRE, 'LOGO1BA']
98100

99101
SECOND_CYCLE_TYPES = [
100102
TrainingType.AGGREGATION.name,
@@ -111,6 +113,8 @@ def ensure_consistency_until_n_plus_6(
111113
cutover_date: Date,
112114
title: str,
113115
end_date: Optional[Date] = DAY_BEFORE_NEXT,
116+
start_time: Optional[datetime.time] = None,
117+
end_time: Optional[datetime.time] = None,
114118
):
115119
current_academic_year = AcademicYear.objects.current()
116120
academic_years = AcademicYear.objects.min_max_years(current_academic_year.year - 1, current_academic_year.year + 6)
@@ -129,6 +133,8 @@ def ensure_consistency_until_n_plus_6(
129133
defaults={
130134
'start_date': datetime.date(ac_year.year + cutover_date.annee, cutover_date.mois, cutover_date.jour),
131135
'end_date': ac_end_date,
136+
'start_time': start_time,
137+
'end_time': end_time,
132138
'title': title,
133139
},
134140
)
@@ -558,6 +564,8 @@ def ensure_consistency_until_n_plus_6(cls):
558564
event_reference=cls.event_reference,
559565
cutover_date=cls.cutover_date,
560566
end_date=cls.end_date,
567+
start_time=datetime.time(9, 0),
568+
end_time=datetime.time(16, 0),
561569
title="Admission - Contingenté non-résident (au sens du décret)",
562570
)
563571

@@ -575,6 +583,7 @@ class AdmissionPoolMedicineDentistryStandardPeriodCalendar(PoolCalendar):
575583
cutover_date = Date(jour=6, mois=9, annee=0)
576584
end_date = Date(jour=30, mois=9, annee=0)
577585

586+
578587
@classmethod
579588
def ensure_consistency_until_n_plus_6(cls):
580589
ensure_consistency_until_n_plus_6(
@@ -586,14 +595,30 @@ def ensure_consistency_until_n_plus_6(cls):
586595

587596
@classmethod
588597
def matches_criteria(
589-
cls,
590-
proposition: 'PropositionGenerale',
591-
formation: Formation | DoctoratFormation,
592-
**kwargs,
598+
cls,
599+
proposition: 'PropositionGenerale',
600+
formation: Formation | DoctoratFormation,
601+
**kwargs,
593602
) -> bool:
594603
"""Candidat souhaitant s'inscrire à un bachelier en médecine ou dentisterie"""
595604
return (
596-
isinstance(proposition, PropositionGenerale)
597-
and formation.type == TrainingType.BACHELOR
598-
and formation.est_formation_medecine_ou_dentisterie is True
605+
isinstance(proposition, PropositionGenerale)
606+
and formation.type == TrainingType.BACHELOR
607+
and formation.est_formation_medecine_ou_dentisterie is True
608+
)
609+
610+
611+
class AdmissionNonResidentQuotaResultPublication(AcademicEventSessionCalendarHelper):
612+
event_reference = AcademicCalendarTypes.ADMISSION_NON_RESIDENT_QUOTA_RESULT_PUBLICATION.name
613+
cutover_date = Date(jour=2, mois=9, annee=0)
614+
end_date = None
615+
616+
@classmethod
617+
def ensure_consistency_until_n_plus_6(cls):
618+
ensure_consistency_until_n_plus_6(
619+
event_reference=cls.event_reference,
620+
cutover_date=cls.cutover_date,
621+
start_time=datetime.time(18, 0),
622+
end_date=cls.end_date,
623+
title="Admission: publication of the result of the random draw for the non-resident quota holder trainings",
599624
)

0 commit comments

Comments
 (0)