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 }} +