diff --git a/api/serializers/submission.py b/api/serializers/submission.py
index e4f82b7e7..35102a936 100644
--- a/api/serializers/submission.py
+++ b/api/serializers/submission.py
@@ -6,7 +6,7 @@
# The core business involves the administration of students, teachers,
# courses, programs and so on.
#
-# Copyright (C) 2015-2022 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
@@ -55,5 +55,5 @@ class PropositionErrorsSerializer(serializers.Serializer):
class SubmitPropositionSerializer(serializers.Serializer):
annee = serializers.IntegerField()
- pool = serializers.ChoiceField(choices=[calendar.event_reference for calendar in ICalendrierInscription.pools])
+ pool = serializers.ChoiceField(choices=[calendar.event_reference for calendar in ICalendrierInscription.all_pools])
elements_confirmation = serializers.JSONField()
diff --git a/ddd/admission/domain/service/i_calendrier_inscription.py b/ddd/admission/domain/service/i_calendrier_inscription.py
index 40aa41be8..5e5d9b372 100644
--- a/ddd/admission/domain/service/i_calendrier_inscription.py
+++ b/ddd/admission/domain/service/i_calendrier_inscription.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
@@ -64,7 +64,6 @@ class ICalendrierInscription(interface.DomainService):
DoctorateAdmissionCalendar(),
ContinuingEducationAdmissionCalendar(),
AdmissionPoolExternalEnrollmentChangeCalendar(),
- AdmissionPoolExternalReorientationCalendar(),
AdmissionPoolVipCalendar(),
AdmissionPoolHueUclPathwayChangeCalendar(),
AdmissionPoolInstituteChangeCalendar(),
@@ -74,6 +73,10 @@ class ICalendrierInscription(interface.DomainService):
AdmissionPoolHue5ForeignResidencyCalendar(),
AdmissionPoolNonResidentQuotaCalendar(),
]
+ priority_pools = [
+ AdmissionPoolExternalReorientationCalendar(),
+ ]
+ all_pools = priority_pools + pools
# Les inscriptions pour une formation contingentée pour un candidat non résident au sens du décret via osis
# sont interdites pour le moment
@@ -106,7 +109,7 @@ def determiner_annee_academique_et_pot(
and getattr(proposition.comptabilite, 'type_situation_assimilation', None)
)
ue_plus_5 = cls.est_ue_plus_5(identification, situation_assimilation)
- annees = cls.get_annees_academiques_pour_calcul(type_formation=type_formation)
+ annees_prioritaires, annees = cls.get_annees_academiques_pour_calcul(type_formation=type_formation)
changements_etablissement = profil_candidat_translator.get_changements_etablissement(matricule_candidat, annees)
log_messages = [
@@ -124,20 +127,36 @@ def determiner_annee_academique_et_pot(
proposition={('Proposition(' + pformat(attr.asdict(proposition)) + ')') if proposition else 'None'},
""",
]
+ current_kwargs = dict(
+ logs=log_messages,
+ pool_ouverts=pool_ouverts,
+ sigle=formation_id.sigle,
+ ue_plus_5=ue_plus_5,
+ access_diplomas=titres_acces.get_valid_conditions(),
+ training_type=type_formation,
+ residential_address=residential_address,
+ annee_derniere_inscription_ucl=identification.annee_derniere_inscription_ucl,
+ matricule_candidat=matricule_candidat,
+ changements_etablissement=changements_etablissement,
+ proposition=proposition,
+ )
+
+ for annee in annees_prioritaires:
+ pool = cls.determiner_pool_pour_annee_academique(
+ pools=cls.priority_pools,
+ annee_academique=annee,
+ **current_kwargs,
+ )
+ if pool:
+ logger.debug('\n'.join(log_messages))
+ return InfosDetermineesDTO(annee, pool)
+ log_messages.append("")
+
for annee in annees:
pool = cls.determiner_pool_pour_annee_academique(
- log_messages,
- pool_ouverts,
+ pools=cls.pools,
annee_academique=annee,
- sigle=formation_id.sigle,
- ue_plus_5=ue_plus_5,
- access_diplomas=titres_acces.get_valid_conditions(),
- training_type=type_formation,
- residential_address=residential_address,
- annee_derniere_inscription_ucl=identification.annee_derniere_inscription_ucl,
- matricule_candidat=matricule_candidat,
- changements_etablissement=changements_etablissement,
- proposition=proposition,
+ **current_kwargs,
)
if pool:
logger.debug('\n'.join(log_messages))
@@ -152,9 +171,10 @@ def determiner_pool_pour_annee_academique(
cls,
logs: List[str],
pool_ouverts: List[Tuple[str, int]],
+ pools: List[PoolCalendar],
**kwargs,
) -> Optional['AcademicCalendarTypes']:
- for pool in cls.pools:
+ for pool in pools:
annee = kwargs['annee_academique']
logs.append(
f"{str(AcademicCalendarTypes.get_value(pool.event_reference)):<70} {annee}"
@@ -271,7 +291,11 @@ def get_pool_ouverts(cls) -> List[Tuple[str, int]]:
raise NotImplementedError
@classmethod
- def get_annees_academiques_pour_calcul(cls, type_formation: TrainingType) -> List[int]:
+ def get_annees_academiques_pour_calcul(cls, type_formation: TrainingType) -> Tuple[List[int], List[int]]:
+ """
+ Retourne un tuple contenant les deux listes des années académiques utilisées dans le calcul des pots, la
+ première pour les pots prioritaires et la seconde pour les autres pots.
+ """
raise NotImplementedError
@classmethod
diff --git a/ddd/admission/formation_generale/domain/model/proposition.py b/ddd/admission/formation_generale/domain/model/proposition.py
index 89e132451..1604198db 100644
--- a/ddd/admission/formation_generale/domain/model/proposition.py
+++ b/ddd/admission/formation_generale/domain/model/proposition.py
@@ -310,7 +310,7 @@ def soumettre(
self.pot_calcule = pool
self.elements_confirmation = elements_confirmation
self.soumise_le = now()
- if pool != AcademicCalendarTypes.ADMISSION_POOL_HUE_UCL_PATHWAY_CHANGE:
+ if pool != AcademicCalendarTypes.ADMISSION_POOL_EXTERNAL_REORIENTATION:
self.attestation_inscription_reguliere = []
if pool != AcademicCalendarTypes.ADMISSION_POOL_EXTERNAL_ENROLLMENT_CHANGE:
self.formulaire_modification_inscription = []
diff --git a/ddd/admission/test/domain/service/test_calendrier_inscription.py b/ddd/admission/test/domain/service/test_calendrier_inscription.py
index 2e24be77c..3638dc27e 100644
--- a/ddd/admission/test/domain/service/test_calendrier_inscription.py
+++ b/ddd/admission/test/domain/service/test_calendrier_inscription.py
@@ -241,6 +241,7 @@ def test_verification_calendrier_inscription_reorientation_validee(self):
proposition = PropositionFactory(est_bachelier_en_reorientation=True)
profil = ProfilCandidatFactory(matricule=proposition.matricule_candidat)
self.profil_candidat_translator.profil_candidats.append(profil.identification)
+ self.profil_candidat_translator.get_coordonnees = lambda m: profil.coordonnees
dto = CalendrierInscriptionInMemory.determiner_annee_academique_et_pot(
formation_id=proposition.formation_id,
proposition=proposition,
@@ -250,6 +251,24 @@ def test_verification_calendrier_inscription_reorientation_validee(self):
profil_candidat_translator=self.profil_candidat_translator,
)
self.assertEqual(dto.pool, AcademicCalendarTypes.ADMISSION_POOL_EXTERNAL_REORIENTATION)
+ self.assertEqual(dto.annee, 2022)
+
+ @freezegun.freeze_time('2022-03-15')
+ def test_verification_calendrier_inscription_reorientation_validee_hors_periode(self):
+ # Nous ne sommes pas dans la période réorientation mais le candidat l'a validée
+ proposition = PropositionFactory(est_bachelier_en_reorientation=True)
+ profil = ProfilCandidatFactory(matricule=proposition.matricule_candidat)
+ self.profil_candidat_translator.profil_candidats.append(profil.identification)
+ self.profil_candidat_translator.get_coordonnees = lambda m: profil.coordonnees
+ dto = CalendrierInscriptionInMemory.determiner_annee_academique_et_pot(
+ formation_id=proposition.formation_id,
+ proposition=proposition,
+ matricule_candidat=proposition.matricule_candidat,
+ titres_acces=Titres(AdmissionConditionsDTOFactory()),
+ type_formation=TrainingType.BACHELOR,
+ profil_candidat_translator=self.profil_candidat_translator,
+ )
+ self.assertNotEqual(dto.pool, AcademicCalendarTypes.ADMISSION_POOL_EXTERNAL_REORIENTATION)
@freezegun.freeze_time('2022-12-15')
def test_verification_calendrier_inscription_reorientation_non_choisie(self):
diff --git a/forms/__init__.py b/forms/__init__.py
index 81cae57cf..62e907a5a 100644
--- a/forms/__init__.py
+++ b/forms/__init__.py
@@ -356,3 +356,22 @@ def clean(self, value):
return html.unescape(cleaned_value)
return cleaned_value
+
+
+class AutoGrowTextareaWidget(forms.Textarea):
+ """A textarea widget whose minimum height is automatically adjusted to fit its content."""
+
+ template_name = "admission/widgets/autogrow_textarea.html"
+
+ def __init__(self, attrs=None):
+ if not attrs:
+ attrs = {}
+
+ attrs['onInput'] = 'this.parentNode.dataset.value = this.value'
+
+ super().__init__(attrs)
+
+ class Media:
+ css = {
+ 'all': ('admission/autogrow_textarea.css',),
+ }
diff --git a/forms/admission/checklist.py b/forms/admission/checklist.py
index 80e6d818e..9687827d2 100644
--- a/forms/admission/checklist.py
+++ b/forms/admission/checklist.py
@@ -47,12 +47,6 @@
)
from admission.constants import CONTEXT_GENERAL, CONTEXT_DOCTORATE
-from admission.models import GeneralEducationAdmission, DoctorateAdmission
-from admission.models.base import training_campus_subquery
-from admission.models.checklist import (
- RefusalReason,
- AdditionalApprovalCondition,
-)
from admission.ddd import DUREE_MINIMALE_PROGRAMME, DUREE_MAXIMALE_PROGRAMME
from admission.ddd.admission.domain.model.enums.authentification import EtatAuthentificationParcours
from admission.ddd.admission.domain.model.enums.condition_acces import recuperer_conditions_acces_par_formation
@@ -79,9 +73,16 @@
EMPTY_CHOICE_AS_LIST,
get_initial_choices_for_additional_approval_conditions,
AdmissionHTMLCharField,
+ AutoGrowTextareaWidget,
)
from admission.forms import get_academic_year_choices
from admission.forms.admission.document import ChangeRequestDocumentForm
+from admission.models import GeneralEducationAdmission, DoctorateAdmission
+from admission.models.base import training_campus_subquery
+from admission.models.checklist import (
+ RefusalReason,
+ AdditionalApprovalCondition,
+)
from admission.views.autocomplete.learning_unit_years import LearningUnitYearAutocomplete
from admission.views.common.detail_tabs.comments import (
COMMENT_TAG_SIC,
@@ -114,7 +115,7 @@
class CommentForm(forms.Form):
comment = forms.CharField(
- widget=forms.Textarea(
+ widget=AutoGrowTextareaWidget(
attrs={
'rows': 2,
'hx-trigger': 'keyup changed delay:2s',
diff --git a/infrastructure/admission/domain/service/calendrier_inscription.py b/infrastructure/admission/domain/service/calendrier_inscription.py
index ee8e5a457..121b51712 100644
--- a/infrastructure/admission/domain/service/calendrier_inscription.py
+++ b/infrastructure/admission/domain/service/calendrier_inscription.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
@@ -39,9 +39,9 @@
class CalendrierInscription(ICalendrierInscription):
@classmethod
- def get_annees_academiques_pour_calcul(cls, type_formation: TrainingType) -> List[int]:
+ def get_annees_academiques_pour_calcul(cls, type_formation: TrainingType) -> Tuple[List[int], List[int]]:
year = AnneeInscriptionFormationTranslator().recuperer_annee_selon_type_formation(type_formation)
- return [year, year - 1, year + 1, year + 2]
+ return ([year - 1, year], [year, year - 1, year + 1, year + 2])
@classmethod
def get_pool_ouverts(cls) -> List[Tuple[str, int]]:
diff --git a/infrastructure/admission/domain/service/in_memory/calendrier_inscription.py b/infrastructure/admission/domain/service/in_memory/calendrier_inscription.py
index 1fa7b8e36..0f1c4ac5f 100644
--- a/infrastructure/admission/domain/service/in_memory/calendrier_inscription.py
+++ b/infrastructure/admission/domain/service/in_memory/calendrier_inscription.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
@@ -24,12 +24,18 @@
#
# ##############################################################################
from datetime import date, timedelta
-from typing import List, Tuple
+from typing import List, Tuple, Optional
+from admission.constants import CONTEXT_GENERAL, CONTEXT_DOCTORATE, CONTEXT_CONTINUING
+from admission.ddd.admission.domain.service.i_annee_inscription_formation import IAnneeInscriptionFormationTranslator
from admission.ddd.admission.domain.service.i_calendrier_inscription import ICalendrierInscription
from admission.ddd.admission.dtos import IdentificationDTO
from admission.ddd.admission.enums import TypeSituationAssimilation
+from admission.infrastructure.admission.domain.service.in_memory.annee_inscription_formation import (
+ AnneeInscriptionFormationInMemoryTranslator,
+)
from admission.infrastructure.admission.domain.service.in_memory.profil_candidat import ProfilCandidatInMemoryTranslator
+from base.models.enums.academic_calendar_type import AcademicCalendarTypes
from base.models.enums.education_group_types import TrainingType
from base.tests.factories.academic_year import get_current_year
from osis_profile import PLUS_5_ISO_CODES
@@ -41,24 +47,35 @@ class CalendrierInscriptionInMemory(ICalendrierInscription):
pool.cutover_date,
getattr(pool, 'end_date', None),
)
- for pool in ICalendrierInscription.pools
+ for pool in ICalendrierInscription.all_pools
}
@classmethod
- def get_annees_academiques_pour_calcul(cls, type_formation: TrainingType) -> List[int]:
- return cls._get_annees_academiques_pour_calcul()
+ def get_annees_academiques_pour_calcul(cls, type_formation: TrainingType) -> Tuple[List[int], List[int]]:
+ from admission.infrastructure.admission.domain.service.annee_inscription_formation import (
+ ADMISSION_CONTEXT_BY_OSIS_EDUCATION_TYPE,
+ )
- @classmethod
- def _get_annees_academiques_pour_calcul(cls) -> List[int]:
- current_year = get_current_year()
- return [current_year, current_year - 1, current_year + 1, current_year + 2]
+ current_year = AnneeInscriptionFormationInMemoryTranslator.recuperer(
+ {
+ CONTEXT_GENERAL: AcademicCalendarTypes.GENERAL_EDUCATION_ENROLLMENT,
+ CONTEXT_DOCTORATE: AcademicCalendarTypes.DOCTORATE_EDUCATION_ENROLLMENT,
+ CONTEXT_CONTINUING: AcademicCalendarTypes.CONTINUING_EDUCATION_ENROLLMENT,
+ }[ADMISSION_CONTEXT_BY_OSIS_EDUCATION_TYPE[type_formation.name]]
+ )
+
+ return (
+ [current_year - 1, current_year],
+ [current_year, current_year - 1, current_year + 1, current_year + 2],
+ )
@classmethod
def get_pool_ouverts(cls) -> List[Tuple[str, int]]:
opened = []
today = date.today()
+ annees = [today.year, today.year - 1, today.year + 1, today.year + 2]
for pool_name, dates in cls.periodes_ouvertes.items():
- for annee in cls._get_annees_academiques_pour_calcul():
+ for annee in annees:
date_debut, date_fin = cls._get_dates_completes(annee, dates[0], dates[1])
if date_debut <= today <= date_fin:
opened.append((pool_name, annee))
diff --git a/schema.yml b/schema.yml
index 75b7c2dde..276f83168 100644
--- a/schema.yml
+++ b/schema.yml
@@ -11004,10 +11004,10 @@ components:
type: integer
pool:
enum:
+ - ADMISSION_POOL_EXTERNAL_REORIENTATION
- DOCTORATE_EDUCATION_ENROLLMENT
- CONTINUING_EDUCATION_ENROLLMENT
- ADMISSION_POOL_EXTERNAL_ENROLLMENT_CHANGE
- - ADMISSION_POOL_EXTERNAL_REORIENTATION
- ADMISSION_POOL_VIP
- ADMISSION_POOL_HUE_UCL_PATHWAY_CHANGE
- ADMISSION_POOL_INSTITUT_CHANGE
diff --git a/static/admission/autogrow_textarea.css b/static/admission/autogrow_textarea.css
new file mode 100644
index 000000000..1a9673c4a
--- /dev/null
+++ b/static/admission/autogrow_textarea.css
@@ -0,0 +1,42 @@
+/*
+ *
+ * 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/.
+ *
+ */
+
+.autogrow-textarea-wrapper {
+ display: grid;
+}
+
+.autogrow-textarea-wrapper:after {
+ content: attr(data-value) ' ';
+ visibility: hidden;
+ display: block;
+ white-space: pre-wrap;
+}
+
+.autogrow-textarea-wrapper textarea, .autogrow-textarea-wrapper:after {
+ min-height: 100% !important;
+ grid-area: 1 / 1 / 1 / 1;
+ padding: 0.5em;
+}
diff --git a/templates/admission/continuing_education/checklist.html b/templates/admission/continuing_education/checklist.html
index 1eb56f14e..54a47ed78 100644
--- a/templates/admission/continuing_education/checklist.html
+++ b/templates/admission/continuing_education/checklist.html
@@ -268,16 +268,6 @@
$(this).find('.next-option-button').prop('disabled', optionsNumber <=1);
});
- function autogrow () {
- if (this.scrollHeight > this.clientHeight) {
- this.style.height = `${this.scrollHeight}px`;
- }
- }
-
- $('#tabs-content').on('input', 'textarea[name$="-comment"]', function () {
- autogrow.call(this);
- });
-
const menuItems = $('#checklist-menu *[data-toggle="tab"]');
menuItems.on('show.bs.tab', function (e) {
@@ -300,9 +290,6 @@
const tabPaneId = $(this).attr('href');
window.location.hash = tabPaneId;
- // refresh comment height
- autogrow.call($(tabPaneId).find('textarea[name$="-comment"]')[0]);
-
refreshDocuments(tabPaneId);
// refresh info viewer
@@ -352,8 +339,6 @@
// Activate tab from hash on first load
$(`#checklist-menu *[data-toggle="tab"][href="${defaultTab}"]`).click();
-
- $('textarea[name="comment"]:visible').each(autogrow);
});
let bound = false;
@@ -453,10 +438,6 @@
}
});
- $('#tabs-content').on('htmx:afterSettle', function(event){
- $('textarea[name$="-comment"]:visible').each(autogrow);
- });
-
$('#tabs-content').on('htmx:afterRequest', function(event) {
// Do not remove spinner if we are going to refresh the page
if (event.originalEvent.detail.xhr.getResponseHeader('hx-refresh') === 'true') {
@@ -543,6 +524,7 @@
{{ block.super }}
+