From 09ff7f5d5dcff5550c03b1cf1f5a73aa90ff5f31 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Wed, 4 May 2022 16:28:34 +0200 Subject: [PATCH 01/60] ajout du champs titre_court pour les retours de formsemestres --- app/api/departements.py | 56 ++++++++++++++++++-------------------- app/api/etudiants.py | 3 +- app/api/formsemestres.py | 4 +-- app/models/formsemestre.py | 23 ++++++++++++++++ 4 files changed, 54 insertions(+), 32 deletions(-) diff --git a/app/api/departements.py b/app/api/departements.py index 482bceca..3802693f 100644 --- a/app/api/departements.py +++ b/app/api/departements.py @@ -117,34 +117,32 @@ def liste_semestres_courant(dept: str): Exemple de résultat : [ { - "titre": "master machine info", - "gestion_semestrielle": false, - "scodoc7_id": null, - "date_debut": "01/09/2021", - "bul_bgcolor": null, - "date_fin": "15/12/2022", - "resp_can_edit": false, - "dept_id": 1, - "etat": true, - "resp_can_change_ens": false, - "id": 1, - "modalite": "FI", - "ens_can_edit_eval": false, - "formation_id": 1, - "gestion_compensation": false, - "elt_sem_apo": null, - "semestre_id": 1, - "bul_hide_xml": false, - "elt_annee_apo": null, - "block_moyennes": false, - "formsemestre_id": 1, - "titre_num": "master machine info semestre 1", - "date_debut_iso": "2021-09-01", - "date_fin_iso": "2022-12-15", - "responsables": [ - 3, - 2 - ] + "date_fin": "31/08/2022", + "resp_can_edit": false, + "dept_id": 1, + "etat": true, + "resp_can_change_ens": true, + "id": 1, + "modalite": "FI", + "ens_can_edit_eval": false, + "formation_id": 1, + "gestion_compensation": false, + "elt_sem_apo": null, + "semestre_id": 1, + "bul_hide_xml": false, + "elt_annee_apo": null, + "titre": "Semestre test", + "block_moyennes": false, + "scodoc7_id": null, + "date_debut": "01/09/2021", + "gestion_semestrielle": false, + "bul_bgcolor": "white", + "formsemestre_id": 1, + "titre_num": "Semestre test semestre 1", + "date_debut_iso": "2021-09-01", + "date_fin_iso": "2022-08-31", + "responsables": [] + "titre_court": BUT MMI }, ... ] @@ -156,7 +154,7 @@ def liste_semestres_courant(dept: str): semestres = models.FormSemestre.query.filter_by(dept_id=dept.id, etat=True) # Mise en forme des données - data = [d.to_dict() for d in semestres] + data = [d.to_dict_api() for d in semestres] return jsonify(data) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index c04495ab..92aff67c 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -144,6 +144,7 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) "date_debut_iso": "2021-09-01", "date_fin_iso": "2022-08-31", "responsables": [] + "titre_court": BUT MMI }, ... ] @@ -156,7 +157,7 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) models.FormSemestreInscription.formsemestre_id == models.FormSemestre.id, ).order_by(models.FormSemestre.date_debut) - return jsonify([formsemestre.to_dict() for formsemestre in formsemestres]) + return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres]) @bp.route( diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 122abf14..87497d75 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -56,7 +56,7 @@ def formsemestre(formsemestre_id: int): formsemetre = models.FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() # Mise en forme des données - data = formsemetre.to_dict() + data = formsemetre.to_dict_api() return jsonify(data) @@ -103,7 +103,7 @@ def formsemestre_apo(etape_apo: str): FormSemestreEtape.formsemestre_id == FormSemestre.id, ) - return jsonify([formsemestre.to_dict() for formsemestre in formsemestres]) + return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres]) @bp.route( diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index fda72383..4831e589 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -141,6 +141,28 @@ class FormSemestre(db.Model): d["responsables"] = [u.id for u in self.responsables] return d + def to_dict_api(self): + """ + Un dict avec les informations sur le semestre destiné à l'api + """ + d = dict(self.__dict__) + d.pop("_sa_instance_state", None) + d["formsemestre_id"] = self.id + d["titre_num"] = self.titre_num() + if self.date_debut: + d["date_debut"] = self.date_debut.strftime("%d/%m/%Y") + d["date_debut_iso"] = self.date_debut.isoformat() + else: + d["date_debut"] = d["date_debut_iso"] = "" + if self.date_fin: + d["date_fin"] = self.date_fin.strftime("%d/%m/%Y") + d["date_fin_iso"] = self.date_fin.isoformat() + else: + d["date_fin"] = d["date_fin_iso"] = "" + d["responsables"] = [u.id for u in self.responsables] + d["titre_court"] = self.formation.acronyme + return d + def get_infos_dict(self) -> dict: """Un dict avec des informations sur le semestre pour les bulletins et autres templates @@ -373,6 +395,7 @@ class FormSemestre(db.Model): if self.semestre_id == sco_codes_parcours.NO_SEMESTRE_ID: return self.titre return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}" + # return f"{self.formation.acronyme} S{self.semestre_id}" def sem_modalite(self) -> str: """Le semestre et la modalité, ex "S2 FI" ou "S3 APP" """ From b5e4017315c3ebe0682e603460b014822b966d27 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Wed, 4 May 2022 16:30:13 +0200 Subject: [PATCH 02/60] ajout tests unitaire pour evaluations et absences --- tests/api/test_api_absences.py | 68 +++++++++++++++++++++++-------- tests/api/test_api_evaluations.py | 30 ++++++++++++++ 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/tests/api/test_api_absences.py b/tests/api/test_api_absences.py index 0918af53..1e1fca64 100644 --- a/tests/api/test_api_absences.py +++ b/tests/api/test_api_absences.py @@ -19,62 +19,98 @@ Utilisation : import requests from tests.api.setup_test_api import SCODOC_URL, CHECK_CERTIFICATE, HEADERS +from tests.api.tools_test_api import verify_fields # absences def test_absences(): - r = requests.get( - SCODOC_URL + "/ScoDoc/api/absences/etudid/", - headers=HEADERS, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 + + fields = ["jour", "matin", "estabs", "estjust", "description", "begin", "end"] r = requests.get( - SCODOC_URL + "/ScoDoc/api/absences/nip/", + SCODOC_URL + "/ScoDoc/api/absences/etudid/1", headers=HEADERS, verify=CHECK_CERTIFICATE, ) + + abs = r.json()[0] + + fields_OK = verify_fields(abs, fields) + assert r.status_code == 200 + assert fields_OK is True r = requests.get( - SCODOC_URL + "/ScoDoc/api/absences/ine/", + SCODOC_URL + "/ScoDoc/api/absences/nip/1", headers=HEADERS, verify=CHECK_CERTIFICATE, ) + + abs = r.json()[0] + + fields_OK = verify_fields(abs, fields) assert r.status_code == 200 + assert fields_OK is True + + r = requests.get( + SCODOC_URL + "/ScoDoc/api/absences/ine/1", + headers=HEADERS, + verify=CHECK_CERTIFICATE, + ) + + abs = r.json()[0] + + fields_OK = verify_fields(abs, fields) + assert r.status_code == 200 + assert fields_OK is True # absences_justify def test_absences_justify(): + fields = ["jour", "matin", "estabs", "estjust", "description", "begin", "end"] r = requests.get( SCODOC_URL + "/ScoDoc/api/absences/etudid/1/just", headers=HEADERS, verify=CHECK_CERTIFICATE, ) + + abs = r.json()[0] + + fields_OK = verify_fields(abs, fields) assert r.status_code == 200 + assert fields_OK is True r = requests.get( SCODOC_URL + "/ScoDoc/api/absences/nip/1/just", headers=HEADERS, verify=CHECK_CERTIFICATE, ) + + abs = r.json()[0] + + fields_OK = verify_fields(abs, fields) assert r.status_code == 200 + assert fields_OK is True r = requests.get( SCODOC_URL + "/ScoDoc/api/absences/ine/1/just", headers=HEADERS, verify=CHECK_CERTIFICATE, ) + + abs = r.json()[0] + + fields_OK = verify_fields(abs, fields) assert r.status_code == 200 + assert fields_OK is True # abs_groupe_etat -def test_abs_groupe_etat(): - r = requests.get( - SCODOC_URL - + "/ScoDoc/api/absences/abs_group_etat/?group_id=&date_debut=date_debut&date_fin=date_fin", - headers=HEADERS, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 +# def test_abs_groupe_etat(): +# r = requests.get( +# SCODOC_URL +# + "/ScoDoc/api/absences/abs_group_etat/?group_id=&date_debut=date_debut&date_fin=date_fin", +# headers=HEADERS, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 diff --git a/tests/api/test_api_evaluations.py b/tests/api/test_api_evaluations.py index 1fb6ffbd..5fe3eb93 100644 --- a/tests/api/test_api_evaluations.py +++ b/tests/api/test_api_evaluations.py @@ -20,15 +20,45 @@ Utilisation : import requests from tests.api.setup_test_api import SCODOC_URL, CHECK_CERTIFICATE, HEADERS +from tests.api.tools_test_api import verify_fields + # evaluations def test_evaluations(): + fields = [ + "moduleimpl_id", + "jour", + "heure_debut", + "description", + "coefficient", + "publish_incomplete", + "numero", + "id", + "heure_fin", + "note_max", + "visibulletin", + "evaluation_type", + "evaluation_id", + "jouriso", + "duree", + "descrheure", + "matin", + "apresmidi", + ] + r = requests.get( SCODOC_URL + "/ScoDoc/api/evaluations/1", headers=HEADERS, verify=CHECK_CERTIFICATE, ) + + eval = r.json()[0] + + fields_OK = verify_fields(eval, fields) + assert r.status_code == 200 + # assert len(r.json()) == 1 + assert fields_OK is True # evaluation_notes From 2cc1ab13d37c56d0eaccc58c7fae46b2d0e2ad06 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Thu, 5 May 2022 14:30:43 +0200 Subject: [PATCH 03/60] =?UTF-8?q?sp=C3=A9cification=20des=20exemple=20de?= =?UTF-8?q?=20responsables=20+=20changement=20nom=20fonction=20semestre=5F?= =?UTF-8?q?index=20en=20programme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/departements.py | 55 +++++++------- app/api/etudiants.py | 61 ++++++++-------- app/api/formations.py | 4 +- app/api/formsemestres.py | 146 +++++++++++++++++++------------------ app/models/formsemestre.py | 1 - 5 files changed, 141 insertions(+), 126 deletions(-) diff --git a/app/api/departements.py b/app/api/departements.py index 6da2b90e..ae8bc775 100644 --- a/app/api/departements.py +++ b/app/api/departements.py @@ -15,7 +15,7 @@ from app.scodoc.sco_permissions import Permission @token_permission_required(Permission.APIView) def departements(): """ - Retourne la liste des ids de départements visibles + Retourne la liste des départements visibles Exemple de résultat : [ @@ -118,31 +118,34 @@ def liste_semestres_courant(dept: str): [ { "date_fin": "31/08/2022", - "resp_can_edit": false, - "dept_id": 1, - "etat": true, - "resp_can_change_ens": true, - "id": 1, - "modalite": "FI", - "ens_can_edit_eval": false, - "formation_id": 1, - "gestion_compensation": false, - "elt_sem_apo": null, - "semestre_id": 1, - "bul_hide_xml": false, - "elt_annee_apo": null, - "titre": "Semestre test", - "block_moyennes": false, - "scodoc7_id": null, - "date_debut": "01/09/2021", - "gestion_semestrielle": false, - "bul_bgcolor": "white", - "formsemestre_id": 1, - "titre_num": "Semestre test semestre 1", - "date_debut_iso": "2021-09-01", - "date_fin_iso": "2022-08-31", - "responsables": [] - "titre_court": BUT MMI + "resp_can_edit": false, + "dept_id": 1, + "etat": true, + "resp_can_change_ens": true, + "id": 1, + "modalite": "FI", + "ens_can_edit_eval": false, + "formation_id": 1, + "gestion_compensation": false, + "elt_sem_apo": null, + "semestre_id": 1, + "bul_hide_xml": false, + "elt_annee_apo": null, + "titre": "Semestre test", + "block_moyennes": false, + "scodoc7_id": null, + "date_debut": "01/09/2021", + "gestion_semestrielle": false, + "bul_bgcolor": "white", + "formsemestre_id": 1, + "titre_num": "Semestre test semestre 1", + "date_debut_iso": "2021-09-01", + "date_fin_iso": "2022-08-31", + "responsables": [ + 12, + 42 + ], + "titre_court": "BUT MMI" }, ... ] diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 34566e57..042c72b4 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -120,35 +120,38 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) Exemple de résultat : [ - { - "date_fin": "31/08/2022", - "resp_can_edit": false, - "dept_id": 1, - "etat": true, - "resp_can_change_ens": true, - "id": 1, - "modalite": "FI", - "ens_can_edit_eval": false, - "formation_id": 1, - "gestion_compensation": false, - "elt_sem_apo": null, - "semestre_id": 1, - "bul_hide_xml": false, - "elt_annee_apo": null, - "titre": "Semestre test", - "block_moyennes": false, - "scodoc7_id": null, - "date_debut": "01/09/2021", - "gestion_semestrielle": false, - "bul_bgcolor": "white", - "formsemestre_id": 1, - "titre_num": "Semestre test semestre 1", - "date_debut_iso": "2021-09-01", - "date_fin_iso": "2022-08-31", - "responsables": [] - "titre_court": BUT MMI - }, - ... + { + "date_fin": "31/08/2022", + "resp_can_edit": false, + "dept_id": 1, + "etat": true, + "resp_can_change_ens": true, + "id": 1, + "modalite": "FI", + "ens_can_edit_eval": false, + "formation_id": 1, + "gestion_compensation": false, + "elt_sem_apo": null, + "semestre_id": 1, + "bul_hide_xml": false, + "elt_annee_apo": null, + "titre": "Semestre test", + "block_moyennes": false, + "scodoc7_id": null, + "date_debut": "01/09/2021", + "gestion_semestrielle": false, + "bul_bgcolor": "white", + "formsemestre_id": 1, + "titre_num": "Semestre test semestre 1", + "date_debut_iso": "2021-09-01", + "date_fin_iso": "2022-08-31", + "responsables": [ + 12, + 42 + ], + "titre_court": "BUT MMI" + }, + ... ] """ # Récupération de l'étudiant diff --git a/app/api/formations.py b/app/api/formations.py index 3724c96a..e57f74f7 100644 --- a/app/api/formations.py +++ b/app/api/formations.py @@ -16,7 +16,7 @@ from app.scodoc.sco_permissions import Permission @token_permission_required(Permission.APIView) def formations_ids(): """ - Retourne la liste de toutes les formations (tous départements) + Retourne la liste de toutes les id de formations (tous départements) Exemple de résultat : [ 17, 99, 32 ] """ @@ -79,6 +79,7 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False): Retourne la formation, avec UE, matières, modules formation_id : l'id d'une formation + export_ids : True ou False, si l'on veut ou non exporter les ids Exemple de résultat : { @@ -294,6 +295,7 @@ def moduleimpls_sem(formsemestre_id: int): def referentiel_competences(formation_id: int): """ Retourne le référentiel de compétences + formation_id : l'id d'une formation return json, ou null si pas de référentiel associé. diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 24ec5eeb..a9116a94 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -25,38 +25,41 @@ def formsemestre(formsemestre_id: int): Exemple de résultat : { - "date_fin": "31/08/2022", - "resp_can_edit": false, - "dept_id": 1, - "etat": true, - "resp_can_change_ens": true, - "id": 1, - "modalite": "FI", - "ens_can_edit_eval": false, - "formation_id": 1, - "gestion_compensation": false, - "elt_sem_apo": null, - "semestre_id": 1, - "bul_hide_xml": false, - "elt_annee_apo": null, - "titre": "Semestre test", - "block_moyennes": false, - "scodoc7_id": null, - "date_debut": "01/09/2021", - "gestion_semestrielle": false, - "bul_bgcolor": "white", - "formsemestre_id": 1, - "titre_num": "Semestre test semestre 1", - "date_debut_iso": "2021-09-01", - "date_fin_iso": "2022-08-31", - "responsables": [] <<< A DOCUMENTER XXX - } - + "date_fin": "31/08/2022", + "resp_can_edit": false, + "dept_id": 1, + "etat": true, + "resp_can_change_ens": true, + "id": 1, + "modalite": "FI", + "ens_can_edit_eval": false, + "formation_id": 1, + "gestion_compensation": false, + "elt_sem_apo": null, + "semestre_id": 1, + "bul_hide_xml": false, + "elt_annee_apo": null, + "titre": "Semestre test", + "block_moyennes": false, + "scodoc7_id": null, + "date_debut": "01/09/2021", + "gestion_semestrielle": false, + "bul_bgcolor": "white", + "formsemestre_id": 1, + "titre_num": "Semestre test semestre 1", + "date_debut_iso": "2021-09-01", + "date_fin_iso": "2022-08-31", + "responsables": [ + 12, + 42 + ], + "titre_court": "BUT MMI" + } """ formsemestre = models.FormSemestre.query.filter_by( id=formsemestre_id ).first_or_404() - data = formsemestre.to_dict() + data = formsemestre.to_dict_api() return jsonify(data) @@ -70,42 +73,47 @@ def formsemestre_apo(etape_apo: str): etape_apo : un code étape apogée Exemple de résultat : - [ - { - "date_fin": "31/08/2022", - "resp_can_edit": false, - "dept_id": 1, - "etat": true, - "resp_can_change_ens": true, - "id": 1, - "modalite": "FI", - "ens_can_edit_eval": false, - "formation_id": 1, - "gestion_compensation": false, - "elt_sem_apo": null, - "semestre_id": 1, - "bul_hide_xml": false, - "elt_annee_apo": null, - "titre": "Semestre test", - "block_moyennes": false, - "scodoc7_id": null, - "date_debut": "01/09/2021", - "gestion_semestrielle": false, - "bul_bgcolor": "white", - "formsemestre_id": 1, - "titre_num": "Semestre test semestre 1", - "date_debut_iso": "2021-09-01", - "date_fin_iso": "2022-08-31", - "responsables": [] - }, ... - ] + [ + { + "date_fin": "31/08/2022", + "resp_can_edit": false, + "dept_id": 1, + "etat": true, + "resp_can_change_ens": true, + "id": 1, + "modalite": "FI", + "ens_can_edit_eval": false, + "formation_id": 1, + "gestion_compensation": false, + "elt_sem_apo": null, + "semestre_id": 1, + "bul_hide_xml": false, + "elt_annee_apo": null, + "titre": "Semestre test", + "block_moyennes": false, + "scodoc7_id": null, + "date_debut": "01/09/2021", + "gestion_semestrielle": false, + "bul_bgcolor": "white", + "formsemestre_id": 1, + "titre_num": "Semestre test semestre 1", + "date_debut_iso": "2021-09-01", + "date_fin_iso": "2022-08-31", + "responsables": [ + 12, + 42 + ], + "titre_court": "BUT MMI" + }, + ... + ] """ formsemestres = FormSemestre.query.filter( FormSemestreEtape.etape_apo == etape_apo, FormSemestreEtape.formsemestre_id == FormSemestre.id, ) - return jsonify([formsemestre.to_dict() for formsemestre in formsemestres]) + return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres]) @bp.route("/formsemestre//bulletins", methods=["GET"]) @@ -348,12 +356,12 @@ def bulletins(formsemestre_id: int): # ) # @token_auth.login_required # @token_permission_required(Permission.APIView) -# def semestre_index(formsemestre_id: int): # XXX nom bizarre ?? +# def programme(formsemestre_id: int): # """ # Retourne la liste des Ues, ressources et SAE d'un semestre - +# # formsemestre_id : l'id d'un formsemestre - +# # Exemple de résultat : # { # "ues": [ @@ -421,34 +429,34 @@ def bulletins(formsemestre_id: int): # ] # } # """ - +# # formsemestre: FormSemestre = models.FormSemestre.query.filter_by( # id=formsemestre_id # ).first_or_404() - +# # ues = formsemestre.query_ues() - +# # ues_dict = [] # ressources = [] # saes = [] - +# # for ue in ues: # ues_dict.append(ue.to_dict()) # ressources = ue.get_ressources() # saes = ue.get_saes() - +# # data_ressources = [] # for ressource in ressources: # data_ressources.append(ressource.to_dict()) - +# # data_saes = [] # for sae in saes: # data_saes.append(sae.to_dict()) - +# # data = { # "ues": ues_dict, # "ressources": data_ressources, # "saes": data_saes, # } - +# # return data diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 4831e589..4095ef5c 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -395,7 +395,6 @@ class FormSemestre(db.Model): if self.semestre_id == sco_codes_parcours.NO_SEMESTRE_ID: return self.titre return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}" - # return f"{self.formation.acronyme} S{self.semestre_id}" def sem_modalite(self) -> str: """Le semestre et la modalité, ex "S2 FI" ou "S3 APP" """ From d06b11eb4c56fb7038bdef55cb5cb50906252a4e Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Thu, 5 May 2022 16:33:29 +0200 Subject: [PATCH 04/60] =?UTF-8?q?commencement=20du=20choix=20de=20format?= =?UTF-8?q?=20de=20retour=20pour=20le=20bulletin=20d'un=20=C3=A9tudiant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/etudiants.py | 55 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 042c72b4..9db4b7e3 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -11,6 +11,8 @@ from app.api.tools import get_etu_from_etudid_or_nip_or_ine from app.models import FormSemestreInscription, FormSemestre, Identite from app.scodoc import sco_bulletins from app.scodoc import sco_groups +from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud +from app.scodoc.sco_bulletins_pdf import get_etud_bulletins_pdf from app.scodoc.sco_permissions import Permission @@ -168,32 +170,62 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) @bp.route( "/etudiant/etudid//formsemestre//bulletin", methods=["GET"], - defaults={"version": "long"}, + defaults={"version": "long", "format": "json"}, ) @bp.route( "/etudiant/nip//formsemestre//bulletin", methods=["GET"], - defaults={"version": "long"}, + defaults={"version": "long", "format": "json"}, ) @bp.route( "/etudiant/ine//formsemestre//bulletin", methods=["GET"], - defaults={"version": "long"}, + defaults={"version": "long", "format": "json"}, +) +@bp.route( + "/etudiant/etudid//formsemestre//bulletin/pdf", + methods=["GET"], + defaults={"version": "long", "format": "pdf"}, +) +@bp.route( + "/etudiant/nip//formsemestre//bulletin/pdf", + methods=["GET"], + defaults={"version": "long", "format": "pdf"}, +) +@bp.route( + "/etudiant/ine//formsemestre//bulletin/pdf", + methods=["GET"], + defaults={"version": "long", "format": "pdf"}, ) @bp.route( "/etudiant/etudid//formsemestre//bulletin/short", methods=["GET"], - defaults={"version": "short"}, + defaults={"version": "short", "format": "json"}, ) @bp.route( "/etudiant/nip//formsemestre//bulletin/short", methods=["GET"], - defaults={"version": "short"}, + defaults={"version": "short", "format": "json"}, ) @bp.route( "/etudiant/ine//formsemestre//bulletin/short", methods=["GET"], - defaults={"version": "short"}, + defaults={"version": "short", "format": "json"}, +) +@bp.route( + "/etudiant/etudid//formsemestre//bulletin/short/pdf", + methods=["GET"], + defaults={"version": "short", "format": "pdf"}, +) +@bp.route( + "/etudiant/nip//formsemestre//bulletin/short/pdf", + methods=["GET"], + defaults={"version": "short", "format": "pdf"}, +) +@bp.route( + "/etudiant/ine//formsemestre//bulletin/short/pdf", + methods=["GET"], + defaults={"version": "short", "format": "pdf"}, ) @token_auth.login_required @token_permission_required(Permission.APIView) @@ -203,6 +235,7 @@ def etudiant_bulletin_semestre( nip: int = None, ine: int = None, version="long", + format="json", ): """ Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné @@ -397,8 +430,14 @@ def etudiant_bulletin_semestre( message="La requête ne peut être traitée en l’état actuel.\n " "Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", ) - - return sco_bulletins.get_formsemestre_bulletin_etud_json(formsemestre, etu, version) + if format == "json": + return sco_bulletins.get_formsemestre_bulletin_etud_json( + formsemestre, etu, version + ) + else: + etudid = etu.id + print(etudid) + return do_formsemestre_bulletinetud(formsemestre, etudid, version, format) @bp.route( From 6ba603f92ad6a4ecb6ffe30bbadbedda7bd7462b Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Fri, 6 May 2022 12:17:01 +0200 Subject: [PATCH 05/60] =?UTF-8?q?Impl=C3=A9mentation=20de=20abs=5Fgroup=5F?= =?UTF-8?q?etat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/absences.py | 95 +++++++++++++++++++++++++++++---------------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/app/api/absences.py b/app/api/absences.py index b37a256a..db11ebb8 100644 --- a/app/api/absences.py +++ b/app/api/absences.py @@ -123,40 +123,69 @@ def absences_just(etudid: int = None, nip: int = None, ine: int = None): return jsonify(abs_just) -# XXX TODO INACHEVEE -# @bp.route( -# "/absences/abs_group_etat/", -# methods=["GET"], -# ) -# @bp.route( -# "/absences/abs_group_etat/group_id//date_debut//date_fin/", -# methods=["GET"], -# ) -# @token_auth.login_required -# @token_permission_required(Permission.APIView) -# def abs_groupe_etat( # XXX A REVOIR XXX -# group_id: int, date_debut, date_fin, with_boursier=True, format="html" -# ): -# """ -# Liste des absences d'un ou plusieurs groupes entre deux dates -# """ -# return error_response(501, message="Not implemented") +@bp.route( + "/absences/abs_group_etat/", + methods=["GET"], +) +@bp.route( + "/absences/abs_group_etat/group_id//date_debut//date_fin/", + methods=["GET"], +) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def abs_groupe_etat(group_id: int, date_debut=None, date_fin=None): + """ + Liste des absences d'un groupe (possibilité de choisir entre deux dates) -# # Fonction utilisée : app.scodoc.sco_groups.get_group_members() et app.scodoc.sco_abs.list_abs_date() + group_id = l'id du groupe + date_debut = None par défaut, sinon la date ISO du début de notre filtre + date_fin = None par défaut, sinon la date ISO de la fin de notre filtre -# try: -# # Utilisation de la fonction get_group_members -# members = get_group_members(group_id) -# except ValueError: -# return error_response( -# 409, message="La requête ne peut être traitée en l’état actuel" -# ) + Exemple de résultat : + [ + { + "etudid": 1, + "list_abs": [] + }, + { + "etudid": 2, + "list_abs": [ + { + "jour": "Fri, 15 Apr 2022 00:00:00 GMT", + "matin": true, + "estabs": true, + "estjust": true, + "description": "", + "begin": "2022-04-15 08:00:00", + "end": "2022-04-15 11:59:59" + }, + { + "jour": "Fri, 15 Apr 2022 00:00:00 GMT", + "matin": false, + "estabs": true, + "estjust": false, + "description": "", + "begin": "2022-04-15 12:00:00", + "end": "2022-04-15 17:59:59" + }, + ] + }, + ... + ] + """ + # Fonction utilisée : app.scodoc.sco_groups.get_group_members() et app.scodoc.sco_abs.list_abs_date() -# data = [] -# # Filtre entre les deux dates renseignées -# for member in members: -# abs = sco_abs.list_abs_date(member.id, date_debut, date_fin) -# data.append(abs) + # Utilisation de la fonction get_group_members + members = get_group_members(group_id) -# # return jsonify(data) # XXX TODO faire en sorte de pouvoir renvoyer sa (ex to_dict() dans absences) -# return error_response(501, message="Not implemented") + data = [] + # Filtre entre les deux dates renseignées + for member in members: + print(member) + abs = { + "etudid": member["etudid"], + "list_abs": sco_abs.list_abs_date(member["etudid"], date_debut, date_fin), + } + data.append(abs) + + return jsonify(data) From 864d90e22c2eca70e8e4c08439e96e4184450e1b Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Fri, 6 May 2022 16:05:34 +0200 Subject: [PATCH 06/60] ajout des docstring des fonctions de test --- tests/api/test_api_absences.py | 23 ++++++++-- tests/api/test_api_departements.py | 22 ++++++++- tests/api/test_api_etudiants.py | 47 +++++++++++++++++--- tests/api/test_api_evaluations.py | 10 ++++- tests/api/test_api_formations.py | 41 ++++++++++------- tests/api/test_api_formsemestre.py | 61 +++++++++++++------------ tests/api/test_api_jury.py | 71 ++++-------------------------- tests/api/test_api_partitions.py | 17 +++++-- tests/api/test_api_permissions.py | 2 +- 9 files changed, 170 insertions(+), 124 deletions(-) diff --git a/tests/api/test_api_absences.py b/tests/api/test_api_absences.py index a0a90c57..1ab83b18 100644 --- a/tests/api/test_api_absences.py +++ b/tests/api/test_api_absences.py @@ -28,7 +28,12 @@ NIP = "1" # absences def test_absences(api_headers): """ - Route: /absences/etudid/ + Test 'absences' + + Routes : + - /absences/etudid/ + - /absences/nip/ + - /absences/ine/ """ r = requests.get( f"{API_URL}/absences/etudid/{ETUDID}", @@ -55,7 +60,12 @@ def test_absences(api_headers): # absences_justify def test_absences_justify(api_headers): """ - Route: /absences/etudid//just + Test 'absences_just' + + Routes : + - /absences/etudid//just + - /absences/nip//just + - /absences/ine//just """ r = requests.get( API_URL + f"/absences/etudid/{ETUDID}/just", @@ -85,10 +95,15 @@ def test_absences_justify(api_headers): # XXX TODO # def test_abs_groupe_etat(api_headers): # """ -# Route: +# Test 'abs_groupe_etat' +# +# Routes : +# - /absences/abs_group_etat/ +# - /absences/abs_group_etat/group_id//date_debut//date_fin/ # """ # r = requests.get( -# API_URL + "/absences/abs_group_etat/?group_id=&date_debut=date_debut&date_fin=date_fin", +# API_URL + "/absences/abs_group_etat/group_id//date_debut//" +# "date_fin/", # headers=api_headers, # verify=CHECK_CERTIFICATE, # ) diff --git a/tests/api/test_api_departements.py b/tests/api/test_api_departements.py index 872aa104..bb7dec66 100644 --- a/tests/api/test_api_departements.py +++ b/tests/api/test_api_departements.py @@ -24,7 +24,13 @@ from tests.api.tools_test_api import verify_fields def test_departements(api_headers): - "check liste de sdépartements" + """ + Test 'departements' + + Route : + - /departements + """ + fields = [ "id", "acronym", @@ -49,6 +55,13 @@ def test_departements(api_headers): def test_list_etudiants(api_headers): + """ + Test 'list_etudiants' + + Routes : + - /departements//etudiants/list + - /departements//etudiants/list/ + """ fields = { "civilite", "code_ine", @@ -107,6 +120,12 @@ def test_list_etudiants(api_headers): # liste_semestres_courant def test_semestres_courant(api_headers): + """ + Test 'liste_semestres_courant' + + Route : + - /departements//semestres_courants + """ fields = [ "titre", "gestion_semestrielle", @@ -133,6 +152,7 @@ def test_semestres_courant(api_headers): "date_debut_iso", "date_fin_iso", "responsables", + "titre_court", ] r = requests.get( diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py index ec6a23d1..db53475d 100644 --- a/tests/api/test_api_etudiants.py +++ b/tests/api/test_api_etudiants.py @@ -26,7 +26,11 @@ from tests.api.tools_test_api import verify_fields # etudiants_courant def test_etudiants_courant(api_headers): """ - Route: /etudiants/courant + Test 'etudiants_courant' + + Routes : + - /etudiants/courant + - /etudiants/courant/long """ fields = [ "id", @@ -98,7 +102,12 @@ def test_etudiants_courant(api_headers): def test_etudiant(api_headers): """ - Route: + Test 'etudiant' + + Routes : + - /etudiant/etudid/ + - /etudiant/nip/ + - /etudiant/ine/ """ fields = [ "civilite", @@ -169,7 +178,12 @@ def test_etudiant(api_headers): def test_etudiant_formsemestres(api_headers): """ - Route: /etudiant/etudid//formsemestres + Test 'etudiant_formsemestres' + + Routes : + - /etudiant/etudid//formsemestres + - /etudiant/nip//formsemestres + - /etudiant/ine//formsemestres """ fields = [ "date_fin", @@ -197,6 +211,7 @@ def test_etudiant_formsemestres(api_headers): "date_debut_iso", "date_fin_iso", "responsables", + "titre_court", ] ######### Test etudid ######### @@ -248,8 +263,22 @@ def test_etudiant_formsemestres(api_headers): def test_etudiant_bulletin_semestre(api_headers): """ - Route: /etudiant/etudid//formsemestre//bulletin - """ + Test 'etudiant_bulletin_semestre' + + Routes : + - /etudiant/etudid//formsemestre//bulletin + - /etudiant/nip//formsemestre//bulletin + - /etudiant/ine//formsemestre//bulletin + - /etudiant/etudid//formsemestre//bulletin/pdf + - /etudiant/nip//formsemestre//bulletin/pdf + - /etudiant/ine//formsemestre//bulletin/pdf + - /etudiant/etudid//formsemestre//bulletin/short + - /etudiant/nip//formsemestre//bulletin/short + - /etudiant/ine//formsemestre//bulletin/short + - /etudiant/etudid//formsemestre//bulletin/short/pdf + - /etudiant/nip//formsemestre//bulletin/short/pdf + - /etudiant/ine//formsemestre//bulletin/short/pdf + """ ######### Test etudid ######### r = requests.get( @@ -285,8 +314,12 @@ def test_etudiant_bulletin_semestre(api_headers): def test_etudiant_groups(api_headers): """ - Route: - /etudiant/etudid//formsemestre//groups + Test 'etudiant_groups' + + Routes : + - /etudiant/etudid//formsemestre//groups + - /etudiant/nip//formsemestre//groups + - /etudiant/ine//formsemestre//groups """ fields = [ "partition_id", diff --git a/tests/api/test_api_evaluations.py b/tests/api/test_api_evaluations.py index 976eef61..1453f3a1 100644 --- a/tests/api/test_api_evaluations.py +++ b/tests/api/test_api_evaluations.py @@ -24,7 +24,10 @@ from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers def test_evaluations(api_headers): """ - Route: /evaluation/ + Test 'evaluations' + + Route : + - /evaluations/ """ r = requests.get( API_URL + "/evaluations/1", @@ -38,7 +41,10 @@ def test_evaluations(api_headers): # TODO car pas d'évaluations créées à ce stade # def test_evaluation_notes(api_headers): # """ -# Route: /evaluation/eval_notes/ +# Test 'evaluation_notes' +# +# Route : +# - /evaluation/eval_notes/ # """ # r = requests.get( # API_URL + "/evaluation/eval_notes/1", diff --git a/tests/api/test_api_formations.py b/tests/api/test_api_formations.py index b8f159ad..3f65ac6e 100644 --- a/tests/api/test_api_formations.py +++ b/tests/api/test_api_formations.py @@ -26,7 +26,10 @@ from tests.api.tools_test_api import verify_fields # formations def test_formations_ids(api_headers): """ - Route: /formations_ids + Test 'formations_ids' + + Routes : + - /formations_ids """ r = requests.get( API_URL + "/formations_ids", @@ -44,7 +47,10 @@ def test_formations_ids(api_headers): # formations_by_id def test_formations_by_id(api_headers): """ - Route: /formations/ + Test 'formations_by_id' + + Routes : + - /formations/ """ fields = [ "id", @@ -75,7 +81,11 @@ def test_formations_by_id(api_headers): def test_formation_export(api_headers): """ - Route: /formations/formation_export/ + Test 'formation_export_by_formation_id' + + Routes : + - /formations/formation_export/ + - /formations/formation_export//with_ids """ fields = [ "id", @@ -105,19 +115,12 @@ def test_formation_export(api_headers): # TODO tester le contenu de certains champs -# TODO -# def test_formsemestre_apo(api_headers): -# r = requests.get( -# API_URL + "/formations/apo/", -# headers=api_headers, -# verify=CHECK_CERTIFICATE, -# ) -# assert r.status_code == 200 - - def test_moduleimpl(api_headers): """ - Route: /formations/moduleimpl/ + Test 'moduleimpl' + + Route : + - /formations/moduleimpl/ """ fields = [ "id", @@ -145,7 +148,10 @@ def test_moduleimpl(api_headers): def test_moduleimpls_sem(api_headers): """ - Route: /formations/moduleimpl/formsemestre//list + Test 'moduleimpls_sem' + + Route : + - /formations/moduleimpl/formsemestre//list """ fields = [ "id", @@ -175,7 +181,10 @@ def test_moduleimpls_sem(api_headers): def test_referentiel_competences(api_headers): """ - Route: "/formations//referentiel_competences", + Test 'referentiel_competences' + + Route : + - /formations//referentiel_competences """ r = requests.get( API_URL + "/formations/1/referentiel_competences", diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py index 724713a1..568e7e8d 100644 --- a/tests/api/test_api_formsemestre.py +++ b/tests/api/test_api_formsemestre.py @@ -25,7 +25,10 @@ from tests.api.tools_test_api import verify_fields def test_formsemestre(api_headers): """ - Route: + Test 'formsemestre' + + Route : + - /formsemestre/ """ r = requests.get( API_URL + "/formsemestre/1", @@ -69,35 +72,28 @@ def test_formsemestre(api_headers): assert fields_ok is True -def test_etudiant_bulletin(api_headers): - """ - Route: - """ - r = requests.get( - API_URL + "/formsemestre/1/etudiant/etudid/1/bulletin", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - - r = requests.get( - API_URL + "/formsemestre/1/etudiant/nip/1/bulletin", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - - r = requests.get( - API_URL + "/formsemestre/1/etudiant/ine/1/bulletin", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 +# TODO +# def test_formsemestre_apo(api_headers): +# """ +# Test 'formsemestre_apo' +# +# Route : +# - /formsemestre/apo/ +# """ +# r = requests.get( +# API_URL + "/formations/apo/", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 def test_bulletins(api_headers): """ - Route: + Test 'bulletins' + + Route : + - /formsemestre//bulletins """ r = requests.get( API_URL + "/formsemestre/1/bulletins", @@ -109,6 +105,12 @@ def test_bulletins(api_headers): # # jury # def test_jury(): +# """ +# Test 'jury' +# +# Route : +# - /formsemestre//jury +# """ # r = requests.get( # API_URL + "/formsemestre/1/jury", # headers=api_headers, @@ -117,9 +119,12 @@ def test_bulletins(api_headers): # assert r.status_code == 200 # TODO A revoir -def test_semestre_index(api_headers): +def test_programme(api_headers): """ - Route: TODO + Test 'programme' + + Route : + - /formsemestre//programme """ ue_fields = [ "semestre_idx", diff --git a/tests/api/test_api_jury.py b/tests/api/test_api_jury.py index 13b0a0f1..003f148f 100644 --- a/tests/api/test_api_jury.py +++ b/tests/api/test_api_jury.py @@ -24,10 +24,13 @@ from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers def test_jury_preparation(api_headers): """ - Route: + Test 'jury_preparation' + + Route : + - /jury/formsemestre//preparation_jury """ r = requests.get( - SCODOC_URL + API_URL + "/ScoDoc/api/jury/formsemestre//preparation_jury", headers=api_headers, verify=CHECK_CERTIFICATE, @@ -37,7 +40,10 @@ def test_jury_preparation(api_headers): def test_jury_decisions(api_headers): """ - Route: + Test 'jury_decisions' + + Route : + - /jury/formsemestre//decisions_jury """ r = requests.get( API_URL + "/jury/formsemestre//decisions_jury", @@ -45,62 +51,3 @@ def test_jury_decisions(api_headers): verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - - -# set_decision_jury -def test_set_decision_jury(api_headers): - r = requests.get( - SCODOC_URL - + "/ScoDoc/api/jury/set_decision/etudid?etudid=&formsemestre_id=" - "&jury=&devenir=&assiduite=", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - - r = requests.get( - SCODOC_URL - + "/ScoDoc/api/jury/set_decision/nip?etudid=&formsemestre_id=" - "&jury=&devenir=&assiduite=", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - - r = requests.get( - SCODOC_URL - + "/ScoDoc/api/jury/set_decision/ine?etudid=&formsemestre_id=" - "&jury=&devenir=&assiduite=", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - - -def test_annule_decision_jury(api_headers): - """ - Route: - """ - r = requests.get( - SCODOC_URL - + "/ScoDoc/api/jury/etudid//formsemestre//annule_decision", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - - r = requests.get( - SCODOC_URL - + "/ScoDoc/api/jury/nip//formsemestre//annule_decision", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - - r = requests.get( - SCODOC_URL - + "/ScoDoc/api/jury/ine//formsemestre//annule_decision", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 diff --git a/tests/api/test_api_partitions.py b/tests/api/test_api_partitions.py index 0ceb4e33..aa8994f8 100644 --- a/tests/api/test_api_partitions.py +++ b/tests/api/test_api_partitions.py @@ -25,7 +25,10 @@ from tests.api.tools_test_api import verify_fields def test_partition(api_headers): """ - Route: + Test 'partition' + + Route : + - /partitions/ """ fields = [ "partition_id", @@ -52,7 +55,11 @@ def test_partition(api_headers): def test_etud_in_group(api_headers): """ - Route: + Test 'etud_in_group' + + Routes : + - /partitions/groups/ + - /partitions/groups//etat/ """ fields = [ "etudid", @@ -116,7 +123,11 @@ def test_etud_in_group(api_headers): # # set_groups # def test_set_groups(api_headers): # """ -# Route: +# Test 'set_groups' +# +# Routes : +# - /partitions/set_groups/partition//groups//delete/" +# "/create/ # """ # r = requests.get( # SCODOC_URL diff --git a/tests/api/test_api_permissions.py b/tests/api/test_api_permissions.py index aa80f36f..4a9f2952 100644 --- a/tests/api/test_api_permissions.py +++ b/tests/api/test_api_permissions.py @@ -29,7 +29,7 @@ def test_permissions(api_headers): # Ce test va récupérer toutes les routes de l'API app = create_app(RunningConfig) assert app - # Les routes de l'API avec GET, excluant les logos pour le momeent XXX + # Les routes de l'API avec GET, excluant les logos pour le moment XXX api_rules = [ r for r in app.url_map.iter_rules() From 61830180a5fc841eb6b7b42c7955c75fa6ddb0b7 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Mon, 9 May 2022 16:26:23 +0200 Subject: [PATCH 07/60] merge du master + ajout de abs_group_etat + ajout de test pour departements et etudiants --- app/api/absences.py | 22 +- app/api/departements.py | 160 +++++---- app/api/etudiants.py | 156 ++++----- app/api/evaluations.py | 2 +- app/api/formations.py | 75 +--- app/api/formsemestres.py | 322 ++++++++---------- app/api/partitions.py | 12 +- app/api/tools.py | 8 +- app/but/bulletin_but_pdf.py | 7 +- app/models/etudiants.py | 1 + app/models/evaluations.py | 5 +- app/models/formsemestre.py | 14 +- app/pe/pe_settag.py | 4 +- app/scodoc/sco_abs_views.py | 2 +- app/scodoc/sco_bulletins_legacy.py | 11 +- app/scodoc/sco_dept.py | 6 +- app/scodoc/sco_formsemestre_inscriptions.py | 13 +- app/scodoc/sco_moduleimpl_inscriptions.py | 9 +- app/scodoc/sco_page_etud.py | 23 +- app/scodoc/sco_preferences.py | 17 +- app/scodoc/sco_saisie_notes.py | 37 +- migrations/env.py | 22 +- ...773_scodoc_9_0_4_ajout_id_scodoc7_pour_.py | 14 +- ...added_cascade_on_absences_notifications.py | 29 +- .../39818df276aa_cascades_sur_itemsuivi.py | 52 ++- ...065fb2d20_flag_bloquage_calcul_moyennes.py | 13 +- ...7947e5_scodoc_9_0_4_code_module_en_text.py | 26 +- .../6cfc21a7ae1b_coefs_modules_but.py | 27 +- .../75cf18659984_cascade_tags_modules.py | 45 ++- .../versions/92789d50f6b6_refcomp_index.py | 89 +++-- ...217bf588f4c_scodoc_9_0_13_essai_cascade.py | 47 ++- .../versions/c8efc54586d8_ue_semestre_idx.py | 14 +- ...b2d0092_scodoc_9_0_5_ajout_dept_id_sur_.py | 18 +- ...e7d2e01be1_augmente_taille_codes_apogee.py | 92 +++-- .../f73251d1d825_table_configuration_site.py | 23 +- ...13c9fbd_modif_contrainte_sur_formations.py | 22 +- misc/SuppressAccents.py | 129 ++++--- tests/api/test_api_departements.py | 187 +++++----- tests/api/test_api_etudiants.py | 191 +++-------- tests/api/test_api_formations.py | 132 ++----- tests/api/test_api_formsemestre.py | 196 ++++------- tests/api/test_api_permissions.py | 2 + tests/api/tools_test_api.py | 134 +++++++- tests/unit/test_export_xml.py | 2 +- 44 files changed, 1169 insertions(+), 1243 deletions(-) diff --git a/app/api/absences.py b/app/api/absences.py index db11ebb8..b7c5ee07 100644 --- a/app/api/absences.py +++ b/app/api/absences.py @@ -5,7 +5,7 @@ from flask import jsonify from app.api import bp from app.api.errors import error_response from app.api.auth import token_auth, token_permission_required -from app.api.tools import get_etu_from_etudid_or_nip_or_ine +from app.api.tools import get_etud_from_etudid_or_nip_or_ine from app.scodoc import notesdb as ndb from app.scodoc import sco_abs @@ -50,12 +50,11 @@ def absences(etudid: int = None, nip: int = None, ine: int = None): """ if etudid is None: # Récupération de l'étudiant - etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) + etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) if etud is None: return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel.\n " - "Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", + 404, + message="id de l'étudiant (etudid, nip, ine) inconnu", ) etudid = etud.etudid @@ -103,16 +102,13 @@ def absences_just(etudid: int = None, nip: int = None, ine: int = None): ] """ if etudid is None: - # Récupération de l'étudiant - try: - etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) - etudid = etu.etudid - except AttributeError: + etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) + if etud is None: return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel.\n " - "Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", + 404, + message="id de l'étudiant (etudid, nip, ine) inconnu", ) + etudid = etud.etudid # Récupération des absences justifiées de l'étudiant abs_just = [ diff --git a/app/api/departements.py b/app/api/departements.py index ae8bc775..fca5d6a8 100644 --- a/app/api/departements.py +++ b/app/api/departements.py @@ -1,57 +1,66 @@ ############################################### Departements ########################################################## -import json from flask import jsonify +import app from app import models from app.api import bp from app.api.auth import token_auth, token_permission_required -from app.api.errors import error_response +from app.models import Departement, FormSemestre from app.scodoc.sco_permissions import Permission -@bp.route("/departements", methods=["GET"]) +def get_departement(dept_ident: str) -> Departement: + "Le departement, par id ou acronyme. Erreur 404 si pas trouvé." + try: + dept_id = int(dept_ident) + except ValueError: + dept_id = None + if dept_id is None: + return Departement.query.filter_by(acronym=dept_ident).first_or_404() + return Departement.query.get_or_404(dept_id) + + +@bp.route("/departements_ids", methods=["GET"]) @token_auth.login_required @token_permission_required(Permission.APIView) -def departements(): +def departements_ids(): + """Liste des ids de départements""" + return jsonify([dept.id for dept in Departement.query]) + + +@bp.route("/departement/", methods=["GET"]) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def departement(dept_ident: str): """ - Retourne la liste des départements visibles + Info sur un département. Accès par id ou acronyme. Exemple de résultat : - [ { "id": 1, "acronym": "TAPI", "description": null, "visible": true, "date_creation": "Fri, 15 Apr 2022 12:19:28 GMT" - }, - { - "id": 2, - "acronym": "MMI", - "description": null, - "visible": false, - "date_creation": "Fri, 18 Apr 2022 11:20:8 GMT" - }, - ... - ] + } """ - # Récupération de tous les départements - depts = models.Departement.query.all() - - # Mise en place de la liste avec tous les départements - data = [d.to_dict() for d in depts] - - return jsonify(data) + dept = get_departement(dept_ident) + return jsonify(dept.to_dict()) -@bp.route("/departements//etudiants/list", methods=["GET"]) -@bp.route( - "/departements//etudiants/list/", methods=["GET"] -) +@bp.route("/departements", methods=["GET"]) @token_auth.login_required @token_permission_required(Permission.APIView) -def list_etudiants(dept: str, formsemestre_id=None): +def departements(): + """Liste les départements""" + return jsonify([dept.to_dict() for dept in Departement.query]) + + +@bp.route("/departement//etudiants", methods=["GET"]) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def list_etudiants(dept_ident: str): """ Retourne la liste des étudiants d'un département @@ -61,54 +70,39 @@ def list_etudiants(dept: str, formsemestre_id=None): Exemple de résultat : [ { - "civilite": "X", - "code_ine": null, - "code_nip": null, + "civilite": "M", + "ine": "7899X61616", + "nip": "F6777H88", "date_naissance": null, - "email": null, + "email": "toto@toto.fr", "emailperso": null, "etudid": 18, "nom": "MOREL", "prenom": "JACQUES" }, - { - "civilite": "X", - "code_ine": null, - "code_nip": null, - "date_naissance": null, - "email": null, - "emailperso": null, - "etudid": 19, - "nom": "FOURNIER", - "prenom": "ANNE" - }, ... ] """ - # Si le formsemestre_id a été renseigné - if formsemestre_id is not None: - # Récupération du formsemestre - formsemestre = models.FormSemestre.query.filter_by( - id=formsemestre_id - ).first_or_404() - # Récupération du département - departement = formsemestre.departement + # Le département, spécifié par un id ou un acronyme + dept = get_departement(dept_ident) - # Si le formsemestre_id n'a pas été renseigné - else: - # Récupération du formsemestre - departement = models.Departement.query.filter_by(acronym=dept).first_or_404() - - # Mise en forme des données - list_etu = [etu.to_dict_bul(include_urls=False) for etu in departement.etudiants] - - return jsonify(list_etu) + return jsonify([etud.to_dict_short() for etud in dept.etudiants]) -@bp.route("/departements//semestres_courants", methods=["GET"]) +@bp.route("/departement//formsemestres_ids", methods=["GET"]) @token_auth.login_required @token_permission_required(Permission.APIView) -def liste_semestres_courant(dept: str): +def formsemestres_ids(dept_ident: str): + """liste des ids formsemestre du département""" + # Le département, spécifié par un id ou un acronyme + dept = get_departement(dept_ident) + return jsonify([formsemestre.id for formsemestre in dept.formsemestres]) + + +@bp.route("/departement//formsemestres_courants", methods=["GET"]) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def liste_semestres_courant(dept_ident: str): """ Liste des semestres actifs d'un départements donné @@ -117,11 +111,16 @@ def liste_semestres_courant(dept: str): Exemple de résultat : [ { - "date_fin": "31/08/2022", + "titre": "master machine info", + "gestion_semestrielle": false, + "scodoc7_id": null, + "date_debut": "01/09/2021", + "bul_bgcolor": null, + "date_fin": "15/12/2022", "resp_can_edit": false, "dept_id": 1, "etat": true, - "resp_can_change_ens": true, + "resp_can_change_ens": false, "id": 1, "modalite": "FI", "ens_can_edit_eval": false, @@ -131,32 +130,27 @@ def liste_semestres_courant(dept: str): "semestre_id": 1, "bul_hide_xml": false, "elt_annee_apo": null, - "titre": "Semestre test", "block_moyennes": false, - "scodoc7_id": null, - "date_debut": "01/09/2021", - "gestion_semestrielle": false, - "bul_bgcolor": "white", "formsemestre_id": 1, - "titre_num": "Semestre test semestre 1", + "titre_num": "master machine info semestre 1", "date_debut_iso": "2021-09-01", - "date_fin_iso": "2022-08-31", + "date_fin_iso": "2022-12-15", "responsables": [ - 12, - 42 - ], - "titre_court": "BUT MMI" + 3, + 2 + ] }, ... ] """ - # Récupération des départements comportant l'acronym mit en paramètre - dept = models.Departement.query.filter_by(acronym=dept).first_or_404() + # Le département, spécifié par un id ou un acronyme + dept = get_departement(dept_ident) - # Récupération des semestres suivant id_dept - semestres = models.FormSemestre.query.filter_by(dept_id=dept.id, etat=True) + # Les semestres en cours de ce département + formsemestres = models.FormSemestre.query.filter( + FormSemestre.dept_id == dept.id, + FormSemestre.date_debut <= app.db.func.now(), + FormSemestre.date_fin >= app.db.func.now(), + ) - # Mise en forme des données - data = [d.to_dict_api() for d in semestres] - - return jsonify(data) + return jsonify([d.to_dict() for d in formsemestres]) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index dd9935df..afeb8aba 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -7,12 +7,10 @@ from app import models from app.api import bp from app.api.errors import error_response from app.api.auth import token_auth, token_permission_required -from app.api.tools import get_etu_from_etudid_or_nip_or_ine +from app.api.tools import get_etud_from_etudid_or_nip_or_ine from app.models import FormSemestreInscription, FormSemestre, Identite from app.scodoc import sco_bulletins from app.scodoc import sco_groups -from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud -from app.scodoc.sco_bulletins_pdf import get_etud_bulletins_pdf from app.scodoc.sco_permissions import Permission @@ -22,7 +20,7 @@ from app.scodoc.sco_permissions import Permission @token_permission_required(Permission.APIView) def etudiants_courant(long=False): """ - Retourne la liste des étudiants courant + Liste des étudiants inscrits dans un formsemestre actuellement en cours. Exemple de résultat : [ @@ -43,7 +41,6 @@ def etudiants_courant(long=False): ... ] """ - # Récupération de tous les étudiants etuds = Identite.query.filter( Identite.id == FormSemestreInscription.etudid, FormSemestreInscription.formsemestre_id == FormSemestre.id, @@ -99,8 +96,12 @@ def etudiant(etudid: int = None, nip: int = None, ine: int = None): } """ # Récupération de l'étudiant - etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) - + etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) + if etud is None: + return error_response( + 404, + message="id de l'étudiant (etudid, nip, ine) inconnu", + ) # Mise en forme des données data = etud.to_dict_bul(include_urls=False) @@ -122,110 +123,81 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) Exemple de résultat : [ - { - "date_fin": "31/08/2022", - "resp_can_edit": false, - "dept_id": 1, - "etat": true, - "resp_can_change_ens": true, - "id": 1, - "modalite": "FI", - "ens_can_edit_eval": false, - "formation_id": 1, - "gestion_compensation": false, - "elt_sem_apo": null, - "semestre_id": 1, - "bul_hide_xml": false, - "elt_annee_apo": null, - "titre": "Semestre test", - "block_moyennes": false, - "scodoc7_id": null, - "date_debut": "01/09/2021", - "gestion_semestrielle": false, - "bul_bgcolor": "white", - "formsemestre_id": 1, - "titre_num": "Semestre test semestre 1", - "date_debut_iso": "2021-09-01", - "date_fin_iso": "2022-08-31", - "responsables": [ - 12, - 42 - ], - "titre_court": "BUT MMI" - }, - ... + { + "date_fin": "31/08/2022", + "resp_can_edit": false, + "dept_id": 1, + "etat": true, + "resp_can_change_ens": true, + "id": 1, + "modalite": "FI", + "ens_can_edit_eval": false, + "formation_id": 1, + "gestion_compensation": false, + "elt_sem_apo": null, + "semestre_id": 1, + "bul_hide_xml": false, + "elt_annee_apo": null, + "titre": "Semestre test", + "block_moyennes": false, + "scodoc7_id": null, + "date_debut": "01/09/2021", + "gestion_semestrielle": false, + "bul_bgcolor": "white", + "formsemestre_id": 1, + "titre_num": "Semestre test semestre 1", + "date_debut_iso": "2021-09-01", + "date_fin_iso": "2022-08-31", + "responsables": [] + }, + ... ] """ # Récupération de l'étudiant - etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) + etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) + if etud is None: + return error_response( + 404, + message="id de l'étudiant (etudid, nip, ine) inconnu", + ) formsemestres = models.FormSemestre.query.filter( models.FormSemestreInscription.etudid == etud.id, models.FormSemestreInscription.formsemestre_id == models.FormSemestre.id, ).order_by(models.FormSemestre.date_debut) - return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres]) + return jsonify([formsemestre.to_dict() for formsemestre in formsemestres]) @bp.route( "/etudiant/etudid//formsemestre//bulletin", methods=["GET"], - defaults={"version": "long", "format": "json"}, + defaults={"version": "long"}, ) @bp.route( "/etudiant/nip//formsemestre//bulletin", methods=["GET"], - defaults={"version": "long", "format": "json"}, + defaults={"version": "long"}, ) @bp.route( "/etudiant/ine//formsemestre//bulletin", methods=["GET"], - defaults={"version": "long", "format": "json"}, -) -@bp.route( - "/etudiant/etudid//formsemestre//bulletin/pdf", - methods=["GET"], - defaults={"version": "long", "format": "pdf"}, -) -@bp.route( - "/etudiant/nip//formsemestre//bulletin/pdf", - methods=["GET"], - defaults={"version": "long", "format": "pdf"}, -) -@bp.route( - "/etudiant/ine//formsemestre//bulletin/pdf", - methods=["GET"], - defaults={"version": "long", "format": "pdf"}, + defaults={"version": "long"}, ) @bp.route( "/etudiant/etudid//formsemestre//bulletin/short", methods=["GET"], - defaults={"version": "short", "format": "json"}, + defaults={"version": "short"}, ) @bp.route( "/etudiant/nip//formsemestre//bulletin/short", methods=["GET"], - defaults={"version": "short", "format": "json"}, + defaults={"version": "short"}, ) @bp.route( "/etudiant/ine//formsemestre//bulletin/short", methods=["GET"], - defaults={"version": "short", "format": "json"}, -) -@bp.route( - "/etudiant/etudid//formsemestre//bulletin/short/pdf", - methods=["GET"], - defaults={"version": "short", "format": "pdf"}, -) -@bp.route( - "/etudiant/nip//formsemestre//bulletin/short/pdf", - methods=["GET"], - defaults={"version": "short", "format": "pdf"}, -) -@bp.route( - "/etudiant/ine//formsemestre//bulletin/short/pdf", - methods=["GET"], - defaults={"version": "short", "format": "pdf"}, + defaults={"version": "short"}, ) @token_auth.login_required @token_permission_required(Permission.APIView) @@ -235,7 +207,6 @@ def etudiant_bulletin_semestre( nip: int = None, ine: int = None, version="long", - format="json", ): """ Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné @@ -421,23 +392,15 @@ def etudiant_bulletin_semestre( app.set_sco_dept(dept.acronym) - # Récupération de l'étudiant - try: - etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) - except AttributeError: + etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) + if etud is None: return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel.\n " - "Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", + 404, + message="id de l'étudiant (etudid, nip, ine) inconnu", ) - if format == "json": - return sco_bulletins.get_formsemestre_bulletin_etud_json( - formsemestre, etu, version - ) - else: - etudid = etu.id - print(etudid) - return do_formsemestre_bulletinetud(formsemestre, etudid, version, format) + return sco_bulletins.get_formsemestre_bulletin_etud_json( + formsemestre, etud, version + ) @bp.route( @@ -490,12 +453,11 @@ def etudiant_groups( ] """ if etudid is None: - etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) + etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) if etud is None: return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel.\n " - "Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", + 404, + message="id de l'étudiant (etudid, nip, ine) inconnu", ) etudid = etud.etudid diff --git a/app/api/evaluations.py b/app/api/evaluations.py index 7fb1e6c5..cef58a42 100644 --- a/app/api/evaluations.py +++ b/app/api/evaluations.py @@ -99,7 +99,7 @@ def evaluation_notes(evaluation_id: int): data = do_evaluation_get_all_notes(evaluation_id) except AttributeError: # ??? return error_response( - 409, + 404, message="La requête ne peut être traitée en l’état actuel.", ) diff --git a/app/api/formations.py b/app/api/formations.py index e57f74f7..f8842b90 100644 --- a/app/api/formations.py +++ b/app/api/formations.py @@ -29,10 +29,10 @@ def formations_ids(): return jsonify(data) -@bp.route("/formations/", methods=["GET"]) +@bp.route("/formation/", methods=["GET"]) @token_auth.login_required @token_permission_required(Permission.APIView) -def formations_by_id(formation_id: int): +def formation_by_id(formation_id: int): """ Retourne une formation en fonction d'un id donné @@ -63,12 +63,12 @@ def formations_by_id(formation_id: int): @bp.route( - "/formations/formation_export/", + "/formation/formation_export/", methods=["GET"], defaults={"export_ids": False}, ) @bp.route( - "/formations/formation_export//with_ids", + "/formation/formation_export//with_ids", methods=["GET"], defaults={"export_ids": True}, ) @@ -177,16 +177,12 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False): # Utilisation de la fonction formation_export data = sco_formations.formation_export(formation_id, export_ids) except ValueError: - return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel. \n" - "Veillez vérifier la conformité du 'formation_id'", - ) + return error_response(500, message="Erreur inconnue") return jsonify(data) -@bp.route("/formations/moduleimpl/", methods=["GET"]) +@bp.route("/formation/moduleimpl/", methods=["GET"]) @token_auth.login_required @token_permission_required(Permission.APIView) def moduleimpl(moduleimpl_id: int): @@ -199,7 +195,6 @@ def moduleimpl(moduleimpl_id: int): { "id": 1, "formsemestre_id": 1, - "computation_expr": null, "module_id": 1, "responsable_id": 2, "moduleimpl_id": 1, @@ -231,63 +226,7 @@ def moduleimpl(moduleimpl_id: int): @bp.route( - "/formations/moduleimpl/formsemestre//list", - methods=["GET"], -) -@token_auth.login_required -@token_permission_required(Permission.APIView) -def moduleimpls_sem(formsemestre_id: int): - """ - Retourne la liste des moduleimpl d'un semestre - - formsemestre_id : l'id d'un formsemestre - - Exemple d'utilisation : - [ - { - "id": 1, - "formsemestre_id": 1, - "computation_expr": null, - "module_id": 1, - "responsable_id": 2, - "module": { - "heures_tp": 0.0, - "code_apogee": "", - "titre": "Initiation aux r\u00e9seaux informatiques", - "coefficient": 1.0, - "module_type": 2, - "id": 1, - "ects": null, - "abbrev": "Init aux r\u00e9seaux informatiques", - "ue_id": 1, - "code": "R101", - "formation_id": 1, - "heures_cours": 0.0, - "matiere_id": 1, - "heures_td": 0.0, - "semestre_id": 1, - "numero": 10, - "module_id": 1 - }, - "moduleimpl_id": 1, - "ens": [] - }, - ... - ] - """ - formsemestre = models.FormSemestre.query.filter_by( - id=formsemestre_id - ).first_or_404() - - moduleimpls = formsemestre.modimpls_sorted - - data = [moduleimpl.to_dict() for moduleimpl in moduleimpls] - - return jsonify(data) - - -@bp.route( - "/formations//referentiel_competences", + "/formation//referentiel_competences", methods=["GET"], ) @token_auth.login_required diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index a9116a94..577b2dbc 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -4,14 +4,11 @@ from flask import jsonify import app from app import models from app.api import bp -from app.api.errors import error_response from app.api.auth import token_auth, token_permission_required -from app.api.tools import get_etu_from_etudid_or_nip_or_ine -from app.models import FormSemestre, FormSemestreEtape +from app.models import Departement, FormSemestre, FormSemestreEtape from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json -from app.scodoc.sco_bulletins_json import make_json_formsemestre_bulletinetud from app.scodoc.sco_permissions import Permission -from app.scodoc.sco_pvjury import formsemestre_pvjury +from app.scodoc.sco_utils import ModuleType @bp.route("/formsemestre/", methods=["GET"]) @@ -19,47 +16,50 @@ from app.scodoc.sco_pvjury import formsemestre_pvjury @token_permission_required(Permission.APIView) def formsemestre(formsemestre_id: int): """ - Retourne l'information sur le formsemestre correspondant au formsemestre_id + Information sur le formsemestre indiqué. - formsemestre_id : l'id d'un formsemestre + formsemestre_id : l'id du formsemestre Exemple de résultat : { - "date_fin": "31/08/2022", - "resp_can_edit": false, - "dept_id": 1, - "etat": true, - "resp_can_change_ens": true, - "id": 1, - "modalite": "FI", - "ens_can_edit_eval": false, - "formation_id": 1, - "gestion_compensation": false, - "elt_sem_apo": null, - "semestre_id": 1, - "bul_hide_xml": false, - "elt_annee_apo": null, - "titre": "Semestre test", - "block_moyennes": false, - "scodoc7_id": null, - "date_debut": "01/09/2021", - "gestion_semestrielle": false, - "bul_bgcolor": "white", - "formsemestre_id": 1, - "titre_num": "Semestre test semestre 1", - "date_debut_iso": "2021-09-01", - "date_fin_iso": "2022-08-31", - "responsables": [ - 12, - 42 - ], - "titre_court": "BUT MMI" - } + "block_moyennes": false, + "bul_bgcolor": "white", + "bul_hide_xml": false, + "date_debut_iso": "2021-09-01", + "date_debut": "01/09/2021", + "date_fin_iso": "2022-08-31", + "date_fin": "31/08/2022", + "dept_id": 1, + "elt_annee_apo": null, + "elt_sem_apo": null, + "ens_can_edit_eval": false, + "etat": true, + "formation_id": 1, + "formsemestre_id": 1, + "gestion_compensation": false, + "gestion_semestrielle": false, + "id": 1, + "modalite": "FI", + "resp_can_change_ens": true, + "resp_can_edit": false, + "responsables": [1, 99], // uids + "scodoc7_id": null, + "semestre_id": 1, + "titre_formation" : "BUT GEA", + "titre_num": "BUT GEA semestre 1", + "titre": "BUT GEA", + } + """ - formsemestre = models.FormSemestre.query.filter_by( + formsemestre: FormSemestre = models.FormSemestre.query.filter_by( id=formsemestre_id ).first_or_404() - data = formsemestre.to_dict_api() + data = formsemestre.to_dict() + # Pour le moment on a besoin de fixer le departement + # pour accéder aux préferences + dept = Departement.query.get(formsemestre.dept_id) + app.set_sco_dept(dept.acronym) + data["session_id"] = formsemestre.session_id() return jsonify(data) @@ -73,47 +73,17 @@ def formsemestre_apo(etape_apo: str): etape_apo : un code étape apogée Exemple de résultat : - [ - { - "date_fin": "31/08/2022", - "resp_can_edit": false, - "dept_id": 1, - "etat": true, - "resp_can_change_ens": true, - "id": 1, - "modalite": "FI", - "ens_can_edit_eval": false, - "formation_id": 1, - "gestion_compensation": false, - "elt_sem_apo": null, - "semestre_id": 1, - "bul_hide_xml": false, - "elt_annee_apo": null, - "titre": "Semestre test", - "block_moyennes": false, - "scodoc7_id": null, - "date_debut": "01/09/2021", - "gestion_semestrielle": false, - "bul_bgcolor": "white", - "formsemestre_id": 1, - "titre_num": "Semestre test semestre 1", - "date_debut_iso": "2021-09-01", - "date_fin_iso": "2022-08-31", - "responsables": [ - 12, - 42 - ], - "titre_court": "BUT MMI" - }, - ... - ] + [ + { ...formsemestre... + }, ... + ] """ formsemestres = FormSemestre.query.filter( FormSemestreEtape.etape_apo == etape_apo, FormSemestreEtape.formsemestre_id == FormSemestre.id, ) - return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres]) + return jsonify([formsemestre.to_dict() for formsemestre in formsemestres]) @bp.route("/formsemestre//bulletins", methods=["GET"]) @@ -349,114 +319,96 @@ def bulletins(formsemestre_id: int): # return jsonify(data) -# XXX A spécifier et compléter TODO -# @bp.route( -# "/formsemestre//programme", -# methods=["GET"], -# ) -# @token_auth.login_required -# @token_permission_required(Permission.APIView) -# def programme(formsemestre_id: int): -# """ -# Retourne la liste des Ues, ressources et SAE d'un semestre -# -# formsemestre_id : l'id d'un formsemestre -# -# Exemple de résultat : -# { -# "ues": [ -# { -# "type": 0, -# "formation_id": 1, -# "ue_code": "UCOD11", -# "id": 1, -# "ects": 12.0, -# "acronyme": "RT1.1", -# "is_external": false, -# "numero": 1, -# "code_apogee": "", -# "titre": "Administrer les r\u00e9seaux et l\u2019Internet", -# "coefficient": 0.0, -# "semestre_idx": 1, -# "color": "#B80004", -# "ue_id": 1 -# }, -# ... -# ], -# "ressources": [ -# { -# "titre": "Fondamentaux de la programmation", -# "coefficient": 1.0, -# "module_type": 2, -# "id": 17, -# "ects": null, -# "abbrev": null, -# "ue_id": 3, -# "code": "R107", -# "formation_id": 1, -# "heures_cours": 0.0, -# "matiere_id": 3, -# "heures_td": 0.0, -# "semestre_id": 1, -# "heures_tp": 0.0, -# "numero": 70, -# "code_apogee": "", -# "module_id": 17 -# }, -# ... -# ], -# "saes": [ -# { -# "titre": "Se pr\u00e9senter sur Internet", -# "coefficient": 1.0, -# "module_type": 3, -# "id": 14, -# "ects": null, -# "abbrev": null, -# "ue_id": 3, -# "code": "SAE14", -# "formation_id": 1, -# "heures_cours": 0.0, -# "matiere_id": 3, -# "heures_td": 0.0, -# "semestre_id": 1, -# "heures_tp": 0.0, -# "numero": 40, -# "code_apogee": "", -# "module_id": 14 -# }, -# ... -# ] -# } -# """ -# -# formsemestre: FormSemestre = models.FormSemestre.query.filter_by( -# id=formsemestre_id -# ).first_or_404() -# -# ues = formsemestre.query_ues() -# -# ues_dict = [] -# ressources = [] -# saes = [] -# -# for ue in ues: -# ues_dict.append(ue.to_dict()) -# ressources = ue.get_ressources() -# saes = ue.get_saes() -# -# data_ressources = [] -# for ressource in ressources: -# data_ressources.append(ressource.to_dict()) -# -# data_saes = [] -# for sae in saes: -# data_saes.append(sae.to_dict()) -# -# data = { -# "ues": ues_dict, -# "ressources": data_ressources, -# "saes": data_saes, -# } -# -# return data +@bp.route( + "/formsemestre//programme", + methods=["GET"], +) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def formsemestre_programme(formsemestre_id: int): + """ + Retourne la liste des Ues, ressources et SAE d'un semestre + + formsemestre_id : l'id d'un formsemestre + + Exemple de résultat : + { + "ues": [ + { + "type": 0, + "formation_id": 1, + "ue_code": "UCOD11", + "id": 1, + "ects": 12.0, + "acronyme": "RT1.1", + "is_external": false, + "numero": 1, + "code_apogee": "", + "titre": "Administrer les r\u00e9seaux et l\u2019Internet", + "coefficient": 0.0, + "semestre_idx": 1, + "color": "#B80004", + "ue_id": 1 + }, + ... + ], + "ressources": [ + { + "ens": [ 10, 18 ], + "formsemestre_id": 1, + "id": 15, + "module": { + "abbrev": "Programmer", + "code": "SAE15", + "code_apogee": "V7GOP", + "coefficient": 1.0, + "formation_id": 1, + "heures_cours": 0.0, + "heures_td": 0.0, + "heures_tp": 0.0, + "id": 15, + "matiere_id": 3, + "module_id": 15, + "module_type": 3, + "numero": 50, + "semestre_id": 1, + "titre": "Programmer en Python", + "ue_id": 3 + }, + "module_id": 15, + "moduleimpl_id": 15, + "responsable_id": 2 + }, + ... + ], + "saes": [ + { + ... + }, + ... + ], + "modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ] + } + """ + formsemestre: FormSemestre = models.FormSemestre.query.filter_by( + id=formsemestre_id + ).first_or_404() + + ues = formsemestre.query_ues() + m_list = { + ModuleType.RESSOURCE: [], + ModuleType.SAE: [], + ModuleType.STANDARD: [], + } + for modimpl in formsemestre.modimpls_sorted: + d = modimpl.to_dict() + m_list[modimpl.module.module_type].append(d) + + return jsonify( + { + "ues": [ue.to_dict() for ue in ues], + "ressources": m_list[ModuleType.RESSOURCE], + "saes": m_list[ModuleType.SAE], + "modules": m_list[ModuleType.STANDARD], + } + ) diff --git a/app/api/partitions.py b/app/api/partitions.py index ed75fd11..1ee74c46 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -113,11 +113,7 @@ def etud_in_group(group_id: int, etat=None): data = get_group_members(group_id, etat) if len(data) == 0: - return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel. \n" - "Aucun groupe ne correspond au 'group_id' renseigné", - ) + return error_response(404, message="group_id inconnu") return jsonify(data) @@ -146,8 +142,4 @@ def set_groups( setGroups(partition_id, groups_lists, groups_to_create, groups_to_delete) return error_response(200, message="Groups set") except ValueError: - return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel. \n" - "Veillez vérifier la conformité des éléments passé en paramètres", - ) + return error_response(404, message="Erreur") diff --git a/app/api/tools.py b/app/api/tools.py index 35b07cf2..e03d8334 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -1,15 +1,17 @@ from app import models -def get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine): +def get_etud_from_etudid_or_nip_or_ine( + etudid=None, nip=None, ine=None +) -> models.Identite: """ - Fonction qui retourne un etudiant en fonction de l'etudid, code nip et code ine rentré en paramètres + etudiant en fonction de l'etudid, code nip et code ine rentré en paramètres etudid : None ou un int etudid nip : None ou un int code_nip ine : None ou un int code_ine - Exemple de résultat: + Return None si étudiant inexistant. """ if etudid is None: if nip is None: # si ine diff --git a/app/but/bulletin_but_pdf.py b/app/but/bulletin_but_pdf.py index 36d11d1e..e3e40c51 100644 --- a/app/but/bulletin_but_pdf.py +++ b/app/but/bulletin_but_pdf.py @@ -127,6 +127,9 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): def ue_rows(self, rows: list, ue_acronym: str, ue: dict, title_bg: tuple): "Décrit une UE dans la table synthèse: titre, sous-titre et liste modules" + if (ue["type"] == UE_SPORT) and len(ue.get("modules", [])) == 0: + # ne mentionne l'UE que s'il y a des modules + return # 1er ligne titre UE moy_ue = ue.get("moyenne") t = { @@ -206,7 +209,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): for mod_code, mod in ue["modules"].items(): rows.append( { - "titre": f"{mod_code} {mod['titre']}", + "titre": f"{mod_code or ''} {mod['titre'] or ''}", } ) self.evaluations_rows(rows, mod["evaluations"]) @@ -313,7 +316,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): "lignes des évaluations" for e in evaluations: t = { - "titre": f"{e['description']}", + "titre": f"{e['description'] or ''}", "moyenne": e["note"]["value"], "_moyenne_pdf": Paragraph( f"""{e["note"]["value"]}""" diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 917b0136..912136e6 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -146,6 +146,7 @@ class Identite(db.Model): return { "id": self.id, "nip": self.code_nip, + "ine": self.code_ine, "nom": self.nom, "nom_usuel": self.nom_usuel, "prenom": self.prenom, diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 46244a9a..84383adc 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -178,11 +178,12 @@ def evaluation_enrich_dict(e): else: e["descrheure"] = "" # matin, apresmidi: utile pour se referer aux absences: - if heure_debut_dt < datetime.time(12, 00): + + if e["jour"] and heure_debut_dt < datetime.time(12, 00): e["matin"] = 1 else: e["matin"] = 0 - if heure_fin_dt > datetime.time(12, 00): + if e["jour"] and heure_fin_dt > datetime.time(12, 00): e["apresmidi"] = 1 else: e["apresmidi"] = 0 diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 4095ef5c..5bcef071 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -139,6 +139,7 @@ class FormSemestre(db.Model): else: d["date_fin"] = d["date_fin_iso"] = "" d["responsables"] = [u.id for u in self.responsables] + d["titre_formation"] = self.titre_formation() return d def to_dict_api(self): @@ -351,9 +352,10 @@ class FormSemestre(db.Model): ANNEE=annee universitaire de debut (exemple: un S2 de 2013-2014 sera S2-2013) """ - imputation_dept = sco_preferences.get_preference("ImputationDept", self.id) + prefs = sco_preferences.SemPreferences(dept_id=self.dept_id) + imputation_dept = prefs["ImputationDept"] if not imputation_dept: - imputation_dept = sco_preferences.get_preference("DeptName") + imputation_dept = prefs["DeptName"] imputation_dept = imputation_dept.upper() parcours_name = self.formation.get_parcours().NAME modalite = self.modalite @@ -368,7 +370,7 @@ class FormSemestre(db.Model): scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month) ) return scu.sanitize_string( - "-".join((imputation_dept, parcours_name, modalite, semestre_id, annee_sco)) + f"{imputation_dept}-{parcours_name}-{modalite}-{semestre_id}-{annee_sco}" ) def titre_annee(self) -> str: @@ -380,6 +382,12 @@ class FormSemestre(db.Model): titre_annee += "-" + str(self.date_fin.year) return titre_annee + def titre_formation(self): + """Titre avec formation, court, pour passerelle: "BUT R&T" + (méthode de formsemestre car on pourrait ajouter le semestre, ou d'autres infos, à voir) + """ + return self.formation.acronyme + def titre_mois(self) -> str: """Le titre et les dates du semestre, pour affichage dans des listes Ex: "BUT QLIO (PN 2022) semestre 1 FI (Sept 2022 - Jan 2023)" diff --git a/app/pe/pe_settag.py b/app/pe/pe_settag.py index f4ada213..beee42f9 100644 --- a/app/pe/pe_settag.py +++ b/app/pe/pe_settag.py @@ -97,7 +97,7 @@ class SetTag(pe_tagtable.TableTag): """Mémorise les semtag nécessaires au jury.""" self.SemTagDict = {fid: SemTagDict[fid] for fid in self.get_Fids_in_settag()} if PE_DEBUG >= 1: - pe_print(u" => %d semestres fusionnés" % len(self.SemTagDict)) + pe_print(" => %d semestres fusionnés" % len(self.SemTagDict)) # ------------------------------------------------------------------------------------------------------------------- def comp_data_settag(self): @@ -243,7 +243,7 @@ class SetTagInterClasse(pe_tagtable.TableTag): fid: SetTagDict[fid] for fid in self.get_Fids_in_settag() if fid != None } if PE_DEBUG >= 1: - pe_print(u" => %d semestres utilisés" % len(self.SetTagDict)) + pe_print(" => %d semestres utilisés" % len(self.SetTagDict)) # ------------------------------------------------------------------------------------------------------------------- def comp_data_settag(self): diff --git a/app/scodoc/sco_abs_views.py b/app/scodoc/sco_abs_views.py index a4e1d3a7..8427777f 100644 --- a/app/scodoc/sco_abs_views.py +++ b/app/scodoc/sco_abs_views.py @@ -965,7 +965,7 @@ def _tables_abs_etud( )[0] if format == "html": ex.append( - f"""{mod["module"]["code"] or "(module sans code)"}""" ) diff --git a/app/scodoc/sco_bulletins_legacy.py b/app/scodoc/sco_bulletins_legacy.py index 314abb0d..1dc01ad5 100644 --- a/app/scodoc/sco_bulletins_legacy.py +++ b/app/scodoc/sco_bulletins_legacy.py @@ -132,10 +132,13 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator): if sco_preferences.get_preference( "bul_show_minmax_mod", formsemestre_id ): - rang_minmax = '%s [%s, %s]' % ( - mod["mod_rang_txt"], - scu.fmt_note(mod["stats"]["min"]), - scu.fmt_note(mod["stats"]["max"]), + rang_minmax = ( + '%s [%s, %s]' + % ( + mod["mod_rang_txt"], + scu.fmt_note(mod["stats"]["min"]), + scu.fmt_note(mod["stats"]["max"]), + ) ) else: rang_minmax = mod["mod_rang_txt"] # vide si pas option rang diff --git a/app/scodoc/sco_dept.py b/app/scodoc/sco_dept.py index 14c47651..42f3db76 100644 --- a/app/scodoc/sco_dept.py +++ b/app/scodoc/sco_dept.py @@ -131,8 +131,10 @@ def index_html(showcodes=0, showsemtable=0): if not showsemtable: H.append( f"""
-

Voir tous les semestres ({len(othersems)} verrouillés) +

Voir table des semestres (dont {len(othersems)} + verrouillé{'s' if len(othersems) else ''})

""" ) diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py index b3fe9853..11563c0d 100644 --- a/app/scodoc/sco_formsemestre_inscriptions.py +++ b/app/scodoc/sco_formsemestre_inscriptions.py @@ -534,11 +534,14 @@ def formsemestre_inscription_option(etudid, formsemestre_id): ue_status = nt.get_etud_ue_status(etudid, ue_id) if ue_status and ue_status["is_capitalized"]: sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"]) - ue_descr += ' (capitalisée le %s)' % ( - sem_origin["formsemestre_id"], - etudid, - sem_origin["titreannee"], - ndb.DateISOtoDMY(ue_status["event_date"]), + ue_descr += ( + ' (capitalisée le %s)' + % ( + sem_origin["formsemestre_id"], + etudid, + sem_origin["titreannee"], + ndb.DateISOtoDMY(ue_status["event_date"]), + ) ) descr.append( ( diff --git a/app/scodoc/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py index 76871cf3..ce706318 100644 --- a/app/scodoc/sco_moduleimpl_inscriptions.py +++ b/app/scodoc/sco_moduleimpl_inscriptions.py @@ -303,9 +303,12 @@ def moduleimpl_inscriptions_stats(formsemestre_id): ) for mod in options: if can_change: - c_link = '%s' % ( - mod["moduleimpl_id"], - mod["descri"] or "(inscrire des étudiants)", + c_link = ( + '%s' + % ( + mod["moduleimpl_id"], + mod["descri"] or "(inscrire des étudiants)", + ) ) else: c_link = mod["descri"] diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index 3a99d223..a030f514 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -296,17 +296,18 @@ def ficheEtud(etudid=None): if not sco_permissions_check.can_suppress_annotation(a["id"]): a["dellink"] = "" else: - a[ - "dellink" - ] = '%s' % ( - etudid, - a["id"], - scu.icontag( - "delete_img", - border="0", - alt="suppress", - title="Supprimer cette annotation", - ), + a["dellink"] = ( + '%s' + % ( + etudid, + a["id"], + scu.icontag( + "delete_img", + border="0", + alt="suppress", + title="Supprimer cette annotation", + ), + ) ) author = sco_users.user_info(a["author"]) alist.append( diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index 9ccb6362..49445bc9 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -132,9 +132,12 @@ def clear_base_preferences(): g._SCO_BASE_PREFERENCES = {} # { dept_id: BasePreferences instance } -def get_base_preferences(): - """Return global preferences for the current department""" - dept_id = g.scodoc_dept_id +def get_base_preferences(dept_id: int = None): + """Return global preferences for the specified department + or the current departement + """ + if dept_id is None: + dept_id = g.scodoc_dept_id if not hasattr(g, "_SCO_BASE_PREFERENCES"): g._SCO_BASE_PREFERENCES = {} if not dept_id in g._SCO_BASE_PREFERENCES: @@ -142,12 +145,12 @@ def get_base_preferences(): return g._SCO_BASE_PREFERENCES[dept_id] -def get_preference(name, formsemestre_id=None): +def get_preference(name, formsemestre_id=None, dept_id=None): """Returns value of named preference. All preferences have a sensible default value, so this function always returns a usable value for all defined preferences names. """ - return get_base_preferences().get(formsemestre_id, name) + return get_base_preferences(dept_id=dept_id).get(formsemestre_id, name) def _convert_pref_type(p, pref_spec): @@ -2145,9 +2148,9 @@ class BasePreferences(object): class SemPreferences: """Preferences for a formsemestre""" - def __init__(self, formsemestre_id=None): + def __init__(self, formsemestre_id=None, dept_id=None): self.formsemestre_id = formsemestre_id - self.base_prefs = get_base_preferences() + self.base_prefs = get_base_preferences(dept_id=dept_id) def __getitem__(self, name): return self.base_prefs.get(self.formsemestre_id, name) diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 9cda2372..733ef173 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -799,22 +799,22 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]): evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id}) if not evals: raise ScoValueError("invalid evaluation_id") - E = evals[0] - M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] + eval_dict = evals[0] + M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=eval_dict["moduleimpl_id"])[0] formsemestre_id = M["formsemestre_id"] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) mod_responsable = sco_users.user_info(M["responsable_id"]) - if E["jour"]: - indication_date = ndb.DateDMYtoISO(E["jour"]) + if eval_dict["jour"]: + indication_date = ndb.DateDMYtoISO(eval_dict["jour"]) else: - indication_date = scu.sanitize_filename(E["description"])[:12] - evalname = "%s-%s" % (Mod["code"], indication_date) + indication_date = scu.sanitize_filename(eval_dict["description"])[:12] + eval_name = "%s-%s" % (Mod["code"], indication_date) - if E["description"]: - evaltitre = "%s du %s" % (E["description"], E["jour"]) + if eval_dict["description"]: + evaltitre = "%s du %s" % (eval_dict["description"], eval_dict["jour"]) else: - evaltitre = "évaluation du %s" % E["jour"] + evaltitre = "évaluation du %s" % eval_dict["jour"] description = "%s en %s (%s) resp. %s" % ( evaltitre, Mod["abbrev"] or "", @@ -847,7 +847,7 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]): # une liste de liste de chaines: lignes de la feuille de calcul L = [] - etuds = _get_sorted_etuds(E, etudids, formsemestre_id) + etuds = _get_sorted_etuds(eval_dict, etudids, formsemestre_id) for e in etuds: etudid = e["etudid"] groups = sco_groups.get_etud_groups(etudid, formsemestre_id) @@ -865,8 +865,10 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]): ] ) - filename = "notes_%s_%s" % (evalname, gr_title_filename) - xls = sco_excel.excel_feuille_saisie(E, sem["titreannee"], description, lines=L) + filename = "notes_%s_%s" % (eval_name, gr_title_filename) + xls = sco_excel.excel_feuille_saisie( + eval_dict, sem["titreannee"], description, lines=L + ) return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE) # return sco_excel.send_excel_file(xls, filename) @@ -1008,10 +1010,9 @@ def saisie_notes(evaluation_id, group_ids=[]): return "\n".join(H) -def _get_sorted_etuds(E, etudids, formsemestre_id): - sem = sco_formsemestre.get_formsemestre(formsemestre_id) +def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int): notes_db = sco_evaluation_db.do_evaluation_get_all_notes( - E["evaluation_id"] + eval_dict["evaluation_id"] ) # Notes existantes cnx = ndb.GetDBConnexion() etuds = [] @@ -1028,9 +1029,9 @@ def _get_sorted_etuds(E, etudids, formsemestre_id): e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id) # Information sur absence (tenant compte de la demi-journée) - jour_iso = ndb.DateDMYtoISO(E["jour"]) + jour_iso = ndb.DateDMYtoISO(eval_dict["jour"]) warn_abs_lst = [] - if E["matin"]: + if eval_dict["matin"]: nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=1) nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=1) if nbabs: @@ -1038,7 +1039,7 @@ def _get_sorted_etuds(E, etudids, formsemestre_id): warn_abs_lst.append("absent justifié le matin !") else: warn_abs_lst.append("absent le matin !") - if E["apresmidi"]: + if eval_dict["apresmidi"]: nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=0) nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=0) if nbabs: diff --git a/migrations/env.py b/migrations/env.py index 68feded2..0c6eddcb 100755 --- a/migrations/env.py +++ b/migrations/env.py @@ -14,17 +14,17 @@ config = context.config # Interpret the config file for Python logging. # This line sets up loggers basically. fileConfig(config.config_file_name) -logger = logging.getLogger('alembic.env') +logger = logging.getLogger("alembic.env") # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata config.set_main_option( - 'sqlalchemy.url', - str(current_app.extensions['migrate'].db.get_engine().url).replace( - '%', '%%')) -target_metadata = current_app.extensions['migrate'].db.metadata + "sqlalchemy.url", + str(current_app.extensions["migrate"].db.get_engine().url).replace("%", "%%"), +) +target_metadata = current_app.extensions["migrate"].db.metadata # other values from the config, defined by the needs of env.py, # can be acquired: @@ -45,9 +45,7 @@ def run_migrations_offline(): """ url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True - ) + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) with context.begin_transaction(): context.run_migrations() @@ -65,20 +63,20 @@ def run_migrations_online(): # when there are no changes to the schema # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html def process_revision_directives(context, revision, directives): - if getattr(config.cmd_opts, 'autogenerate', False): + if getattr(config.cmd_opts, "autogenerate", False): script = directives[0] if script.upgrade_ops.is_empty(): directives[:] = [] - logger.info('No changes in schema detected.') + logger.info("No changes in schema detected.") - connectable = current_app.extensions['migrate'].db.get_engine() + connectable = current_app.extensions["migrate"].db.get_engine() with connectable.connect() as connection: context.configure( connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives, - **current_app.extensions['migrate'].configure_args + **current_app.extensions["migrate"].configure_args ) with context.begin_transaction(): diff --git a/migrations/versions/017e32eb4773_scodoc_9_0_4_ajout_id_scodoc7_pour_.py b/migrations/versions/017e32eb4773_scodoc_9_0_4_ajout_id_scodoc7_pour_.py index 41f5f29a..8aff9461 100644 --- a/migrations/versions/017e32eb4773_scodoc_9_0_4_ajout_id_scodoc7_pour_.py +++ b/migrations/versions/017e32eb4773_scodoc_9_0_4_ajout_id_scodoc7_pour_.py @@ -10,21 +10,23 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '017e32eb4773' -down_revision = '6b071b7947e5' +revision = "017e32eb4773" +down_revision = "6b071b7947e5" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('identite', sa.Column('scodoc7_id', sa.Text(), nullable=True)) - op.add_column('notes_formsemestre', sa.Column('scodoc7_id', sa.Text(), nullable=True)) + op.add_column("identite", sa.Column("scodoc7_id", sa.Text(), nullable=True)) + op.add_column( + "notes_formsemestre", sa.Column("scodoc7_id", sa.Text(), nullable=True) + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('notes_formsemestre', 'scodoc7_id') - op.drop_column('identite', 'scodoc7_id') + op.drop_column("notes_formsemestre", "scodoc7_id") + op.drop_column("identite", "scodoc7_id") # ### end Alembic commands ### diff --git a/migrations/versions/1efe07413835_added_cascade_on_absences_notifications.py b/migrations/versions/1efe07413835_added_cascade_on_absences_notifications.py index c568000c..0985efb5 100644 --- a/migrations/versions/1efe07413835_added_cascade_on_absences_notifications.py +++ b/migrations/versions/1efe07413835_added_cascade_on_absences_notifications.py @@ -10,21 +10,38 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '1efe07413835' -down_revision = '75cf18659984' +revision = "1efe07413835" +down_revision = "75cf18659984" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('absences_notifications_formsemestre_id_fkey', 'absences_notifications', type_='foreignkey') - op.create_foreign_key(None, 'absences_notifications', 'notes_formsemestre', ['formsemestre_id'], ['id'], ondelete='CASCADE') + op.drop_constraint( + "absences_notifications_formsemestre_id_fkey", + "absences_notifications", + type_="foreignkey", + ) + op.create_foreign_key( + None, + "absences_notifications", + "notes_formsemestre", + ["formsemestre_id"], + ["id"], + ondelete="CASCADE", + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 'absences_notifications', type_='foreignkey') - op.create_foreign_key('absences_notifications_formsemestre_id_fkey', 'absences_notifications', 'notes_formsemestre', ['formsemestre_id'], ['id']) + op.drop_constraint(None, "absences_notifications", type_="foreignkey") + op.create_foreign_key( + "absences_notifications_formsemestre_id_fkey", + "absences_notifications", + "notes_formsemestre", + ["formsemestre_id"], + ["id"], + ) # ### end Alembic commands ### diff --git a/migrations/versions/39818df276aa_cascades_sur_itemsuivi.py b/migrations/versions/39818df276aa_cascades_sur_itemsuivi.py index a64002be..7cbd8801 100644 --- a/migrations/versions/39818df276aa_cascades_sur_itemsuivi.py +++ b/migrations/versions/39818df276aa_cascades_sur_itemsuivi.py @@ -10,25 +10,57 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '39818df276aa' -down_revision = '1efe07413835' +revision = "39818df276aa" +down_revision = "1efe07413835" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('itemsuivi_tags_assoc_tag_id_fkey', 'itemsuivi_tags_assoc', type_='foreignkey') - op.drop_constraint('itemsuivi_tags_assoc_itemsuivi_id_fkey', 'itemsuivi_tags_assoc', type_='foreignkey') - op.create_foreign_key(None, 'itemsuivi_tags_assoc', 'itemsuivi', ['itemsuivi_id'], ['id'], ondelete='CASCADE') - op.create_foreign_key(None, 'itemsuivi_tags_assoc', 'itemsuivi_tags', ['tag_id'], ['id'], ondelete='CASCADE') + op.drop_constraint( + "itemsuivi_tags_assoc_tag_id_fkey", "itemsuivi_tags_assoc", type_="foreignkey" + ) + op.drop_constraint( + "itemsuivi_tags_assoc_itemsuivi_id_fkey", + "itemsuivi_tags_assoc", + type_="foreignkey", + ) + op.create_foreign_key( + None, + "itemsuivi_tags_assoc", + "itemsuivi", + ["itemsuivi_id"], + ["id"], + ondelete="CASCADE", + ) + op.create_foreign_key( + None, + "itemsuivi_tags_assoc", + "itemsuivi_tags", + ["tag_id"], + ["id"], + ondelete="CASCADE", + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 'itemsuivi_tags_assoc', type_='foreignkey') - op.drop_constraint(None, 'itemsuivi_tags_assoc', type_='foreignkey') - op.create_foreign_key('itemsuivi_tags_assoc_itemsuivi_id_fkey', 'itemsuivi_tags_assoc', 'itemsuivi', ['itemsuivi_id'], ['id']) - op.create_foreign_key('itemsuivi_tags_assoc_tag_id_fkey', 'itemsuivi_tags_assoc', 'itemsuivi_tags', ['tag_id'], ['id']) + op.drop_constraint(None, "itemsuivi_tags_assoc", type_="foreignkey") + op.drop_constraint(None, "itemsuivi_tags_assoc", type_="foreignkey") + op.create_foreign_key( + "itemsuivi_tags_assoc_itemsuivi_id_fkey", + "itemsuivi_tags_assoc", + "itemsuivi", + ["itemsuivi_id"], + ["id"], + ) + op.create_foreign_key( + "itemsuivi_tags_assoc_tag_id_fkey", + "itemsuivi_tags_assoc", + "itemsuivi_tags", + ["tag_id"], + ["id"], + ) # ### end Alembic commands ### diff --git a/migrations/versions/669065fb2d20_flag_bloquage_calcul_moyennes.py b/migrations/versions/669065fb2d20_flag_bloquage_calcul_moyennes.py index 9479c828..9d50cb4a 100644 --- a/migrations/versions/669065fb2d20_flag_bloquage_calcul_moyennes.py +++ b/migrations/versions/669065fb2d20_flag_bloquage_calcul_moyennes.py @@ -10,19 +10,24 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '669065fb2d20' -down_revision = 'a217bf588f4c' +revision = "669065fb2d20" +down_revision = "a217bf588f4c" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('notes_formsemestre', sa.Column('block_moyennes', sa.Boolean(), server_default='false', nullable=False)) + op.add_column( + "notes_formsemestre", + sa.Column( + "block_moyennes", sa.Boolean(), server_default="false", nullable=False + ), + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('notes_formsemestre', 'block_moyennes') + op.drop_column("notes_formsemestre", "block_moyennes") # ### end Alembic commands ### diff --git a/migrations/versions/6b071b7947e5_scodoc_9_0_4_code_module_en_text.py b/migrations/versions/6b071b7947e5_scodoc_9_0_4_code_module_en_text.py index c16e8582..9a41e8dd 100644 --- a/migrations/versions/6b071b7947e5_scodoc_9_0_4_code_module_en_text.py +++ b/migrations/versions/6b071b7947e5_scodoc_9_0_4_code_module_en_text.py @@ -10,25 +10,31 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '6b071b7947e5' -down_revision = '993ce4a01d57' +revision = "6b071b7947e5" +down_revision = "993ce4a01d57" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('notes_modules', 'code', - existing_type=sa.VARCHAR(length=32), - type_=sa.Text(), - existing_nullable=False) + op.alter_column( + "notes_modules", + "code", + existing_type=sa.VARCHAR(length=32), + type_=sa.Text(), + existing_nullable=False, + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('notes_modules', 'code', - existing_type=sa.Text(), - type_=sa.VARCHAR(length=32), - existing_nullable=False) + op.alter_column( + "notes_modules", + "code", + existing_type=sa.Text(), + type_=sa.VARCHAR(length=32), + existing_nullable=False, + ) # ### end Alembic commands ### diff --git a/migrations/versions/6cfc21a7ae1b_coefs_modules_but.py b/migrations/versions/6cfc21a7ae1b_coefs_modules_but.py index 78bc6d74..94eece1c 100644 --- a/migrations/versions/6cfc21a7ae1b_coefs_modules_but.py +++ b/migrations/versions/6cfc21a7ae1b_coefs_modules_but.py @@ -10,26 +10,33 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '6cfc21a7ae1b' -down_revision = 'ada0d1f3d84f' +revision = "6cfc21a7ae1b" +down_revision = "ada0d1f3d84f" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('module_ue_coef', - sa.Column('module_id', sa.Integer(), nullable=False), - sa.Column('ue_id', sa.Integer(), nullable=False), - sa.Column('coef', sa.Float(), nullable=False), - sa.ForeignKeyConstraint(['module_id'], ['notes_modules.id'], ), - sa.ForeignKeyConstraint(['ue_id'], ['notes_ue.id'], ), - sa.PrimaryKeyConstraint('module_id', 'ue_id') + op.create_table( + "module_ue_coef", + sa.Column("module_id", sa.Integer(), nullable=False), + sa.Column("ue_id", sa.Integer(), nullable=False), + sa.Column("coef", sa.Float(), nullable=False), + sa.ForeignKeyConstraint( + ["module_id"], + ["notes_modules.id"], + ), + sa.ForeignKeyConstraint( + ["ue_id"], + ["notes_ue.id"], + ), + sa.PrimaryKeyConstraint("module_id", "ue_id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('module_ue_coef') + op.drop_table("module_ue_coef") # ### end Alembic commands ### diff --git a/migrations/versions/75cf18659984_cascade_tags_modules.py b/migrations/versions/75cf18659984_cascade_tags_modules.py index 1a498b07..db034e14 100644 --- a/migrations/versions/75cf18659984_cascade_tags_modules.py +++ b/migrations/versions/75cf18659984_cascade_tags_modules.py @@ -10,25 +10,50 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '75cf18659984' -down_revision = 'd74b4e16fb3c' +revision = "75cf18659984" +down_revision = "d74b4e16fb3c" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('notes_modules_tags_tag_id_fkey', 'notes_modules_tags', type_='foreignkey') - op.drop_constraint('notes_modules_tags_module_id_fkey', 'notes_modules_tags', type_='foreignkey') - op.create_foreign_key(None, 'notes_modules_tags', 'notes_tags', ['tag_id'], ['id'], ondelete='CASCADE') - op.create_foreign_key(None, 'notes_modules_tags', 'notes_modules', ['module_id'], ['id'], ondelete='CASCADE') + op.drop_constraint( + "notes_modules_tags_tag_id_fkey", "notes_modules_tags", type_="foreignkey" + ) + op.drop_constraint( + "notes_modules_tags_module_id_fkey", "notes_modules_tags", type_="foreignkey" + ) + op.create_foreign_key( + None, "notes_modules_tags", "notes_tags", ["tag_id"], ["id"], ondelete="CASCADE" + ) + op.create_foreign_key( + None, + "notes_modules_tags", + "notes_modules", + ["module_id"], + ["id"], + ondelete="CASCADE", + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 'notes_modules_tags', type_='foreignkey') - op.drop_constraint(None, 'notes_modules_tags', type_='foreignkey') - op.create_foreign_key('notes_modules_tags_module_id_fkey', 'notes_modules_tags', 'notes_modules', ['module_id'], ['id']) - op.create_foreign_key('notes_modules_tags_tag_id_fkey', 'notes_modules_tags', 'notes_tags', ['tag_id'], ['id']) + op.drop_constraint(None, "notes_modules_tags", type_="foreignkey") + op.drop_constraint(None, "notes_modules_tags", type_="foreignkey") + op.create_foreign_key( + "notes_modules_tags_module_id_fkey", + "notes_modules_tags", + "notes_modules", + ["module_id"], + ["id"], + ) + op.create_foreign_key( + "notes_modules_tags_tag_id_fkey", + "notes_modules_tags", + "notes_tags", + ["tag_id"], + ["id"], + ) # ### end Alembic commands ### diff --git a/migrations/versions/92789d50f6b6_refcomp_index.py b/migrations/versions/92789d50f6b6_refcomp_index.py index 8182597d..adc190ed 100644 --- a/migrations/versions/92789d50f6b6_refcomp_index.py +++ b/migrations/versions/92789d50f6b6_refcomp_index.py @@ -10,46 +10,77 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '92789d50f6b6' -down_revision = '00ad500fb118' +revision = "92789d50f6b6" +down_revision = "00ad500fb118" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('modules_acs') - op.drop_table('app_crit') - op.add_column('apc_annee_parcours', sa.Column('ordre', sa.Integer(), nullable=True)) - op.drop_column('apc_annee_parcours', 'numero') - op.create_index(op.f('ix_apc_app_critique_code'), 'apc_app_critique', ['code'], unique=False) - op.create_unique_constraint('apc_competence_referentiel_id_titre_key', 'apc_competence', ['referentiel_id', 'titre']) - op.create_index(op.f('ix_apc_competence_titre'), 'apc_competence', ['titre'], unique=False) - op.add_column('apc_referentiel_competences', sa.Column('scodoc_date_loaded', sa.DateTime(), nullable=True)) - op.add_column('apc_referentiel_competences', sa.Column('scodoc_orig_filename', sa.Text(), nullable=True)) + op.drop_table("modules_acs") + op.drop_table("app_crit") + op.add_column("apc_annee_parcours", sa.Column("ordre", sa.Integer(), nullable=True)) + op.drop_column("apc_annee_parcours", "numero") + op.create_index( + op.f("ix_apc_app_critique_code"), "apc_app_critique", ["code"], unique=False + ) + op.create_unique_constraint( + "apc_competence_referentiel_id_titre_key", + "apc_competence", + ["referentiel_id", "titre"], + ) + op.create_index( + op.f("ix_apc_competence_titre"), "apc_competence", ["titre"], unique=False + ) + op.add_column( + "apc_referentiel_competences", + sa.Column("scodoc_date_loaded", sa.DateTime(), nullable=True), + ) + op.add_column( + "apc_referentiel_competences", + sa.Column("scodoc_orig_filename", sa.Text(), nullable=True), + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('apc_referentiel_competences', 'scodoc_orig_filename') - op.drop_column('apc_referentiel_competences', 'scodoc_date_loaded') - op.drop_index(op.f('ix_apc_competence_titre'), table_name='apc_competence') - op.drop_constraint('apc_competence_referentiel_id_titre_key', 'apc_competence', type_='unique') - op.drop_index(op.f('ix_apc_app_critique_code'), table_name='apc_app_critique') - op.add_column('apc_annee_parcours', sa.Column('numero', sa.INTEGER(), autoincrement=False, nullable=True)) - op.drop_column('apc_annee_parcours', 'ordre') - op.create_table('app_crit', - sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('app_crit_id_seq'::regclass)"), autoincrement=True, nullable=False), - sa.Column('code', sa.TEXT(), autoincrement=False, nullable=False), - sa.Column('titre', sa.TEXT(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name='app_crit_pkey'), - postgresql_ignore_search_path=False + op.drop_column("apc_referentiel_competences", "scodoc_orig_filename") + op.drop_column("apc_referentiel_competences", "scodoc_date_loaded") + op.drop_index(op.f("ix_apc_competence_titre"), table_name="apc_competence") + op.drop_constraint( + "apc_competence_referentiel_id_titre_key", "apc_competence", type_="unique" ) - op.create_table('modules_acs', - sa.Column('module_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('ac_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['ac_id'], ['app_crit.id'], name='modules_acs_ac_id_fkey'), - sa.ForeignKeyConstraint(['module_id'], ['notes_modules.id'], name='modules_acs_module_id_fkey') + op.drop_index(op.f("ix_apc_app_critique_code"), table_name="apc_app_critique") + op.add_column( + "apc_annee_parcours", + sa.Column("numero", sa.INTEGER(), autoincrement=False, nullable=True), + ) + op.drop_column("apc_annee_parcours", "ordre") + op.create_table( + "app_crit", + sa.Column( + "id", + sa.INTEGER(), + server_default=sa.text("nextval('app_crit_id_seq'::regclass)"), + autoincrement=True, + nullable=False, + ), + sa.Column("code", sa.TEXT(), autoincrement=False, nullable=False), + sa.Column("titre", sa.TEXT(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint("id", name="app_crit_pkey"), + postgresql_ignore_search_path=False, + ) + op.create_table( + "modules_acs", + sa.Column("module_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("ac_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint( + ["ac_id"], ["app_crit.id"], name="modules_acs_ac_id_fkey" + ), + sa.ForeignKeyConstraint( + ["module_id"], ["notes_modules.id"], name="modules_acs_module_id_fkey" + ), ) # ### end Alembic commands ### diff --git a/migrations/versions/a217bf588f4c_scodoc_9_0_13_essai_cascade.py b/migrations/versions/a217bf588f4c_scodoc_9_0_13_essai_cascade.py index 3e0c53b5..40c45ddf 100644 --- a/migrations/versions/a217bf588f4c_scodoc_9_0_13_essai_cascade.py +++ b/migrations/versions/a217bf588f4c_scodoc_9_0_13_essai_cascade.py @@ -10,27 +10,50 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'a217bf588f4c' -down_revision = 'f73251d1d825' +revision = "a217bf588f4c" +down_revision = "f73251d1d825" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('notes_semset_formsemestre', 'semset_id', - existing_type=sa.INTEGER(), - nullable=False) - op.drop_constraint('notes_semset_formsemestre_semset_id_fkey', 'notes_semset_formsemestre', type_='foreignkey') - op.create_foreign_key(None, 'notes_semset_formsemestre', 'notes_semset', ['semset_id'], ['id'], ondelete='CASCADE') + op.alter_column( + "notes_semset_formsemestre", + "semset_id", + existing_type=sa.INTEGER(), + nullable=False, + ) + op.drop_constraint( + "notes_semset_formsemestre_semset_id_fkey", + "notes_semset_formsemestre", + type_="foreignkey", + ) + op.create_foreign_key( + None, + "notes_semset_formsemestre", + "notes_semset", + ["semset_id"], + ["id"], + ondelete="CASCADE", + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 'notes_semset_formsemestre', type_='foreignkey') - op.create_foreign_key('notes_semset_formsemestre_semset_id_fkey', 'notes_semset_formsemestre', 'notes_semset', ['semset_id'], ['id']) - op.alter_column('notes_semset_formsemestre', 'semset_id', - existing_type=sa.INTEGER(), - nullable=True) + op.drop_constraint(None, "notes_semset_formsemestre", type_="foreignkey") + op.create_foreign_key( + "notes_semset_formsemestre_semset_id_fkey", + "notes_semset_formsemestre", + "notes_semset", + ["semset_id"], + ["id"], + ) + op.alter_column( + "notes_semset_formsemestre", + "semset_id", + existing_type=sa.INTEGER(), + nullable=True, + ) # ### end Alembic commands ### diff --git a/migrations/versions/c8efc54586d8_ue_semestre_idx.py b/migrations/versions/c8efc54586d8_ue_semestre_idx.py index 7eeb0e7e..180529b8 100644 --- a/migrations/versions/c8efc54586d8_ue_semestre_idx.py +++ b/migrations/versions/c8efc54586d8_ue_semestre_idx.py @@ -10,21 +10,23 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'c8efc54586d8' -down_revision = '6cfc21a7ae1b' +revision = "c8efc54586d8" +down_revision = "6cfc21a7ae1b" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('notes_ue', sa.Column('semestre_idx', sa.Integer(), nullable=True)) - op.create_index(op.f('ix_notes_ue_semestre_idx'), 'notes_ue', ['semestre_idx'], unique=False) + op.add_column("notes_ue", sa.Column("semestre_idx", sa.Integer(), nullable=True)) + op.create_index( + op.f("ix_notes_ue_semestre_idx"), "notes_ue", ["semestre_idx"], unique=False + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_notes_ue_semestre_idx'), table_name='notes_ue') - op.drop_column('notes_ue', 'semestre_idx') + op.drop_index(op.f("ix_notes_ue_semestre_idx"), table_name="notes_ue") + op.drop_column("notes_ue", "semestre_idx") # ### end Alembic commands ### diff --git a/migrations/versions/d3d92b2d0092_scodoc_9_0_5_ajout_dept_id_sur_.py b/migrations/versions/d3d92b2d0092_scodoc_9_0_5_ajout_dept_id_sur_.py index 8ab88cac..13623afa 100644 --- a/migrations/versions/d3d92b2d0092_scodoc_9_0_5_ajout_dept_id_sur_.py +++ b/migrations/versions/d3d92b2d0092_scodoc_9_0_5_ajout_dept_id_sur_.py @@ -10,23 +10,25 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'd3d92b2d0092' -down_revision = '017e32eb4773' +revision = "d3d92b2d0092" +down_revision = "017e32eb4773" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('itemsuivi_tags', sa.Column('dept_id', sa.Integer(), nullable=True)) - op.create_index(op.f('ix_itemsuivi_tags_dept_id'), 'itemsuivi_tags', ['dept_id'], unique=False) - op.create_foreign_key(None, 'itemsuivi_tags', 'departement', ['dept_id'], ['id']) + op.add_column("itemsuivi_tags", sa.Column("dept_id", sa.Integer(), nullable=True)) + op.create_index( + op.f("ix_itemsuivi_tags_dept_id"), "itemsuivi_tags", ["dept_id"], unique=False + ) + op.create_foreign_key(None, "itemsuivi_tags", "departement", ["dept_id"], ["id"]) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 'itemsuivi_tags', type_='foreignkey') - op.drop_index(op.f('ix_itemsuivi_tags_dept_id'), table_name='itemsuivi_tags') - op.drop_column('itemsuivi_tags', 'dept_id') + op.drop_constraint(None, "itemsuivi_tags", type_="foreignkey") + op.drop_index(op.f("ix_itemsuivi_tags_dept_id"), table_name="itemsuivi_tags") + op.drop_column("itemsuivi_tags", "dept_id") # ### end Alembic commands ### diff --git a/migrations/versions/f6e7d2e01be1_augmente_taille_codes_apogee.py b/migrations/versions/f6e7d2e01be1_augmente_taille_codes_apogee.py index d1b2a9e3..3740714e 100644 --- a/migrations/versions/f6e7d2e01be1_augmente_taille_codes_apogee.py +++ b/migrations/versions/f6e7d2e01be1_augmente_taille_codes_apogee.py @@ -10,49 +10,73 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'f6e7d2e01be1' -down_revision = 'd3d92b2d0092' +revision = "f6e7d2e01be1" +down_revision = "d3d92b2d0092" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('notes_formsemestre_etapes', 'etape_apo', - existing_type=sa.VARCHAR(length=16), - type_=sa.String(length=24), - existing_nullable=True) - op.alter_column('notes_formsemestre_inscription', 'etape', - existing_type=sa.VARCHAR(length=16), - type_=sa.String(length=24), - existing_nullable=True) - op.alter_column('notes_modules', 'code_apogee', - existing_type=sa.VARCHAR(length=16), - type_=sa.String(length=24), - existing_nullable=True) - op.alter_column('notes_ue', 'code_apogee', - existing_type=sa.VARCHAR(length=16), - type_=sa.String(length=24), - existing_nullable=True) + op.alter_column( + "notes_formsemestre_etapes", + "etape_apo", + existing_type=sa.VARCHAR(length=16), + type_=sa.String(length=24), + existing_nullable=True, + ) + op.alter_column( + "notes_formsemestre_inscription", + "etape", + existing_type=sa.VARCHAR(length=16), + type_=sa.String(length=24), + existing_nullable=True, + ) + op.alter_column( + "notes_modules", + "code_apogee", + existing_type=sa.VARCHAR(length=16), + type_=sa.String(length=24), + existing_nullable=True, + ) + op.alter_column( + "notes_ue", + "code_apogee", + existing_type=sa.VARCHAR(length=16), + type_=sa.String(length=24), + existing_nullable=True, + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('notes_ue', 'code_apogee', - existing_type=sa.String(length=24), - type_=sa.VARCHAR(length=16), - existing_nullable=True) - op.alter_column('notes_modules', 'code_apogee', - existing_type=sa.String(length=24), - type_=sa.VARCHAR(length=16), - existing_nullable=True) - op.alter_column('notes_formsemestre_inscription', 'etape', - existing_type=sa.String(length=24), - type_=sa.VARCHAR(length=16), - existing_nullable=True) - op.alter_column('notes_formsemestre_etapes', 'etape_apo', - existing_type=sa.String(length=24), - type_=sa.VARCHAR(length=16), - existing_nullable=True) + op.alter_column( + "notes_ue", + "code_apogee", + existing_type=sa.String(length=24), + type_=sa.VARCHAR(length=16), + existing_nullable=True, + ) + op.alter_column( + "notes_modules", + "code_apogee", + existing_type=sa.String(length=24), + type_=sa.VARCHAR(length=16), + existing_nullable=True, + ) + op.alter_column( + "notes_formsemestre_inscription", + "etape", + existing_type=sa.String(length=24), + type_=sa.VARCHAR(length=16), + existing_nullable=True, + ) + op.alter_column( + "notes_formsemestre_etapes", + "etape_apo", + existing_type=sa.String(length=24), + type_=sa.VARCHAR(length=16), + existing_nullable=True, + ) # ### end Alembic commands ### diff --git a/migrations/versions/f73251d1d825_table_configuration_site.py b/migrations/versions/f73251d1d825_table_configuration_site.py index e5973288..bbe2ceb0 100644 --- a/migrations/versions/f73251d1d825_table_configuration_site.py +++ b/migrations/versions/f73251d1d825_table_configuration_site.py @@ -10,26 +10,29 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'f73251d1d825' -down_revision = 'f6e7d2e01be1' +revision = "f73251d1d825" +down_revision = "f6e7d2e01be1" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('scodoc_site_config', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=128), nullable=False), - sa.Column('value', sa.Text(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + "scodoc_site_config", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=128), nullable=False), + sa.Column("value", sa.Text(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_scodoc_site_config_name"), "scodoc_site_config", ["name"], unique=False ) - op.create_index(op.f('ix_scodoc_site_config_name'), 'scodoc_site_config', ['name'], unique=False) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_scodoc_site_config_name'), table_name='scodoc_site_config') - op.drop_table('scodoc_site_config') + op.drop_index(op.f("ix_scodoc_site_config_name"), table_name="scodoc_site_config") + op.drop_table("scodoc_site_config") # ### end Alembic commands ### diff --git a/migrations/versions/f86c013c9fbd_modif_contrainte_sur_formations.py b/migrations/versions/f86c013c9fbd_modif_contrainte_sur_formations.py index 8d888c68..d8e1abfc 100644 --- a/migrations/versions/f86c013c9fbd_modif_contrainte_sur_formations.py +++ b/migrations/versions/f86c013c9fbd_modif_contrainte_sur_formations.py @@ -10,21 +10,31 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'f86c013c9fbd' -down_revision = '669065fb2d20' +revision = "f86c013c9fbd" +down_revision = "669065fb2d20" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('notes_formations_acronyme_titre_version_key', 'notes_formations', type_='unique') - op.create_unique_constraint(None, 'notes_formations', ['dept_id', 'acronyme', 'titre', 'version']) + op.drop_constraint( + "notes_formations_acronyme_titre_version_key", + "notes_formations", + type_="unique", + ) + op.create_unique_constraint( + None, "notes_formations", ["dept_id", "acronyme", "titre", "version"] + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 'notes_formations', type_='unique') - op.create_unique_constraint('notes_formations_acronyme_titre_version_key', 'notes_formations', ['acronyme', 'titre', 'version']) + op.drop_constraint(None, "notes_formations", type_="unique") + op.create_unique_constraint( + "notes_formations_acronyme_titre_version_key", + "notes_formations", + ["acronyme", "titre", "version"], + ) # ### end Alembic commands ### diff --git a/misc/SuppressAccents.py b/misc/SuppressAccents.py index 3b119e59..b5ad591b 100644 --- a/misc/SuppressAccents.py +++ b/misc/SuppressAccents.py @@ -7,65 +7,84 @@ Source: http://wikipython.flibuste.net/moin.py/JouerAvecUnicode#head-1213938516c """ _reptable = {} + + def _fill_reptable(): _corresp = [ - (u"A", [0x00C0,0x00C1,0x00C2,0x00C3,0x00C4,0x00C5,0x0100,0x0102,0x0104]), - (u"AE", [0x00C6]), - (u"a", [0x00E0,0x00E1,0x00E2,0x00E3,0x00E4,0x00E5,0x0101,0x0103,0x0105]), - (u"ae", [0x00E6]), - (u"C", [0x00C7,0x0106,0x0108,0x010A,0x010C]), - (u"c", [0x00E7,0x0107,0x0109,0x010B,0x010D]), - (u"D", [0x00D0,0x010E,0x0110]), - (u"d", [0x00F0,0x010F,0x0111]), - (u"E", [0x00C8,0x00C9,0x00CA,0x00CB,0x0112,0x0114,0x0116,0x0118,0x011A]), - (u"e", [0x00E8,0x00E9,0x00EA,0x00EB,0x0113,0x0115,0x0117,0x0119,0x011B]), - (u"G", [0x011C,0x011E,0x0120,0x0122]), - (u"g", [0x011D,0x011F,0x0121,0x0123]), - (u"H", [0x0124,0x0126]), - (u"h", [0x0125,0x0127]), - (u"I", [0x00CC,0x00CD,0x00CE,0x00CF,0x0128,0x012A,0x012C,0x012E,0x0130]), - (u"i", [0x00EC,0x00ED,0x00EE,0x00EF,0x0129,0x012B,0x012D,0x012F,0x0131]), - (u"IJ", [0x0132]), - (u"ij", [0x0133]), - (u"J", [0x0134]), - (u"j", [0x0135]), - (u"K", [0x0136]), - (u"k", [0x0137,0x0138]), - (u"L", [0x0139,0x013B,0x013D,0x013F,0x0141]), - (u"l", [0x013A,0x013C,0x013E,0x0140,0x0142]), - (u"N", [0x00D1,0x0143,0x0145,0x0147,0x014A]), - (u"n", [0x00F1,0x0144,0x0146,0x0148,0x0149,0x014B]), - (u"O", [0x00D2,0x00D3,0x00D4,0x00D5,0x00D6,0x00D8,0x014C,0x014E,0x0150]), - (u"o", [0x00F2,0x00F3,0x00F4,0x00F5,0x00F6,0x00F8,0x014D,0x014F,0x0151]), - (u"OE", [0x0152]), - (u"oe", [0x0153]), - (u"R", [0x0154,0x0156,0x0158]), - (u"r", [0x0155,0x0157,0x0159]), - (u"S", [0x015A,0x015C,0x015E,0x0160]), - (u"s", [0x015B,0x015D,0x015F,0x01610,0x017F]), - (u"T", [0x0162,0x0164,0x0166]), - (u"t", [0x0163,0x0165,0x0167]), - (u"U", [0x00D9,0x00DA,0x00DB,0x00DC,0x0168,0x016A,0x016C,0x016E,0x0170,0x172]), - (u"u", [0x00F9,0x00FA,0x00FB,0x00FC,0x0169,0x016B,0x016D,0x016F,0x0171]), - (u"W", [0x0174]), - (u"w", [0x0175]), - (u"Y", [0x00DD,0x0176,0x0178]), - (u"y", [0x00FD,0x00FF,0x0177]), - (u"Z", [0x0179,0x017B,0x017D]), - (u"z", [0x017A,0x017C,0x017E]), - (u"2", [0x00B2]), # deux exposant - (u" ", [0x00A0]), #   - (u"", [0xB0]), # degre - (u"", [0xA9]), # copyright - (u"1/2", [0xBD]), # 1/2 - ] + ("A", [0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x0100, 0x0102, 0x0104]), + ("AE", [0x00C6]), + ("a", [0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x0101, 0x0103, 0x0105]), + ("ae", [0x00E6]), + ("C", [0x00C7, 0x0106, 0x0108, 0x010A, 0x010C]), + ("c", [0x00E7, 0x0107, 0x0109, 0x010B, 0x010D]), + ("D", [0x00D0, 0x010E, 0x0110]), + ("d", [0x00F0, 0x010F, 0x0111]), + ("E", [0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x0112, 0x0114, 0x0116, 0x0118, 0x011A]), + ("e", [0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x0113, 0x0115, 0x0117, 0x0119, 0x011B]), + ("G", [0x011C, 0x011E, 0x0120, 0x0122]), + ("g", [0x011D, 0x011F, 0x0121, 0x0123]), + ("H", [0x0124, 0x0126]), + ("h", [0x0125, 0x0127]), + ("I", [0x00CC, 0x00CD, 0x00CE, 0x00CF, 0x0128, 0x012A, 0x012C, 0x012E, 0x0130]), + ("i", [0x00EC, 0x00ED, 0x00EE, 0x00EF, 0x0129, 0x012B, 0x012D, 0x012F, 0x0131]), + ("IJ", [0x0132]), + ("ij", [0x0133]), + ("J", [0x0134]), + ("j", [0x0135]), + ("K", [0x0136]), + ("k", [0x0137, 0x0138]), + ("L", [0x0139, 0x013B, 0x013D, 0x013F, 0x0141]), + ("l", [0x013A, 0x013C, 0x013E, 0x0140, 0x0142]), + ("N", [0x00D1, 0x0143, 0x0145, 0x0147, 0x014A]), + ("n", [0x00F1, 0x0144, 0x0146, 0x0148, 0x0149, 0x014B]), + ("O", [0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D8, 0x014C, 0x014E, 0x0150]), + ("o", [0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F8, 0x014D, 0x014F, 0x0151]), + ("OE", [0x0152]), + ("oe", [0x0153]), + ("R", [0x0154, 0x0156, 0x0158]), + ("r", [0x0155, 0x0157, 0x0159]), + ("S", [0x015A, 0x015C, 0x015E, 0x0160]), + ("s", [0x015B, 0x015D, 0x015F, 0x01610, 0x017F]), + ("T", [0x0162, 0x0164, 0x0166]), + ("t", [0x0163, 0x0165, 0x0167]), + ( + "U", + [ + 0x00D9, + 0x00DA, + 0x00DB, + 0x00DC, + 0x0168, + 0x016A, + 0x016C, + 0x016E, + 0x0170, + 0x172, + ], + ), + ("u", [0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x0169, 0x016B, 0x016D, 0x016F, 0x0171]), + ("W", [0x0174]), + ("w", [0x0175]), + ("Y", [0x00DD, 0x0176, 0x0178]), + ("y", [0x00FD, 0x00FF, 0x0177]), + ("Z", [0x0179, 0x017B, 0x017D]), + ("z", [0x017A, 0x017C, 0x017E]), + ("2", [0x00B2]), # deux exposant + (" ", [0x00A0]), #   + ("", [0xB0]), # degre + ("", [0xA9]), # copyright + ("1/2", [0xBD]), # 1/2 + ] global _reptable - for repchar,codes in _corresp : - for code in codes : + for repchar, codes in _corresp: + for code in codes: _reptable[code] = repchar + _fill_reptable() -def suppression_diacritics(s) : + + +def suppression_diacritics(s): """Suppression des accents et autres marques. @param s: le texte à nettoyer. @@ -73,6 +92,6 @@ def suppression_diacritics(s) : @return: le texte nettoyé de ses marques diacritiques. @rtype: unicode """ - if isinstance(s,str) : - s = unicode(s,"utf8","replace") + if isinstance(s, str): + s = unicode(s, "utf8", "replace") return s.translate(_reptable) diff --git a/tests/api/test_api_departements.py b/tests/api/test_api_departements.py index bb7dec66..bc0b5c4a 100644 --- a/tests/api/test_api_departements.py +++ b/tests/api/test_api_departements.py @@ -20,112 +20,100 @@ Utilisation : import requests from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers -from tests.api.tools_test_api import verify_fields +from tests.api.tools_test_api import verify_fields, DEPARTEMENT_FIELDS def test_departements(api_headers): + """ " + Routes: /departements_ids, /departement, /departement//formsemestres_ids + """ - Test 'departements' - - Route : - - /departements - """ - - fields = [ - "id", - "acronym", - "description", - "visible", - "date_creation", - ] - + # --- Liste des ids r = requests.get( - API_URL + "/departements", + API_URL + "/departements_ids", headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 1 + departements_ids = r.json() + assert isinstance(departements_ids, list) + assert len(departements_ids) > 0 + assert all(isinstance(x, int) for x in departements_ids) - dept = r.json()[0] + dept_id = departements_ids[0] + # --- Infos sur un département, accès par id + r = requests.get( + f"{API_URL}/departement/{dept_id}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + dept_a = r.json() + assert verify_fields(dept_a, DEPARTEMENT_FIELDS) is True + # --- Infos sur un département, accès par acronyme4 + r = requests.get( + f"{API_URL}/departement/{dept_a['acronym']}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + dept_b = r.json() + assert dept_a == dept_b - fields_OK = verify_fields(dept, fields) - - assert fields_OK is True + # Liste des formsemestres + r = requests.get( + f"{API_URL}/departement/{dept_a['acronym']}/formsemestres_ids", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + dept_ids = r.json() + assert isinstance(dept_ids, list) + assert all(isinstance(x, int) for x in dept_ids) + assert len(dept_ids) > 0 + assert dept_id in dept_ids def test_list_etudiants(api_headers): - """ - Test 'list_etudiants' - - Routes : - - /departements//etudiants/list - - /departements//etudiants/list/ - """ - fields = { - "civilite", - "code_ine", - "code_nip", - "date_naissance", - "email", - "emailperso", - "etudid", - "nom", - "prenom", - "nomprenom", - "lieu_naissance", - "dept_naissance", - "nationalite", - "boursier", - "id", - "codepostaldomicile", - "paysdomicile", - "telephonemobile", - "typeadresse", - "domicile", - "villedomicile", - "telephone", - "fax", - "description", - } + fields = {"id", "nip", "ine", "nom", "nom_usuel", "prenom", "civilite"} r = requests.get( - API_URL + "/departements/TAPI/etudiants/list", + API_URL + "/departement/TAPI/etudiants", headers=api_headers, verify=CHECK_CERTIFICATE, ) - - etu = r.json()[0] - - fields_OK = verify_fields(etu, fields) - assert r.status_code == 200 - assert len(r.json()) == 16 - assert fields_OK is True + etud = r.json()[0] + assert verify_fields(etud, fields) is True + assert isinstance(etud["id"], int) - r = requests.get( - API_URL + "/departements/TAPI/etudiants/list/1", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - - etu = r.json()[0] - - fields_OK = verify_fields(etu, fields) - - assert r.status_code == 200 - assert len(r.json()) == 16 - assert fields_OK is True + # Vérification que chaque id, nip et ine sont uniques (EN CHANTIER) + # all_uniques = True + # d = dict() + # i = 0 + # + # for etu in r.json(): + # d[i] = [etu["id"], etu["nip"], etu["ine"]] + # i += 1 + # + # d[4][2] = 65 + # + # for i in range(len(d)-1): + # if d[i][0] == d[i+1][0]: + # all_uniques = False + # else: + # if d[i][1] == d[i+1][1]: + # all_uniques = False + # else: + # if d[i][2] == d[i+1][2]: + # all_uniques = False + # i += 1 + # + # assert all_uniques is True # liste_semestres_courant def test_semestres_courant(api_headers): - """ - Test 'liste_semestres_courant' - - Route : - - /departements//semestres_courants - """ fields = [ "titre", "gestion_semestrielle", @@ -149,22 +137,39 @@ def test_semestres_courant(api_headers): "block_moyennes", "formsemestre_id", "titre_num", + "titre_formation", "date_debut_iso", "date_fin_iso", "responsables", - "titre_court", ] - + dept_id = 1 r = requests.get( - API_URL + "/departements/TAPI/semestres_courants", + f"{API_URL}/departement/{dept_id}", headers=api_headers, verify=CHECK_CERTIFICATE, ) - - sem = r.json()[0] - - fields_OK = verify_fields(sem, fields) - assert r.status_code == 200 - assert len(r.json()) == 1 - assert fields_OK is True + dept = r.json() + assert dept["id"] == dept_id + # Accès via acronyme + r = requests.get( + f"{API_URL}/departement/{dept['acronym']}/formsemestres_courants", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + result_a = r.json() + assert isinstance(result_a, list) # liste de formsemestres + assert len(result_a) > 0 + sem = result_a[0] + assert verify_fields(sem, fields) is True + + # accès via dept_id + r = requests.get( + f"{API_URL}/departement/{dept['id']}/formsemestres_courants", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + result_b = r.json() + assert result_a == result_b diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py index db53475d..b641c757 100644 --- a/tests/api/test_api_etudiants.py +++ b/tests/api/test_api_etudiants.py @@ -16,30 +16,19 @@ Utilisation : Lancer : pytest tests/api/test_api_etudiants.py """ -from random import randint import requests from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers from tests.api.tools_test_api import verify_fields +from tests.api.tools_test_api import ETUD_FIELDS, FSEM_FIELDS + -# etudiants_courant def test_etudiants_courant(api_headers): """ - Test 'etudiants_courant' - - Routes : - - /etudiants/courant - - /etudiants/courant/long + Route: /etudiants/courant """ - fields = [ - "id", - "nip", - "nom", - "nom_usuel", - "prenom", - "civilite", - ] + fields = {"id", "nip", "nom", "prenom", "civilite"} r = requests.get( API_URL + "/etudiants/courant", @@ -48,43 +37,26 @@ def test_etudiants_courant(api_headers): ) assert r.status_code == 200 etudiants = r.json() - assert len(etudiants) == 16 # XXX HARDCODED + assert len(etudiants) > 0 etud = etudiants[-1] + assert verify_fields(etud, fields) is True + assert isinstance(etud["id"], int) + assert isinstance(etud["nip"], str) + assert isinstance(etud["nom"], str) + assert isinstance(etud["prenom"], str) + assert isinstance(etud["civilite"], str) - fields_ok = verify_fields(etud, fields) + all_unique = True + list_ids = [etu["id"] for etu in etudiants] - assert fields_ok is True + for i in range(len(etudiants) - 1): + if etudiants.count(list_ids[i]) > 1: + all_unique = False + + assert all_unique is True ########## Version long ################ - - fields_long = [ - "civilite", - "code_ine", - "code_nip", - "date_naissance", - "email", - "emailperso", - "etudid", - "nom", - "prenom", - "nomprenom", - "lieu_naissance", - "dept_naissance", - "nationalite", - "boursier", - "id", - "codepostaldomicile", - "paysdomicile", - "telephonemobile", - "typeadresse", - "domicile", - "villedomicile", - "telephone", - "fax", - "description", - ] - r = requests.get( API_URL + "/etudiants/courant/long", headers=api_headers, @@ -95,46 +67,13 @@ def test_etudiants_courant(api_headers): assert len(etudiants) == 16 # HARDCODED etud = etudiants[-1] - fields_ok = verify_fields(etud, fields_long) - - assert fields_ok is True + assert verify_fields(etud, ETUD_FIELDS) is True def test_etudiant(api_headers): """ - Test 'etudiant' - - Routes : - - /etudiant/etudid/ - - /etudiant/nip/ - - /etudiant/ine/ + Route: """ - fields = [ - "civilite", - "code_ine", - "code_nip", - "date_naissance", - "email", - "emailperso", - "etudid", - "nom", - "prenom", - "nomprenom", - "lieu_naissance", - "dept_naissance", - "nationalite", - "boursier", - "id", - "domicile", - "villedomicile", - "telephone", - "fax", - "description", - "codepostaldomicile", - "paysdomicile", - "telephonemobile", - "typeadresse", - ] ######### Test etudid ######### r = requests.get( @@ -144,10 +83,8 @@ def test_etudiant(api_headers): ) assert r.status_code == 200 etud = r.json() - assert len(etud) == 24 # ? HARDCODED - fields_ok = verify_fields(etud, fields) - assert fields_ok is True + assert verify_fields(etud, ETUD_FIELDS) is True ######### Test code nip ######### @@ -158,8 +95,7 @@ def test_etudiant(api_headers): ) assert r.status_code == 200 etud = r.json() - assert len(etud) == 24 - fields_ok = verify_fields(etud, fields) + fields_ok = verify_fields(etud, ETUD_FIELDS) assert fields_ok is True ######### Test code ine ######### @@ -172,47 +108,14 @@ def test_etudiant(api_headers): assert r.status_code == 200 etud = r.json() assert len(etud) == 24 - fields_ok = verify_fields(etud, fields) + fields_ok = verify_fields(etud, ETUD_FIELDS) assert fields_ok is True def test_etudiant_formsemestres(api_headers): """ - Test 'etudiant_formsemestres' - - Routes : - - /etudiant/etudid//formsemestres - - /etudiant/nip//formsemestres - - /etudiant/ine//formsemestres + Route: /etudiant/etudid//formsemestres """ - fields = [ - "date_fin", - "resp_can_edit", - "dept_id", - "etat", - "resp_can_change_ens", - "id", - "modalite", - "ens_can_edit_eval", - "formation_id", - "gestion_compensation", - "elt_sem_apo", - "semestre_id", - "bul_hide_xml", - "elt_annee_apo", - "titre", - "block_moyennes", - "scodoc7_id", - "date_debut", - "gestion_semestrielle", - "bul_bgcolor", - "formsemestre_id", - "titre_num", - "date_debut_iso", - "date_fin_iso", - "responsables", - "titre_court", - ] ######### Test etudid ######### @@ -226,9 +129,7 @@ def test_etudiant_formsemestres(api_headers): assert len(formsemestres) == 1 formsemestre = formsemestres[0] - - fields_ok = verify_fields(formsemestre, fields) - assert fields_ok is True + assert verify_fields(formsemestre, FSEM_FIELDS) is True ######### Test code nip ######### r = requests.get( @@ -241,9 +142,7 @@ def test_etudiant_formsemestres(api_headers): assert len(formsemestres) == 1 formsemestre = formsemestres[0] - - fields_ok = verify_fields(formsemestre, fields) - assert fields_ok is True + assert verify_fields(formsemestre, FSEM_FIELDS) is True ######### Test code ine ######### r = requests.get( @@ -256,29 +155,13 @@ def test_etudiant_formsemestres(api_headers): assert len(formsemestres) == 1 formsemestre = formsemestres[0] - - fields_ok = verify_fields(formsemestre, fields) - assert fields_ok is True + assert verify_fields(formsemestre, FSEM_FIELDS) is True def test_etudiant_bulletin_semestre(api_headers): """ - Test 'etudiant_bulletin_semestre' - - Routes : - - /etudiant/etudid//formsemestre//bulletin - - /etudiant/nip//formsemestre//bulletin - - /etudiant/ine//formsemestre//bulletin - - /etudiant/etudid//formsemestre//bulletin/pdf - - /etudiant/nip//formsemestre//bulletin/pdf - - /etudiant/ine//formsemestre//bulletin/pdf - - /etudiant/etudid//formsemestre//bulletin/short - - /etudiant/nip//formsemestre//bulletin/short - - /etudiant/ine//formsemestre//bulletin/short - - /etudiant/etudid//formsemestre//bulletin/short/pdf - - /etudiant/nip//formsemestre//bulletin/short/pdf - - /etudiant/ine//formsemestre//bulletin/short/pdf - """ + Route: /etudiant/etudid//formsemestre//bulletin + """ ######### Test etudid ######### r = requests.get( @@ -311,15 +194,19 @@ def test_etudiant_bulletin_semestre(api_headers): bul = r.json() assert len(bul) == 13 # HARDCODED + ### --- Test étudiant inexistant + r = requests.get( + API_URL + "/etudiant/ine/189919919119191/formsemestre/1/bulletin", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 404 + def test_etudiant_groups(api_headers): """ - Test 'etudiant_groups' - - Routes : - - /etudiant/etudid//formsemestre//groups - - /etudiant/nip//formsemestre//groups - - /etudiant/ine//formsemestre//groups + Route: + /etudiant/etudid//formsemestre//groups """ fields = [ "partition_id", diff --git a/tests/api/test_api_formations.py b/tests/api/test_api_formations.py index 3f65ac6e..b61037da 100644 --- a/tests/api/test_api_formations.py +++ b/tests/api/test_api_formations.py @@ -21,15 +21,12 @@ import requests from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers from tests.api.tools_test_api import verify_fields +from tests.api.tools_test_api import FORMATION_FIELDS, MODIMPL_FIELDS -# formations def test_formations_ids(api_headers): """ - Test 'formations_ids' - - Routes : - - /formations_ids + Route: /formations_ids """ r = requests.get( API_URL + "/formations_ids", @@ -44,150 +41,67 @@ def test_formations_ids(api_headers): assert all(isinstance(x, int) for x in formations_ids) -# formations_by_id def test_formations_by_id(api_headers): """ - Test 'formations_by_id' - - Routes : - - /formations/ + Route: /formation/ """ - fields = [ - "id", - "acronyme", - "titre_officiel", - "formation_code", - "code_specialite", - "dept_id", - "titre", - "version", - "type_parcours", - "referentiel_competence_id", - "formation_id", - ] - r = requests.get( - API_URL + "/formations/1", + API_URL + "/formation/1", headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 formation = r.json() - - fields_ok = verify_fields(formation, fields) - assert fields_ok is True + assert verify_fields(formation, FORMATION_FIELDS) is True # TODO tester le contenu de certains champs def test_formation_export(api_headers): """ - Test 'formation_export_by_formation_id' - - Routes : - - /formations/formation_export/ - - /formations/formation_export//with_ids + Route: /formation/formation_export/ """ - fields = [ - "id", - "acronyme", - "titre_officiel", - "formation_code", - "code_specialite", - "dept_id", - "titre", - "version", - "type_parcours", - "referentiel_competence_id", - "formation_id", - "ue", - ] r = requests.get( - API_URL + "/formations/formation_export/1", + API_URL + "/formation/formation_export/1", headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - export_formation = r.json() - - fields_ok = verify_fields(export_formation, fields) - assert fields_ok is True + assert verify_fields(export_formation, FORMATION_FIELDS) is True # TODO tester le contenu de certains champs +# TODO +# def test_formsemestre_apo(api_headers): +# r = requests.get( +# API_URL + "/formation/apo/", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 + + def test_moduleimpl(api_headers): """ - Test 'moduleimpl' - - Route : - - /formations/moduleimpl/ + Route: /formation/moduleimpl/ """ - fields = [ - "id", - "formsemestre_id", - "computation_expr", - "module_id", - "responsable_id", - "moduleimpl_id", - "ens", - "module", - ] - r = requests.get( - API_URL + "/formations/moduleimpl/1", + API_URL + "/formation/moduleimpl/1", headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 moduleimpl = r.json() - - fields_ok = verify_fields(moduleimpl, fields) - assert fields_ok is True + assert verify_fields(moduleimpl, MODIMPL_FIELDS) is True # TODO tester le contenu de certains champs -def test_moduleimpls_sem(api_headers): - """ - Test 'moduleimpls_sem' - - Route : - - /formations/moduleimpl/formsemestre//list - """ - fields = [ - "id", - "formsemestre_id", - "computation_expr", - "module_id", - "responsable_id", - "moduleimpl_id", - "ens", - "module", - "moduleimpl_id", - "ens", - ] - r = requests.get( - API_URL + "/formations/moduleimpl/formsemestre/1/list", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - moduleimpls = r.json() - moduleimpl = moduleimpls[0] - - fields_ok = verify_fields(moduleimpl, fields) - assert len(moduleimpls) == 21 # XXX HARDCODED ! - assert fields_ok is True - - def test_referentiel_competences(api_headers): """ - Test 'referentiel_competences' - - Route : - - /formations//referentiel_competences + Route: "/formation//referentiel_competences", """ r = requests.get( - API_URL + "/formations/1/referentiel_competences", + API_URL + "/formation/1/referentiel_competences", headers=api_headers, verify=CHECK_CERTIFICATE, ) diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py index 568e7e8d..57d79772 100644 --- a/tests/api/test_api_formsemestre.py +++ b/tests/api/test_api_formsemestre.py @@ -18,17 +18,16 @@ Utilisation : """ import requests +from app.api.formsemestres import formsemestre from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers -from tests.api.tools_test_api import verify_fields +from tests.api.tools_test_api import MODIMPL_FIELDS, verify_fields +from tests.api.tools_test_api import FSEM_FIELDS, UE_FIELDS, MODULE_FIELDS def test_formsemestre(api_headers): """ - Test 'formsemestre' - - Route : - - /formsemestre/ + Route: /formsemestre/ """ r = requests.get( API_URL + "/formsemestre/1", @@ -36,64 +35,48 @@ def test_formsemestre(api_headers): verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - formsemestre = r.json() - - fields = [ - "date_fin", - "resp_can_edit", - "dept_id", - "etat", - "resp_can_change_ens", - "id", - "modalite", - "ens_can_edit_eval", - "formation_id", - "gestion_compensation", - "elt_sem_apo", - "semestre_id", - "bul_hide_xml", - "elt_annee_apo", - "titre", - "block_moyennes", - "scodoc7_id", - "date_debut", - "gestion_semestrielle", - "bul_bgcolor", - "formsemestre_id", - "titre_num", - "date_debut_iso", - "date_fin_iso", - "responsables", - ] - - fields_ok = verify_fields(formsemestre, fields) - - assert fields_ok is True + assert verify_fields(formsemestre, FSEM_FIELDS) -# TODO -# def test_formsemestre_apo(api_headers): -# """ -# Test 'formsemestre_apo' -# -# Route : -# - /formsemestre/apo/ -# """ -# r = requests.get( -# API_URL + "/formations/apo/", -# headers=api_headers, -# verify=CHECK_CERTIFICATE, -# ) -# assert r.status_code == 200 +def test_etudiant_bulletin(api_headers): + """ + Route: + """ + formsemestre_id = 1 + r = requests.get( + f"{API_URL}/etudiant/etudid/1/formsemestre/{formsemestre_id}/bulletin", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + bull_a = r.json() + + r = requests.get( + f"{API_URL}/etudiant/nip/1/formsemestre/{formsemestre_id}/bulletin", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + bull_b = r.json() + + r = requests.get( + f"{API_URL}/etudiant/ine/1/formsemestre/{formsemestre_id}/bulletin", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + bull_c = r.json() + # elimine les dates de publication pour comparer les autres champs + del bull_a["date"] + del bull_b["date"] + del bull_c["date"] + assert bull_a == bull_b == bull_c def test_bulletins(api_headers): """ - Test 'bulletins' - - Route : - - /formsemestre//bulletins + Route: """ r = requests.get( API_URL + "/formsemestre/1/bulletins", @@ -105,12 +88,6 @@ def test_bulletins(api_headers): # # jury # def test_jury(): -# """ -# Test 'jury' -# -# Route : -# - /formsemestre//jury -# """ # r = requests.get( # API_URL + "/formsemestre/1/jury", # headers=api_headers, @@ -118,70 +95,11 @@ def test_bulletins(api_headers): # ) # assert r.status_code == 200 -# TODO A revoir -def test_programme(api_headers): + +def test_formsemestre_programme(api_headers): """ - Test 'programme' - - Route : - - /formsemestre//programme + Route: /formsemestre/1/programme """ - ue_fields = [ - "semestre_idx", - "type", - "formation_id", - "ue_code", - "id", - "ects", - "acronyme", - "is_external", - "numero", - "code_apogee", - "titre", - "coefficient", - "color", - "ue_id", - ] - - ressource_fields = [ - "heures_tp", - "code_apogee", - "titre", - "coefficient", - "module_type", - "id", - "ects", - "abbrev", - "ue_id", - "code", - "formation_id", - "heures_cours", - "matiere_id", - "heures_td", - "semestre_id", - "numero", - "module_id", - ] - - sae_fields = [ - "heures_tp", - "code_apogee", - "titre", - "coefficient", - "module_type", - "id", - "ects", - "abbrev", - "ue_id", - "code", - "formation_id", - "heures_cours", - "matiere_id", - "heures_td", - "semestre_id", - "numero", - "module_id", - ] r = requests.get( API_URL + "/formsemestre/1/programme", @@ -189,16 +107,22 @@ def test_programme(api_headers): verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 3 + prog = r.json() + assert isinstance(prog, dict) + assert "ues" in prog + assert "modules" in prog + assert "ressources" in prog + assert "saes" in prog + assert isinstance(prog["ues"], list) + assert isinstance(prog["modules"], list) + ue = prog["ues"][0] + modules = prog["modules"] + # Il y a toujours au moins une SAE et une ressources dans notre base de test + ressource = prog["ressources"][0] + sae = prog["saes"][0] - ue = r.json()["ues"][0] - ressource = r.json()["ressources"][0] - sae = r.json()["saes"][0] - - fields_ue_OK = verify_fields(ue, ue_fields) - fields_ressource_OK = verify_fields(ressource, ressource_fields) - fields_sae_OK = verify_fields(sae, sae_fields) - - assert fields_ue_OK is True - assert fields_ressource_OK is True - assert fields_sae_OK is True + assert verify_fields(ue, UE_FIELDS) + if len(modules) > 1: + assert verify_fields(modules[0], MODIMPL_FIELDS) + assert verify_fields(ressource, MODIMPL_FIELDS) + assert verify_fields(sae, MODIMPL_FIELDS) diff --git a/tests/api/test_api_permissions.py b/tests/api/test_api_permissions.py index 4a9f2952..2ba8a23d 100644 --- a/tests/api/test_api_permissions.py +++ b/tests/api/test_api_permissions.py @@ -43,6 +43,8 @@ def test_permissions(api_headers): # "date_debut": # "date_fin": "dept": "TAPI", + "dept_ident": "TAPI", + "dept_id": 1, "etape_apo": "???", "etat": "I", "evaluation_id": 1, diff --git a/tests/api/tools_test_api.py b/tests/api/tools_test_api.py index d0a224c5..5aefb87b 100644 --- a/tests/api/tools_test_api.py +++ b/tests/api/tools_test_api.py @@ -2,16 +2,138 @@ """ -def verify_fields(json_response: dict, fields: set) -> bool: +def verify_fields(json_response: dict, expected_fields: set) -> bool: """ Vérifie si les champs attendu de la réponse json sont présents json_response : la réponse de la requête - fields : ensemble des champs à vérifier + expected_fields : ensemble des champs à vérifier Retourne True ou False """ - for field in json_response: - if field not in fields: - return False - return True + return all(field in json_response for field in expected_fields) + + +DEPARTEMENT_FIELDS = [ + "id", + "acronym", + "description", + "visible", + "date_creation", +] + +ETUD_FIELDS = { + "boursier", + "civilite", + "code_ine", + "code_nip", + "codepostaldomicile", + "date_naissance", + "dept_naissance", + "description", + "domicile", + "email", + "emailperso", + "etudid", + "id", + "lieu_naissance", + "nationalite", + "nom", + "nomprenom", + "paysdomicile", + "prenom", + "telephone", + "telephonemobile", + "typeadresse", + "villedomicile", +} + +FORMATION_FIELDS = { + "id", + "acronyme", + "titre_officiel", + "formation_code", + "code_specialite", + "dept_id", + "titre", + "version", + "type_parcours", + "referentiel_competence_id", + "formation_id", +} + +FSEM_FIELDS = { + "block_moyennes", + "bul_bgcolor", + "bul_hide_xml", + "date_debut_iso", + "date_debut", + "date_fin_iso", + "date_fin", + "dept_id", + "elt_annee_apo", + "elt_sem_apo", + "ens_can_edit_eval", + "etat", + "formation_id", + "formsemestre_id", + "gestion_compensation", + "gestion_semestrielle", + "id", + "modalite", + "resp_can_change_ens", + "resp_can_edit", + "responsables", + "semestre_id", + "titre_formation", + "titre_num", + "titre", +} + +MODIMPL_FIELDS = { + "id", + "formsemestre_id", + "computation_expr", + "module_id", + "responsable_id", + "moduleimpl_id", + "ens", + "module", +} + +MODULE_FIELDS = { + "heures_tp", + "code_apogee", + "titre", + "coefficient", + "module_type", + "id", + "ects", + "abbrev", + "ue_id", + "code", + "formation_id", + "heures_cours", + "matiere_id", + "heures_td", + "semestre_id", + "numero", + "module_id", +} + +UE_FIELDS = { + "semestre_idx", + "type", + "formation_id", + "ue_code", + "id", + "ects", + "acronyme", + "is_external", + "numero", + "code_apogee", + "titre", + "coefficient", + "color", + "ue_id", +} diff --git a/tests/unit/test_export_xml.py b/tests/unit/test_export_xml.py index c220638c..c14f4c1a 100644 --- a/tests/unit/test_export_xml.py +++ b/tests/unit/test_export_xml.py @@ -138,4 +138,4 @@ def test_export_xml(test_client): """ - assert xmls_compare(table_xml, expected_result) \ No newline at end of file + assert xmls_compare(table_xml, expected_result) From c4fce43b1a311234ee81b1ecfd4b89a660b26889 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Wed, 11 May 2022 16:17:43 +0200 Subject: [PATCH 08/60] tests departement --- app/api/tools.py | 2 +- tests/api/test_api_departements.py | 37 ++++-------------------------- tests/api/tools_test_api.py | 29 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/app/api/tools.py b/app/api/tools.py index e03d8334..72e98a3c 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -1,6 +1,6 @@ from app import models - +### TODO Faire en sorte de renvoyer l'étudiant qui a son inscription la plus récente def get_etud_from_etudid_or_nip_or_ine( etudid=None, nip=None, ine=None ) -> models.Identite: diff --git a/tests/api/test_api_departements.py b/tests/api/test_api_departements.py index d0337530..bd68fc03 100644 --- a/tests/api/test_api_departements.py +++ b/tests/api/test_api_departements.py @@ -23,6 +23,7 @@ from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers from tests.api.tools_test_api import ( verify_fields, DEPARTEMENT_FIELDS, + FORMSEMESTRE_FIELDS, verify_occurences_ids_etus, ) @@ -194,34 +195,6 @@ def test_list_etudiants(api_headers): # liste_semestres_courant def test_semestres_courant(api_headers): - fields = [ - "titre", - "gestion_semestrielle", - "scodoc7_id", - "date_debut", - "bul_bgcolor", - "date_fin", - "resp_can_edit", - "dept_id", - "etat", - "resp_can_change_ens", - "id", - "modalite", - "ens_can_edit_eval", - "formation_id", - "gestion_compensation", - "elt_sem_apo", - "semestre_id", - "bul_hide_xml", - "elt_annee_apo", - "block_moyennes", - "formsemestre_id", - "titre_num", - "titre_formation", - "date_debut_iso", - "date_fin_iso", - "responsables", - ] dept_id = 1 r = requests.get( f"{API_URL}/departement/{dept_id}", @@ -240,10 +213,6 @@ def test_semestres_courant(api_headers): ) assert r.status_code == 200 result_a = r.json() - assert isinstance(result_a, list) # liste de formsemestres - assert len(result_a) > 0 - sem = result_a[0] - assert verify_fields(sem, fields) is True # accès via dept_id r = requests.get( @@ -254,3 +223,7 @@ def test_semestres_courant(api_headers): assert r.status_code == 200 result_b = r.json() assert result_a == result_b + assert isinstance(result_a, list) # liste de formsemestres + assert len(result_a) > 0 + sem = result_a[0] + assert verify_fields(sem, FORMSEMESTRE_FIELDS) is True diff --git a/tests/api/tools_test_api.py b/tests/api/tools_test_api.py index 1d0c429d..40d44381 100644 --- a/tests/api/tools_test_api.py +++ b/tests/api/tools_test_api.py @@ -82,6 +82,35 @@ FORMATION_FIELDS = { "formation_id", } +FORMSEMESTRE_FIELDS = [ + "titre", + "gestion_semestrielle", + "scodoc7_id", + "date_debut", + "bul_bgcolor", + "date_fin", + "resp_can_edit", + "dept_id", + "etat", + "resp_can_change_ens", + "id", + "modalite", + "ens_can_edit_eval", + "formation_id", + "gestion_compensation", + "elt_sem_apo", + "semestre_id", + "bul_hide_xml", + "elt_annee_apo", + "block_moyennes", + "formsemestre_id", + "titre_num", + "titre_formation", + "date_debut_iso", + "date_fin_iso", + "responsables", +] + FSEM_FIELDS = { "block_moyennes", "bul_bgcolor", From 43cbb8537c0c6f549ccf11bc2096fbb2f0062b4e Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Thu, 12 May 2022 16:14:36 +0200 Subject: [PATCH 09/60] =?UTF-8?q?premier=20jet=20du=20probl=C3=A8me=20d'?= =?UTF-8?q?=C3=A9tudiants=20changeant=20de=20d=C3=A9partement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/etudiants.py | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index a41634f9..82975ad7 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -1,4 +1,5 @@ #################################################### Etudiants ######################################################## +import json from flask import jsonify @@ -14,6 +15,50 @@ from app.scodoc import sco_groups from app.scodoc.sco_permissions import Permission +@bp.route("/etudiant/test/etudid/", methods=["GET"]) +@bp.route("/etudiant/test/nip/", methods=["GET"]) +@bp.route("/etudiant/test/ine/", methods=["GET"]) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def etudiant_test(etudid: int = None, nip: int = None, ine: int = None): + + # Récupération de tous les étudiants qui portent le même code_nip + etus = models.Identite.query.filter_by(code_nip=nip).all() + + # Mise en place d'une liste des différents etudid de l'étudiant + list_id_etu = [] + for etu in etus: + list_id_etu.append(etu.id) + + list_res = [] + # Pour chaque etudid de l'étudiant + for i in range(len(list_id_etu)): + etudid = list_id_etu[i] + + # on va chercher les formsemestres auquel il est inscrit + formsemestres = models.FormSemestre.query.filter( + models.FormSemestreInscription.etudid == etudid, + models.FormSemestreInscription.formsemestre_id == models.FormSemestre.id, + ).order_by(models.FormSemestre.date_debut) + + # récupération du dernier en date + res = formsemestres[-1] + list_res.append(res) + + # trie les formsemestres trouvé de l'étudiants par date + def sort_by_key(list): + return list.date_debut + + list_res_sorted = sorted(list_res, key=sort_by_key) + + # récupération du dernier formsemestre en date tout départements confondu + res = list_res_sorted[-1].to_dict() + + return jsonify(res) + + # return error_response(504, message="not implemented") + + @bp.route("/etudiants/courant", defaults={"long": False}) @bp.route("/etudiants/courant/long", defaults={"long": True}) @token_auth.login_required From 9905286168799acf8e9eea0416339bd5bc93059a Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Fri, 13 May 2022 14:37:02 +0200 Subject: [PATCH 10/60] =?UTF-8?q?ajout=20des=20nouvelles=20routes=20pour?= =?UTF-8?q?=20r=C3=A9cup=C3=A9rer=20la=20liste=20des=20=C3=A9tudiants=20d'?= =?UTF-8?q?un=20semestre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/formsemestres.py | 48 +++++++++++++++++++++++++++++++++ tests/api/test_api_etudiants.py | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 577b2dbc..eb1a47e9 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -7,6 +7,7 @@ from app.api import bp from app.api.auth import token_auth, token_permission_required from app.models import Departement, FormSemestre, FormSemestreEtape from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json +from app.scodoc.sco_groups import get_etud_groups from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import ModuleType @@ -412,3 +413,50 @@ def formsemestre_programme(formsemestre_id: int): "modules": m_list[ModuleType.STANDARD], } ) + + +@bp.route( + "/formsemestre//etudiants/inscrits", + methods=["GET"], + defaults={"etat": "I"}, +) +@bp.route( + "/formsemestre//etudiants/demissionnaires", + methods=["GET"], + defaults={"etat": "D"}, +) +@bp.route( + "/formsemestre//etudiants/defaillants", + methods=["GET"], + defaults={"etat": "DEF"}, +) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def formsemestre_etudiants(formsemestre_id: int, etat: str): + """ + Retourne la liste des étudiants d'un semestre + + formsemestre_id : l'id d'un semestre + """ + # fonction to use : sco_groups.get_etud_groups + + formsemestre = models.FormSemestre.query.filter_by( + id=formsemestre_id + ).first_or_404() + + # Récupération des étudiants du formsemestre + etuds = [etu.to_dict_short() for etu in formsemestre.etuds] + + res = [] + # Trie des étudiants suivant leur état d'inscription voulu + for etu in etuds: + formsemestre_inscription = models.FormSemestreInscription.query.filter_by( + formsemestre_id=formsemestre_id, etudid=etu["id"] + ).first_or_404() + if formsemestre_inscription.etat == etat: + res.append(etu) + # Ajout des groups de chaques étudiants + for etu in res: + etu["groups"] = get_etud_groups(etu["id"], formsemestre_id) + + return jsonify(res) diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py index 4e38ecb1..c0f0470f 100644 --- a/tests/api/test_api_etudiants.py +++ b/tests/api/test_api_etudiants.py @@ -101,7 +101,7 @@ def test_etudiant(api_headers): ) assert r.status_code == 200 etud = r.json() - assert len(etud) == 25 + assert len(etud) == 26 fields_ok = verify_fields(etud, ETUD_FIELDS) assert fields_ok is True From 8637c81f788a274a2a3211ea6b2952a92f7c1669 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Fri, 13 May 2022 15:56:54 +0200 Subject: [PATCH 11/60] ajustement de la route de base pour formsemestre_etudiants --- app/api/formsemestres.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index eb1a47e9..d4444b94 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -416,7 +416,7 @@ def formsemestre_programme(formsemestre_id: int): @bp.route( - "/formsemestre//etudiants/inscrits", + "/formsemestre//etudiants", methods=["GET"], defaults={"etat": "I"}, ) From 53f3ec72945634cc02d884102e800288d7a62b52 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Mon, 16 May 2022 15:14:51 +0200 Subject: [PATCH 12/60] correction des pluriels dans les routes --- app/api/etudiants.py | 53 +++++++++++++++++++++++++++++++++++-------- app/api/partitions.py | 4 ++-- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index a193553f..76dd2347 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -20,11 +20,11 @@ from app.scodoc import sco_groups from app.scodoc.sco_permissions import Permission -@bp.route("/etudiants/courant", defaults={"long": False}) -@bp.route("/etudiants/courant/long", defaults={"long": True}) +@bp.route("/etudiants/courants", defaults={"long": False}) +@bp.route("/etudiants/courants/long", defaults={"long": True}) @token_auth.login_required @token_permission_required(Permission.APIView) -def etudiants_courant(long=False): +def etudiants_courants(long=False): """ Liste des étudiants inscrits dans un formsemestre actuellement en cours. @@ -236,41 +236,74 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) @bp.route( "/etudiant/etudid//formsemestre//bulletin", methods=["GET"], - defaults={"version": "long"}, + defaults={"version": "long", "pdf": False}, ) @bp.route( "/etudiant/nip//formsemestre//bulletin", methods=["GET"], - defaults={"version": "long"}, + defaults={"version": "long", "pdf": False}, ) @bp.route( "/etudiant/ine//formsemestre//bulletin", methods=["GET"], - defaults={"version": "long"}, + defaults={"version": "long", "pdf": False}, ) +# Version PDF non fonctionnelle +# @bp.route( +# "/etudiant/etudid//formsemestre//bulletin/pdf", +# methods=["GET"], +# defaults={"version": "long", "pdf": True}, +# ) +# @bp.route( +# "/etudiant/nip//formsemestre//bulletin/pdf", +# methods=["GET"], +# defaults={"version": "long", "pdf": True}, +# ) +# @bp.route( +# "/etudiant/ine//formsemestre//bulletin/pdf", +# methods=["GET"], +# defaults={"version": "long", "pdf": True}, +# ) @bp.route( "/etudiant/etudid//formsemestre//bulletin/short", methods=["GET"], - defaults={"version": "short"}, + defaults={"version": "short", "pdf": False}, ) @bp.route( "/etudiant/nip//formsemestre//bulletin/short", methods=["GET"], - defaults={"version": "short"}, + defaults={"version": "short", "pdf": False}, ) @bp.route( "/etudiant/ine//formsemestre//bulletin/short", methods=["GET"], - defaults={"version": "short"}, + defaults={"version": "short", "pdf": False}, ) +# Version PDF non fonctionnelle +# @bp.route( +# "/etudiant/etudid//formsemestre//bulletin/short/pdf", +# methods=["GET"], +# defaults={"version": "short", "pdf": True}, +# ) +# @bp.route( +# "/etudiant/nip//formsemestre//bulletin/short/pdf", +# methods=["GET"], +# defaults={"version": "short", "pdf": True}, +# ) +# @bp.route( +# "/etudiant/ine//formsemestre//bulletin/short/pdf", +# methods=["GET"], +# defaults={"version": "short", "pdf": True}, +# ) @token_auth.login_required @token_permission_required(Permission.APIView) -def etudiant_bulletin_semestre( +def etudiant_bulletin_semestre( # XXX TODO Ajouter la possibilité de retourner en version pdf formsemestre_id, etudid: int = None, nip: str = None, ine: str = None, version="long", + # pdf: bool = False, ): """ Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné diff --git a/app/api/partitions.py b/app/api/partitions.py index 1ee74c46..8ab9a4da 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -52,8 +52,8 @@ def partition(formsemestre_id: int): return jsonify(data) -@bp.route("/partitions/groups/", methods=["GET"]) -@bp.route("/partitions/groups//etat/", methods=["GET"]) +@bp.route("/partition/group/", methods=["GET"]) +@bp.route("/partition/group//etat/", methods=["GET"]) @token_auth.login_required @token_permission_required(Permission.APIView) def etud_in_group(group_id: int, etat=None): From 8c481bd0d114d3b29a037dd71a62b8b438149423 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Mon, 16 May 2022 15:15:33 +0200 Subject: [PATCH 13/60] ajout du code de abs_groupe_etat --- app/api/absences.py | 95 +++++++++++++++++++++++++++++---------------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/app/api/absences.py b/app/api/absences.py index 51c4673e..16a3e751 100644 --- a/app/api/absences.py +++ b/app/api/absences.py @@ -9,6 +9,7 @@ from app.models import Identite from app.scodoc import notesdb as ndb from app.scodoc import sco_abs +from app.scodoc.sco_groups import get_group_members from app.scodoc.sco_permissions import Permission @@ -106,40 +107,68 @@ def absences_just(etudid: int = None): return jsonify(abs_just) -# XXX TODO INACHEVEE -# @bp.route( -# "/absences/abs_group_etat/", -# methods=["GET"], -# ) -# @bp.route( -# "/absences/abs_group_etat/group_id//date_debut//date_fin/", -# methods=["GET"], -# ) -# @token_auth.login_required -# @token_permission_required(Permission.APIView) -# def abs_groupe_etat( # XXX A REVOIR XXX -# group_id: int, date_debut, date_fin, with_boursier=True, format="html" -# ): -# """ -# Liste des absences d'un ou plusieurs groupes entre deux dates -# """ -# return error_response(501, message="Not implemented") +@bp.route( + "/absences/abs_group_etat/", + methods=["GET"], +) +@bp.route( + "/absences/abs_group_etat/group_id//date_debut//date_fin/", + methods=["GET"], +) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def abs_groupe_etat(group_id: int, date_debut=None, date_fin=None): + """ + Liste des absences d'un groupe (possibilité de choisir entre deux dates) -# # Fonction utilisée : app.scodoc.sco_groups.get_group_members() et app.scodoc.sco_abs.list_abs_date() + group_id = l'id du groupe + date_debut = None par défaut, sinon la date ISO du début de notre filtre + date_fin = None par défaut, sinon la date ISO de la fin de notre filtre -# try: -# # Utilisation de la fonction get_group_members -# members = get_group_members(group_id) -# except ValueError: -# return error_response( -# 404, message="La requête ne peut être traitée en l’état actuel" -# ) + Exemple de résultat : + [ + { + "etudid": 1, + "list_abs": [] + }, + { + "etudid": 2, + "list_abs": [ + { + "jour": "Fri, 15 Apr 2022 00:00:00 GMT", + "matin": true, + "estabs": true, + "estjust": true, + "description": "", + "begin": "2022-04-15 08:00:00", + "end": "2022-04-15 11:59:59" + }, + { + "jour": "Fri, 15 Apr 2022 00:00:00 GMT", + "matin": false, + "estabs": true, + "estjust": false, + "description": "", + "begin": "2022-04-15 12:00:00", + "end": "2022-04-15 17:59:59" + }, + ] + }, + ... + ] + """ + # Fonction utilisée : app.scodoc.sco_groups.get_group_members() et app.scodoc.sco_abs.list_abs_date() -# data = [] -# # Filtre entre les deux dates renseignées -# for member in members: -# abs = sco_abs.list_abs_date(member.id, date_debut, date_fin) -# data.append(abs) + # Utilisation de la fonction get_group_members + members = get_group_members(group_id) -# # return jsonify(data) # XXX TODO faire en sorte de pouvoir renvoyer sa (ex to_dict() dans absences) -# return error_response(501, message="Not implemented") + data = [] + # Filtre entre les deux dates renseignées + for member in members: + abs = { + "etudid": member["etudid"], + "list_abs": sco_abs.list_abs_date(member["etudid"], date_debut, date_fin), + } + data.append(abs) + + return jsonify(data) From ffcc0897f2bbf4e5f7c4f6fbeaa99c6a18a0180e Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Mon, 16 May 2022 15:45:11 +0200 Subject: [PATCH 14/60] =?UTF-8?q?mise=20=C3=A0=20jour=20de=20la=20fonction?= =?UTF-8?q?=20renvoyant=20un=20=C3=A9tudiant=20suivant=20son=20etudid,=20n?= =?UTF-8?q?ip=20ou=20ine=20pour=20renvoyer=20la=20derni=C3=A8re=20instance?= =?UTF-8?q?=20de=20celui=20si?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/etudiants.py | 23 ++--------------------- app/api/tools.py | 38 ++++++++++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 76dd2347..e53056f1 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -14,6 +14,7 @@ import app from app.api import bp from app.api.errors import error_response from app.api.auth import token_auth, token_permission_required +from app.api.tools import get_last_instance_etud_from_etudid_or_nip_or_ine from app.models import Departement, FormSemestreInscription, FormSemestre, Identite from app.scodoc import sco_bulletins from app.scodoc import sco_groups @@ -104,27 +105,7 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None): "description": "" } """ - if etudid is not None: - etud = Identite.query.get(etudid) - else: - if nip is not None: - query = Identite.query.filter_by(code_nip=nip) - elif ine is not None: - query = Identite.query.filter_by(code_ine=ine) - else: - return error_response( - 404, - message="parametre manquant", - ) - if query.count() > 1: # cas rare d'un étudiant présent dans plusieurs depts - etuds = [] - for e in query: - admission = e.admission.first() - etuds.append((((admission.annee or 0) if admission else 0), e)) - etuds.sort() - etud = etuds[-1][1] - else: - etud = query.first() + etud = get_last_instance_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) if etud is None: return error_response( diff --git a/app/api/tools.py b/app/api/tools.py index 72e98a3c..462a5cbf 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -1,11 +1,14 @@ from app import models -### TODO Faire en sorte de renvoyer l'étudiant qui a son inscription la plus récente -def get_etud_from_etudid_or_nip_or_ine( +from app.api.errors import error_response +from app.models import Identite + + +def get_last_instance_etud_from_etudid_or_nip_or_ine( etudid=None, nip=None, ine=None ) -> models.Identite: """ - etudiant en fonction de l'etudid, code nip et code ine rentré en paramètres + Retourne l'instance de l'etudiant la plus récente en fonction de l'etudid, code nip et code ine rentré en paramètres etudid : None ou un int etudid nip : None ou un int code_nip @@ -13,12 +16,27 @@ def get_etud_from_etudid_or_nip_or_ine( Return None si étudiant inexistant. """ - if etudid is None: - if nip is None: # si ine - etud = models.Identite.query.filter_by(code_ine=str(ine)).first() - else: # si nip - etud = models.Identite.query.filter_by(code_nip=str(nip)).first() - else: # si etudid - etud = models.Identite.query.filter_by(id=etudid).first() + if etudid is not None: + etud = Identite.query.get(etudid) + else: + if nip is not None: + query = Identite.query.filter_by(code_nip=nip) + elif ine is not None: + query = Identite.query.filter_by(code_ine=ine) + else: + return error_response( + 404, + message="parametre manquant", + ) + if query.count() > 1: # cas rare d'un étudiant présent dans plusieurs depts + etuds = [] + for e in query: + admission = e.admission.first() + etuds.append((((admission.annee or 0) if admission else 0), e)) + etuds.sort() + etud = etuds[-1][1] + else: + etud = query.first() return etud + From 0b792d5af4603d44a1013f84690a17be0d797b37 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Mon, 16 May 2022 16:00:24 +0200 Subject: [PATCH 15/60] correction conflits avec origin/master --- app/api/evaluations.py | 3 ++- app/models/etudiants.py | 3 +-- app/models/evaluations.py | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/api/evaluations.py b/app/api/evaluations.py index cef58a42..7d7f9e1a 100644 --- a/app/api/evaluations.py +++ b/app/api/evaluations.py @@ -7,6 +7,7 @@ from app import models from app.api import bp from app.api.auth import token_auth, token_permission_required from app.api.errors import error_response +from app.models import Evaluation from app.scodoc.sco_evaluation_db import do_evaluation_get_all_notes from app.scodoc.sco_permissions import Permission @@ -46,7 +47,7 @@ def evaluations(moduleimpl_id: int): ] """ # Récupération de toutes les évaluations - evals = models.Evaluation.query.filter_by(id=moduleimpl_id) + evals = Evaluation.query.filter_by(id=moduleimpl_id) # Mise en forme des données data = [d.to_dict() for d in evals] diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 4c6672e2..2e9292c1 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -171,7 +171,6 @@ class Identite(db.Model): """ from app.scodoc import sco_photos - dept = models.Departement.query.filter_by(id=self.dept_id).first_or_404() d = { "civilite": self.civilite, "code_ine": self.code_ine or "", @@ -180,7 +179,7 @@ class Identite(db.Model): if self.date_naissance else "", "dept_id": self.dept_id, - "dept_acronym": dept.acronym, + "dept_acronym": self.departement.acronym, "email": self.get_first_email() or "", "emailperso": self.get_first_email("emailperso"), "etudid": self.id, diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 84383adc..afe8e791 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -54,7 +54,12 @@ class Evaluation(db.Model): # ScoDoc7 output_formators e["evaluation_id"] = self.id e["jour"] = ndb.DateISOtoDMY(e["jour"]) + e["date_debut"] = datetime.datetime.combine( + self.jour, self.heure_debut + ).isoformat() + e["date_fin"] = datetime.datetime.combine(self.jour, self.heure_fin).isoformat() e["numero"] = ndb.int_null_is_zero(e["numero"]) + e["poids"] = self.get_ue_poids_dict() # { ue_id : poids } return evaluation_enrich_dict(e) def from_dict(self, data): @@ -153,7 +158,7 @@ class EvaluationUEPoids(db.Model): # Fonction héritée de ScoDoc7 à refactorer def evaluation_enrich_dict(e): - """add or convert some fileds in an evaluation dict""" + """add or convert some fields in an evaluation dict""" # For ScoDoc7 compat heure_debut_dt = e["heure_debut"] or datetime.time( 8, 00 From 2bb0ac548dc68dc3b4f5f21e8c4eea2cd290c4a5 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Mon, 16 May 2022 16:04:33 +0200 Subject: [PATCH 16/60] merge from master --- app/api/etudiants.py | 2 +- app/api/evaluations.py | 1 + app/api/tools.py | 1 - app/models/evaluations.py | 2 -- app/scodoc/sco_recapcomplet.py | 4 +++- app/views/notes.py | 2 +- sco_version.py | 2 +- tests/api/exemple-api-basic.py | 4 +++- tests/api/setup_test_api.py | 1 + tests/api/tools_test_api.py | 2 ++ 10 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index e53056f1..3784e938 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -278,7 +278,7 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) # ) @token_auth.login_required @token_permission_required(Permission.APIView) -def etudiant_bulletin_semestre( # XXX TODO Ajouter la possibilité de retourner en version pdf +def etudiant_bulletin_semestre( # XXX TODO Ajouter la possibilité de retourner en version pdf formsemestre_id, etudid: int = None, nip: str = None, diff --git a/app/api/evaluations.py b/app/api/evaluations.py index 7d7f9e1a..f72b85bd 100644 --- a/app/api/evaluations.py +++ b/app/api/evaluations.py @@ -4,6 +4,7 @@ from flask import jsonify import app from app import models +from app.models import Evaluation from app.api import bp from app.api.auth import token_auth, token_permission_required from app.api.errors import error_response diff --git a/app/api/tools.py b/app/api/tools.py index 462a5cbf..75b8cad4 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -39,4 +39,3 @@ def get_last_instance_etud_from_etudid_or_nip_or_ine( etud = query.first() return etud - diff --git a/app/models/evaluations.py b/app/models/evaluations.py index afe8e791..ef2a6288 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -5,8 +5,6 @@ import datetime from app import db -from app.models import formsemestre -from app.models.formsemestre import FormSemestre from app.models.moduleimpls import ModuleImpl from app.models.ues import UniteEns diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index bbb1aee0..1c5c19eb 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -32,7 +32,7 @@ import time from xml.etree import ElementTree from flask import g, request -from flask import url_for +from flask import abort, url_for from app import log from app.but import bulletin_but @@ -83,6 +83,8 @@ def formsemestre_recapcomplet( force_publishing: publie les xml et json même si bulletins non publiés selected_etudid: etudid sélectionné (pour scroller au bon endroit) """ + if not isinstance(formsemestre_id, int): + abort(404) formsemestre = FormSemestre.query.get_or_404(formsemestre_id) file_formats = {"csv", "json", "xls", "xlsx", "xlsall", "xml"} supported_formats = file_formats | {"html", "evals"} diff --git a/app/views/notes.py b/app/views/notes.py index 86c4f6f7..f924acb9 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -292,7 +292,7 @@ def formsemestre_bulletinetud( format = format or "html" if not isinstance(formsemestre_id, int): - raise ValueError("formsemestre_id must be an integer !") + abort(404, description="formsemestre_id must be an integer !") formsemestre = FormSemestre.query.get_or_404(formsemestre_id) if etudid: etud = models.Identite.query.get_or_404(etudid) diff --git a/sco_version.py b/sco_version.py index aaeef296..120447d3 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.2.20" +SCOVERSION = "9.2.21" SCONAME = "ScoDoc" diff --git a/tests/api/exemple-api-basic.py b/tests/api/exemple-api-basic.py index 1c7d61b8..a581a878 100644 --- a/tests/api/exemple-api-basic.py +++ b/tests/api/exemple-api-basic.py @@ -33,7 +33,7 @@ except NameError: load_dotenv(os.path.join(BASEDIR, ".env")) CHK_CERT = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) -SCODOC_URL = os.environ["SCODOC_URL"] +SCODOC_URL = os.environ["SCODOC_URL"] or "http://localhost:5000" API_URL = SCODOC_URL + "/ScoDoc/api" SCODOC_USER = os.environ["SCODOC_USER"] SCODOC_PASSWORD = os.environ["SCODOC_PASSWORD"] @@ -110,6 +110,8 @@ print("\n".join([s["titre_num"] for s in sems])) sems = GET(f"/etudiant/nip/{code_nip}/formsemestres") print("\n".join([s["titre_num"] for s in sems])) +# Evaluation +evals = GET("/evaluations/1") # # --- Recupere la liste de tous les semestres: # sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !") diff --git a/tests/api/setup_test_api.py b/tests/api/setup_test_api.py index 4a9d4539..9791a975 100644 --- a/tests/api/setup_test_api.py +++ b/tests/api/setup_test_api.py @@ -25,6 +25,7 @@ SCODOC_URL = os.environ["SCODOC_URL"] API_URL = SCODOC_URL + "/ScoDoc/api" API_USER = os.environ.get("API_USER", "test") API_PASSWORD = os.environ.get("API_PASSWD", "test") +DEPT_ACRONYM = "TAPI" print(f"SCODOC_URL={SCODOC_URL}") print(f"API URL={API_URL}") diff --git a/tests/api/tools_test_api.py b/tests/api/tools_test_api.py index 40d44381..e2a36510 100644 --- a/tests/api/tools_test_api.py +++ b/tests/api/tools_test_api.py @@ -49,6 +49,8 @@ ETUD_FIELDS = { "code_nip", "codepostaldomicile", "date_naissance", + "dept_acronym", + "dept_id", "dept_naissance", "description", "domicile", From 4d0b18b548786bb9820c7c656b630e4d0c362b39 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Tue, 17 May 2022 16:07:46 +0200 Subject: [PATCH 17/60] renforcement des tests etudiants --- app/api/etudiants.py | 2 +- tests/api/test_api_etudiants.py | 242 +++++++++++++++++++++++++++++--- 2 files changed, 223 insertions(+), 21 deletions(-) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 3784e938..ee1c2fbb 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -284,7 +284,7 @@ def etudiant_bulletin_semestre( # XXX TODO Ajouter la possibilité de retourner nip: str = None, ine: str = None, version="long", - # pdf: bool = False, + pdf: bool = False, ): """ Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py index c0f0470f..090a8545 100644 --- a/tests/api/test_api_etudiants.py +++ b/tests/api/test_api_etudiants.py @@ -24,6 +24,11 @@ from tests.api.tools_test_api import verify_fields, verify_occurences_ids_etus from tests.api.tools_test_api import ETUD_FIELDS, FSEM_FIELDS +ETUDID = 1 +NIP = "1" +INE = "1" + + def test_etudiants_courant(api_headers): """ Route: /etudiants/courant @@ -31,7 +36,7 @@ def test_etudiants_courant(api_headers): fields = {"id", "nip", "nom", "prenom", "civilite"} r = requests.get( - API_URL + "/etudiants/courant", + API_URL + "/etudiants/courants", headers=api_headers, verify=CHECK_CERTIFICATE, ) @@ -52,7 +57,7 @@ def test_etudiants_courant(api_headers): ########## Version long ################ r = requests.get( - API_URL + "/etudiants/courant/long", + API_URL + "/etudiants/courants/long", headers=api_headers, verify=CHECK_CERTIFICATE, ) @@ -71,40 +76,115 @@ def test_etudiant(api_headers): ######### Test etudid ######### r = requests.get( - API_URL + "/etudiant/etudid/1", + API_URL + "/etudiant/etudid/" + str(ETUDID), headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 etud = r.json() - assert verify_fields(etud, ETUD_FIELDS) is True + code_nip = r.json()["code_nip"] + code_ine = r.json()["code_ine"] + ######### Test code nip ######### r = requests.get( - API_URL + "/etudiant/nip/1", + API_URL + "/etudiant/nip/" + code_nip, headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - etud = r.json() + etud_nip = r.json() fields_ok = verify_fields(etud, ETUD_FIELDS) assert fields_ok is True ######### Test code ine ######### r = requests.get( - API_URL + "/etudiant/ine/1", + API_URL + "/etudiant/ine/" + code_ine, + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + etud_ine = r.json() + assert len(etud) == 26 + fields_ok = verify_fields(etud, ETUD_FIELDS) + assert fields_ok is True + + assert etud == etud_nip == etud_ine + + +def test_etudiants(api_headers): + """ + Route : /etudiants/etudid/, /etudiants/nip/, /etudiants/ine/ + """ + ######### Test etudid ######### + r = requests.get( + API_URL + "/etudiants/etudid/" + str(ETUDID), headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 etud = r.json() - assert len(etud) == 26 - fields_ok = verify_fields(etud, ETUD_FIELDS) + code_nip = etud[0]["code_nip"] + code_ine = etud[0]["code_ine"] + + assert isinstance(etud, list) + assert len(etud) == 1 + fields_ok = verify_fields(etud[0], ETUD_FIELDS) assert fields_ok is True + ######### Test code nip ######### + + r = requests.get( + API_URL + "/etudiants/nip/" + code_nip, + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + etud_nip = r.json() + + assert isinstance(etud_nip, list) + fields_ok = verify_fields(etud_nip[0], ETUD_FIELDS) + assert fields_ok is True + + all_unique = True + list_ids = [etud["id"] for etud in etud_nip] + for id in list_ids: + if list_ids.count(id) > 1: + all_unique = False + assert all_unique is True + + ######### Test code ine ######### + + r = requests.get( + API_URL + "/etudiants/ine/" + code_ine, + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + etud_ine = r.json() + + assert isinstance(etud_ine, list) + fields_ok = verify_fields(etud_ine[0], ETUD_FIELDS) + assert fields_ok is True + + all_unique = True + list_ids = [etud["id"] for etud in etud_ine] + for id in list_ids: + if list_ids.count(id) > 1: + all_unique = False + assert all_unique is True + + ####### Erreurs ####### + r = requests.get( + API_URL + "/etudiants/etudid/", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 404 + def test_etudiant_formsemestres(api_headers): """ @@ -114,20 +194,52 @@ def test_etudiant_formsemestres(api_headers): ######### Test etudid ######### r = requests.get( - API_URL + "/etudiant/etudid/1/formsemestres", + API_URL + "/etudiant/etudid/" + str(ETUDID) + "/formsemestres", headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - formsemestres = r.json() - assert len(formsemestres) == 1 + list_formsemestres = r.json() + assert len(list_formsemestres) == 1 + formsemestre = list_formsemestres[0] + assert isinstance(formsemestre["id"], int) + assert isinstance(formsemestre["bul_bgcolor"], str) + assert isinstance(formsemestre["date_debut"], str) + assert isinstance(formsemestre["date_fin"], str) + assert isinstance(formsemestre["resp_can_edit"], bool) + assert isinstance(formsemestre["dept_id"], int) + assert isinstance(formsemestre["etat"], bool) + assert isinstance(formsemestre["resp_can_change_ens"], bool) + assert isinstance(formsemestre["modalite"], str) + assert isinstance(formsemestre["ens_can_edit_eval"], bool) + assert isinstance(formsemestre["formation_id"], int) + assert isinstance(formsemestre["gestion_compensation"], bool) + assert formsemestre["elt_sem_apo"] is None or isinstance( + formsemestre["elt_sem_apo"], str + ) + assert isinstance(formsemestre["semestre_id"], int) + assert isinstance(formsemestre["bul_hide_xml"], bool) + assert formsemestre["elt_annee_apo"] is None or isinstance( + formsemestre["elt_annee_apo"], str + ) + assert isinstance(formsemestre["titre"], str) + assert isinstance(formsemestre["block_moyennes"], bool) + assert formsemestre["scodoc7_id"] is None or isinstance( + formsemestre["scodoc7_id"], int + ) + assert isinstance(formsemestre["gestion_semestrielle"], bool) + assert isinstance(formsemestre["formsemestre_id"], int) + assert isinstance(formsemestre["titre_num"], str) + assert isinstance(formsemestre["date_debut_iso"], str) + assert isinstance(formsemestre["date_fin_iso"], str) + assert isinstance(formsemestre["responsables"], list) + assert isinstance(formsemestre["titre_formation"], str) - formsemestre = formsemestres[0] assert verify_fields(formsemestre, FSEM_FIELDS) is True ######### Test code nip ######### r = requests.get( - API_URL + "/etudiant/nip/1/formsemestres", + API_URL + "/etudiant/nip/" + str(NIP) + "/formsemestres", headers=api_headers, verify=CHECK_CERTIFICATE, ) @@ -140,7 +252,7 @@ def test_etudiant_formsemestres(api_headers): ######### Test code ine ######### r = requests.get( - API_URL + "/etudiant/ine/1/formsemestres", + API_URL + "/etudiant/ine/" + str(INE) + "/formsemestres", headers=api_headers, verify=CHECK_CERTIFICATE, ) @@ -156,10 +268,12 @@ def test_etudiant_bulletin_semestre(api_headers): """ Route: /etudiant/etudid//formsemestre//bulletin """ + ##################### LONG ######################## + ######### Test etudid ######### r = requests.get( - API_URL + "/etudiant/etudid/1/formsemestre/1/bulletin", + API_URL + "/etudiant/etudid/" + str(ETUDID) + "/formsemestre/1/bulletin", headers=api_headers, verify=CHECK_CERTIFICATE, ) @@ -170,7 +284,7 @@ def test_etudiant_bulletin_semestre(api_headers): ######### Test code nip ######### r = requests.get( - API_URL + "/etudiant/nip/1/formsemestre/1/bulletin", + API_URL + "/etudiant/nip/" + str(NIP) + "/formsemestre/1/bulletin", headers=api_headers, verify=CHECK_CERTIFICATE, ) @@ -180,7 +294,7 @@ def test_etudiant_bulletin_semestre(api_headers): ######### Test code ine ######### r = requests.get( - API_URL + "/etudiant/ine/1/formsemestre/1/bulletin", + API_URL + "/etudiant/ine/" + str(INE) + "/formsemestre/1/bulletin", headers=api_headers, verify=CHECK_CERTIFICATE, ) @@ -188,6 +302,94 @@ def test_etudiant_bulletin_semestre(api_headers): bul = r.json() assert len(bul) == 13 # HARDCODED + ################### LONG + PDF ##################### + + # ######### Test etudid ######### + # + # r = requests.get( + # API_URL + "/etudiant/etudid/" + str(ETUDID) + "/formsemestre/1/bulletin/pdf", + # headers=api_headers, + # verify=CHECK_CERTIFICATE, + # ) + # assert r.status_code == 200 + # + # ######### Test code nip ######### + # + # r = requests.get( + # API_URL + "/etudiant/nip/" + str(NIP) + "/formsemestre/1/bulletin/pdf", + # headers=api_headers, + # verify=CHECK_CERTIFICATE, + # ) + # assert r.status_code == 200 + # + # ######### Test code ine ######### + # r = requests.get( + # API_URL + "/etudiant/ine/" + str(INE) + "/formsemestre/1/bulletin/pdf", + # headers=api_headers, + # verify=CHECK_CERTIFICATE, + # ) + # assert r.status_code == 200 + + ################### SHORT ##################### + + ######### Test etudid ######### + r = requests.get( + API_URL + "/etudiant/etudid/" + str(ETUDID) + "/formsemestre/1/bulletin/short", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + bul = r.json() + assert len(bul) == 13 # HARDCODED + + ######### Test code nip ######### + + r = requests.get( + API_URL + "/etudiant/nip/" + str(NIP) + "/formsemestre/1/bulletin/short", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + bul = r.json() + assert len(bul) == 13 # HARDCODED + + ######### Test code ine ######### + r = requests.get( + API_URL + "/etudiant/ine/" + str(INE) + "/formsemestre/1/bulletin/short", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + bul = r.json() + assert len(bul) == 13 # HARDCODED + + ################### SHORT + PDF ##################### + + # ######### Test etudid ######### + # r = requests.get( + # API_URL + "/etudiant/etudid/" + str(ETUDID) + "/formsemestre/1/bulletin/short/pdf", + # headers=api_headers, + # verify=CHECK_CERTIFICATE, + # ) + # assert r.status_code == 200 + # + # ######### Test code nip ######### + # + # r = requests.get( + # API_URL + "/etudiant/nip/" + str(NIP) + "/formsemestre/1/bulletin/short/pdf", + # headers=api_headers, + # verify=CHECK_CERTIFICATE, + # ) + # assert r.status_code == 200 + # + # ######### Test code ine ######### + # r = requests.get( + # API_URL + "/etudiant/ine/" + str(INE) + "/formsemestre/1/bulletin/short/pdf", + # headers=api_headers, + # verify=CHECK_CERTIFICATE, + # ) + # assert r.status_code == 200 + ### --- Test étudiant inexistant r = requests.get( API_URL + "/etudiant/ine/189919919119191/formsemestre/1/bulletin", @@ -230,7 +432,7 @@ def test_etudiant_groups(api_headers): ######### Test code nip ######### r = requests.get( - API_URL + "/etudiant/nip/1/formsemestre/1/groups", + API_URL + "/etudiant/nip/" + str(NIP) + "/formsemestre/1/groups", headers=api_headers, verify=CHECK_CERTIFICATE, ) @@ -243,7 +445,7 @@ def test_etudiant_groups(api_headers): ######### Test code ine ######### r = requests.get( - API_URL + "/etudiant/ine/1/formsemestre/1/groups", + API_URL + "/etudiant/ine/" + str(INE) + "/formsemestre/1/groups", headers=api_headers, verify=CHECK_CERTIFICATE, ) From bd2326a89a893fa9bab577a7e51a6fa9b0992f1c Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Wed, 18 May 2022 16:02:56 +0200 Subject: [PATCH 18/60] ajout de tests unitaires pour le bulletin version long --- tests/api/test_api_etudiants.py | 354 +++++++++++++++++++++++++++++++- tests/api/tools_test_api.py | 246 +++++++++++++++++++++- 2 files changed, 596 insertions(+), 4 deletions(-) diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py index 090a8545..1167a9d7 100644 --- a/tests/api/test_api_etudiants.py +++ b/tests/api/test_api_etudiants.py @@ -20,7 +20,37 @@ Utilisation : import requests from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers -from tests.api.tools_test_api import verify_fields, verify_occurences_ids_etus +from tests.api.tools_test_api import ( + verify_fields, + verify_occurences_ids_etus, + BULLETIN_FIELDS, + BULLETIN_ETUDIANT_FIELDS, + BULLETIN_FORMATION_FIELDS, + BULLETIN_OPTIONS_FIELDS, + BULLETIN_RESSOURCES_FIELDS, + BULLETIN_SAES_FIELDS, + BULLETIN_UES_FIELDS, + BULLETIN_SEMESTRE_FIELDS, + BULLETIN_UES_RT11_RESSOURCES_FIELDS, + BULLETIN_UES_RT11_SAES_FIELDS, + BULLETIN_UES_RT21_RESSOURCES_FIELDS, + BULLETIN_UES_RT31_RESSOURCES_FIELDS, + BULLETIN_UES_RT21_SAES_FIELDS, + BULLETIN_UES_RT31_SAES_FIELDS, + BULLETIN_SEMESTRE_ABSENCES_FIELDS, + BULLETIN_SEMESTRE_ECTS_FIELDS, + BULLETIN_SEMESTRE_NOTES_FIELDS, + BULLETIN_SEMESTRE_RANG_FIELDS, + BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_FIELDS, + BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_FIELDS, + BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_POIDS_FIELDS, + BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS, + BULLETIN_UES_UE_FIELDS, + BULLETIN_UES_UE_MOYENNE_FIELDS, + BULLETIN_UES_UE_RESSOURCES_RESSOURCE_FIELDS, + BULLETIN_UES_UE_SAES_SAE_FIELDS, + BULLETIN_UES_UE_ECTS_FIELDS, +) from tests.api.tools_test_api import ETUD_FIELDS, FSEM_FIELDS @@ -278,8 +308,326 @@ def test_etudiant_bulletin_semestre(api_headers): verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - bul = r.json() - assert len(bul) == 13 # HARDCODED + bulletin = r.json() + assert len(bulletin) == 13 # HARDCODED + + assert verify_fields(bulletin, BULLETIN_FIELDS) is True + assert isinstance(bulletin["version"], str) + assert isinstance(bulletin["type"], str) + assert isinstance(bulletin["date"], str) + assert isinstance(bulletin["publie"], bool) + assert isinstance(bulletin["etudiant"], dict) + assert isinstance(bulletin["formation"], dict) + assert isinstance(bulletin["formsemestre_id"], int) + assert isinstance(bulletin["etat_inscription"], str) + assert isinstance(bulletin["options"], dict) + assert isinstance(bulletin["ressources"], dict) + assert isinstance(bulletin["saes"], dict) + assert isinstance(bulletin["ues"], dict) + assert isinstance(bulletin["semestre"], dict) + + bulletin_etud = bulletin["etudiant"] + assert verify_fields(bulletin_etud, BULLETIN_ETUDIANT_FIELDS) is True + assert isinstance(bulletin_etud["civilite"], str) + assert isinstance(bulletin_etud["code_ine"], str) + assert isinstance(bulletin_etud["code_nip"], str) + assert isinstance(bulletin_etud["date_naissance"], str) + assert isinstance(bulletin_etud["dept_id"], int) + assert isinstance(bulletin_etud["dept_acronym"], str) + assert isinstance(bulletin_etud["email"], str) + assert isinstance(bulletin_etud["emailperso"], str) + assert isinstance(bulletin_etud["etudid"], int) + assert isinstance(bulletin_etud["nom"], str) + assert isinstance(bulletin_etud["prenom"], str) + assert isinstance(bulletin_etud["nomprenom"], str) + assert isinstance(bulletin_etud["lieu_naissance"], str) + assert isinstance(bulletin_etud["dept_naissance"], str) + assert isinstance(bulletin_etud["nationalite"], str) + assert isinstance(bulletin_etud["fiche_url"], str) + assert isinstance(bulletin_etud["photo_url"], str) + assert isinstance(bulletin_etud["id"], int) + assert isinstance(bulletin_etud["domicile"], str) + assert isinstance(bulletin_etud["villedomicile"], str) + assert isinstance(bulletin_etud["telephone"], str) + assert isinstance(bulletin_etud["fax"], str) + assert isinstance(bulletin_etud["description"], str) + assert isinstance(bulletin_etud["codepostaldomicile"], str) + assert isinstance(bulletin_etud["paysdomicile"], str) + assert isinstance(bulletin_etud["telephonemobile"], str) + assert isinstance(bulletin_etud["typeadresse"], str) + + bulletin_formation = bulletin["formation"] + assert verify_fields(bulletin_formation, BULLETIN_FORMATION_FIELDS) is True + assert isinstance(bulletin_formation["id"], int) + assert isinstance(bulletin_formation["acronyme"], str) + assert isinstance(bulletin_formation["titre_officiel"], str) + assert isinstance(bulletin_formation["titre"], str) + + bulletin_options = bulletin["options"] + assert verify_fields(bulletin_options, BULLETIN_OPTIONS_FIELDS) is True + assert isinstance(bulletin_options["show_abs"], bool) + assert isinstance(bulletin_options["show_abs_modules"], bool) + assert isinstance(bulletin_options["show_ects"], bool) + assert isinstance(bulletin_options["show_codemodules"], bool) + assert isinstance(bulletin_options["show_matieres"], bool) + assert isinstance(bulletin_options["show_rangs"], bool) + assert isinstance(bulletin_options["show_ue_rangs"], bool) + assert isinstance(bulletin_options["show_mod_rangs"], bool) + assert isinstance(bulletin_options["show_moypromo"], bool) + assert isinstance(bulletin_options["show_minmax"], bool) + assert isinstance(bulletin_options["show_minmax_mod"], bool) + assert isinstance(bulletin_options["show_minmax_eval"], bool) + assert isinstance(bulletin_options["show_coef"], bool) + assert isinstance(bulletin_options["show_ue_cap_details"], bool) + assert isinstance(bulletin_options["show_ue_cap_current"], bool) + assert isinstance(bulletin_options["show_temporary"], bool) + assert isinstance(bulletin_options["temporary_txt"], str) + assert isinstance(bulletin_options["show_uevalid"], bool) + assert isinstance(bulletin_options["show_date_inscr"], bool) + + bulletin_ressources = bulletin["ressources"] + assert verify_fields(bulletin_ressources, BULLETIN_RESSOURCES_FIELDS) is True + assert isinstance(bulletin_ressources, dict) + + for ressource in bulletin_ressources.values(): + assert ( + verify_fields( + ressource, BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_FIELDS + ) + is True + ) + assert isinstance(ressource, dict) + assert isinstance(ressource["evaluations"], list) + for evaluation in ressource["evaluations"]: + assert ( + verify_fields( + evaluation, + BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_FIELDS, + ) + is True + ) + assert isinstance(evaluation["id"], int) + assert evaluation["description"] is None or isinstance( + evaluation["description"], str + ) + assert evaluation["date"] is None or isinstance(evaluation["date"], str) + assert isinstance(evaluation["heure_debut"], str) + assert isinstance(evaluation["heure_fin"], str) + assert isinstance(evaluation["coef"], str) + assert isinstance(evaluation["poids"], dict) + assert isinstance(evaluation["note"], dict) + assert isinstance(evaluation["url"], str) + + assert ( + verify_fields( + evaluation["poids"], + BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_POIDS_FIELDS, + ) + is True + ) + assert isinstance(evaluation["poids"]["RT1.1"], float) + assert isinstance(evaluation["poids"]["RT2.1"], float) + assert isinstance(evaluation["poids"]["RT3.1"], float) + + assert ( + verify_fields( + evaluation["note"], + BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS, + ) + is True + ) + assert isinstance(evaluation["note"]["value"], str) + assert isinstance(evaluation["note"]["min"], str) + assert isinstance(evaluation["note"]["max"], str) + assert isinstance(evaluation["note"]["moy"], str) + + bulletin_saes = bulletin["saes"] + assert verify_fields(bulletin_saes, BULLETIN_SAES_FIELDS) is True + assert isinstance(bulletin_saes, dict) + + for sae in bulletin_saes.values(): + assert ( + verify_fields(sae, BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_FIELDS) + is True + ) + assert isinstance(sae, dict) + assert isinstance(sae["evaluations"], list) + for evaluation in sae["evaluations"]: + assert ( + verify_fields( + evaluation, + BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_FIELDS, + ) + is True + ) + assert isinstance(evaluation["id"], int) + assert evaluation["description"] is None or isinstance( + evaluation["description"], str + ) + assert evaluation["date"] is None or isinstance(evaluation["date"], str) + assert isinstance(evaluation["heure_debut"], str) + assert isinstance(evaluation["heure_fin"], str) + assert isinstance(evaluation["coef"], str) + assert isinstance(evaluation["poids"], dict) + assert isinstance(evaluation["note"], dict) + assert isinstance(evaluation["url"], str) + + assert ( + verify_fields( + evaluation["poids"], + BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_POIDS_FIELDS, + ) + is True + ) + assert isinstance(evaluation["poids"]["RT1.1"], float) + assert isinstance(evaluation["poids"]["RT2.1"], float) + assert isinstance(evaluation["poids"]["RT3.1"], float) + + assert ( + verify_fields( + evaluation["note"], + BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS, + ) + is True + ) + assert isinstance(evaluation["note"]["value"], str) + assert isinstance(evaluation["note"]["min"], str) + assert isinstance(evaluation["note"]["max"], str) + assert isinstance(evaluation["note"]["moy"], str) + + bulletin_ues = bulletin["ues"] + assert verify_fields(bulletin_ues, BULLETIN_UES_FIELDS) is True + assert isinstance(bulletin_ues, dict) + + for (key_ue, value_ue) in bulletin_ues.items(): + assert verify_fields(value_ue, BULLETIN_UES_UE_FIELDS) is True + assert isinstance(value_ue["id"], int) + assert isinstance(value_ue["titre"], str) + assert isinstance(value_ue["numero"], int) + assert isinstance(value_ue["type"], int) + assert isinstance(value_ue["color"], str) + assert value_ue["competence"] is None or isinstance(value_ue["competence"], str) + assert isinstance(value_ue["moyenne"], dict) + assert isinstance(value_ue["bonus"], str) + assert isinstance(value_ue["malus"], str) + assert value_ue["capitalise"] is None or isinstance(value_ue["capitalise"], str) + assert isinstance(value_ue["ressources"], dict) + assert isinstance(value_ue["saes"], dict) + assert isinstance(value_ue["ECTS"], dict) + + assert ( + verify_fields(value_ue["moyenne"], BULLETIN_UES_UE_MOYENNE_FIELDS) is True + ) + assert isinstance(value_ue["moyenne"]["value"], str) + assert isinstance(value_ue["moyenne"]["min"], str) + assert isinstance(value_ue["moyenne"]["max"], str) + assert isinstance(value_ue["moyenne"]["moy"], str) + assert isinstance(value_ue["moyenne"]["rang"], str) + assert isinstance(value_ue["moyenne"]["total"], int) + + if key_ue == "RT1.1": + assert ( + verify_fields( + bulletin_ues[key_ue]["ressources"], + BULLETIN_UES_RT11_RESSOURCES_FIELDS, + ) + is True + ) + assert ( + verify_fields( + bulletin_ues[key_ue]["saes"], BULLETIN_UES_RT11_SAES_FIELDS + ) + is True + ) + elif key_ue == "RT2.1": + assert ( + verify_fields( + bulletin_ues[key_ue]["ressources"], + BULLETIN_UES_RT21_RESSOURCES_FIELDS, + ) + is True + ) + assert ( + verify_fields( + bulletin_ues[key_ue]["saes"], BULLETIN_UES_RT21_SAES_FIELDS + ) + is True + ) + elif key_ue == "RT3.1": + assert ( + verify_fields( + bulletin_ues[key_ue]["ressources"], + BULLETIN_UES_RT31_RESSOURCES_FIELDS, + ) + is True + ) + assert ( + verify_fields( + bulletin_ues[key_ue]["saes"], BULLETIN_UES_RT31_SAES_FIELDS + ) + is True + ) + + for ressource in value_ue["ressources"].values(): + assert ( + verify_fields(ressource, BULLETIN_UES_UE_RESSOURCES_RESSOURCE_FIELDS) + is True + ) + assert isinstance(ressource["id"], int) + assert isinstance(ressource["coef"], float) + assert isinstance(ressource["moyenne"], str) + + for sae in value_ue["saes"].values(): + assert verify_fields(sae, BULLETIN_UES_UE_SAES_SAE_FIELDS) is True + assert isinstance(sae["id"], int) + assert isinstance(sae["coef"], float) + assert isinstance(sae["moyenne"], str) + + assert verify_fields(value_ue["ECTS"], BULLETIN_UES_UE_ECTS_FIELDS) is True + assert isinstance(value_ue["ECTS"]["acquis"], float) + assert isinstance(value_ue["ECTS"]["total"], float) + + bulletin_semestre = bulletin["semestre"] + assert verify_fields(bulletin_semestre, BULLETIN_SEMESTRE_FIELDS) is True + assert isinstance(bulletin_semestre["etapes"], list) + assert isinstance(bulletin_semestre["date_debut"], str) + assert isinstance(bulletin_semestre["date_fin"], str) + assert isinstance(bulletin_semestre["annee_universitaire"], str) + assert isinstance(bulletin_semestre["numero"], int) + assert isinstance(bulletin_semestre["inscription"], str) + assert isinstance(bulletin_semestre["groupes"], list) + assert isinstance(bulletin_semestre["absences"], dict) + assert isinstance(bulletin_semestre["ECTS"], dict) + assert isinstance(bulletin_semestre["notes"], dict) + assert isinstance(bulletin_semestre["rang"], dict) + + assert ( + verify_fields(bulletin_semestre["absences"], BULLETIN_SEMESTRE_ABSENCES_FIELDS) + is True + ) + assert isinstance(bulletin_semestre["absences"]["injustifie"], int) + assert isinstance(bulletin_semestre["absences"]["total"], int) + + assert ( + verify_fields(bulletin_semestre["ECTS"], BULLETIN_SEMESTRE_ECTS_FIELDS) is True + ) + assert isinstance(bulletin_semestre["ECTS"]["acquis"], int) + assert isinstance(bulletin_semestre["ECTS"]["total"], float) + + assert ( + verify_fields(bulletin_semestre["notes"], BULLETIN_SEMESTRE_NOTES_FIELDS) + is True + ) + assert isinstance(bulletin_semestre["notes"]["value"], str) + assert isinstance(bulletin_semestre["notes"]["min"], str) + assert isinstance(bulletin_semestre["notes"]["max"], str) + assert isinstance(bulletin_semestre["notes"]["moy"], str) + + assert ( + verify_fields(bulletin_semestre["rang"], BULLETIN_SEMESTRE_RANG_FIELDS) is True + ) + assert isinstance(bulletin_semestre["rang"]["value"], str) + assert isinstance(bulletin_semestre["rang"]["total"], int) ######### Test code nip ######### diff --git a/tests/api/tools_test_api.py b/tests/api/tools_test_api.py index e2a36510..a71a7604 100644 --- a/tests/api/tools_test_api.py +++ b/tests/api/tools_test_api.py @@ -15,7 +15,14 @@ def verify_fields(json_response: dict, expected_fields: set) -> bool: return all(field in json_response for field in expected_fields) -def verify_occurences_ids_etus(json_response): +def verify_occurences_ids_etus(json_response) -> bool: + """ + Vérifie si il n'y a pas deux fois le même id dans la liste d'étudiant donnée en paramètres + + json_response : la réponse de la requête + + Retourne True ou False + """ list_etu = json.loads(json_response) list_ids = [etu["id"] for etu in list_etu] @@ -188,3 +195,240 @@ UE_FIELDS = { "color", "ue_id", } + +BULLETIN_FIELDS = { + "version", + "type", + "date", + "publie", + "etudiant", + "formation", + "formsemestre_id", + "etat_inscription", + "options", + "ressources", + "saes", + "ues", + "semestre", +} + + +BULLETIN_ETUDIANT_FIELDS = { + "civilite", + "code_ine", + "code_nip", + "date_naissance", + "dept_id", + "dept_acronym", + "email", + "emailperso", + "etudid", + "nom", + "prenom", + "nomprenom", + "lieu_naissance", + "dept_naissance", + "nationalite", + "boursier", + "fiche_url", + "photo_url", + "id", + "domicile", + "villedomicile", + "telephone", + "fax", + "description", + "codepostaldomicile", + "paysdomicile", + "telephonemobile", + "typeadresse", +} + +BULLETIN_FORMATION_FIELDS = {"id", "acronyme", "titre_officiel", "titre"} + +BULLETIN_OPTIONS_FIELDS = { + "show_abs", + "show_abs_modules", + "show_ects", + "show_codemodules", + "show_matieres", + "show_rangs", + "show_ue_rangs", + "show_mod_rangs", + "show_moypromo", + "show_minmax", + "show_minmax_mod", + "show_minmax_eval", + "show_coef", + "show_ue_cap_details", + "show_ue_cap_current", + "show_temporary", + "temporary_txt", + "show_uevalid", + "show_date_inscr", +} + +BULLETIN_RESSOURCES_FIELDS = { + "R101", + "R102", + "R103", + "R104", + "R105", + "R106", + "R107", + "R108", + "R109", + "R110", + "R111", + "R112", + "R113", + "R114", + "R115", +} + +BULLETIN_SAES_FIELDS = { + "SAE11", + "SAE12", + "SAE13", + "SAE14", + "SAE15", + "SAE16", +} + +########### RESSOURCES ET SAES ########### +BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_FIELDS = { + "id", + "titre", + "code_apogee", + "url", + "moyenne", + "evaluations", +} + +BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_FIELDS = { + "id", + "description", + "date", + "heure_debut", + "heure_fin", + "coef", + "poids", + "note", + "url", +} + +BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_POIDS_FIELDS = { + "RT1.1", + "RT2.1", + "RT3.1", +} + +BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS = { + "value", + "min", + "max", + "moy", +} + + +########### UES ########### +BULLETIN_UES_FIELDS = {"RT1.1", "RT2.1", "RT3.1"} + +BULLETIN_UES_UE_FIELDS = { + "id", + "titre", + "numero", + "type", + "color", + "competence", + "moyenne", + "bonus", + "malus", + "capitalise", + "ressources", + "saes", + "ECTS", +} + +BULLETIN_UES_UE_MOYENNE_FIELDS = {"value", "min", "max", "moy", "rang", "total"} + +BULLETIN_UES_RT11_RESSOURCES_FIELDS = { + "R101", + "R102", + "R103", + "R104", + "R106", + "R108", + "R110", + "R111", + "R112", + "R113", + "R114", +} + +BULLETIN_UES_RT21_RESSOURCES_FIELDS = { + "R101", + "R103", + "R104", + "R105", + "R110", + "R111", + "R112", + "R113", + "R114", + "R115", +} + +BULLETIN_UES_RT31_RESSOURCES_FIELDS = { + "R101", + "R107", + "R108", + "R109", + "R110", + "R111", + "R112", + "R115", +} + +BULLETIN_UES_UE_RESSOURCES_RESSOURCE_FIELDS = {"id", "coef", "moyenne"} + +BULLETIN_UES_RT11_SAES_FIELDS = { + "SAE11", + "SAE12", +} + +BULLETIN_UES_RT21_SAES_FIELDS = {"SAE13"} + +BULLETIN_UES_RT31_SAES_FIELDS = { + "SAE14", + "SAE15", +} + +BULLETIN_UES_UE_SAES_SAE_FIELDS = {"id", "coef", "moyenne"} + + +BULLETIN_UES_UE_ECTS_FIELDS = {"acquis", "total"} + + +########### SEMESTRE ########### +BULLETIN_SEMESTRE_FIELDS = { + "etapes", + "date_debut", + "date_fin", + "annee_universitaire", + "numero", + "inscription", + "groupes", + "absences", + "ECTS", + "notes", + "rang", +} + +BULLETIN_SEMESTRE_ABSENCES_FIELDS = {"injustifie", "total"} + +BULLETIN_SEMESTRE_ECTS_FIELDS = {"acquis", "total"} + +BULLETIN_SEMESTRE_NOTES_FIELDS = {"value", "min", "moy", "max"} + +BULLETIN_SEMESTRE_RANG_FIELDS = {"value", "total"} From 76bb83c55a194ccf4e435cc8707de04117f840c8 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Thu, 19 May 2022 16:17:27 +0200 Subject: [PATCH 19/60] =?UTF-8?q?=C3=A9bauche=20retour=20de=20bulletin=20p?= =?UTF-8?q?df=20d'un=20formsemestre=20d'un=20etudiant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/etudiants.py | 16 ++++++---- app/scodoc/sco_bulletins_pdf.py | 53 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index ee1c2fbb..1b11fa6f 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -18,6 +18,7 @@ from app.api.tools import get_last_instance_etud_from_etudid_or_nip_or_ine from app.models import Departement, FormSemestreInscription, FormSemestre, Identite from app.scodoc import sco_bulletins from app.scodoc import sco_groups +from app.scodoc.sco_bulletins_pdf import get_etud_bulletins_pdf, get_bulletin_etud_formsemestre from app.scodoc.sco_permissions import Permission @@ -230,11 +231,11 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) defaults={"version": "long", "pdf": False}, ) # Version PDF non fonctionnelle -# @bp.route( -# "/etudiant/etudid//formsemestre//bulletin/pdf", -# methods=["GET"], -# defaults={"version": "long", "pdf": True}, -# ) +@bp.route( + "/etudiant/etudid//formsemestre//bulletin/pdf", + methods=["GET"], + defaults={"version": "long", "pdf": True}, +) # @bp.route( # "/etudiant/nip//formsemestre//bulletin/pdf", # methods=["GET"], @@ -461,6 +462,11 @@ def etudiant_bulletin_semestre( # XXX TODO Ajouter la possibilité de retourner ) app.set_sco_dept(dept.acronym) + + if pdf: + return get_bulletin_etud_formsemestre(etudid, formsemestre_id, version) + + return sco_bulletins.get_formsemestre_bulletin_etud_json( formsemestre, etud, version ) diff --git a/app/scodoc/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py index 501bd98c..8d10b37f 100644 --- a/app/scodoc/sco_bulletins_pdf.py +++ b/app/scodoc/sco_bulletins_pdf.py @@ -271,6 +271,59 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"): return pdfdoc, filename +def get_bulletin_etud_formsemestre_pdf(etudid: int, formsemestre_id: int, version: str = "long"): + """ + Retourne le bulletin au format pdf d'un semestre d'un etudiant + + etudid : l'id de l'étudiant + formsemestre_id : l'id d'un semestre + """ + from app.scodoc import sco_bulletins + + etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] + fragments = [] + bookmarks = {} + filigrannes = {} + + formsemestre = FormSemestre.query.get(formsemestre_id) + frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud( + formsemestre, + etudid, + format="pdfpart", + version=version, + ) + # fragments += frag + # filigrannes[i] = filigranne + # bookmarks[i] = sem["session_id"] # eg RT-DUT-FI-S1-2015 + + infos = {"DeptName": sco_preferences.get_preference("DeptName")} + if request: + server_name = request.url_root + else: + server_name = "" + try: + sco_pdf.PDFLOCK.acquire() + pdfdoc = assemble_bulletins_pdf( + None, + fragments, + etud["nomprenom"], + infos, + bookmarks, + filigranne=filigrannes, + server_name=server_name, + ) + finally: + sco_pdf.PDFLOCK.release() + # + filename = "bul-%s" % (etud["nomprenom"]) + filename = ( + scu.unescape_html(filename).replace(" ", "_").replace("&", "").replace(".", "") + + ".pdf" + ) + + return pdfdoc, filename + + def get_filigranne(etud_etat: str, prefs, decision_sem=None) -> str: """Texte à placer en "filigranne" sur le bulletin pdf""" if etud_etat == scu.DEMISSION: From 62e57d9ca0847ab12a2db0fab0a4888f5c1d7ca7 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Fri, 20 May 2022 16:28:41 +0200 Subject: [PATCH 20/60] premiers essais pdf bulletin etud sem --- app/api/etudiants.py | 4 ++-- app/scodoc/sco_bulletins_pdf.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 1b11fa6f..6bf1cc3f 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -18,7 +18,7 @@ from app.api.tools import get_last_instance_etud_from_etudid_or_nip_or_ine from app.models import Departement, FormSemestreInscription, FormSemestre, Identite from app.scodoc import sco_bulletins from app.scodoc import sco_groups -from app.scodoc.sco_bulletins_pdf import get_etud_bulletins_pdf, get_bulletin_etud_formsemestre +from app.scodoc.sco_bulletins_pdf import get_bulletin_etud_formsemestre_pdf from app.scodoc.sco_permissions import Permission @@ -464,7 +464,7 @@ def etudiant_bulletin_semestre( # XXX TODO Ajouter la possibilité de retourner app.set_sco_dept(dept.acronym) if pdf: - return get_bulletin_etud_formsemestre(etudid, formsemestre_id, version) + return get_bulletin_etud_formsemestre_pdf(etudid, formsemestre_id, version) return sco_bulletins.get_formsemestre_bulletin_etud_json( diff --git a/app/scodoc/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py index 8d10b37f..7fc8f983 100644 --- a/app/scodoc/sco_bulletins_pdf.py +++ b/app/scodoc/sco_bulletins_pdf.py @@ -75,7 +75,7 @@ import sco_version def assemble_bulletins_pdf( formsemestre_id: int, - story: list, + story, bul_title: str, infos, pagesbookmarks=None, @@ -107,7 +107,7 @@ def assemble_bulletins_pdf( preferences=sco_preferences.SemPreferences(formsemestre_id), ) ) - document.multiBuild(story) + document.build(story) data = report.getvalue() return data @@ -281,19 +281,19 @@ def get_bulletin_etud_formsemestre_pdf(etudid: int, formsemestre_id: int, versio from app.scodoc import sco_bulletins etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - fragments = [] + # fragments = [] bookmarks = {} filigrannes = {} formsemestre = FormSemestre.query.get(formsemestre_id) - frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud( + fragments, filigranne = sco_bulletins.do_formsemestre_bulletinetud( formsemestre, etudid, format="pdfpart", version=version, ) - # fragments += frag - # filigrannes[i] = filigranne + # fragments.append(frag) + filigrannes[0] = filigranne # bookmarks[i] = sem["session_id"] # eg RT-DUT-FI-S1-2015 infos = {"DeptName": sco_preferences.get_preference("DeptName")} @@ -304,7 +304,7 @@ def get_bulletin_etud_formsemestre_pdf(etudid: int, formsemestre_id: int, versio try: sco_pdf.PDFLOCK.acquire() pdfdoc = assemble_bulletins_pdf( - None, + formsemestre_id, fragments, etud["nomprenom"], infos, From f4aa04bb7609d4531ecef9704f6ac8f69c26e08e Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Mon, 23 May 2022 15:46:36 +0200 Subject: [PATCH 21/60] =?UTF-8?q?correction=20bulletin=20etudiant=20pdf=20?= =?UTF-8?q?+=20suppression=20de=20la=20fonction=20pr=C3=A9c=C3=A9dement=20?= =?UTF-8?q?cr=C3=A9er=20qui=20=C3=A9tait=20inutil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/etudiants.py | 11 ++++--- app/scodoc/sco_bulletins_pdf.py | 53 --------------------------------- 2 files changed, 7 insertions(+), 57 deletions(-) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 6bf1cc3f..1d33b04b 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -8,7 +8,7 @@ API : accès aux étudiants """ -from flask import jsonify +from flask import jsonify, make_response import app from app.api import bp @@ -18,7 +18,7 @@ from app.api.tools import get_last_instance_etud_from_etudid_or_nip_or_ine from app.models import Departement, FormSemestreInscription, FormSemestre, Identite from app.scodoc import sco_bulletins from app.scodoc import sco_groups -from app.scodoc.sco_bulletins_pdf import get_bulletin_etud_formsemestre_pdf +from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud from app.scodoc.sco_permissions import Permission @@ -464,8 +464,11 @@ def etudiant_bulletin_semestre( # XXX TODO Ajouter la possibilité de retourner app.set_sco_dept(dept.acronym) if pdf: - return get_bulletin_etud_formsemestre_pdf(etudid, formsemestre_id, version) - + response = make_response( + do_formsemestre_bulletinetud(formsemestre, etudid, version, "pdf") + ) + response.headers["Content-Type"] = "application/json" + return response return sco_bulletins.get_formsemestre_bulletin_etud_json( formsemestre, etud, version diff --git a/app/scodoc/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py index 7fc8f983..e0ac4e14 100644 --- a/app/scodoc/sco_bulletins_pdf.py +++ b/app/scodoc/sco_bulletins_pdf.py @@ -271,59 +271,6 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"): return pdfdoc, filename -def get_bulletin_etud_formsemestre_pdf(etudid: int, formsemestre_id: int, version: str = "long"): - """ - Retourne le bulletin au format pdf d'un semestre d'un etudiant - - etudid : l'id de l'étudiant - formsemestre_id : l'id d'un semestre - """ - from app.scodoc import sco_bulletins - - etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - # fragments = [] - bookmarks = {} - filigrannes = {} - - formsemestre = FormSemestre.query.get(formsemestre_id) - fragments, filigranne = sco_bulletins.do_formsemestre_bulletinetud( - formsemestre, - etudid, - format="pdfpart", - version=version, - ) - # fragments.append(frag) - filigrannes[0] = filigranne - # bookmarks[i] = sem["session_id"] # eg RT-DUT-FI-S1-2015 - - infos = {"DeptName": sco_preferences.get_preference("DeptName")} - if request: - server_name = request.url_root - else: - server_name = "" - try: - sco_pdf.PDFLOCK.acquire() - pdfdoc = assemble_bulletins_pdf( - formsemestre_id, - fragments, - etud["nomprenom"], - infos, - bookmarks, - filigranne=filigrannes, - server_name=server_name, - ) - finally: - sco_pdf.PDFLOCK.release() - # - filename = "bul-%s" % (etud["nomprenom"]) - filename = ( - scu.unescape_html(filename).replace(" ", "_").replace("&", "").replace(".", "") - + ".pdf" - ) - - return pdfdoc, filename - - def get_filigranne(etud_etat: str, prefs, decision_sem=None) -> str: """Texte à placer en "filigranne" sur le bulletin pdf""" if etud_etat == scu.DEMISSION: From e6ee7802d62b930759b50ae5f9162093b7c45254 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Mon, 23 May 2022 15:47:51 +0200 Subject: [PATCH 22/60] =?UTF-8?q?ajout=20de=20la=20route=20'set=5Fformseme?= =?UTF-8?q?stre=5Fetud=5Fabs'=20pour=20pouvoir=20set=20une=20liste=20d'abs?= =?UTF-8?q?ence=20d'un=20=C3=A9tudiant=20sur=20tout=20un=20semestre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/absences.py | 92 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/app/api/absences.py b/app/api/absences.py index 16a3e751..b231575b 100644 --- a/app/api/absences.py +++ b/app/api/absences.py @@ -5,10 +5,11 @@ from flask import jsonify from app.api import bp from app.api.errors import error_response from app.api.auth import token_auth, token_permission_required -from app.models import Identite +from app.models import Identite, FormSemestre from app.scodoc import notesdb as ndb from app.scodoc import sco_abs +from app.scodoc.sco_abs import list_abs_date, annule_absence, annule_justif, add_abslist from app.scodoc.sco_groups import get_group_members from app.scodoc.sco_permissions import Permission @@ -172,3 +173,92 @@ def abs_groupe_etat(group_id: int, date_debut=None, date_fin=None): data.append(abs) return jsonify(data) + + +@bp.route( + "/absences/formsemestre//etudid//set_etud_abs", + methods=["GET"], + defaults={"just_or_not": 0}, +) +@bp.route( + "/absences/formsemestre//etudid//set_etud_abs/only_not_just", + methods=["GET"], + defaults={"just_or_not": 1}, +) +@bp.route( + "/absences/formsemestre//etudid//set_etud_abs/only_just", + methods=["GET"], + defaults={"just_or_not": 2}, +) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def set_formsemestre_etud_abs(formsemestre_id: int, etudid: int, just_or_not: int = 0): + """ + Set la liste des absences d'un étudiant sur tout un semestre. + (les absences existant pour cet étudiant sur cette période sont effacées) + + formsemestre_id : l'id d'un semestre + etudid : l'id d'un étudiant + + Exemple de résultat : + [ + { + "matin": true, + "estabs": true, + "estjust": true, + "description": "", + "begin": "2022-04-15 08:00:00", + "end": "2022-04-15 11:59:59" + }, + ... + ] + """ + formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() + date_debut = formsemestre.date_debut + date_fin = formsemestre.date_fin + + list_abs = list_abs_date(etudid, date_debut, date_fin) + + if just_or_not == 0: + for abs in list_abs: + jour = abs["jour"].isoformat() + if abs["matin"] is True: + annule_absence(etudid, jour, True) + annule_justif(etudid, jour, True) + else: + annule_absence(etudid, jour, False) + annule_justif(etudid, jour, False) + add_abslist(list_abs) + # return jsonify(list_abs) + + elif just_or_not == 1: + list_abs_not_just = [] + for abs in list_abs: + if abs["estjust"] is False: + list_abs_not_just.append(abs) + for abs in list_abs: + jour = abs["jour"].isoformat() + if abs["matin"] is True: + annule_absence(etudid, jour, True) + annule_justif(etudid, jour, True) + else: + annule_absence(etudid, jour, False) + annule_justif(etudid, jour, False) + add_abslist(list_abs_not_just) + # return jsonify(res) + + elif just_or_not == 2: + list_abs_just = [] + for abs in list_abs: + if abs["estjust"] is True: + list_abs_just.append(abs) + for abs in list_abs: + jour = abs["jour"].isoformat() + if abs["matin"] is True: + annule_absence(etudid, jour, True) + annule_justif(etudid, jour, True) + else: + annule_absence(etudid, jour, False) + annule_justif(etudid, jour, False) + add_abslist(list_abs_just) + # return jsonify(res) From fc4348d150f4e142e07f60d48d3df7d93e707851 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Tue, 24 May 2022 12:12:39 +0200 Subject: [PATCH 23/60] ajustement de la route reset_etud_abs --- app/api/absences.py | 64 ++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/app/api/absences.py b/app/api/absences.py index b231575b..a9556d85 100644 --- a/app/api/absences.py +++ b/app/api/absences.py @@ -176,51 +176,38 @@ def abs_groupe_etat(group_id: int, date_debut=None, date_fin=None): @bp.route( - "/absences/formsemestre//etudid//set_etud_abs", - methods=["GET"], + "/absences/etudid//list_abs//reset_etud_abs", + methods=["POST"], defaults={"just_or_not": 0}, ) @bp.route( - "/absences/formsemestre//etudid//set_etud_abs/only_not_just", - methods=["GET"], + "/absences/etudid//list_abs//reset_etud_abs/only_not_just", + methods=["POST"], defaults={"just_or_not": 1}, ) @bp.route( - "/absences/formsemestre//etudid//set_etud_abs/only_just", - methods=["GET"], + "/absences/etudid//list_abs//reset_etud_abs/only_just", + methods=["POST"], defaults={"just_or_not": 2}, ) @token_auth.login_required -@token_permission_required(Permission.APIView) -def set_formsemestre_etud_abs(formsemestre_id: int, etudid: int, just_or_not: int = 0): +@token_permission_required(Permission.APIAbsChange) +def reset_etud_abs(etudid: int, list_abs, just_or_not: int = 0): """ Set la liste des absences d'un étudiant sur tout un semestre. (les absences existant pour cet étudiant sur cette période sont effacées) - formsemestre_id : l'id d'un semestre etudid : l'id d'un étudiant - - Exemple de résultat : - [ - { - "matin": true, - "estabs": true, - "estjust": true, - "description": "", - "begin": "2022-04-15 08:00:00", - "end": "2022-04-15 11:59:59" - }, - ... - ] + list_abs : json d'absences + just_or_not : 0 (pour les absences justifiées et non justifiées), + 1 (pour les absences justifiées), + 2 (pour les absences non justifiées) """ - formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() - date_debut = formsemestre.date_debut - date_fin = formsemestre.date_fin - - list_abs = list_abs_date(etudid, date_debut, date_fin) - + # Toutes les absences if just_or_not == 0: + # suppression des absences et justificatif déjà existant pour éviter les doublons for abs in list_abs: + # Récupération de la date au format iso jour = abs["jour"].isoformat() if abs["matin"] is True: annule_absence(etudid, jour, True) @@ -228,15 +215,20 @@ def set_formsemestre_etud_abs(formsemestre_id: int, etudid: int, just_or_not: in else: annule_absence(etudid, jour, False) annule_justif(etudid, jour, False) - add_abslist(list_abs) - # return jsonify(list_abs) + # Ajout de la liste d'absences en base + add_abslist(list_abs) + + # Uniquement les absences justifiées elif just_or_not == 1: list_abs_not_just = [] + # Trie des absences justifiées for abs in list_abs: if abs["estjust"] is False: list_abs_not_just.append(abs) + # suppression des absences et justificatif déjà existant pour éviter les doublons for abs in list_abs: + # Récupération de la date au format iso jour = abs["jour"].isoformat() if abs["matin"] is True: annule_absence(etudid, jour, True) @@ -244,15 +236,20 @@ def set_formsemestre_etud_abs(formsemestre_id: int, etudid: int, just_or_not: in else: annule_absence(etudid, jour, False) annule_justif(etudid, jour, False) - add_abslist(list_abs_not_just) - # return jsonify(res) + # Ajout de la liste d'absences en base + add_abslist(list_abs_not_just) + + # Uniquement les absences non justifiées elif just_or_not == 2: list_abs_just = [] + # Trie des absences non justifiées for abs in list_abs: if abs["estjust"] is True: list_abs_just.append(abs) + # suppression des absences et justificatif déjà existant pour éviter les doublons for abs in list_abs: + # Récupération de la date au format iso jour = abs["jour"].isoformat() if abs["matin"] is True: annule_absence(etudid, jour, True) @@ -260,5 +257,6 @@ def set_formsemestre_etud_abs(formsemestre_id: int, etudid: int, just_or_not: in else: annule_absence(etudid, jour, False) annule_justif(etudid, jour, False) + + # Ajout de la liste d'absences en base add_abslist(list_abs_just) - # return jsonify(res) From 248e78a188445d4e5787adcf6c2f7e9c343bfb57 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Tue, 24 May 2022 16:02:11 +0200 Subject: [PATCH 24/60] correction de str en string pour la route reset_etud_abs --- app/api/absences.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/api/absences.py b/app/api/absences.py index a9556d85..2d3e3022 100644 --- a/app/api/absences.py +++ b/app/api/absences.py @@ -176,17 +176,17 @@ def abs_groupe_etat(group_id: int, date_debut=None, date_fin=None): @bp.route( - "/absences/etudid//list_abs//reset_etud_abs", + "/absences/etudid//list_abs//reset_etud_abs", methods=["POST"], defaults={"just_or_not": 0}, ) @bp.route( - "/absences/etudid//list_abs//reset_etud_abs/only_not_just", + "/absences/etudid//list_abs//reset_etud_abs/only_not_just", methods=["POST"], defaults={"just_or_not": 1}, ) @bp.route( - "/absences/etudid//list_abs//reset_etud_abs/only_just", + "/absences/etudid//list_abs//reset_etud_abs/only_just", methods=["POST"], defaults={"just_or_not": 2}, ) From 2db5e04a59d28d0b9e4b8fb90fffe0bdff2ab2ab Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Mon, 30 May 2022 16:08:04 +0200 Subject: [PATCH 25/60] =?UTF-8?q?=C3=A9bauche=20etat=5Fevals=20ticket=20#3?= =?UTF-8?q?87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/formsemestres.py | 59 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 7dfd2f64..393422b4 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -7,6 +7,8 @@ from app.api import bp from app.api.auth import token_auth, token_permission_required from app.models import Departement, FormSemestre, FormSemestreEtape from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json +from app.scodoc.sco_evaluation_recap import evaluations_recap_table +from app.scodoc.sco_evaluations import do_evaluation_etat_in_sem, do_evaluation_etat from app.scodoc.sco_groups import get_etud_groups from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import ModuleType @@ -461,3 +463,60 @@ def formsemestre_etudiants(formsemestre_id: int, etat: str): etu["groups"] = get_etud_groups(etu["id"], formsemestre_id) return jsonify(res) + + +@bp.route("/formsemestre//etat_evals", methods=["GET"]) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def etat_evals(formsemestre_id: int): + """ + Retourne les informations sur l'état des évaluations d'un semestre donnée + + formsemestre_id : l'id d'un semestre + + Exemple de résultat : + + """ + # fonction utilisé : do_evaluation_etat() + + formsemestre = models.FormSemestre.query.filter_by( + id=formsemestre_id + ).first_or_404() + + dept = Departement.query.get(formsemestre.dept_id) + + app.set_sco_dept(dept.acronym) + + list_ues = formsemestre.query_ues() + + moduleimpls = formsemestre.modimpls + + res = [] + ues = {} + modules = [] + + for moduleimpl in moduleimpls: + x = {} + module = models.Module.query.filter_by(id=moduleimpl.id).first_or_404() + x["id"] = module.id + x["titre"] = module.titre + + list_eval = [] + for eval in moduleimpl.evaluations: + list_eval.append(do_evaluation_etat(eval.id)) + x["evaluations"] = list_eval + + print(list_eval) + + modules.append(x) + + for ue in list_ues: + ues[ue.acronyme] = modules + + # print(ues) + print( + "###############################################################################" + ) + # print(modules) + + return jsonify(do_evaluation_etat_in_sem(formsemestre_id)) From 73fd6bfde5d005dced009ecd3d411ee7f0217c81 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Tue, 31 May 2022 16:08:57 +0200 Subject: [PATCH 26/60] avancement sur etat_evals, manque les 'saisie_notes' --- app/api/formsemestres.py | 82 +++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 393422b4..c89f0835 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -5,13 +5,17 @@ import app from app import models from app.api import bp from app.api.auth import token_auth, token_permission_required -from app.models import Departement, FormSemestre, FormSemestreEtape +from app.comp import res_sem +from app.comp.moy_mod import ModuleImplResults +from app.comp.res_compat import NotesTableCompat +from app.models import Departement, FormSemestre, FormSemestreEtape, Module, ModuleImpl from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json from app.scodoc.sco_evaluation_recap import evaluations_recap_table from app.scodoc.sco_evaluations import do_evaluation_etat_in_sem, do_evaluation_etat from app.scodoc.sco_groups import get_etud_groups from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import ModuleType +import app.scodoc.sco_utils as scu @bp.route("/formsemestre/", methods=["GET"]) @@ -477,11 +481,7 @@ def etat_evals(formsemestre_id: int): Exemple de résultat : """ - # fonction utilisé : do_evaluation_etat() - - formsemestre = models.FormSemestre.query.filter_by( - id=formsemestre_id - ).first_or_404() + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) dept = Departement.query.get(formsemestre.dept_id) @@ -489,34 +489,54 @@ def etat_evals(formsemestre_id: int): list_ues = formsemestre.query_ues() - moduleimpls = formsemestre.modimpls + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - res = [] ues = {} - modules = [] - - for moduleimpl in moduleimpls: - x = {} - module = models.Module.query.filter_by(id=moduleimpl.id).first_or_404() - x["id"] = module.id - x["titre"] = module.titre - - list_eval = [] - for eval in moduleimpl.evaluations: - list_eval.append(do_evaluation_etat(eval.id)) - x["evaluations"] = list_eval - - print(list_eval) - - modules.append(x) for ue in list_ues: - ues[ue.acronyme] = modules + modules = [] + mods = ue.modules - # print(ues) - print( - "###############################################################################" - ) - # print(modules) + for mod in mods: + dict_module = {} + moduleimpl = ModuleImpl.query.get_or_404(mod.id) - return jsonify(do_evaluation_etat_in_sem(formsemestre_id)) + modimpl_results: ModuleImplResults = nt.modimpls_results[moduleimpl.id] + + dict_module["id"] = mod.id + dict_module["titre"] = mod.titre + + list_eval = [] + for evaluation in moduleimpl.evaluations: + eval_etat = modimpl_results.evaluations_etat[evaluation.id] + eval = {} + + eval["id"] = evaluation.id + eval["description"] = evaluation.description + eval["datetime_epreuve"] = evaluation.jour + eval["heure_fin"] = evaluation.heure_fin.isoformat() + eval["comptee"] = "oui" if eval_etat.is_complete else "non" + eval["inscrits"] = modimpl_results.nb_inscrits_module + eval["manquantes"] = len( + modimpl_results.evals_etudids_sans_note[evaluation.id] + ) + eval["ABS"] = sum( + modimpl_results.evals_notes[evaluation.id] == scu.NOTES_ABSENCE + ) + eval["ATT"] = eval_etat.nb_attente + eval["EXC"] = sum( + modimpl_results.evals_notes[evaluation.id] == scu.NOTES_NEUTRALISE + ) + eval["saisie_notes"] = { + "datetime_debut": "", + "datetime_fin": "", + "datetime_mediane": "", + } + + list_eval.append(eval) + + dict_module["evaluations"] = list_eval + modules.append(dict_module) + ues[ue.acronyme] = modules + + return jsonify(ues) From 95fc21fd5a920cfa6e2ac8f96fc747375474fbd4 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Wed, 1 Jun 2022 15:53:25 +0200 Subject: [PATCH 27/60] =?UTF-8?q?route=20etat=5Fevals=20fini,=20reste=20?= =?UTF-8?q?=C3=A0=20faire=20tests=20unitaires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/formsemestres.py | 41 ++++++++++++++++++++++++++++++++++++---- app/api/tools.py | 13 +++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index c89f0835..15d31491 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -1,14 +1,18 @@ ########################################## Formsemestres ############################################################## +import statistics + from flask import jsonify import app from app import models from app.api import bp from app.api.auth import token_auth, token_permission_required +from app.api.tools import calculate_median from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults from app.comp.res_compat import NotesTableCompat from app.models import Departement, FormSemestre, FormSemestreEtape, Module, ModuleImpl +from app.scodoc import sco_evaluation_db from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json from app.scodoc.sco_evaluation_recap import evaluations_recap_table from app.scodoc.sco_evaluations import do_evaluation_etat_in_sem, do_evaluation_etat @@ -481,12 +485,14 @@ def etat_evals(formsemestre_id: int): Exemple de résultat : """ + # Récupération du semestre formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + # Set du dept dept = Departement.query.get(formsemestre.dept_id) - app.set_sco_dept(dept.acronym) + # Récupération des Ues list_ues = formsemestre.query_ues() nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) @@ -527,10 +533,37 @@ def etat_evals(formsemestre_id: int): eval["EXC"] = sum( modimpl_results.evals_notes[evaluation.id] == scu.NOTES_NEUTRALISE ) + + # Récupération de toutes les notes de l'évaluation + notes = models.NotesNotes.query.filter_by( + evaluation_id=evaluation.id + ).all() + + # Si il y a plus d'une note saisie pour l'évaluation + if len(notes) > 1: + # Trie des notes en fonction de leurs dates + notes_sorted = sorted(notes, key=lambda note: note.date) + + date_debut = notes_sorted[0].date + date_fin = notes_sorted[-1].date + + # Récupération de l'id de la note médiane + list_id_notes_sorted = [note.id for note in notes_sorted] + id_mediane = calculate_median(list_id_notes_sorted) + + date_mediane = "" + for n in notes_sorted: + if n.id == id_mediane: + date_mediane = n.date + else: + date_debut = None + date_fin = None + date_mediane = None + eval["saisie_notes"] = { - "datetime_debut": "", - "datetime_fin": "", - "datetime_mediane": "", + "datetime_debut": date_debut, + "datetime_fin": date_fin, + "datetime_mediane": date_mediane, } list_eval.append(eval) diff --git a/app/api/tools.py b/app/api/tools.py index 75b8cad4..517a51f4 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -39,3 +39,16 @@ def get_last_instance_etud_from_etudid_or_nip_or_ine( etud = query.first() return etud + + +def calculate_median_list_notes(list): + """ + Retourne la mediane d'une liste de notes + list : une liste préalablement sorted de préférence + """ + list_len = len(list) + if list_len < 1: + return None + + # Ici si la longueur est paire on prend, on prend le +1 car un indice ne peux pas avoir de nombre floatant + return list[int((list_len) / 2)] From 832a25f7dcaad280e1bd3062b4aeacf166674387 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Wed, 1 Jun 2022 16:05:56 +0200 Subject: [PATCH 28/60] =?UTF-8?q?ajout=20de=20l'exemple=20de=20r=C3=A9sult?= =?UTF-8?q?at=20pour=20etat=5Fevals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/formsemestres.py | 45 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 15d31491..05a106b1 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -483,7 +483,50 @@ def etat_evals(formsemestre_id: int): formsemestre_id : l'id d'un semestre Exemple de résultat : - + { + "RT1.1": [ + { + "id": 1, + "titre": "Initiation aux réseaux informatiques", + "evaluations": [ + { + "id": 1, + "description": null, + "datetime_epreuve": null, + "heure_fin": "09:00:00", + "comptee": "oui", + "inscrits": 16, + "manquantes": 0, + "ABS": 0, + "ATT": 0, + "EXC": 0, + "saisie_notes": { + "datetime_debut": "Wed, 01 Jun 2022 10:37:59 GMT", + "datetime_fin": "Wed, 01 Jun 2022 13:18:09 GMT", + "datetime_mediane": "Wed, 01 Jun 2022 12:38:57 GMT" + } + }, + { + "id": 22, + "description": null, + "datetime_epreuve": "Tue, 31 May 2022 00:00:00 GMT", + "heure_fin": "08:00:00", + "comptee": "oui", + "inscrits": 16, + "manquantes": 0, + "ABS": 0, + "ATT": 0, + "EXC": 0, + "saisie_notes": { + "datetime_debut": "Wed, 01 Jun 2022 12:04:19 GMT", + "datetime_fin": "Wed, 01 Jun 2022 12:04:36 GMT", + "datetime_mediane": "Wed, 01 Jun 2022 14:04:27 GMT" + } + }, + ] + }, + ] + } """ # Récupération du semestre formsemestre = FormSemestre.query.get_or_404(formsemestre_id) From 36bfd9ecade038fca162d39840c4f322f1e3d787 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Thu, 2 Jun 2022 09:08:04 +0200 Subject: [PATCH 29/60] corrections de la fonction etat_evals() --- app/api/formsemestres.py | 8 ++++++-- app/api/tools.py | 12 ------------ 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 05a106b1..c1ec6e00 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -583,7 +583,7 @@ def etat_evals(formsemestre_id: int): ).all() # Si il y a plus d'une note saisie pour l'évaluation - if len(notes) > 1: + if len(notes) >= 1: # Trie des notes en fonction de leurs dates notes_sorted = sorted(notes, key=lambda note: note.date) @@ -592,7 +592,11 @@ def etat_evals(formsemestre_id: int): # Récupération de l'id de la note médiane list_id_notes_sorted = [note.id for note in notes_sorted] - id_mediane = calculate_median(list_id_notes_sorted) + + # Ici si la longueur est paire on prend, on prend le +1 car un indice ne peux pas avoir de nombre floatant + id_mediane = list_id_notes_sorted[ + int((len(list_id_notes_sorted)) / 2) + ] date_mediane = "" for n in notes_sorted: diff --git a/app/api/tools.py b/app/api/tools.py index 517a51f4..462a5cbf 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -40,15 +40,3 @@ def get_last_instance_etud_from_etudid_or_nip_or_ine( return etud - -def calculate_median_list_notes(list): - """ - Retourne la mediane d'une liste de notes - list : une liste préalablement sorted de préférence - """ - list_len = len(list) - if list_len < 1: - return None - - # Ici si la longueur est paire on prend, on prend le +1 car un indice ne peux pas avoir de nombre floatant - return list[int((list_len) / 2)] From f04265c78e2a06bd7aecc11b41c32abc94b6ba41 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Thu, 2 Jun 2022 16:18:47 +0200 Subject: [PATCH 30/60] =?UTF-8?q?ajout=20de=20notes=20aux=20=C3=A9valuatio?= =?UTF-8?q?ns=20dans=20la=20cr=C3=A9ation=20de=20la=20fakedatabase=20+=20d?= =?UTF-8?q?=C3=A9but=20tests=20unitaires=20etat=5Fevals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/api/test_api_formsemestre.py | 38 +++++++++++++++++++ .../fakedatabase/create_test_api_database.py | 27 ++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py index bdcf7e93..956992b9 100644 --- a/tests/api/test_api_formsemestre.py +++ b/tests/api/test_api_formsemestre.py @@ -131,3 +131,41 @@ def test_formsemestre_programme(api_headers): assert verify_fields(modules[0], MODIMPL_FIELDS) assert verify_fields(ressource, MODIMPL_FIELDS) assert verify_fields(sae, MODIMPL_FIELDS) + + +# def test_formsemestre_etudiants(api_headers): +# """ +# Route: /formsemestre//etudiants, /formsemestre//etudiants/demissionnaires, /formsemestre//etudiants/defaillants +# """ +# r = requests.get( +# API_URL + "/formsemestre//etudiants", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 +# +# r = requests.get( +# API_URL + "/formsemestre//etudiants/demissionnaires", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 +# +# r = requests.get( +# API_URL + "/formsemestre//etudiants/defaillants", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 + + +def test_etat_evals(api_headers): + """ + Route : /formsemestre//etat_evals + """ + r = requests.get( + API_URL + "/formsemestre/1/etat_evals", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index bfe4800e..3744fc10 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -28,7 +28,7 @@ import sys from app.auth.models import Role, User from app import models -from app.models import Departement, Formation, FormSemestre, Identite +from app.models import Departement, Formation, FormSemestre, Identite, ModuleImpl from app import db from app.scodoc import ( sco_cache, @@ -38,6 +38,7 @@ from app.scodoc import ( sco_groups, ) from app.scodoc.sco_permissions import Permission +from app.scodoc.sco_saisie_notes import notes_add from tools.fakeportal.gen_nomprenoms import nomprenom random.seed(12345678) # tests reproductibles @@ -188,6 +189,29 @@ def create_evaluations(formsemestre: FormSemestre): evaluation_id = sco_evaluation_db.do_evaluation_create(**args) +def saisie_note_evaluations(formsemestre: FormSemestre, user: User): + """ + Saisie les notes des evaluations d'un semestre + """ + # Récupération des id des étudiants du semestre + list_etudids = [etud.id for etud in formsemestre.etuds] + list_ues = formsemestre.query_ues() + + def create_list_etudid_random_notes(): + """ + Retourne une liste de tuple (etudid, note_random) + """ + return [(etudid, random.uniform(0.0, 20.0)) for etudid in list_etudids] + + for ue in list_ues: + mods = ue.modules + for mod in mods: + moduleimpl = ModuleImpl.query.get_or_404(mod.id) + for evaluation in moduleimpl.evaluations: + notes = create_list_etudid_random_notes() + notes_add(user, evaluation.id, notes) + + def init_test_database(): """Appelé par la commande `flask init-test-database` @@ -201,6 +225,7 @@ def init_test_database(): formsemestre = create_formsemestre(formation, user_lecteur) create_evaluations(formsemestre) inscrit_etudiants(etuds, formsemestre) + saisie_note_evaluations(formsemestre, user_lecteur) # à compléter # - groupes # - absences From c0474f109d8da33b9b0917ad9f513c7ca9b392c2 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Fri, 3 Jun 2022 16:20:59 +0200 Subject: [PATCH 31/60] =?UTF-8?q?ajout=20de=20l'al=C3=A9atoire=20dans=20la?= =?UTF-8?q?=20saisies=20des=20notes=20(prob=20avec=20les=20dates)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fakedatabase/create_test_api_database.py | 61 +++++++++++++++++-- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index 3744fc10..41710890 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -195,21 +195,70 @@ def saisie_note_evaluations(formsemestre: FormSemestre, user: User): """ # Récupération des id des étudiants du semestre list_etudids = [etud.id for etud in formsemestre.etuds] - list_ues = formsemestre.query_ues() - def create_list_etudid_random_notes(): + def add_random_notes(evaluation_id, new_list_notes_eval=None, not_all=False): """ - Retourne une liste de tuple (etudid, note_random) + Permet d'ajouter des notes aléatoires à une évaluation """ - return [(etudid, random.uniform(0.0, 20.0)) for etudid in list_etudids] + if not_all: + percent = 80 / 100 + len_list_etudids = len(list_etudids) + new_list_etudids = random.sample( + list_etudids, k=int(percent * len_list_etudids) + ) + # new_list_etudids = [note.etudid for note in new_list_notes_eval] + list_tuple_notes = [ + (etudid, random.uniform(0.0, 20.0)) for etudid in new_list_etudids + ] + notes_add(user, evaluation_id, list_tuple_notes) + else: + list_tuple_notes = [ + (etudid, random.uniform(0.0, 20.0)) for etudid in list_etudids + ] + notes_add(user, evaluation_id, list_tuple_notes) + + def saisir_notes(evaluation_id: int, condition: int, list_notes_eval): + """ + Permet de saisir les notes de manière aléatoire suivant une condition + Définition des valeurs de condition : + 0 : all_notes_saisies + 1 : all_notes_manquantes + 2 : some_notes_manquantes + """ + if condition == 0 or condition == 2: + date_debut = formsemestre.date_debut + date_fin = formsemestre.date_fin + if condition == 0: + add_random_notes(evaluation_id) + for note in list_notes_eval: + note.date = date_debut + random.random() * (date_fin - date_debut) + db.session.add(note) + db.session.commit() + else: + percent = 80 / 100 + len_list_notes_eval = len(list_notes_eval) + new_list_notes_eval = random.sample( + list_notes_eval, k=int(percent * len_list_notes_eval) + ) + add_random_notes(evaluation_id, new_list_notes_eval, True) + for note in new_list_notes_eval: + note.date = date_debut + random.random() * (date_fin - date_debut) + db.session.add(note) + db.session.commit() + + list_ues = formsemestre.query_ues() for ue in list_ues: mods = ue.modules for mod in mods: moduleimpl = ModuleImpl.query.get_or_404(mod.id) for evaluation in moduleimpl.evaluations: - notes = create_list_etudid_random_notes() - notes_add(user, evaluation.id, notes) + # Récupération de toutes les notes de l'évaluation + notes_eval = models.NotesNotes.query.filter_by( + evaluation_id=evaluation.id + ).all() + condition_saisie_notes = random.randint(0, 2) + saisir_notes(evaluation.id, condition_saisie_notes, notes_eval) def init_test_database(): From d3e7ababd8598ba4c6639995063ae134e94c7cbe Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Wed, 8 Jun 2022 16:13:29 +0200 Subject: [PATCH 32/60] =?UTF-8?q?ajout=20de=20routes=20sp=C3=A9cifiquement?= =?UTF-8?q?=20r=C3=A9serv=C3=A9=20pour=20le=20debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/__init__.py | 1 + app/api/debug.py | 49 +++++++++++++++++++++++++++++++++++++++++++++ app/models/notes.py | 11 ++++++++++ 3 files changed, 61 insertions(+) create mode 100644 app/api/debug.py diff --git a/app/api/__init__.py b/app/api/__init__.py index 9be4d3b6..c51421f0 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -31,3 +31,4 @@ from app.api import evaluations from app.api import jury from app.api import absences from app.api import logos +from app.api import debug diff --git a/app/api/debug.py b/app/api/debug.py new file mode 100644 index 00000000..ba609407 --- /dev/null +++ b/app/api/debug.py @@ -0,0 +1,49 @@ +from app.api import bp +from app.api.auth import token_auth, token_permission_required +from app.models import NotesNotes, FormSemestre +from app.scodoc.sco_permissions import Permission +from flask import jsonify +from app import models +from app import db +import time +import random +import datetime + + +@bp.route("/create_note", methods=["GET"]) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def create_note(): + note = NotesNotes() + db.session.add(note) + db.session.commit() + + return jsonify(note.to_dict()) + + +@bp.route("/change_value_note/", methods=["GET"]) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def change_value_note(note_id: int): + note = NotesNotes.query.get_or_404(note_id) + note.value = 10 + db.session.commit() + + return jsonify(note.to_dict()) + + +@bp.route( + "/change_date_note/", methods=["GET"] +) # XXX TODO test avec notes_add() en plus +@token_auth.login_required +@token_permission_required(Permission.APIView) +def change_date_note(note_id: int): + + formsemestre = FormSemestre.query.get_or_404(1) + date_debut = formsemestre.date_debut + date_fin = formsemestre.date_fin + note = NotesNotes.query.get_or_404(note_id) + note.date = date_debut + random.random() * (date_fin - date_debut) + db.session.commit() + + return jsonify(note.to_dict()) diff --git a/app/models/notes.py b/app/models/notes.py index 6da4ef5d..7754997e 100644 --- a/app/models/notes.py +++ b/app/models/notes.py @@ -47,6 +47,17 @@ class NotesNotes(db.Model): date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) uid = db.Column(db.Integer, db.ForeignKey("user.id")) + def to_dict(self): + return { + "id": self.id, + "etudid": self.etudid, + "evaluation_id": self.evaluation_id, + "value": self.value, + "comment": self.comment, + "date": self.date, + "uid": self.uid, + } + class NotesNotesLog(db.Model): """Historique des modifs sur notes (anciennes entrees de notes_notes)""" From 25422b7f81796bf489860a11934c7aa2e9de1ec2 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Thu, 9 Jun 2022 11:08:08 +0200 Subject: [PATCH 33/60] =?UTF-8?q?correction=20bug=20g=C3=A9n=C3=A9ration?= =?UTF-8?q?=20date=20random=20pour=20tests=20unitaires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/notes.py | 8 ++ .../fakedatabase/create_test_api_database.py | 92 +++++++++---------- 2 files changed, 52 insertions(+), 48 deletions(-) diff --git a/app/models/notes.py b/app/models/notes.py index 7754997e..a5e90be1 100644 --- a/app/models/notes.py +++ b/app/models/notes.py @@ -47,6 +47,14 @@ class NotesNotes(db.Model): date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) uid = db.Column(db.Integer, db.ForeignKey("user.id")) + def __init__(self, etudid, evaluation_id, value, comment, date, uid): + self.etudid = etudid + self.evaluation_id = evaluation_id + self.value = value + self.comment = comment + self.date = date + self.uid = uid + def to_dict(self): return { "id": self.id, diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index 41710890..0195a82d 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -10,7 +10,7 @@ FLASK_DEBUG=1 2) En tant qu'utilisateur scodoc, lancer: - tools/create_database.sh SCODOC_TEST_API + tools/create_database.sh SCODOC_TEST_API_EVAL flask db upgrade flask sco-db-init --erase flask init-test-database @@ -24,11 +24,19 @@ """ import datetime import random +import time import sys from app.auth.models import Role, User from app import models -from app.models import Departement, Formation, FormSemestre, Identite, ModuleImpl +from app.models import ( + Departement, + Formation, + FormSemestre, + Identite, + ModuleImpl, + NotesNotes, +) from app import db from app.scodoc import ( sco_cache, @@ -189,35 +197,21 @@ def create_evaluations(formsemestre: FormSemestre): evaluation_id = sco_evaluation_db.do_evaluation_create(**args) -def saisie_note_evaluations(formsemestre: FormSemestre, user: User): +def saisie_notes_evaluations(formsemestre: FormSemestre, user: User): """ Saisie les notes des evaluations d'un semestre """ - # Récupération des id des étudiants du semestre - list_etudids = [etud.id for etud in formsemestre.etuds] + etuds = formsemestre.etuds + list_etuds = [] + for etu in etuds: + list_etuds.append(etu) - def add_random_notes(evaluation_id, new_list_notes_eval=None, not_all=False): - """ - Permet d'ajouter des notes aléatoires à une évaluation - """ - if not_all: - percent = 80 / 100 - len_list_etudids = len(list_etudids) - new_list_etudids = random.sample( - list_etudids, k=int(percent * len_list_etudids) - ) - # new_list_etudids = [note.etudid for note in new_list_notes_eval] - list_tuple_notes = [ - (etudid, random.uniform(0.0, 20.0)) for etudid in new_list_etudids - ] - notes_add(user, evaluation_id, list_tuple_notes) - else: - list_tuple_notes = [ - (etudid, random.uniform(0.0, 20.0)) for etudid in list_etudids - ] - notes_add(user, evaluation_id, list_tuple_notes) + date_debut = formsemestre.date_debut + date_fin = formsemestre.date_fin - def saisir_notes(evaluation_id: int, condition: int, list_notes_eval): + list_ues = formsemestre.query_ues() + + def saisir_notes(evaluation_id: int, condition: int): """ Permet de saisir les notes de manière aléatoire suivant une condition Définition des valeurs de condition : @@ -226,39 +220,41 @@ def saisie_note_evaluations(formsemestre: FormSemestre, user: User): 2 : some_notes_manquantes """ if condition == 0 or condition == 2: - date_debut = formsemestre.date_debut - date_fin = formsemestre.date_fin if condition == 0: - add_random_notes(evaluation_id) - for note in list_notes_eval: - note.date = date_debut + random.random() * (date_fin - date_debut) + for etu in list_etuds: + note = NotesNotes( + etu.id, + evaluation_id, + random.uniform(0, 20), + "", + date_debut + random.random() * (date_fin - date_debut), + user.id, + ) db.session.add(note) - db.session.commit() + db.session.commit() else: percent = 80 / 100 - len_list_notes_eval = len(list_notes_eval) - new_list_notes_eval = random.sample( - list_notes_eval, k=int(percent * len_list_notes_eval) - ) - add_random_notes(evaluation_id, new_list_notes_eval, True) - for note in new_list_notes_eval: - note.date = date_debut + random.random() * (date_fin - date_debut) + len_etuds = len(list_etuds) + new_list_etuds = random.sample(list_etuds, k=int(percent * len_etuds)) + for etu in new_list_etuds: + note = NotesNotes( + etu.id, + evaluation_id, + random.uniform(0, 20), + "", + date_debut + random.random() * (date_fin - date_debut), + user.id, + ) db.session.add(note) - db.session.commit() - - list_ues = formsemestre.query_ues() + db.session.commit() for ue in list_ues: mods = ue.modules for mod in mods: moduleimpl = ModuleImpl.query.get_or_404(mod.id) for evaluation in moduleimpl.evaluations: - # Récupération de toutes les notes de l'évaluation - notes_eval = models.NotesNotes.query.filter_by( - evaluation_id=evaluation.id - ).all() condition_saisie_notes = random.randint(0, 2) - saisir_notes(evaluation.id, condition_saisie_notes, notes_eval) + saisir_notes(evaluation.id, condition_saisie_notes) def init_test_database(): @@ -274,7 +270,7 @@ def init_test_database(): formsemestre = create_formsemestre(formation, user_lecteur) create_evaluations(formsemestre) inscrit_etudiants(etuds, formsemestre) - saisie_note_evaluations(formsemestre, user_lecteur) + saisie_notes_evaluations(formsemestre, user_lecteur) # à compléter # - groupes # - absences From d245030b659ff71b2b48afffc98926c78fdb7319 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Thu, 9 Jun 2022 15:43:19 +0200 Subject: [PATCH 34/60] ajout des dates au format iso --- app/api/formsemestres.py | 45 ++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index c1ec6e00..bdcb4616 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -7,15 +7,11 @@ import app from app import models from app.api import bp from app.api.auth import token_auth, token_permission_required -from app.api.tools import calculate_median from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults from app.comp.res_compat import NotesTableCompat -from app.models import Departement, FormSemestre, FormSemestreEtape, Module, ModuleImpl -from app.scodoc import sco_evaluation_db +from app.models import Departement, FormSemestre, FormSemestreEtape, ModuleImpl from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json -from app.scodoc.sco_evaluation_recap import evaluations_recap_table -from app.scodoc.sco_evaluations import do_evaluation_etat_in_sem, do_evaluation_etat from app.scodoc.sco_groups import get_etud_groups from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import ModuleType @@ -494,6 +490,7 @@ def etat_evals(formsemestre_id: int): "description": null, "datetime_epreuve": null, "heure_fin": "09:00:00", + "coefficient": "02.00" "comptee": "oui", "inscrits": 16, "manquantes": 0, @@ -501,9 +498,9 @@ def etat_evals(formsemestre_id: int): "ATT": 0, "EXC": 0, "saisie_notes": { - "datetime_debut": "Wed, 01 Jun 2022 10:37:59 GMT", - "datetime_fin": "Wed, 01 Jun 2022 13:18:09 GMT", - "datetime_mediane": "Wed, 01 Jun 2022 12:38:57 GMT" + "datetime_debut": "2021-09-11T00:00:00+02:00", + "datetime_fin": "2022-08-25T00:00:00+02:00", + "datetime_mediane": "2022-03-19T00:00:00+01:00" } }, { @@ -518,9 +515,9 @@ def etat_evals(formsemestre_id: int): "ATT": 0, "EXC": 0, "saisie_notes": { - "datetime_debut": "Wed, 01 Jun 2022 12:04:19 GMT", - "datetime_fin": "Wed, 01 Jun 2022 12:04:36 GMT", - "datetime_mediane": "Wed, 01 Jun 2022 14:04:27 GMT" + "datetime_debut": "2021-09-11T00:00:00+02:00", + "datetime_fin": "2022-08-25T00:00:00+02:00", + "datetime_mediane": "2022-03-19T00:00:00+01:00" } }, ] @@ -562,8 +559,11 @@ def etat_evals(formsemestre_id: int): eval["id"] = evaluation.id eval["description"] = evaluation.description - eval["datetime_epreuve"] = evaluation.jour + eval["datetime_epreuve"] = ( + evaluation.jour.isoformat() if evaluation.jour is not None else None + ) eval["heure_fin"] = evaluation.heure_fin.isoformat() + eval["coefficient"] = evaluation.coefficient eval["comptee"] = "oui" if eval_etat.is_complete else "non" eval["inscrits"] = modimpl_results.nb_inscrits_module eval["manquantes"] = len( @@ -582,6 +582,10 @@ def etat_evals(formsemestre_id: int): evaluation_id=evaluation.id ).all() + date_debut = None + date_fin = None + date_mediane = None + # Si il y a plus d'une note saisie pour l'évaluation if len(notes) >= 1: # Trie des notes en fonction de leurs dates @@ -598,19 +602,20 @@ def etat_evals(formsemestre_id: int): int((len(list_id_notes_sorted)) / 2) ] - date_mediane = "" for n in notes_sorted: if n.id == id_mediane: date_mediane = n.date - else: - date_debut = None - date_fin = None - date_mediane = None eval["saisie_notes"] = { - "datetime_debut": date_debut, - "datetime_fin": date_fin, - "datetime_mediane": date_mediane, + "datetime_debut": date_debut.isoformat() + if date_debut is not None + else None, + "datetime_fin": date_fin.isoformat() + if date_fin is not None + else None, + "datetime_mediane": date_mediane.isoformat() + if date_mediane is not None + else None, } list_eval.append(eval) From d0e179fb7cb3e8b8d0366b5808c1658ed7c19ba3 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Thu, 9 Jun 2022 15:48:24 +0200 Subject: [PATCH 35/60] reset du fichier debug.py --- app/api/debug.py | 76 ++++++++++++++++++------------------------------ 1 file changed, 29 insertions(+), 47 deletions(-) diff --git a/app/api/debug.py b/app/api/debug.py index ba609407..c2ed2eb5 100644 --- a/app/api/debug.py +++ b/app/api/debug.py @@ -1,49 +1,31 @@ -from app.api import bp -from app.api.auth import token_auth, token_permission_required -from app.models import NotesNotes, FormSemestre -from app.scodoc.sco_permissions import Permission -from flask import jsonify -from app import models -from app import db -import time -import random -import datetime +# -*- mode: python -*- +# -*- coding: utf-8 -*- +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# +# 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 2 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. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## -@bp.route("/create_note", methods=["GET"]) -@token_auth.login_required -@token_permission_required(Permission.APIView) -def create_note(): - note = NotesNotes() - db.session.add(note) - db.session.commit() - - return jsonify(note.to_dict()) - - -@bp.route("/change_value_note/", methods=["GET"]) -@token_auth.login_required -@token_permission_required(Permission.APIView) -def change_value_note(note_id: int): - note = NotesNotes.query.get_or_404(note_id) - note.value = 10 - db.session.commit() - - return jsonify(note.to_dict()) - - -@bp.route( - "/change_date_note/", methods=["GET"] -) # XXX TODO test avec notes_add() en plus -@token_auth.login_required -@token_permission_required(Permission.APIView) -def change_date_note(note_id: int): - - formsemestre = FormSemestre.query.get_or_404(1) - date_debut = formsemestre.date_debut - date_fin = formsemestre.date_fin - note = NotesNotes.query.get_or_404(note_id) - note.date = date_debut + random.random() * (date_fin - date_debut) - db.session.commit() - - return jsonify(note.to_dict()) +""" +!!! ATTENTION !!! +Fichier a utilisé uniquement à des fins de debug +""" From 39b913adfd3187922f0339a5cd5f9660e437e906 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Fri, 10 Jun 2022 16:35:28 +0200 Subject: [PATCH 36/60] ajout de tests unitaire pour la route etat_evals --- tests/api/test_api_formsemestre.py | 167 ++++++++++++++++++++++++++++- 1 file changed, 165 insertions(+), 2 deletions(-) diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py index 956992b9..4966692a 100644 --- a/tests/api/test_api_formsemestre.py +++ b/tests/api/test_api_formsemestre.py @@ -21,7 +21,12 @@ import requests from app.api.formsemestres import formsemestre from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers -from tests.api.tools_test_api import MODIMPL_FIELDS, verify_fields +from tests.api.tools_test_api import ( + MODIMPL_FIELDS, + verify_fields, + EVAL_FIELDS, + SAISIE_NOTES_FIELDS, +) from tests.api.tools_test_api import FSEM_FIELDS, UE_FIELDS, MODULE_FIELDS # Etudiant pour les tests @@ -159,7 +164,9 @@ def test_formsemestre_programme(api_headers): # assert r.status_code == 200 -def test_etat_evals(api_headers): +def test_etat_evals( + api_headers, +): # XXX TODO Vérifier les champs, vérifier qu'il n'y a pas de répétition, vérifier que les dates de "saisie_notes" sont corrects (date_debut avant celle de fin mediane OK etc..) """ Route : /formsemestre//etat_evals """ @@ -169,3 +176,159 @@ def test_etat_evals(api_headers): verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 + + etat_evals = r.json() + + assert len(etat_evals) == 3 + + ue = etat_evals[0] + assert len(ue) == 3 + assert isinstance(ue["id"], int) + assert isinstance(ue["titre"], str) + assert isinstance(ue["evaluations"], list) + + eval = ue["evaluations"][0] + assert len(eval) == 11 + assert verify_fields(eval, EVAL_FIELDS) + assert isinstance(eval["id"], int) + assert eval["description"] is None or isinstance(eval["description"], str) + assert isinstance(eval["datetime_epreuve"], str) + assert isinstance(eval["heure_fin"], str) + assert isinstance(eval["coef"], float) + assert isinstance(eval["comptee"], str) + assert isinstance(eval["inscrits"], int) + assert isinstance(eval["manquantes"], int) + assert isinstance(eval["ABS"], int) + assert isinstance(eval["ATT"], int) + assert isinstance(eval["EXC"], int) + assert isinstance(eval["saisie_notes"], list) + + list_eval_id = [e["id"] for e in ue["evaluations"]] + all_unique = True + for id in list_eval_id: + if list_eval_id.count(id) > 1: + all_unique = False + assert all_unique is True + + saisie_notes = eval["saisie_notes"] + assert verify_fields(saisie_notes, SAISIE_NOTES_FIELDS) + assert isinstance(eval["saisie_notes"]["datetime_debut"], str) + assert isinstance(eval["saisie_notes"]["datetime_fin"], str) + assert isinstance(eval["saisie_notes"]["datetime_mediane"], str) + assert ( + eval["saisie_notes"]["datetime_fin"] > eval["saisie_notes"]["datetime_mediane"] + ) + assert eval["saisie_notes"]["datetime_fin"] > eval["saisie_notes"]["datetime_debut"] + assert ( + eval["saisie_notes"]["datetime_mediane"] + > eval["saisie_notes"]["datetime_debut"] + ) + + ue2 = etat_evals[1] + assert len(ue2) == 3 + assert isinstance(ue2["id"], int) + assert isinstance(ue2["titre"], str) + assert isinstance(ue2["evaluations"], list) + + eval2 = ue2["evaluations"][0] + assert len(eval2) == 11 + assert verify_fields(eval2, EVAL_FIELDS) + assert isinstance(eval2["id"], int) + assert eval2["description"] is None or isinstance(eval2["description"], str) + assert isinstance(eval2["datetime_epreuve"], str) + assert isinstance(eval2["heure_fin"], str) + assert isinstance(eval2["coef"], float) + assert isinstance(eval2["comptee"], str) + assert isinstance(eval2["inscrits"], int) + assert isinstance(eval2["manquantes"], int) + assert isinstance(eval2["ABS"], int) + assert isinstance(eval2["ATT"], int) + assert isinstance(eval2["EXC"], int) + assert isinstance(eval2["saisie_notes"], list) + + list_eval_id2 = [e["id"] for e in ue["evaluations"]] + all_unique2 = True + for id in list_eval_id2: + if list_eval_id2.count(id) > 1: + all_unique2 = False + assert all_unique2 is True + + saisie_notes2 = eval2["saisie_notes"] + assert verify_fields(saisie_notes2, SAISIE_NOTES_FIELDS) + assert isinstance(eval2["saisie_notes"]["datetime_debut"], str) + assert isinstance(eval2["saisie_notes"]["datetime_fin"], str) + assert isinstance(eval2["saisie_notes"]["datetime_mediane"], str) + assert ( + eval2["saisie_notes"]["datetime_fin"] + > eval2["saisie_notes"]["datetime_mediane"] + ) + assert ( + eval2["saisie_notes"]["datetime_fin"] > eval2["saisie_notes"]["datetime_debut"] + ) + assert ( + eval2["saisie_notes"]["datetime_mediane"] + > eval2["saisie_notes"]["datetime_debut"] + ) + + ue3 = etat_evals[2] + assert len(ue3) == 3 + assert isinstance(ue3["id"], int) + assert isinstance(ue3["titre"], str) + assert isinstance(ue3["evaluations"], list) + + eval3 = ue3["evaluations"][0] + assert len(eval3) == 11 + assert verify_fields(eval3, EVAL_FIELDS) + assert isinstance(eval3["id"], int) + assert eval3["description"] is None or isinstance(eval3["description"], str) + assert isinstance(eval3["datetime_epreuve"], str) + assert isinstance(eval3["heure_fin"], str) + assert isinstance(eval3["coef"], float) + assert isinstance(eval3["comptee"], str) + assert isinstance(eval3["inscrits"], int) + assert isinstance(eval3["manquantes"], int) + assert isinstance(eval3["ABS"], int) + assert isinstance(eval3["ATT"], int) + assert isinstance(eval3["EXC"], int) + assert isinstance(eval3["saisie_notes"], list) + + list_eval_id3 = [e["id"] for e in ue["evaluations"]] + all_unique3 = True + for id in list_eval_id3: + if list_eval_id3.count(id) > 1: + all_unique3 = False + assert all_unique3 is True + + saisie_notes3 = eval2["saisie_notes"] + assert verify_fields(saisie_notes3, SAISIE_NOTES_FIELDS) + assert isinstance(eval3["saisie_notes"]["datetime_debut"], str) + assert isinstance(eval3["saisie_notes"]["datetime_fin"], str) + assert isinstance(eval3["saisie_notes"]["datetime_mediane"], str) + assert ( + eval3["saisie_notes"]["datetime_fin"] + > eval3["saisie_notes"]["datetime_mediane"] + ) + assert ( + eval3["saisie_notes"]["datetime_fin"] > eval3["saisie_notes"]["datetime_debut"] + ) + assert ( + eval3["saisie_notes"]["datetime_mediane"] + > eval3["saisie_notes"]["datetime_debut"] + ) + + assert ue["id"] != ue2["id"] + assert ue["id"] != ue3["id"] + assert ue2["id"] != ue3["id"] + + assert ue["titre"] != ue2["titre"] + assert ue["titre"] != ue3["titre"] + assert ue2["titre"] != ue3["titre"] + + ##### ERROR ##### + fake_eval_id = 153165161656849846516511321651651 + r = requests.get( + f"{API_URL}/formsemestre/{fake_eval_id}/etat_evals", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 404 From 55df67568764fd163cb01cc19016383ed54abc4f Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Mon, 13 Jun 2022 16:27:30 +0200 Subject: [PATCH 37/60] correction + factorisation des tests de etat_evals --- tests/api/test_api_formsemestre.py | 217 +++++++++++------------------ 1 file changed, 83 insertions(+), 134 deletions(-) diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py index 4966692a..a073e806 100644 --- a/tests/api/test_api_formsemestre.py +++ b/tests/api/test_api_formsemestre.py @@ -166,7 +166,7 @@ def test_formsemestre_programme(api_headers): def test_etat_evals( api_headers, -): # XXX TODO Vérifier les champs, vérifier qu'il n'y a pas de répétition, vérifier que les dates de "saisie_notes" sont corrects (date_debut avant celle de fin mediane OK etc..) +): """ Route : /formsemestre//etat_evals """ @@ -181,148 +181,97 @@ def test_etat_evals( assert len(etat_evals) == 3 - ue = etat_evals[0] - assert len(ue) == 3 - assert isinstance(ue["id"], int) - assert isinstance(ue["titre"], str) - assert isinstance(ue["evaluations"], list) + for ue in etat_evals.values(): + for module in ue: + assert isinstance(module["id"], int) + assert isinstance(module["titre"], str) + assert isinstance(module["evaluations"], list) - eval = ue["evaluations"][0] - assert len(eval) == 11 - assert verify_fields(eval, EVAL_FIELDS) - assert isinstance(eval["id"], int) - assert eval["description"] is None or isinstance(eval["description"], str) - assert isinstance(eval["datetime_epreuve"], str) - assert isinstance(eval["heure_fin"], str) - assert isinstance(eval["coef"], float) - assert isinstance(eval["comptee"], str) - assert isinstance(eval["inscrits"], int) - assert isinstance(eval["manquantes"], int) - assert isinstance(eval["ABS"], int) - assert isinstance(eval["ATT"], int) - assert isinstance(eval["EXC"], int) - assert isinstance(eval["saisie_notes"], list) + for eval in module["evaluations"]: + assert verify_fields(eval, EVAL_FIELDS) + assert isinstance(eval["id"], int) + assert eval["description"] is None or isinstance( + eval["description"], str + ) + assert eval["datetime_epreuve"] is None or isinstance( + eval["datetime_epreuve"], str + ) + assert isinstance(eval["heure_fin"], str) + assert isinstance(eval["coefficient"], float) + assert isinstance(eval["comptee"], str) + assert isinstance(eval["inscrits"], int) + assert isinstance(eval["manquantes"], int) + assert isinstance(eval["ABS"], int) + assert isinstance(eval["ATT"], int) + assert isinstance(eval["EXC"], int) + assert isinstance(eval["saisie_notes"], dict) - list_eval_id = [e["id"] for e in ue["evaluations"]] - all_unique = True - for id in list_eval_id: - if list_eval_id.count(id) > 1: - all_unique = False - assert all_unique is True + list_eval_id = [e["id"] for e in module["evaluations"]] + all_unique = True + for id in list_eval_id: + if list_eval_id.count(id) > 1: + all_unique = False + assert all_unique is True - saisie_notes = eval["saisie_notes"] - assert verify_fields(saisie_notes, SAISIE_NOTES_FIELDS) - assert isinstance(eval["saisie_notes"]["datetime_debut"], str) - assert isinstance(eval["saisie_notes"]["datetime_fin"], str) - assert isinstance(eval["saisie_notes"]["datetime_mediane"], str) - assert ( - eval["saisie_notes"]["datetime_fin"] > eval["saisie_notes"]["datetime_mediane"] - ) - assert eval["saisie_notes"]["datetime_fin"] > eval["saisie_notes"]["datetime_debut"] - assert ( - eval["saisie_notes"]["datetime_mediane"] - > eval["saisie_notes"]["datetime_debut"] - ) + saisie_notes = eval["saisie_notes"] + assert verify_fields(saisie_notes, SAISIE_NOTES_FIELDS) + assert eval["saisie_notes"]["datetime_debut"] is None or isinstance( + eval["saisie_notes"]["datetime_debut"], str + ) + assert eval["saisie_notes"]["datetime_debut"] is None or isinstance( + eval["saisie_notes"]["datetime_fin"], str + ) + assert eval["saisie_notes"]["datetime_debut"] is None or isinstance( + eval["saisie_notes"]["datetime_mediane"], str + ) - ue2 = etat_evals[1] - assert len(ue2) == 3 - assert isinstance(ue2["id"], int) - assert isinstance(ue2["titre"], str) - assert isinstance(ue2["evaluations"], list) + if ( + eval["saisie_notes"]["datetime_fin"] is not None + and eval["saisie_notes"]["datetime_mediane"] is not None + and eval["saisie_notes"]["datetime_debut"] is not None + ): + assert ( + eval["saisie_notes"]["datetime_fin"] + > eval["saisie_notes"]["datetime_mediane"] + ) + assert ( + eval["saisie_notes"]["datetime_fin"] + > eval["saisie_notes"]["datetime_debut"] + ) + assert ( + eval["saisie_notes"]["datetime_mediane"] + > eval["saisie_notes"]["datetime_debut"] + ) - eval2 = ue2["evaluations"][0] - assert len(eval2) == 11 - assert verify_fields(eval2, EVAL_FIELDS) - assert isinstance(eval2["id"], int) - assert eval2["description"] is None or isinstance(eval2["description"], str) - assert isinstance(eval2["datetime_epreuve"], str) - assert isinstance(eval2["heure_fin"], str) - assert isinstance(eval2["coef"], float) - assert isinstance(eval2["comptee"], str) - assert isinstance(eval2["inscrits"], int) - assert isinstance(eval2["manquantes"], int) - assert isinstance(eval2["ABS"], int) - assert isinstance(eval2["ATT"], int) - assert isinstance(eval2["EXC"], int) - assert isinstance(eval2["saisie_notes"], list) + list_id_ue1 = [] + list_titre_ue1 = [] - list_eval_id2 = [e["id"] for e in ue["evaluations"]] - all_unique2 = True - for id in list_eval_id2: - if list_eval_id2.count(id) > 1: - all_unique2 = False - assert all_unique2 is True + list_id_ue2 = [] + list_titre_ue2 = [] - saisie_notes2 = eval2["saisie_notes"] - assert verify_fields(saisie_notes2, SAISIE_NOTES_FIELDS) - assert isinstance(eval2["saisie_notes"]["datetime_debut"], str) - assert isinstance(eval2["saisie_notes"]["datetime_fin"], str) - assert isinstance(eval2["saisie_notes"]["datetime_mediane"], str) - assert ( - eval2["saisie_notes"]["datetime_fin"] - > eval2["saisie_notes"]["datetime_mediane"] - ) - assert ( - eval2["saisie_notes"]["datetime_fin"] > eval2["saisie_notes"]["datetime_debut"] - ) - assert ( - eval2["saisie_notes"]["datetime_mediane"] - > eval2["saisie_notes"]["datetime_debut"] - ) + list_id_ue3 = [] + list_titre_ue3 = [] + i = 0 + for ue in etat_evals.values(): + i += 1 + for module in ue: + if i == 1: + list_id_ue1.append(module["id"]) + list_titre_ue1.append(module["id"]) + elif i == 2: + list_id_ue2.append(module["id"]) + list_titre_ue2.append(module["id"]) + elif i == 3: + list_id_ue3.append(module["id"]) + list_titre_ue3.append(module["id"]) - ue3 = etat_evals[2] - assert len(ue3) == 3 - assert isinstance(ue3["id"], int) - assert isinstance(ue3["titre"], str) - assert isinstance(ue3["evaluations"], list) + assert list_id_ue1 != list_id_ue2 + assert list_id_ue1 != list_titre_ue3 + assert list_id_ue2 != list_titre_ue3 - eval3 = ue3["evaluations"][0] - assert len(eval3) == 11 - assert verify_fields(eval3, EVAL_FIELDS) - assert isinstance(eval3["id"], int) - assert eval3["description"] is None or isinstance(eval3["description"], str) - assert isinstance(eval3["datetime_epreuve"], str) - assert isinstance(eval3["heure_fin"], str) - assert isinstance(eval3["coef"], float) - assert isinstance(eval3["comptee"], str) - assert isinstance(eval3["inscrits"], int) - assert isinstance(eval3["manquantes"], int) - assert isinstance(eval3["ABS"], int) - assert isinstance(eval3["ATT"], int) - assert isinstance(eval3["EXC"], int) - assert isinstance(eval3["saisie_notes"], list) - - list_eval_id3 = [e["id"] for e in ue["evaluations"]] - all_unique3 = True - for id in list_eval_id3: - if list_eval_id3.count(id) > 1: - all_unique3 = False - assert all_unique3 is True - - saisie_notes3 = eval2["saisie_notes"] - assert verify_fields(saisie_notes3, SAISIE_NOTES_FIELDS) - assert isinstance(eval3["saisie_notes"]["datetime_debut"], str) - assert isinstance(eval3["saisie_notes"]["datetime_fin"], str) - assert isinstance(eval3["saisie_notes"]["datetime_mediane"], str) - assert ( - eval3["saisie_notes"]["datetime_fin"] - > eval3["saisie_notes"]["datetime_mediane"] - ) - assert ( - eval3["saisie_notes"]["datetime_fin"] > eval3["saisie_notes"]["datetime_debut"] - ) - assert ( - eval3["saisie_notes"]["datetime_mediane"] - > eval3["saisie_notes"]["datetime_debut"] - ) - - assert ue["id"] != ue2["id"] - assert ue["id"] != ue3["id"] - assert ue2["id"] != ue3["id"] - - assert ue["titre"] != ue2["titre"] - assert ue["titre"] != ue3["titre"] - assert ue2["titre"] != ue3["titre"] + assert list_titre_ue1 != list_titre_ue2 + assert list_titre_ue1 != list_titre_ue3 + assert list_titre_ue2 != list_titre_ue3 ##### ERROR ##### fake_eval_id = 153165161656849846516511321651651 From 5834412dfc7e77abf9cc15498dc10ef14255c7ca Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Wed, 15 Jun 2022 16:01:00 +0200 Subject: [PATCH 38/60] ajout tests unitaires formation --- tests/api/test_api_formations.py | 194 ++++++++++++++++++++++++++++--- 1 file changed, 178 insertions(+), 16 deletions(-) diff --git a/tests/api/test_api_formations.py b/tests/api/test_api_formations.py index b61037da..b98a31e8 100644 --- a/tests/api/test_api_formations.py +++ b/tests/api/test_api_formations.py @@ -20,7 +20,15 @@ Utilisation : import requests from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers -from tests.api.tools_test_api import verify_fields +from tests.api.tools_test_api import ( + verify_fields, + FORMATION_EXPORT_FIELDS, + FORMATION_EXPORT_UE_FIELDS, + FORMATION_EXPORT_UE_MATIERE_FIELDS, + FORMATION_EXPORT_UE_MATIERE_MODULE_FIELDS, + FORMATION_EXPORT_UE_MATIERE_MODULE_COEF_FIELDS, + MODULE_FIELDS, +) from tests.api.tools_test_api import FORMATION_FIELDS, MODIMPL_FIELDS @@ -45,15 +53,52 @@ def test_formations_by_id(api_headers): """ Route: /formation/ """ + id_formation = 1 r = requests.get( - API_URL + "/formation/1", + f"{API_URL}/formation/{id_formation}", headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 formation = r.json() assert verify_fields(formation, FORMATION_FIELDS) is True - # TODO tester le contenu de certains champs + assert isinstance(formation["dept_id"], int) + assert isinstance(formation["acronyme"], str) + assert isinstance(formation["titre_officiel"], str) + assert isinstance(formation["formation_code"], str) + assert formation["code_specialite"] is None or isinstance( + formation["code_specialite"], str + ) + assert isinstance(formation["id"], int) + assert isinstance(formation["titre"], str) + assert isinstance(formation["version"], int) + assert isinstance(formation["type_parcours"], int) + assert formation["referentiel_competence_id"] is None or isinstance( + formation["referentiel_competence_id"], int + ) + assert isinstance(formation["formation_id"], int) + + assert id_formation == formation["formation_id"] + assert id_formation == formation["id"] + + r1 = requests.get( + f"{API_URL}/formation/{formation['formation_id']}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r1.status_code == 200 + formation1 = r1.json() + + assert formation == formation1 + + # ERROR + id_formation_inexistant = 1516476846861656351 + r_error = requests.get( + f"{API_URL}/formation/{id_formation_inexistant}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r_error.status_code == 404 def test_formation_export(api_headers): @@ -67,33 +112,150 @@ def test_formation_export(api_headers): ) assert r.status_code == 200 export_formation = r.json() - assert verify_fields(export_formation, FORMATION_FIELDS) is True - # TODO tester le contenu de certains champs + assert verify_fields(export_formation, FORMATION_EXPORT_FIELDS) is True + assert isinstance(export_formation["dept_id"], int) + assert isinstance(export_formation["acronyme"], str) + assert isinstance(export_formation["titre_officiel"], str) + assert isinstance(export_formation["formation_code"], str) + assert export_formation["code_specialite"] is None or isinstance( + export_formation["code_specialite"], str + ) + assert isinstance(export_formation["id"], int) + assert isinstance(export_formation["titre"], str) + assert isinstance(export_formation["version"], int) + assert isinstance(export_formation["type_parcours"], int) + assert export_formation["referentiel_competence_id"] is None or isinstance( + export_formation["referentiel_competence_id"], int + ) + assert isinstance(export_formation["formation_id"], int) + assert isinstance(export_formation["ue"], list) + ues = export_formation["ue"] -# TODO -# def test_formsemestre_apo(api_headers): -# r = requests.get( -# API_URL + "/formation/apo/", -# headers=api_headers, -# verify=CHECK_CERTIFICATE, -# ) -# assert r.status_code == 200 + for ue in ues: + assert verify_fields(ue, FORMATION_EXPORT_UE_FIELDS) is True + assert isinstance(ue["acronyme"], str) + assert isinstance(ue["numero"], int) + assert isinstance(ue["titre"], str) + assert isinstance(ue["type"], int) + assert isinstance(ue["ue_code"], str) + assert isinstance(ue["ects"], float) + assert isinstance(ue["is_external"], bool) + assert isinstance(ue["code_apogee"], str) + assert isinstance(ue["coefficient"], float) + assert isinstance(ue["semestre_idx"], int) + assert isinstance(ue["color"], str) + assert isinstance(ue["reference"], int) + assert isinstance(ue["matiere"], list) + + matieres = ue["matiere"] + + for matiere in matieres: + assert verify_fields(matiere, FORMATION_EXPORT_UE_MATIERE_FIELDS) + assert isinstance(matiere["titre"], str) + assert isinstance(matiere["numero"], int) + assert isinstance(matiere["module"], list) + + modules = matiere["module"] + for module in modules: + assert verify_fields(module, FORMATION_EXPORT_UE_MATIERE_MODULE_FIELDS) + assert isinstance(module["titre"], str) + assert isinstance(module["abbrev"], str) + assert isinstance(module["code"], str) + assert isinstance(module["heures_cours"], float) + assert isinstance(module["heures_td"], float) + assert isinstance(module["heures_tp"], float) + assert isinstance(module["coefficient"], float) + assert isinstance(module["ects"], str) + assert isinstance(module["semestre_id"], int) + assert isinstance(module["numero"], int) + assert isinstance(module["code_apogee"], str) + assert isinstance(module["module_type"], int) + assert isinstance(module["coefficients"], list) + + coefficients = module["coefficients"] + for coef in coefficients: + assert verify_fields( + coef, FORMATION_EXPORT_UE_MATIERE_MODULE_COEF_FIELDS + ) + assert isinstance(coef["ue_reference"], str) + assert isinstance(coef["coef"], str) + + # ERROR + id_formation_inexistant = 1516476846861656351 + r_error = requests.get( + f"{API_URL}/formation/formation_export/{id_formation_inexistant}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r_error.status_code == 404 def test_moduleimpl(api_headers): """ Route: /formation/moduleimpl/ """ + moduleimpl_id = 1 r = requests.get( - API_URL + "/formation/moduleimpl/1", + f"{API_URL}/formation/moduleimpl/{moduleimpl_id}", headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 moduleimpl = r.json() assert verify_fields(moduleimpl, MODIMPL_FIELDS) is True - # TODO tester le contenu de certains champs + assert isinstance(moduleimpl["id"], int) + assert isinstance(moduleimpl["responsable_id"], int) + assert isinstance(moduleimpl["module_id"], int) + assert isinstance(moduleimpl["formsemestre_id"], int) + assert moduleimpl["computation_expr"] is None or isinstance( + moduleimpl["computation_expr"], str + ) + assert isinstance(moduleimpl["moduleimpl_id"], int) + assert isinstance(moduleimpl["ens"], list) + assert isinstance(moduleimpl["module"], dict) + + module = moduleimpl["module"] + assert verify_fields(module, MODULE_FIELDS) + assert isinstance(module["heures_cours"], float) + assert isinstance(module["semestre_id"], int) + assert isinstance(module["heures_td"], float) + assert isinstance(module["numero"], int) + assert isinstance(module["heures_tp"], float) + assert isinstance(module["code_apogee"], str) + assert isinstance(module["titre"], str) + assert isinstance(module["coefficient"], float) + assert isinstance(module["module_type"], int) + assert isinstance(module["id"], int) + assert module["ects"] is None or isinstance(module["ects"], str) + assert isinstance(module["abbrev"], str) + assert isinstance(module["ue_id"], int) + assert isinstance(module["code"], str) + assert isinstance(module["formation_id"], int) + assert isinstance(module["matiere_id"], int) + assert isinstance(module["module_id"], int) + + assert moduleimpl_id == moduleimpl["id"] + assert moduleimpl_id == moduleimpl["moduleimpl_id"] + + r1 = requests.get( + f"{API_URL}/formation/moduleimpl/{moduleimpl['moduleimpl_id']}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r1.status_code == 200 + moduleimpl1 = r1.json() + + assert moduleimpl == moduleimpl1 + + # ERROR + id_formation_inexistant = 1516476846861656351 + r_error = requests.get( + f"{API_URL}/formation/moduleimpl/{id_formation_inexistant}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r_error.status_code == 404 def test_referentiel_competences(api_headers): @@ -106,4 +268,4 @@ def test_referentiel_competences(api_headers): verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - # XXX A compléter + # XXX TODO ajouter un referentiel competence dans la base de test From 281539dd3b6ffc55e397546668440697d0450060 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Wed, 15 Jun 2022 16:01:48 +0200 Subject: [PATCH 39/60] ajout tests unitaires absences --- tests/api/test_api_absences.py | 156 ++++++++++++++++++++++++++++++--- 1 file changed, 143 insertions(+), 13 deletions(-) diff --git a/tests/api/test_api_absences.py b/tests/api/test_api_absences.py index 696ca043..638b58fe 100644 --- a/tests/api/test_api_absences.py +++ b/tests/api/test_api_absences.py @@ -21,6 +21,12 @@ import requests from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers # Etudiant pour les tests +from tests.api.tools_test_api import ( + verify_fields, + ABSENCES_FIELDS, + ABSENCES_GROUP_ETAT_FIELDS, +) + ETUDID = 1 @@ -29,10 +35,8 @@ def test_absences(api_headers): """ Test 'absences' - Routes : + Route : - /absences/etudid/ - - /absences/nip/ - - /absences/ine/ """ r = requests.get( f"{API_URL}/absences/etudid/{ETUDID}", @@ -40,6 +44,20 @@ def test_absences(api_headers): verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 + absences = r.json() + + assert isinstance(absences, list) + for abs in absences: + assert verify_fields(abs, ABSENCES_FIELDS) is True + assert isinstance(abs["jour"], str) + assert isinstance(abs["matin"], bool) + assert isinstance(abs["estabs"], bool) + assert isinstance(abs["estjust"], bool) + assert isinstance(abs["description"], str) + assert isinstance(abs["begin"], str) + assert isinstance(abs["end"], str) + + assert abs["begin"] < abs["end"] # absences_justify @@ -47,32 +65,144 @@ def test_absences_justify(api_headers): """ Test 'absences_just' - Routes : + Route : - /absences/etudid//just - - /absences/nip//just - - /absences/ine//just """ r = requests.get( - API_URL + f"/absences/etudid/{ETUDID}/just", + f"{API_URL}/absences/etudid/{ETUDID}/just", headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - # TODO vérifier résultat + absences = r.json() + + assert isinstance(absences, list) + for abs in absences: + assert verify_fields(abs, ABSENCES_FIELDS) is True + assert isinstance(abs["jour"], str) + assert isinstance(abs["matin"], bool) + assert isinstance(abs["estabs"], bool) + assert isinstance(abs["estjust"], bool) + assert isinstance(abs["description"], str) + assert isinstance(abs["begin"], str) + assert isinstance(abs["end"], str) + + assert abs["begin"] < abs["end"] + + +def test_abs_groupe_etat(api_headers): + """ + Test 'abs_groupe_etat' + + Routes : + - /absences/abs_group_etat/ + - /absences/abs_group_etat/group_id//date_debut//date_fin/ + """ + group_id = 1 + r = requests.get( + f"{API_URL}/absences/abs_group_etat/{group_id}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + + list_absences = r.json() + + assert isinstance(list_absences, list) + list_id_etu = [] + for etu in list_absences: + list_id_etu.append(etu["etudid"]) + assert verify_fields(etu, ABSENCES_GROUP_ETAT_FIELDS) is True + assert isinstance(etu["etudid"], int) + assert isinstance(etu["list_abs"], list) + + list_abs = etu["list_abs"] + for abs in list_abs: + assert verify_fields(abs, ABSENCES_FIELDS) is True + assert isinstance(abs["jour"], str) + assert isinstance(abs["matin"], bool) + assert isinstance(abs["estabs"], bool) + assert isinstance(abs["estjust"], bool) + assert isinstance(abs["description"], str) + assert isinstance(abs["begin"], str) + assert isinstance(abs["end"], str) + + assert abs["begin"] < abs["end"] + + all_unique = True + for id in list_id_etu: + if list_id_etu.count(id) > 1: + all_unique = False + assert all_unique is True + + date_debut = "Fri, 15 Apr 2021 00:00:00 GMT" + date_fin = "Fri, 18 Apr 2022 00:00:00 GMT" + + r1 = requests.get( + f"{API_URL}/absences/abs_group_etat/group_id/{group_id}/date_debut/{date_debut}/date_fin/{date_fin}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r1.status_code == 200 + + list_absences1 = r.json() + + assert isinstance(list_absences1, list) + list_id_etu1 = [] + for etu in list_absences1: + list_id_etu1.append(etu["etudid"]) + assert verify_fields(etu, ABSENCES_GROUP_ETAT_FIELDS) is True + assert isinstance(etu["etudid"], int) + assert isinstance(etu["list_abs"], list) + + list_abs1 = etu["list_abs"] + for abs in list_abs1: + assert verify_fields(abs, ABSENCES_FIELDS) is True + assert isinstance(abs["jour"], str) + assert isinstance(abs["matin"], bool) + assert isinstance(abs["estabs"], bool) + assert isinstance(abs["estjust"], bool) + assert isinstance(abs["description"], str) + assert isinstance(abs["begin"], str) + assert isinstance(abs["end"], str) + + assert abs["begin"] < abs["end"] + + all_unique1 = True + for id in list_id_etu1: + if list_id_etu1.count(id) > 1: + all_unique1 = False + assert all_unique1 is True # XXX TODO -# def test_abs_groupe_etat(api_headers): +# def reset_etud_abs(api_headers): # """ # Test 'abs_groupe_etat' # # Routes : -# - /absences/abs_group_etat/ -# - /absences/abs_group_etat/group_id//date_debut//date_fin/ +# - /absences/etudid//list_abs//reset_etud_abs +# - /absences/etudid//list_abs//reset_etud_abs/only_not_just +# - /absences/etudid//list_abs//reset_etud_abs/only_just # """ +# list_abs = [] # r = requests.get( -# API_URL + "/absences/abs_group_etat/group_id//date_debut//" -# "date_fin/", +# f"{API_URL}/absences/etudid/{ETUDID}/list_abs/{list_abs}/reset_etud_abs", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 +# +# r_only_not_just = requests.get( +# f"{API_URL}/absences/etudid/{ETUDID}/list_abs/{list_abs}/reset_etud_abs/only_not_just", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 +# +# +# r_only_just = requests.get( +# f"{API_URL}/absences/etudid/{ETUDID}/list_abs/{list_abs}/reset_etud_abs/only_just", # headers=api_headers, # verify=CHECK_CERTIFICATE, # ) From 3a3c3793edce61b5fd5a8f64f1f7858e9410872f Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Wed, 15 Jun 2022 16:03:31 +0200 Subject: [PATCH 40/60] =?UTF-8?q?tests=20unitaires=20formsemestre=20+=20aj?= =?UTF-8?q?out=20des=20fields=20=C3=A0=20tests=20dans=20tools=5Ftest=5Fapi?= =?UTF-8?q?.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/api/test_api_formsemestre.py | 95 +++++++++++------------------- tests/api/tools_test_api.py | 91 +++++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 62 deletions(-) diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py index a073e806..99df5945 100644 --- a/tests/api/test_api_formsemestre.py +++ b/tests/api/test_api_formsemestre.py @@ -49,44 +49,19 @@ def test_formsemestre(api_headers): assert verify_fields(formsemestre, FSEM_FIELDS) -def test_etudiant_bulletin(api_headers): - """ - Route: - """ - formsemestre_id = 1 - r = requests.get( - f"{API_URL}/etudiant/etudid/1/formsemestre/{formsemestre_id}/bulletin", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - bull_a = r.json() - - r = requests.get( - f"{API_URL}/etudiant/nip/{NIP}/formsemestre/{formsemestre_id}/bulletin", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - bull_b = r.json() - - r = requests.get( - f"{API_URL}/etudiant/ine/{INE}/formsemestre/{formsemestre_id}/bulletin", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - bull_c = r.json() - # elimine les dates de publication pour comparer les autres champs - del bull_a["date"] - del bull_b["date"] - del bull_c["date"] - assert bull_a == bull_b == bull_c +# XXX TODO ajouter une etape_apo dans la base de test +# def test_formsemestre_apo(api_headers): +# r = requests.get( +# API_URL + "/formation/apo/", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 def test_bulletins(api_headers): """ - Route: + Route: /formsemestre//bulletins """ r = requests.get( API_URL + "/formsemestre/1/bulletins", @@ -106,6 +81,32 @@ def test_bulletins(api_headers): # assert r.status_code == 200 +# def test_formsemestre_etudiants(api_headers): +# """ +# Route: /formsemestre//etudiants, /formsemestre//etudiants/demissionnaires, /formsemestre//etudiants/defaillants +# """ +# r = requests.get( +# API_URL + "/formsemestre//etudiants", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 +# +# r = requests.get( +# API_URL + "/formsemestre//etudiants/demissionnaires", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 +# +# r = requests.get( +# API_URL + "/formsemestre//etudiants/defaillants", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 + + def test_formsemestre_programme(api_headers): """ Route: /formsemestre/1/programme @@ -138,32 +139,6 @@ def test_formsemestre_programme(api_headers): assert verify_fields(sae, MODIMPL_FIELDS) -# def test_formsemestre_etudiants(api_headers): -# """ -# Route: /formsemestre//etudiants, /formsemestre//etudiants/demissionnaires, /formsemestre//etudiants/defaillants -# """ -# r = requests.get( -# API_URL + "/formsemestre//etudiants", -# headers=api_headers, -# verify=CHECK_CERTIFICATE, -# ) -# assert r.status_code == 200 -# -# r = requests.get( -# API_URL + "/formsemestre//etudiants/demissionnaires", -# headers=api_headers, -# verify=CHECK_CERTIFICATE, -# ) -# assert r.status_code == 200 -# -# r = requests.get( -# API_URL + "/formsemestre//etudiants/defaillants", -# headers=api_headers, -# verify=CHECK_CERTIFICATE, -# ) -# assert r.status_code == 200 - - def test_etat_evals( api_headers, ): diff --git a/tests/api/tools_test_api.py b/tests/api/tools_test_api.py index a71a7604..a86b9ec1 100644 --- a/tests/api/tools_test_api.py +++ b/tests/api/tools_test_api.py @@ -78,12 +78,12 @@ ETUD_FIELDS = { } FORMATION_FIELDS = { - "id", + "dept_id", "acronyme", "titre_officiel", "formation_code", "code_specialite", - "dept_id", + "id", "titre", "version", "type_parcours", @@ -91,6 +91,63 @@ FORMATION_FIELDS = { "formation_id", } +FORMATION_EXPORT_FIELDS = { + "dept_id", + "acronyme", + "titre_officiel", + "formation_code", + "code_specialite", + "id", + "titre", + "version", + "type_parcours", + "referentiel_competence_id", + "formation_id", + "ue", +} + +FORMATION_EXPORT_UE_FIELDS = { + "acronyme", + "numero", + "titre", + "type", + "ue_code", + "ects", + "is_external", + "code_apogee", + "coefficient", + "semestre_idx", + "color", + "reference", + "matiere", +} + +FORMATION_EXPORT_UE_MATIERE_FIELDS = { + "titre", + "numero", + "module", +} + +FORMATION_EXPORT_UE_MATIERE_MODULE_FIELDS = { + "titre", + "abbrev", + "code", + "heures_cours", + "heures_td", + "coefficient", + "ects", + "semestre_id", + "numero", + "code_apogee", + "module_type", + "coefficients", +} + +FORMATION_EXPORT_UE_MATIERE_MODULE_COEF_FIELDS = { + "ue_reference", + "coef", +} + FORMSEMESTRE_FIELDS = [ "titre", "gestion_semestrielle", @@ -432,3 +489,33 @@ BULLETIN_SEMESTRE_ECTS_FIELDS = {"acquis", "total"} BULLETIN_SEMESTRE_NOTES_FIELDS = {"value", "min", "moy", "max"} BULLETIN_SEMESTRE_RANG_FIELDS = {"value", "total"} + + +EVAL_FIELDS = { + "id", + "description", + "datetime_epreuve", + "heure_fin", + "coefficient", + "comptee", + "inscrits", + "manquantes", + "ABS", + "ATT", + "EXC", + "saisie_notes", +} + +SAISIE_NOTES_FIELDS = {"datetime_debut", "datetime_fin", "datetime_mediane"} + +ABSENCES_FIELDS = { + "jour", + "matin", + "estabs", + "estjust", + "description", + "begin", + "end", +} + +ABSENCES_GROUP_ETAT_FIELDS = {"etudid", "list_abs"} From 53a4b6cdd71f1a934a8c4acca6e8f5faacf139f0 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Fri, 17 Jun 2022 15:46:17 +0200 Subject: [PATCH 41/60] ajout de la route /formations --- app/api/formations.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/api/formations.py b/app/api/formations.py index f8842b90..30e77395 100644 --- a/app/api/formations.py +++ b/app/api/formations.py @@ -10,6 +10,22 @@ from app.models.formations import Formation from app.scodoc import sco_formations from app.scodoc.sco_permissions import Permission +@bp.route("/formations", methods=["GET"]) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def formations(): + """ + Retourne la liste de toutes les formations (tous départements) + + Exemple de résultat : + """ + # Récupération de toutes les formations + list_formations = models.Formation.query.all() + + # Mise en forme des données + data = [d.to_dict() for d in list_formations] + + return jsonify(data) @bp.route("/formations_ids", methods=["GET"]) @token_auth.login_required From 1083f60020b9fe827d7f5a60fc3929a8120148c4 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Fri, 17 Jun 2022 15:50:06 +0200 Subject: [PATCH 42/60] =?UTF-8?q?ajout=20ref=20competence=20dans=20la=20cr?= =?UTF-8?q?=C3=A9ation=20de=20la=20fakedatabase=20(pas=20encore=20fonction?= =?UTF-8?q?nel)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/but_refcomp.py | 99 ++++++++++ .../fakedatabase/create_test_api_database.py | 177 ++++++++++++++++++ 2 files changed, 276 insertions(+) diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 0750f1a9..eda9c9a1 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -81,6 +81,40 @@ class ApcReferentielCompetences(db.Model, XMLModel): ) formations = db.relationship("Formation", backref="referentiel_competence") + def __init__( + self, + id, + dept_id, + annexe, + specialite, + specialite_long, + type_titre, + type_structure, + type_departement, + version_orebut, + _xml_attribs, + scodoc_date_loaded, + scodoc_orig_filename, + competences, + parcours, + formations, + ): + self.id = id + self.dept_id = dept_id + self.annexe = annexe + self.specialite = specialite + self.specialite_long = specialite_long + self.type_titre = type_titre + self.type_structure = type_structure + self.type_departement = type_departement + self.version_orebut = version_orebut + self._xml_attribs = _xml_attribs + self.scodoc_date_loaded = scodoc_date_loaded + self.scodoc_orig_filename = scodoc_orig_filename + self.competences = competences + self.parcours = parcours + self.formations = formations + def __repr__(self): return f"" @@ -143,6 +177,32 @@ class ApcCompetence(db.Model, XMLModel): cascade="all, delete-orphan", ) + def __init__( + self, + id, + referentiel_id, + id_orebut, + titre, + titre_long, + couleur, + numero, + _xml_attribs, + situations, + composantes_essentielles, + niveaux, + ): + self.id = id + self.referentiel_id = referentiel_id + self.id_orebut = id_orebut + self.titre = titre + self.titre_long = titre_long + self.couleur = couleur + self.numero = numero + self._xml_attribs = _xml_attribs + self.situations = situations + self.composantes_essentielles = composantes_essentielles + self.niveaux = niveaux + def __repr__(self): return f"" @@ -169,6 +229,12 @@ class ApcSituationPro(db.Model, XMLModel): ) libelle = db.Column(db.Text(), nullable=False) # aucun attribut (le text devient le libellé) + + def __init__(self, id, competence_id, libelle): + self.id = id + self.competence_id = competence_id + self.libelle = libelle + def to_dict(self): return {"libelle": self.libelle} @@ -181,6 +247,11 @@ class ApcComposanteEssentielle(db.Model, XMLModel): ) libelle = db.Column(db.Text(), nullable=False) + def __init__(self, id, competence_id, libelle): + self.id = id + self.competence_id = competence_id + self.libelle = libelle + def to_dict(self): return {"libelle": self.libelle} @@ -201,6 +272,14 @@ class ApcNiveau(db.Model, XMLModel): cascade="all, delete-orphan", ) + def __init__(self, id, competence_id, libelle, annee, ordre, app_critiques): + self.id = id + self.competence_id = competence_id + self.libelle = libelle + self.annee = annee + self.ordre = ordre + self.app_critiques = app_critiques + def __repr__(self): return f"<{self.__class__.__name__} ordre={self.ordre}>" @@ -227,6 +306,13 @@ class ApcAppCritique(db.Model, XMLModel): backref=db.backref("app_critiques", lazy="dynamic"), ) + def __init__(self, id, niveau_id, code, libelle, modules): + self.id = id + self.niveau_id = niveau_id + self.code = code + self.libelle = libelle + self.modules = modules + def to_dict(self) -> dict: return {"libelle": self.libelle} @@ -263,6 +349,14 @@ class ApcParcours(db.Model, XMLModel): cascade="all, delete-orphan", ) + def __init__(self, id, referentiel_id, numero, code, libelle, annes): + self.id = id + self.referentiel_id = referentiel_id + self.numero = numero + self.code = code + self.libelle = libelle + self.annes = annes + def __repr__(self): return f"<{self.__class__.__name__} {self.code}>" @@ -282,6 +376,11 @@ class ApcAnneeParcours(db.Model, XMLModel): ) ordre = db.Column(db.Integer) + def __init__(self, id, parcours_id, ordre): + self.id = id + self.parcours_id = parcours_id + self.ordre = ordre + def __repr__(self): return f"<{self.__class__.__name__} ordre={self.ordre}>" diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index 0195a82d..8fe4e96c 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -36,8 +36,18 @@ from app.models import ( Identite, ModuleImpl, NotesNotes, + ApcReferentielCompetences, + ApcCompetence, ) from app import db +from app.models.but_refcomp import ( + ApcParcours, + ApcAnneeParcours, + ApcSituationPro, + ApcComposanteEssentielle, + ApcNiveau, + ApcAppCritique, +) from app.scodoc import ( sco_cache, sco_evaluation_db, @@ -257,6 +267,172 @@ def saisie_notes_evaluations(formsemestre: FormSemestre, user: User): saisir_notes(evaluation.id, condition_saisie_notes) +def create_ref_comp(formation: Formation): + """ + Créer un referentiel de competences + """ + ### ApcSituationPro ### + apc_situation_pro_id = 1 + apc_situation_pro_competence_id = 1 + apc_situation_pro_libelle = "" + + apc_situation_pro = ApcSituationPro( + apc_situation_pro_id, apc_situation_pro_competence_id, apc_situation_pro_libelle + ) + db.session.add(apc_situation_pro) + db.session.commit() + + ### ApcComposanteEssentielle ### + apc_composante_essentielle_id = 1 + apc_composante_essentielle_competence_id = 1 + apc_composante_essentielle_libelle = "" + + apc_composante_essentielle = ApcComposanteEssentielle( + apc_composante_essentielle_id, + apc_composante_essentielle_competence_id, + apc_composante_essentielle_libelle, + ) + db.session.add(apc_composante_essentielle) + db.session.commit() + + ### ApcAppCritique ### + apc_app_critique_id = 1 + apc_app_critique_niveau_id = 1 + apc_app_critique_code = "" + apc_app_critique_libelle = "" + apc_app_critique_modules = formation.modules + + apc_app_critique = ApcAppCritique( + apc_app_critique_id, + apc_app_critique_niveau_id, + apc_app_critique_code, + apc_app_critique_libelle, + apc_app_critique_modules, + ) + db.session.add(apc_app_critique) + db.session.commit() + + ### ApcNiveau ### + apc_niveau_id = 1 + apc_niveau_competence_id = 1 + apc_niveau_libelle = "" + apc_niveau_annee = "" + apc_niveau_ordre = 1 + apc_niveau_app_critiques = apc_app_critique + + apc_niveau = ApcNiveau( + apc_niveau_id, + apc_niveau_competence_id, + apc_niveau_libelle, + apc_niveau_annee, + apc_niveau_ordre, + apc_niveau_app_critiques, + ) + db.session.add(apc_niveau) + db.session.commit() + + ### ApcCompetence ### + apc_competence_id = 1 + apc_competence_referentiel_id = 1 + apc_competence_id_orebut = "" + apc_competence_titre = "" + apc_competence_titre_long = "" + apc_competence_couleur = "" + apc_competence_numero = 1 + apc_competence_xml_attribs = { # xml_attrib : attribute + "id": "id_orebut", + "nom_court": "titre", # was name + "libelle_long": "titre_long", + } + apc_competence_situations = apc_situation_pro + apc_competence_composantes_essentielles = apc_composante_essentielle + apc_competence_niveaux = apc_niveau + + apc_competence = ApcCompetence( + apc_competence_id, + apc_competence_referentiel_id, + apc_competence_id_orebut, + apc_competence_titre, + apc_competence_titre_long, + apc_competence_couleur, + apc_competence_numero, + apc_competence_xml_attribs, + apc_competence_situations, + apc_competence_composantes_essentielles, + apc_competence_niveaux, + ) + db.session.add(apc_competence) + db.session.commit() + + ### ApcAnneeParcours ### + apc_annee_parcours_id = 1 + apc_annee_parcours_parcours_id = 1 + apc_annee_parcours_ordre = 1 + + ap_annee_parcours = ApcAnneeParcours( + apc_annee_parcours_id, apc_annee_parcours_parcours_id, apc_annee_parcours_ordre + ) + + ### ApcParcours ### + apc_parcours_id = 1 + apc_parcours_referentiel_id = 1 + apc_parcours_numero = 1 + apc_parcours_code = "" + apc_parcours_libelle = "" + apc_parcours_annees = ap_annee_parcours + + apc_parcours = ApcParcours( + apc_parcours_id, + apc_parcours_referentiel_id, + apc_parcours_numero, + apc_parcours_code, + apc_parcours_libelle, + apc_parcours_annees, + ) + db.session.add(apc_parcours) + db.session.commit() + + ### ApcReferentielCompetences ### + apc_referentiel_competences_id = 1 + apc_referentiel_competences_dept_id = 1 + apc_referentiel_competences_annexe = "" + apc_referentiel_competences_specialite = "" + apc_referentiel_competences_specialite_long = "" + apc_referentiel_competences_type_titre = "" + apc_referentiel_competences_type_structure = "" + apc_referentiel_competences_type_departement = "" + apc_referentiel_competences_version_orebut = "" + apc_referentiel_competences_xml_attribs = { + "type": "type_titre", + "version": "version_orebut", + } + apc_referentiel_competences_scodoc_date_loaded = "" + apc_referentiel_competences_scodoc_orig_filename = "" + apc_referentiel_competences_competences = apc_competence + apc_referentiel_competences_parcours = apc_parcours + apc_referentiel_competences_formations = formation + + apc_referentiel_competences = ApcReferentielCompetences( + apc_referentiel_competences_id, + apc_referentiel_competences_dept_id, + apc_referentiel_competences_annexe, + apc_referentiel_competences_specialite, + apc_referentiel_competences_specialite_long, + apc_referentiel_competences_type_titre, + apc_referentiel_competences_type_structure, + apc_referentiel_competences_type_departement, + apc_referentiel_competences_version_orebut, + apc_referentiel_competences_xml_attribs, + apc_referentiel_competences_scodoc_date_loaded, + apc_referentiel_competences_scodoc_orig_filename, + apc_referentiel_competences_competences, + apc_referentiel_competences_parcours, + apc_referentiel_competences_formations, + ) + db.session.add(apc_referentiel_competences) + db.session.commit() + + def init_test_database(): """Appelé par la commande `flask init-test-database` @@ -271,6 +447,7 @@ def init_test_database(): create_evaluations(formsemestre) inscrit_etudiants(etuds, formsemestre) saisie_notes_evaluations(formsemestre, user_lecteur) + create_ref_comp(formation) # à compléter # - groupes # - absences From 457a5a8b0615206025199663a24caa681153b1c4 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Mon, 20 Jun 2022 16:09:22 +0200 Subject: [PATCH 43/60] ajout du ref_competences pour la formation de la base de test --- app/models/but_refcomp.py | 16 +- .../fakedatabase/create_test_api_database.py | 263 +++++++++--------- 2 files changed, 143 insertions(+), 136 deletions(-) diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index eda9c9a1..8b730faa 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -93,11 +93,11 @@ class ApcReferentielCompetences(db.Model, XMLModel): type_departement, version_orebut, _xml_attribs, - scodoc_date_loaded, + #scodoc_date_loaded, scodoc_orig_filename, - competences, - parcours, - formations, + # competences, + # parcours, + # formations, ): self.id = id self.dept_id = dept_id @@ -109,11 +109,11 @@ class ApcReferentielCompetences(db.Model, XMLModel): self.type_departement = type_departement self.version_orebut = version_orebut self._xml_attribs = _xml_attribs - self.scodoc_date_loaded = scodoc_date_loaded + #self.scodoc_date_loaded = scodoc_date_loaded self.scodoc_orig_filename = scodoc_orig_filename - self.competences = competences - self.parcours = parcours - self.formations = formations + # self.competences = competences + # self.parcours = parcours + # self.formations = formations def __repr__(self): return f"" diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index 8fe4e96c..a7f6a11d 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -37,7 +37,7 @@ from app.models import ( ModuleImpl, NotesNotes, ApcReferentielCompetences, - ApcCompetence, + ApcCompetence, Absence, ) from app import db from app.models.but_refcomp import ( @@ -271,126 +271,126 @@ def create_ref_comp(formation: Formation): """ Créer un referentiel de competences """ - ### ApcSituationPro ### - apc_situation_pro_id = 1 - apc_situation_pro_competence_id = 1 - apc_situation_pro_libelle = "" - - apc_situation_pro = ApcSituationPro( - apc_situation_pro_id, apc_situation_pro_competence_id, apc_situation_pro_libelle - ) - db.session.add(apc_situation_pro) - db.session.commit() - - ### ApcComposanteEssentielle ### - apc_composante_essentielle_id = 1 - apc_composante_essentielle_competence_id = 1 - apc_composante_essentielle_libelle = "" - - apc_composante_essentielle = ApcComposanteEssentielle( - apc_composante_essentielle_id, - apc_composante_essentielle_competence_id, - apc_composante_essentielle_libelle, - ) - db.session.add(apc_composante_essentielle) - db.session.commit() - - ### ApcAppCritique ### - apc_app_critique_id = 1 - apc_app_critique_niveau_id = 1 - apc_app_critique_code = "" - apc_app_critique_libelle = "" - apc_app_critique_modules = formation.modules - - apc_app_critique = ApcAppCritique( - apc_app_critique_id, - apc_app_critique_niveau_id, - apc_app_critique_code, - apc_app_critique_libelle, - apc_app_critique_modules, - ) - db.session.add(apc_app_critique) - db.session.commit() - - ### ApcNiveau ### - apc_niveau_id = 1 - apc_niveau_competence_id = 1 - apc_niveau_libelle = "" - apc_niveau_annee = "" - apc_niveau_ordre = 1 - apc_niveau_app_critiques = apc_app_critique - - apc_niveau = ApcNiveau( - apc_niveau_id, - apc_niveau_competence_id, - apc_niveau_libelle, - apc_niveau_annee, - apc_niveau_ordre, - apc_niveau_app_critiques, - ) - db.session.add(apc_niveau) - db.session.commit() - - ### ApcCompetence ### - apc_competence_id = 1 - apc_competence_referentiel_id = 1 - apc_competence_id_orebut = "" - apc_competence_titre = "" - apc_competence_titre_long = "" - apc_competence_couleur = "" - apc_competence_numero = 1 - apc_competence_xml_attribs = { # xml_attrib : attribute - "id": "id_orebut", - "nom_court": "titre", # was name - "libelle_long": "titre_long", - } - apc_competence_situations = apc_situation_pro - apc_competence_composantes_essentielles = apc_composante_essentielle - apc_competence_niveaux = apc_niveau - - apc_competence = ApcCompetence( - apc_competence_id, - apc_competence_referentiel_id, - apc_competence_id_orebut, - apc_competence_titre, - apc_competence_titre_long, - apc_competence_couleur, - apc_competence_numero, - apc_competence_xml_attribs, - apc_competence_situations, - apc_competence_composantes_essentielles, - apc_competence_niveaux, - ) - db.session.add(apc_competence) - db.session.commit() - - ### ApcAnneeParcours ### - apc_annee_parcours_id = 1 - apc_annee_parcours_parcours_id = 1 - apc_annee_parcours_ordre = 1 - - ap_annee_parcours = ApcAnneeParcours( - apc_annee_parcours_id, apc_annee_parcours_parcours_id, apc_annee_parcours_ordre - ) - - ### ApcParcours ### - apc_parcours_id = 1 - apc_parcours_referentiel_id = 1 - apc_parcours_numero = 1 - apc_parcours_code = "" - apc_parcours_libelle = "" - apc_parcours_annees = ap_annee_parcours - - apc_parcours = ApcParcours( - apc_parcours_id, - apc_parcours_referentiel_id, - apc_parcours_numero, - apc_parcours_code, - apc_parcours_libelle, - apc_parcours_annees, - ) - db.session.add(apc_parcours) - db.session.commit() + # ### ApcSituationPro ### + # apc_situation_pro_id = 1 + # apc_situation_pro_competence_id = 1 + # apc_situation_pro_libelle = "" + # + # apc_situation_pro = ApcSituationPro( + # apc_situation_pro_id, apc_situation_pro_competence_id, apc_situation_pro_libelle + # ) + # db.session.add(apc_situation_pro) + # db.session.commit() + # + # ### ApcComposanteEssentielle ### + # apc_composante_essentielle_id = 1 + # apc_composante_essentielle_competence_id = 1 + # apc_composante_essentielle_libelle = "" + # + # apc_composante_essentielle = ApcComposanteEssentielle( + # apc_composante_essentielle_id, + # apc_composante_essentielle_competence_id, + # apc_composante_essentielle_libelle, + # ) + # db.session.add(apc_composante_essentielle) + # db.session.commit() + # + # ### ApcAppCritique ### + # apc_app_critique_id = 1 + # apc_app_critique_niveau_id = 1 + # apc_app_critique_code = "" + # apc_app_critique_libelle = "" + # apc_app_critique_modules = formation.modules + # + # apc_app_critique = ApcAppCritique( + # apc_app_critique_id, + # apc_app_critique_niveau_id, + # apc_app_critique_code, + # apc_app_critique_libelle, + # apc_app_critique_modules, + # ) + # db.session.add(apc_app_critique) + # db.session.commit() + # + # ### ApcNiveau ### + # apc_niveau_id = 1 + # apc_niveau_competence_id = 1 + # apc_niveau_libelle = "" + # apc_niveau_annee = "" + # apc_niveau_ordre = 1 + # apc_niveau_app_critiques = apc_app_critique + # + # apc_niveau = ApcNiveau( + # apc_niveau_id, + # apc_niveau_competence_id, + # apc_niveau_libelle, + # apc_niveau_annee, + # apc_niveau_ordre, + # apc_niveau_app_critiques, + # ) + # db.session.add(apc_niveau) + # db.session.commit() + # + # ### ApcCompetence ### + # apc_competence_id = 1 + # apc_competence_referentiel_id = 1 + # apc_competence_id_orebut = "" + # apc_competence_titre = "" + # apc_competence_titre_long = "" + # apc_competence_couleur = "" + # apc_competence_numero = 1 + # apc_competence_xml_attribs = { # xml_attrib : attribute + # "id": "id_orebut", + # "nom_court": "titre", # was name + # "libelle_long": "titre_long", + # } + # apc_competence_situations = apc_situation_pro + # apc_competence_composantes_essentielles = apc_composante_essentielle + # apc_competence_niveaux = apc_niveau + # + # apc_competence = ApcCompetence( + # apc_competence_id, + # apc_competence_referentiel_id, + # apc_competence_id_orebut, + # apc_competence_titre, + # apc_competence_titre_long, + # apc_competence_couleur, + # apc_competence_numero, + # apc_competence_xml_attribs, + # apc_competence_situations, + # apc_competence_composantes_essentielles, + # apc_competence_niveaux, + # ) + # db.session.add(apc_competence) + # db.session.commit() + # + # ### ApcAnneeParcours ### + # apc_annee_parcours_id = 1 + # apc_annee_parcours_parcours_id = 1 + # apc_annee_parcours_ordre = 1 + # + # ap_annee_parcours = ApcAnneeParcours( + # apc_annee_parcours_id, apc_annee_parcours_parcours_id, apc_annee_parcours_ordre + # ) + # + # ### ApcParcours ### + # apc_parcours_id = 1 + # apc_parcours_referentiel_id = 1 + # apc_parcours_numero = 1 + # apc_parcours_code = "" + # apc_parcours_libelle = "" + # apc_parcours_annees = ap_annee_parcours + # + # apc_parcours = ApcParcours( + # apc_parcours_id, + # apc_parcours_referentiel_id, + # apc_parcours_numero, + # apc_parcours_code, + # apc_parcours_libelle, + # apc_parcours_annees, + # ) + # db.session.add(apc_parcours) + # db.session.commit() ### ApcReferentielCompetences ### apc_referentiel_competences_id = 1 @@ -408,9 +408,9 @@ def create_ref_comp(formation: Formation): } apc_referentiel_competences_scodoc_date_loaded = "" apc_referentiel_competences_scodoc_orig_filename = "" - apc_referentiel_competences_competences = apc_competence - apc_referentiel_competences_parcours = apc_parcours - apc_referentiel_competences_formations = formation + # apc_referentiel_competences_competences = apc_competence + # apc_referentiel_competences_parcours = apc_parcours + # apc_referentiel_competences_formations = formation apc_referentiel_competences = ApcReferentielCompetences( apc_referentiel_competences_id, @@ -423,15 +423,21 @@ def create_ref_comp(formation: Formation): apc_referentiel_competences_type_departement, apc_referentiel_competences_version_orebut, apc_referentiel_competences_xml_attribs, - apc_referentiel_competences_scodoc_date_loaded, + #apc_referentiel_competences_scodoc_date_loaded, apc_referentiel_competences_scodoc_orig_filename, - apc_referentiel_competences_competences, - apc_referentiel_competences_parcours, - apc_referentiel_competences_formations, + # apc_referentiel_competences_competences, + # apc_referentiel_competences_parcours, + # apc_referentiel_competences_formations, ) db.session.add(apc_referentiel_competences) db.session.commit() + formation.referentiel_competence_id = apc_referentiel_competences.id + db.session.commit() + + + + def init_test_database(): """Appelé par la commande `flask init-test-database` @@ -448,6 +454,7 @@ def init_test_database(): inscrit_etudiants(etuds, formsemestre) saisie_notes_evaluations(formsemestre, user_lecteur) create_ref_comp(formation) + add_absences(formsemestre) # à compléter # - groupes # - absences From 809d98df0aa98ca68b84ea1e4adf528592681b31 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Mon, 20 Jun 2022 16:10:39 +0200 Subject: [PATCH 44/60] =?UTF-8?q?ajout=20absences=20al=C3=A9atoires=20pour?= =?UTF-8?q?=20la=20base=20de=20test=20(pas=20encore=20fonctionnel)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/absences.py | 9 +++++++ .../fakedatabase/create_test_api_database.py | 25 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/app/models/absences.py b/app/models/absences.py index 830d46f9..d3ea0035 100644 --- a/app/models/absences.py +++ b/app/models/absences.py @@ -27,6 +27,15 @@ class Absence(db.Model): # XXX TODO: contrainte ajoutée: vérifier suppression du module # (mettre à NULL sans supprimer) + def __init__(self, id, etudid, jour, estabs, estjust, matin, description): + self.id = id, + self.etudid = etudid, + self.jour = jour, + self.estabs = estabs, + self.estjust = estjust, + self.matin = matin, + self.description = description + def to_dict(self): data = { "id": self.id, diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index a7f6a11d..9820e887 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -436,7 +436,32 @@ def create_ref_comp(formation: Formation): db.session.commit() +def add_absences(formsemestre: FormSemestre): + """ + Ajoute des absences en base + """ + date_debut = formsemestre.date_debut + date_fin = formsemestre.date_fin + etuds = formsemestre.etuds + # list_etuds = [] + for etu in etuds: + id_db = 1 + aleatoire = random.randint(0, 1) + if aleatoire == 1: + nb_absences = random.randint(1, 5) + for absence in range(0, nb_absences): + id = id_db + etudid = etu.id + jour = date_debut + random.random() * (date_fin - date_debut) + estabs = True + estjust = True if random.randint(0, 1) == 1 else False + matin = True if random.randint(0, 1) == 1 else False + description = "" + abs = Absence(id, etudid, jour, estabs, estjust, matin, description) + db.session.add(abs) + db.session.commit() + id_db += 1 def init_test_database(): From 9faf1c48ae20c488e8bff6bbce066e2b34442f2d Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Tue, 21 Jun 2022 15:37:37 +0200 Subject: [PATCH 45/60] =?UTF-8?q?ajout=20d'absences=20aux=20=C3=A9tudiants?= =?UTF-8?q?=20de=20mani=C3=A8re=20al=C3=A9atoire=20dans=20le=20script=20de?= =?UTF-8?q?=20cr=C3=A9ation=20de=20la=20base=20de=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/absences.py | 12 ++++++------ tools/fakedatabase/create_test_api_database.py | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/models/absences.py b/app/models/absences.py index d3ea0035..0dea5f18 100644 --- a/app/models/absences.py +++ b/app/models/absences.py @@ -28,12 +28,12 @@ class Absence(db.Model): # (mettre à NULL sans supprimer) def __init__(self, id, etudid, jour, estabs, estjust, matin, description): - self.id = id, - self.etudid = etudid, - self.jour = jour, - self.estabs = estabs, - self.estjust = estjust, - self.matin = matin, + self.id = id + self.etudid = etudid + self.jour = jour + self.estabs = estabs + self.estjust = estjust + self.matin = matin self.description = description def to_dict(self): diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index 9820e887..8c7348c5 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -10,7 +10,7 @@ FLASK_DEBUG=1 2) En tant qu'utilisateur scodoc, lancer: - tools/create_database.sh SCODOC_TEST_API_EVAL + tools/create_database.sh flask db upgrade flask sco-db-init --erase flask init-test-database @@ -37,7 +37,8 @@ from app.models import ( ModuleImpl, NotesNotes, ApcReferentielCompetences, - ApcCompetence, Absence, + ApcCompetence, + Absence, ) from app import db from app.models.but_refcomp import ( @@ -423,7 +424,7 @@ def create_ref_comp(formation: Formation): apc_referentiel_competences_type_departement, apc_referentiel_competences_version_orebut, apc_referentiel_competences_xml_attribs, - #apc_referentiel_competences_scodoc_date_loaded, + # apc_referentiel_competences_scodoc_date_loaded, apc_referentiel_competences_scodoc_orig_filename, # apc_referentiel_competences_competences, # apc_referentiel_competences_parcours, @@ -444,9 +445,8 @@ def add_absences(formsemestre: FormSemestre): date_fin = formsemestre.date_fin etuds = formsemestre.etuds - # list_etuds = [] + id_db = 1 for etu in etuds: - id_db = 1 aleatoire = random.randint(0, 1) if aleatoire == 1: nb_absences = random.randint(1, 5) From cacf437ef7a2ec80861b96a694aef1749dd53fd7 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Tue, 21 Jun 2022 15:59:12 +0200 Subject: [PATCH 46/60] ajout tests unitaires pour la route referentiel_competences --- tests/api/test_api_formations.py | 28 ++++++++++++++++++++++++++-- tests/api/tools_test_api.py | 15 +++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/tests/api/test_api_formations.py b/tests/api/test_api_formations.py index b98a31e8..f2e4bfef 100644 --- a/tests/api/test_api_formations.py +++ b/tests/api/test_api_formations.py @@ -28,6 +28,7 @@ from tests.api.tools_test_api import ( FORMATION_EXPORT_UE_MATIERE_MODULE_FIELDS, FORMATION_EXPORT_UE_MATIERE_MODULE_COEF_FIELDS, MODULE_FIELDS, + REF_COMP_FIELDS, ) from tests.api.tools_test_api import FORMATION_FIELDS, MODIMPL_FIELDS @@ -263,9 +264,32 @@ def test_referentiel_competences(api_headers): Route: "/formation//referentiel_competences", """ r = requests.get( - API_URL + "/formation/1/referentiel_competences", + f"{API_URL}/formation/1/referentiel_competences", headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - # XXX TODO ajouter un referentiel competence dans la base de test + + ref_comp = r.json() + assert verify_fields(ref_comp, REF_COMP_FIELDS) is True + assert isinstance(ref_comp["dept_id"], int) + assert isinstance(ref_comp["annexe"], str) + assert isinstance(ref_comp["specialite"], str) + assert isinstance(ref_comp["specialite_long"], str) + assert isinstance(ref_comp["type_structure"], str) + assert isinstance(ref_comp["type_departement"], str) + assert isinstance(ref_comp["type_titre"], str) + assert isinstance(ref_comp["version_orebut"], str) + assert isinstance(ref_comp["scodoc_date_loaded"], str) + assert isinstance(ref_comp["scodoc_orig_filename"], str) + assert isinstance(ref_comp["competences"], dict) + assert isinstance(ref_comp["parcours"], dict) + + # ERROR + id_formation_inexistant = 1516476846861656351 + r_error = requests.get( + f"{API_URL}/formation/{id_formation_inexistant}/referentiel_competences", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r_error.status_code == 404 diff --git a/tests/api/tools_test_api.py b/tests/api/tools_test_api.py index a86b9ec1..7d5f03b6 100644 --- a/tests/api/tools_test_api.py +++ b/tests/api/tools_test_api.py @@ -508,6 +508,21 @@ EVAL_FIELDS = { SAISIE_NOTES_FIELDS = {"datetime_debut", "datetime_fin", "datetime_mediane"} +REF_COMP_FIELDS = { + "dept_id", + "annexe", + "specialite", + "specialite_long", + "type_structure", + "type_departement", + "type_titre", + "version_orebut", + "scodoc_date_loaded", + "scodoc_orig_filename", + "competences", + "parcours", +} + ABSENCES_FIELDS = { "jour", "matin", From 938090bd32e4dd16bd082cc67efb1ff9b67e796a Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Wed, 22 Jun 2022 16:26:50 +0200 Subject: [PATCH 47/60] ajout tests unitaires formsemestre_etudiants --- tests/api/test_api_formsemestre.py | 154 ++++++++++++++++++++++++----- tests/api/tools_test_api.py | 24 +++++ 2 files changed, 154 insertions(+), 24 deletions(-) diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py index 99df5945..ed6d2791 100644 --- a/tests/api/test_api_formsemestre.py +++ b/tests/api/test_api_formsemestre.py @@ -26,6 +26,7 @@ from tests.api.tools_test_api import ( verify_fields, EVAL_FIELDS, SAISIE_NOTES_FIELDS, + FORMSEMESTRE_ETUS_FIELDS, ) from tests.api.tools_test_api import FSEM_FIELDS, UE_FIELDS, MODULE_FIELDS @@ -81,30 +82,135 @@ def test_bulletins(api_headers): # assert r.status_code == 200 -# def test_formsemestre_etudiants(api_headers): -# """ -# Route: /formsemestre//etudiants, /formsemestre//etudiants/demissionnaires, /formsemestre//etudiants/defaillants -# """ -# r = requests.get( -# API_URL + "/formsemestre//etudiants", -# headers=api_headers, -# verify=CHECK_CERTIFICATE, -# ) -# assert r.status_code == 200 -# -# r = requests.get( -# API_URL + "/formsemestre//etudiants/demissionnaires", -# headers=api_headers, -# verify=CHECK_CERTIFICATE, -# ) -# assert r.status_code == 200 -# -# r = requests.get( -# API_URL + "/formsemestre//etudiants/defaillants", -# headers=api_headers, -# verify=CHECK_CERTIFICATE, -# ) -# assert r.status_code == 200 +def test_formsemestre_etudiants(api_headers): + """ + Route: /formsemestre//etudiants, + /formsemestre//etudiants/demissionnaires, + /formsemestre//etudiants/defaillants + """ + r = requests.get( + API_URL + "/formsemestre/1/etudiants", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + formsemestre_etus = r.json() + assert isinstance(formsemestre_etus, list) + assert verify_fields(formsemestre_etus, FORMSEMESTRE_ETUS_FIELDS) is True + for etu in formsemestre_etus: + assert isinstance(etu["id"], int) + assert isinstance(etu["nip"], str) + assert isinstance(etu["ine"], str) + assert isinstance(etu["nom"], str) + assert etu["nom_usuel"] is None or isinstance(etu["nom_usuel"], str) + assert isinstance(etu["prenom"], str) + assert isinstance(etu["civilite"], str) + assert isinstance(etu["groups"], list) + etu_groups = etu["groups"] + for group in etu_groups: + assert isinstance(group["partition_id"], int) + assert isinstance(group["id"], int) + assert isinstance(group["formsemestre_id"], int) + assert group["partition_name"] is None or isinstance( + group["partition_name"], str + ) + assert isinstance(group["numero"], int) + assert isinstance(group["bul_show_rank"], bool) + assert isinstance(group["show_in_lists"], bool) + assert isinstance(group["group_id"], int) + assert group["group_name"] is None or isinstance(group["group_name"], int) + + ### demissionnaires ### + r_demissionnaires = requests.get( + API_URL + "/formsemestre//etudiants/demissionnaires", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r_demissionnaires.status_code == 200 + formsemestre_etus = r_demissionnaires.json() + assert isinstance(formsemestre_etus, list) + assert verify_fields(formsemestre_etus, FORMSEMESTRE_ETUS_FIELDS) is True + for etu in formsemestre_etus: + assert isinstance(etu["id"], int) + assert isinstance(etu["nip"], str) + assert isinstance(etu["ine"], str) + assert isinstance(etu["nom"], str) + assert etu["nom_usuel"] is None or isinstance(etu["nom_usuel"], str) + assert isinstance(etu["prenom"], str) + assert isinstance(etu["civilite"], str) + assert isinstance(etu["groups"], list) + etu_groups = etu["groups"] + for group in etu_groups: + assert isinstance(group["partition_id"], int) + assert isinstance(group["id"], int) + assert isinstance(group["formsemestre_id"], int) + assert group["partition_name"] is None or isinstance( + group["partition_name"], str + ) + assert isinstance(group["numero"], int) + assert isinstance(group["bul_show_rank"], bool) + assert isinstance(group["show_in_lists"], bool) + assert isinstance(group["group_id"], int) + assert group["group_name"] is None or isinstance(group["group_name"], int) + + ### defaillants ### + r_defaillants = requests.get( + API_URL + "/formsemestre//etudiants/defaillants", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r_defaillants.status_code == 200 + + formsemestre_etus = r_defaillants.json() + assert isinstance(formsemestre_etus, list) + assert verify_fields(formsemestre_etus, FORMSEMESTRE_ETUS_FIELDS) is True + for etu in formsemestre_etus: + assert isinstance(etu["id"], int) + assert isinstance(etu["nip"], str) + assert isinstance(etu["ine"], str) + assert isinstance(etu["nom"], str) + assert etu["nom_usuel"] is None or isinstance(etu["nom_usuel"], str) + assert isinstance(etu["prenom"], str) + assert isinstance(etu["civilite"], str) + assert isinstance(etu["groups"], list) + etu_groups = etu["groups"] + for group in etu_groups: + assert isinstance(group["partition_id"], int) + assert isinstance(group["id"], int) + assert isinstance(group["formsemestre_id"], int) + assert group["partition_name"] is None or isinstance( + group["partition_name"], str + ) + assert isinstance(group["numero"], int) + assert isinstance(group["bul_show_rank"], bool) + assert isinstance(group["show_in_lists"], bool) + assert isinstance(group["group_id"], int) + assert group["group_name"] is None or isinstance(group["group_name"], int) + + assert r != r_demissionnaires != r_defaillants is True + + ### ERROR ### + id_formsemestre_inexistant = 265165689619851621685 + r_error = requests.get( + f"{API_URL}/formsemestre/{id_formsemestre_inexistant}/etudiants", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r_error.status_code == 404 + + r_error_demissionnaires = requests.get( + f"{API_URL}/formsemestre/{id_formsemestre_inexistant}/etudiants/demissionnaires", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r_error_demissionnaires.status_code == 404 + + r_error_defaillants = requests.get( + f"{API_URL}/formsemestre/{id_formsemestre_inexistant}/etudiants/defaillants", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r_error_defaillants.status_code == 404 def test_formsemestre_programme(api_headers): diff --git a/tests/api/tools_test_api.py b/tests/api/tools_test_api.py index 7d5f03b6..41147cf5 100644 --- a/tests/api/tools_test_api.py +++ b/tests/api/tools_test_api.py @@ -534,3 +534,27 @@ ABSENCES_FIELDS = { } ABSENCES_GROUP_ETAT_FIELDS = {"etudid", "list_abs"} + + +FORMSEMESTRE_ETUS_FIELDS = { + "id", + "nip", + "ine", + "nom", + "nom_usuel", + "prenom", + "cvilite", + "groups", +} + +FORMSEMESTRE_ETUS_GROUPS_FIELDS = { + "partition_id", + "id", + "formsemestre_id", + "partition_name", + "numero", + "bul_show_rank", + "show_in_lists", + "group_id", + "group_name", +} From 57414efec476052b5cc573617c0f2b7d59dc0123 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Thu, 23 Jun 2022 16:20:19 +0200 Subject: [PATCH 48/60] ajout des tests de evaluations --- tests/api/test_api_evaluations.py | 77 +++++++++++++++++++++------- tests/api/tools_test_api.py | 83 +++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 17 deletions(-) diff --git a/tests/api/test_api_evaluations.py b/tests/api/test_api_evaluations.py index 1453f3a1..57bb2b5c 100644 --- a/tests/api/test_api_evaluations.py +++ b/tests/api/test_api_evaluations.py @@ -20,6 +20,11 @@ Utilisation : import requests from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers +from tests.api.tools_test_api import ( + verify_fields, + EVALUATIONS_FIELDS, + EVALUATION_FIELDS, +) def test_evaluations(api_headers): @@ -29,27 +34,65 @@ def test_evaluations(api_headers): Route : - /evaluations/ """ + moduleimpl_id = 1 r = requests.get( - API_URL + "/evaluations/1", + f"{API_URL}/evaluations/{moduleimpl_id}", headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - # TODO + list_eval = r.json() + assert isinstance(list_eval, list) + for eval in list_eval: + assert verify_fields(eval, EVALUATIONS_FIELDS) is True + assert isinstance(eval["id"], int) + assert isinstance(eval["jour"], str) + assert isinstance(eval["heure_fin"], str) + assert isinstance(eval["note_max"], float) + assert isinstance(eval["visibulletin"], bool) + assert isinstance(eval["evaluation_type"], int) + assert isinstance(eval["moduleimpl_id"], int) + assert isinstance(eval["heure_debut"], str) + assert eval["description"] is None or isinstance(eval["description"], str) + assert isinstance(eval["coefficient"], float) + assert isinstance(eval["publish_incomplete"], bool) + assert isinstance(eval["numero"], int) + assert isinstance(eval["evaluation_id"], int) + assert eval["date_debut"] is None or isinstance(eval["date_debut"], str) + assert eval["date_fin"] is None or isinstance(eval["date_fin"], str) + assert isinstance(eval["poids"], dict) + assert eval["jouriso"] is None or isinstance(eval["jouriso"], str) + assert isinstance(eval["duree"], str) + assert isinstance(eval["descrheure"], str) + assert isinstance(eval["matin"], int) + assert isinstance(eval["apresmidi"], int) + + assert eval["moduleimpl_id"] == moduleimpl_id -# TODO car pas d'évaluations créées à ce stade -# def test_evaluation_notes(api_headers): -# """ -# Test 'evaluation_notes' -# -# Route : -# - /evaluation/eval_notes/ -# """ -# r = requests.get( -# API_URL + "/evaluation/eval_notes/1", -# headers=api_headers, -# verify=CHECK_CERTIFICATE, -# ) -# assert r.status_code == 200 -# # TODO +def test_evaluation_notes(api_headers): + """ + Test 'evaluation_notes' + + Route : + - /evaluation/eval_notes/ + """ + eval_id = 1 + r = requests.get( + f"{API_URL}/evaluation/eval_notes/{eval_id}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + eval_notes = r.json() + for i in range(1, len(eval_notes)): + assert verify_fields(eval_notes[f"{i}"], EVALUATION_FIELDS) + assert isinstance(eval_notes[f"{i}"]["id"], int) + assert isinstance(eval_notes[f"{i}"]["etudid"], int) + assert isinstance(eval_notes[f"{i}"]["evaluation_id"], int) + assert isinstance(eval_notes[f"{i}"]["value"], float) + assert isinstance(eval_notes[f"{i}"]["comment"], str) + assert isinstance(eval_notes[f"{i}"]["date"], str) + assert isinstance(eval_notes[f"{i}"]["uid"], int) + + assert eval_id == eval_notes[f"{i}"]["evaluation_id"] diff --git a/tests/api/tools_test_api.py b/tests/api/tools_test_api.py index 41147cf5..f6d9c453 100644 --- a/tests/api/tools_test_api.py +++ b/tests/api/tools_test_api.py @@ -558,3 +558,86 @@ FORMSEMESTRE_ETUS_GROUPS_FIELDS = { "group_id", "group_name", } + +EVALUATIONS_FIELDS = { + "id", + "jour", + "heure_fin", + "note_max", + "visibulletin", + "evaluation_type", + "moduleimpl_id", + "heure_debut", + "description", + "coefficient", + "publish_incomplete", + "numero", + "evaluation_id", + "date_debut", + "date_fin", + "poids", + "jouriso", + "duree", + "descrheure", + "matin", + "apresmidi", +} + +EVALUATION_FIELDS = { + "id", + "etudid", + "evaluation_id", + "value", + "comment", + "date", + "uid", +} + + +PARTITIONS_FIELDS = { + "partition_id", + "id", + "formsemestre_id", + "partition_name", + "numero", + "bul_show_rank", + "show_in_lists", +} + +PARTITIONS_GROUPS_ETU_FIELDS = { + "etudid", + "id", + "dept_id", + "nom", + "prenom", + "nom_usuel", + "civilite", + "date_naissance", + "lieu_naissance", + "dept_naissance", + "nationalite", + "statut", + "boursier", + "photo_filename", + "code_nip", + "code_ine", + "scodoc7_id", + "email", + "emailperso", + "domicile", + "codepostaldomicile", + "villedomicile", + "paysdomicile", + "telephone", + "telephonemobile", + "fax", + "typeadresse", + "description", + "group_id", + "etat", + "civilite_str", + "nom_disp", + "nomprenom", + "ne", + "email_default", +} From a1e5c3afabaa27b70ab1c957a758ab0c8b19c11f Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Thu, 23 Jun 2022 16:20:59 +0200 Subject: [PATCH 49/60] ajout de tests unitaires pour partitions --- tests/api/test_api_partitions.py | 186 ++++++++++++++++++++----------- 1 file changed, 118 insertions(+), 68 deletions(-) diff --git a/tests/api/test_api_partitions.py b/tests/api/test_api_partitions.py index aa8994f8..aef489ca 100644 --- a/tests/api/test_api_partitions.py +++ b/tests/api/test_api_partitions.py @@ -20,7 +20,11 @@ Utilisation : import requests from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers -from tests.api.tools_test_api import verify_fields +from tests.api.tools_test_api import ( + verify_fields, + PARTITIONS_FIELDS, + PARTITIONS_GROUPS_ETU_FIELDS, +) def test_partition(api_headers): @@ -30,27 +34,30 @@ def test_partition(api_headers): Route : - /partitions/ """ - fields = [ - "partition_id", - "id", - "formsemestre_id", - "partition_name", - "numero", - "bul_show_rank", - "show_in_lists", - ] - + partition_id = 1 r = requests.get( - API_URL + "/partitions/1", + f"{API_URL}/partitions/{partition_id}", headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 partitions = r.json() assert len(partitions) == 1 + assert isinstance(partitions, list) partition = partitions[0] - fields_ok = verify_fields(partition, fields) - assert fields_ok is True + assert isinstance(partition, dict) + assert verify_fields(partition, PARTITIONS_FIELDS) is True + assert partition_id == partition["partition_id"] + + assert isinstance(partition["partition_id"], int) + assert isinstance(partition["id"], int) + assert isinstance(partition["formsemestre_id"], int) + assert partition["partition_name"] is None or isinstance( + partition["partition_name"], str + ) + assert isinstance(partition["numero"], int) + assert isinstance(partition["bul_show_rank"], bool) + assert isinstance(partition["show_in_lists"], bool) def test_etud_in_group(api_headers): @@ -58,66 +65,109 @@ def test_etud_in_group(api_headers): Test 'etud_in_group' Routes : - - /partitions/groups/ - - /partitions/groups//etat/ + - /partition/group/ + - /partition/group//etat/ """ - fields = [ - "etudid", - "id", - "dept_id", - "nom", - "prenom", - "nom_usuel", - "civilite", - "date_naissance", - "lieu_naissance", - "dept_naissance", - "nationalite", - "statut", - "boursier", - "photo_filename", - "code_nip", - "code_ine", - "scodoc7_id", - "email", - "emailperso", - "domicile", - "codepostaldomicile", - "villedomicile", - "paysdomicile", - "telephone", - "telephonemobile", - "fax", - "typeadresse", - "description", - "group_id", - "etat", - "civilite_str", - "nom_disp", - "nomprenom", - "ne", - "email_default", - ] - + group_id = 1 r = requests.get( - API_URL + "/partitions/groups/1", + f"{API_URL}/partition/group/{group_id}", headers=api_headers, verify=CHECK_CERTIFICATE, ) - - etu = r.json()[0] - - fields_ok = verify_fields(etu, fields) - assert r.status_code == 200 - assert len(r.json()) == 16 - assert fields_ok is True - # r = requests.get( - # API_URL + "/partitions/groups/1/etat/", - # headers=api_headers, - # verify=CHECK_CERTIFICATE, - # ) - # assert r.status_code == 200 + assert isinstance(r.json(), list) + + for etu in r.json(): + assert verify_fields(etu, PARTITIONS_GROUPS_ETU_FIELDS) + assert isinstance(etu["etudid"], int) + assert isinstance(etu["id"], int) + assert isinstance(etu["dept_id"], int) + assert isinstance(etu["nom"], str) + assert isinstance(etu["prenom"], str) + assert isinstance(etu["nom_usuel"], str) + assert isinstance(etu["civilite"], str) + assert etu["date_naissance"] is None or isinstance(etu["date_naissance"], str) + assert etu["lieu_naissance"] is None or isinstance(etu["lieu_naissance"], str) + assert etu["dept_naissance"] is None or isinstance(etu["dept_naissance"], str) + assert etu["nationalite"] is None or isinstance(etu["nationalite"], str) + assert etu["statut"] is None or isinstance(etu["statut"], str) + assert etu["boursier"] is None or isinstance(etu["boursier"], bool) + assert etu["photo_filename"] is None or isinstance(etu["photo_filename"], str) + assert isinstance(etu["code_nip"], str) + assert isinstance(etu["code_ine"], str) + assert etu["scodoc7_id"] is None or isinstance(etu["scodoc7_id"], int) + assert isinstance(etu["email"], str) + assert etu["emailperso"] is None or isinstance(etu["emailperso"], str) + assert etu["domicile"] is None or isinstance(etu["domicile"], str) + assert etu["codepostaldomicile"] is None or isinstance( + etu["codepostaldomicile"], str + ) + assert etu["villedomicile"] is None or isinstance(etu["villedomicile"], str) + assert etu["paysdomicile"] is None or isinstance(etu["paysdomicile"], str) + assert etu["telephone"] is None or isinstance(etu["telephone"], str) + assert etu["telephonemobile"] is None or isinstance(etu["telephonemobile"], str) + assert etu["fax"] is None or isinstance(etu["fax"], str) + assert isinstance(etu["typeadresse"], str) + assert etu["description"] is None or isinstance(etu["description"], int) + assert isinstance(etu["group_id"], int) + assert isinstance(etu["etat"], str) + assert isinstance(etu["civilite_str"], str) + assert isinstance(etu["nom_disp"], str) + assert isinstance(etu["nomprenom"], str) + assert isinstance(etu["ne"], str) + assert isinstance(etu["email_default"], str) + + etat = "I" + r_etat = requests.get( + f"{API_URL}/partition/group/{group_id}/etat/{etat}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r_etat.status_code == 200 + + assert isinstance(r_etat.json(), list) + + for etu in r_etat.json(): + assert verify_fields(etu, PARTITIONS_GROUPS_ETU_FIELDS) + assert isinstance(etu["etudid"], int) + assert isinstance(etu["id"], int) + assert isinstance(etu["dept_id"], int) + assert isinstance(etu["nom"], str) + assert isinstance(etu["prenom"], str) + assert isinstance(etu["nom_usuel"], str) + assert isinstance(etu["civilite"], str) + assert etu["date_naissance"] is None or isinstance(etu["date_naissance"], str) + assert etu["lieu_naissance"] is None or isinstance(etu["lieu_naissance"], str) + assert etu["dept_naissance"] is None or isinstance(etu["dept_naissance"], str) + assert etu["nationalite"] is None or isinstance(etu["nationalite"], str) + assert etu["statut"] is None or isinstance(etu["statut"], str) + assert etu["boursier"] is None or isinstance(etu["boursier"], bool) + assert etu["photo_filename"] is None or isinstance(etu["photo_filename"], str) + assert isinstance(etu["code_nip"], str) + assert isinstance(etu["code_ine"], str) + assert etu["scodoc7_id"] is None or isinstance(etu["scodoc7_id"], int) + assert isinstance(etu["email"], str) + assert etu["emailperso"] is None or isinstance(etu["emailperso"], str) + assert etu["domicile"] is None or isinstance(etu["domicile"], str) + assert etu["codepostaldomicile"] is None or isinstance( + etu["codepostaldomicile"], str + ) + assert etu["villedomicile"] is None or isinstance(etu["villedomicile"], str) + assert etu["paysdomicile"] is None or isinstance(etu["paysdomicile"], str) + assert etu["telephone"] is None or isinstance(etu["telephone"], str) + assert etu["telephonemobile"] is None or isinstance(etu["telephonemobile"], str) + assert etu["fax"] is None or isinstance(etu["fax"], str) + assert isinstance(etu["typeadresse"], str) + assert etu["description"] is None or isinstance(etu["description"], int) + assert isinstance(etu["group_id"], int) + assert isinstance(etu["etat"], str) + assert isinstance(etu["civilite_str"], str) + assert isinstance(etu["nom_disp"], str) + assert isinstance(etu["nomprenom"], str) + assert isinstance(etu["ne"], str) + assert isinstance(etu["email_default"], str) + + assert etat == etu["etat"] # # set_groups From 23e2a5c427e6494bc7798656c8aaa37195593d85 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Fri, 24 Jun 2022 16:03:35 +0200 Subject: [PATCH 50/60] =?UTF-8?q?ajout=20des=20=C3=A9tats=20d'inscriptions?= =?UTF-8?q?=20pour=20les=20=C3=A9tudiants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fakedatabase/create_test_api_database.py | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index 8c7348c5..ee630e54 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -38,7 +38,7 @@ from app.models import ( NotesNotes, ApcReferentielCompetences, ApcCompetence, - Absence, + Absence, FormSemestreEtape, ) from app import db from app.models.but_refcomp import ( @@ -180,13 +180,31 @@ def create_formsemestre( def inscrit_etudiants(etuds: list, formsemestre: FormSemestre): """Inscrit les etudiants aux semestres et à tous ses modules""" for etud in etuds: - sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( - formsemestre.id, - etud.id, - group_ids=[], - etat="I", - method="init db test", - ) + aleatoire = random.randint(0, 10) + if aleatoire <= 3: + sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( + formsemestre.id, + etud.id, + group_ids=[], + etat="I", + method="init db test", + ) + elif 3 < aleatoire <= 6: + sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( + formsemestre.id, + etud.id, + group_ids=[], + etat="D", + method="init db test", + ) + else: + sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( + formsemestre.id, + etud.id, + group_ids=[], + etat="DEF", + method="init db test", + ) def create_evaluations(formsemestre: FormSemestre): @@ -480,6 +498,7 @@ def init_test_database(): saisie_notes_evaluations(formsemestre, user_lecteur) create_ref_comp(formation) add_absences(formsemestre) + create_etape_apo(formsemestre) # à compléter # - groupes # - absences From 7cb6dd66c3c0a7b471288f150afe3b1075d3eba9 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Fri, 24 Jun 2022 16:04:40 +0200 Subject: [PATCH 51/60] debut d'ajout d'etapes apoge pour la base de tests unitaires --- app/models/formsemestre.py | 5 +++++ tools/fakedatabase/create_test_api_database.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index ffba1f3b..16623bbf 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -474,6 +474,11 @@ class FormSemestreEtape(db.Model): # etape_apo aurait du etre not null, mais oublié etape_apo = db.Column(db.String(APO_CODE_STR_LEN), index=True) + def __init__(self, id, formsemestre_id, etape_apo): + self.id = id + self.formsemestre_id = formsemestre_id + self.etape_apo = etape_apo + def __bool__(self): "Etape False if code empty" return self.etape_apo is not None and (len(self.etape_apo) > 0) diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index ee630e54..339d23b3 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -482,6 +482,14 @@ def add_absences(formsemestre: FormSemestre): id_db += 1 +def create_etape_apo(formsemestre: FormSemestre): + """ + Ajoute étape apoge au formsemestre + """ + etape_apo = FormSemestreEtape(id=1, formsemestre_id=formsemestre.id, etape_apo="A1") + formsemestre.etapes = etape_apo + + def init_test_database(): """Appelé par la commande `flask init-test-database` From 35a6427e2f512b0fb20ec4e8c23b0e9efdae1b54 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Mon, 27 Jun 2022 16:11:48 +0200 Subject: [PATCH 52/60] =?UTF-8?q?ajout=20d'=C3=A9tapes=20apo=20dans=20la?= =?UTF-8?q?=20base=20de=20tests=20unitaires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fakedatabase/create_test_api_database.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index 339d23b3..c4ad78f8 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -38,7 +38,8 @@ from app.models import ( NotesNotes, ApcReferentielCompetences, ApcCompetence, - Absence, FormSemestreEtape, + Absence, + FormSemestreEtape, ) from app import db from app.models.but_refcomp import ( @@ -486,8 +487,24 @@ def create_etape_apo(formsemestre: FormSemestre): """ Ajoute étape apoge au formsemestre """ - etape_apo = FormSemestreEtape(id=1, formsemestre_id=formsemestre.id, etape_apo="A1") - formsemestre.etapes = etape_apo + etape_apo1 = FormSemestreEtape( + id=1, formsemestre_id=formsemestre.id, etape_apo="A1" + ) + db.session.add(etape_apo1) + + etape_apo2 = FormSemestreEtape( + id=2, formsemestre_id=formsemestre.id, etape_apo="A2" + ) + db.session.add(etape_apo2) + + etape_apo3 = FormSemestreEtape( + id=3, formsemestre_id=formsemestre.id, etape_apo="A3" + ) + db.session.add(etape_apo3) + + list_etapes = [etape_apo1, etape_apo2, etape_apo3] + formsemestre.etapes = list_etapes + db.session.commit() def init_test_database(): From 85e7818843a118c68058951428c551cc7585e96e Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Mon, 27 Jun 2022 16:12:41 +0200 Subject: [PATCH 53/60] ajout des tests unitaires pour formsemestre_apo --- tests/api/test_api_formsemestre.py | 110 ++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 9 deletions(-) diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py index ed6d2791..719dd3a4 100644 --- a/tests/api/test_api_formsemestre.py +++ b/tests/api/test_api_formsemestre.py @@ -40,24 +40,116 @@ def test_formsemestre(api_headers): """ Route: /formsemestre/ """ + formsemestre_id = 1 r = requests.get( - API_URL + "/formsemestre/1", + f"{API_URL}/formsemestre/{formsemestre_id}", headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 formsemestre = r.json() assert verify_fields(formsemestre, FSEM_FIELDS) + assert isinstance(formsemestre["block_moyennes"], bool) + assert isinstance(formsemestre["bul_bgcolor"], str) + assert isinstance(formsemestre["bul_hide_xml"], bool) + assert isinstance(formsemestre["date_debut_iso"], str) + assert isinstance(formsemestre["date_debut"], str) + assert isinstance(formsemestre["date_fin_iso"], str) + assert isinstance(formsemestre["date_fin"], str) + assert isinstance(formsemestre["dept_id"], int) + assert formsemestre["elt_annee_apo"] is None or isinstance( + formsemestre["elt_annee_apo"], str + ) + assert formsemestre["elt_sem_apo"] is None or isinstance( + formsemestre["elt_sem_apo"], str + ) + assert isinstance(formsemestre["ens_can_edit_eval"], bool) + assert isinstance(formsemestre["etat"], bool) + assert isinstance(formsemestre["formation_id"], int) + assert isinstance(formsemestre["formsemestre_id"], int) + assert isinstance(formsemestre["gestion_compensation"], bool) + assert isinstance(formsemestre["gestion_semestrielle"], bool) + assert isinstance(formsemestre["id"], int) + assert isinstance(formsemestre["modalite"], str) + assert isinstance(formsemestre["resp_can_change_ens"], bool) + assert isinstance(formsemestre["resp_can_edit"], bool) + assert isinstance(formsemestre["responsables"], list) + assert formsemestre["scodoc7_id"] is None or isinstance( + formsemestre["scodoc7_id"], int + ) + assert isinstance(formsemestre["semestre_id"], int) + assert isinstance(formsemestre["titre_formation"], str) + assert isinstance(formsemestre["titre_num"], str) + assert isinstance(formsemestre["titre"], str) + + ### ERROR ### + formsemestre_id_inexistant = 165456165165136513510351 + r = requests.get( + f"{API_URL}/formsemestre/{formsemestre_id_inexistant}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 404 -# XXX TODO ajouter une etape_apo dans la base de test -# def test_formsemestre_apo(api_headers): -# r = requests.get( -# API_URL + "/formation/apo/", -# headers=api_headers, -# verify=CHECK_CERTIFICATE, -# ) -# assert r.status_code == 200 +def test_formsemestre_apo(api_headers): + """ + Route: /formsemestre/apo/ + """ + etape_apo = "A1" + r = requests.get( + f"{API_URL}/formsemestre/apo/{etape_apo}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + list_formsemestre = r.json() + assert isinstance(list_formsemestre, list) + + for formsemestre in list_formsemestre: + assert isinstance(formsemestre, dict) + assert verify_fields(formsemestre, FSEM_FIELDS) + assert isinstance(formsemestre["block_moyennes"], bool) + assert isinstance(formsemestre["bul_bgcolor"], str) + assert isinstance(formsemestre["bul_hide_xml"], bool) + assert isinstance(formsemestre["date_debut_iso"], str) + assert isinstance(formsemestre["date_debut"], str) + assert isinstance(formsemestre["date_fin_iso"], str) + assert isinstance(formsemestre["date_fin"], str) + assert isinstance(formsemestre["dept_id"], int) + assert formsemestre["elt_annee_apo"] is None or isinstance( + formsemestre["elt_annee_apo"], str + ) + assert formsemestre["elt_sem_apo"] is None or isinstance( + formsemestre["elt_sem_apo"], str + ) + assert isinstance(formsemestre["ens_can_edit_eval"], bool) + assert isinstance(formsemestre["etat"], bool) + assert isinstance(formsemestre["formation_id"], int) + assert isinstance(formsemestre["formsemestre_id"], int) + assert isinstance(formsemestre["gestion_compensation"], bool) + assert isinstance(formsemestre["gestion_semestrielle"], bool) + assert isinstance(formsemestre["id"], int) + assert isinstance(formsemestre["modalite"], str) + assert isinstance(formsemestre["resp_can_change_ens"], bool) + assert isinstance(formsemestre["resp_can_edit"], bool) + assert isinstance(formsemestre["responsables"], list) + assert formsemestre["scodoc7_id"] is None or isinstance( + formsemestre["scodoc7_id"], int + ) + assert isinstance(formsemestre["semestre_id"], int) + assert isinstance(formsemestre["titre_formation"], str) + assert isinstance(formsemestre["titre_num"], str) + assert isinstance(formsemestre["titre"], str) + + ### ERROR ### + etape_apo_inexistante = "aoefiaozidaoẑidjnoaiznjd" + r = requests.get( + f"{API_URL}/formsemestre/apo/{etape_apo_inexistante}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 404 def test_bulletins(api_headers): From 329781989244002730f9866f42b4b037671becb5 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Tue, 28 Jun 2022 16:02:15 +0200 Subject: [PATCH 54/60] gestion d'erreurs pour la route formsemestre_apo --- app/api/formsemestres.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index bdcb4616..c086b627 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -7,6 +7,7 @@ import app from app import models from app.api import bp from app.api.auth import token_auth, token_permission_required +from app.api.errors import error_response from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults from app.comp.res_compat import NotesTableCompat @@ -91,7 +92,13 @@ def formsemestre_apo(etape_apo: str): FormSemestreEtape.formsemestre_id == FormSemestre.id, ) - return jsonify([formsemestre.to_dict() for formsemestre in formsemestres]) + res = [formsemestre.to_dict() for formsemestre in formsemestres] + if len(res) == 0: + return error_response( + 404, message="Aucun formsemestre trouvé avec cette étape apogée" + ) + else: + return jsonify(res) @bp.route("/formsemestre//bulletins", methods=["GET"]) From 031dc409be4996ab9f8cc8630697f1c6f7b558c8 Mon Sep 17 00:00:00 2001 From: leonard_montalbano Date: Tue, 28 Jun 2022 16:03:31 +0200 Subject: [PATCH 55/60] correction d'erreurs diverses --- tests/api/test_api_formsemestre.py | 22 +++++++++++++--------- tests/api/tools_test_api.py | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py index 719dd3a4..5853268e 100644 --- a/tests/api/test_api_formsemestre.py +++ b/tests/api/test_api_formsemestre.py @@ -144,12 +144,12 @@ def test_formsemestre_apo(api_headers): ### ERROR ### etape_apo_inexistante = "aoefiaozidaoẑidjnoaiznjd" - r = requests.get( + r_error = requests.get( f"{API_URL}/formsemestre/apo/{etape_apo_inexistante}", headers=api_headers, verify=CHECK_CERTIFICATE, ) - assert r.status_code == 404 + assert r_error.status_code == 404 def test_bulletins(api_headers): @@ -180,16 +180,17 @@ def test_formsemestre_etudiants(api_headers): /formsemestre//etudiants/demissionnaires, /formsemestre//etudiants/defaillants """ + formsemestre_id = 1 r = requests.get( - API_URL + "/formsemestre/1/etudiants", + f"{API_URL}/formsemestre/{formsemestre_id}/etudiants", headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 formsemestre_etus = r.json() assert isinstance(formsemestre_etus, list) - assert verify_fields(formsemestre_etus, FORMSEMESTRE_ETUS_FIELDS) is True for etu in formsemestre_etus: + assert verify_fields(etu, FORMSEMESTRE_ETUS_FIELDS) is True assert isinstance(etu["id"], int) assert isinstance(etu["nip"], str) assert isinstance(etu["ine"], str) @@ -214,15 +215,16 @@ def test_formsemestre_etudiants(api_headers): ### demissionnaires ### r_demissionnaires = requests.get( - API_URL + "/formsemestre//etudiants/demissionnaires", + f"{API_URL}/formsemestre/{formsemestre_id}/etudiants/demissionnaires", headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r_demissionnaires.status_code == 200 formsemestre_etus = r_demissionnaires.json() assert isinstance(formsemestre_etus, list) - assert verify_fields(formsemestre_etus, FORMSEMESTRE_ETUS_FIELDS) is True + for etu in formsemestre_etus: + assert verify_fields(etu, FORMSEMESTRE_ETUS_FIELDS) is True assert isinstance(etu["id"], int) assert isinstance(etu["nip"], str) assert isinstance(etu["ine"], str) @@ -247,7 +249,7 @@ def test_formsemestre_etudiants(api_headers): ### defaillants ### r_defaillants = requests.get( - API_URL + "/formsemestre//etudiants/defaillants", + f"{API_URL}/formsemestre/{formsemestre_id}/etudiants/defaillants", headers=api_headers, verify=CHECK_CERTIFICATE, ) @@ -255,8 +257,8 @@ def test_formsemestre_etudiants(api_headers): formsemestre_etus = r_defaillants.json() assert isinstance(formsemestre_etus, list) - assert verify_fields(formsemestre_etus, FORMSEMESTRE_ETUS_FIELDS) is True for etu in formsemestre_etus: + assert verify_fields(etu, FORMSEMESTRE_ETUS_FIELDS) is True assert isinstance(etu["id"], int) assert isinstance(etu["nip"], str) assert isinstance(etu["ine"], str) @@ -279,7 +281,9 @@ def test_formsemestre_etudiants(api_headers): assert isinstance(group["group_id"], int) assert group["group_name"] is None or isinstance(group["group_name"], int) - assert r != r_demissionnaires != r_defaillants is True + assert r.json() != r_demissionnaires.json() + assert r.json() != r_defaillants.json() + assert r_demissionnaires.json() != r_defaillants.json() ### ERROR ### id_formsemestre_inexistant = 265165689619851621685 diff --git a/tests/api/tools_test_api.py b/tests/api/tools_test_api.py index f6d9c453..438c1db5 100644 --- a/tests/api/tools_test_api.py +++ b/tests/api/tools_test_api.py @@ -543,7 +543,7 @@ FORMSEMESTRE_ETUS_FIELDS = { "nom", "nom_usuel", "prenom", - "cvilite", + "civilite", "groups", } From 9474d420df812e41093ecc750e45b4af4c27e28b Mon Sep 17 00:00:00 2001 From: leonard Date: Tue, 5 Jul 2022 16:09:26 +0200 Subject: [PATCH 56/60] =?UTF-8?q?fix=20probl=C3=A8me=20symlinks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 0 README.md | 0 app/__init__.py | 10 + app/api/etudiants.py | 4 +- app/api/formsemestres.py | 12 +- app/but/apc_edit_ue.py | 106 ++ app/but/bulletin_but.py | 23 +- app/but/forms/jury_but_forms.py | 18 + app/but/forms/refcomp_forms.py | 6 +- app/but/import_refcomp.py | 5 +- app/but/jury_but.py | 1054 +++++++++++++++++ app/but/jury_but_pv.py | 137 +++ app/but/jury_but_recap.py | 424 +++++++ app/but/jury_but_validation_auto.py | 34 + app/but/jury_but_view.py | 173 +++ app/comp/bonus_spo.py | 63 + app/comp/moy_mod.py | 7 +- app/comp/moy_ue.py | 13 +- app/comp/res_but.py | 72 +- app/comp/res_common.py | 45 +- app/comp/res_compat.py | 6 + app/decorators.py | 4 +- app/forms/main/config_apo.py | 8 + app/forms/main/config_logos.py | 11 +- app/models/__init__.py | 16 +- app/models/absences.py | 6 +- app/models/but_pn.py | 5 +- app/models/but_refcomp.py | 208 +++- app/models/but_validations.py | 339 ++++++ app/models/config.py | 20 +- app/models/etudiants.py | 80 +- app/models/events.py | 15 + app/models/formations.py | 64 +- app/models/formsemestre.py | 195 ++- app/models/groups.py | 10 +- app/models/modules.py | 23 +- app/models/notes.py | 6 +- app/models/ues.py | 13 + app/models/validations.py | 51 +- app/scodoc/TrivialFormulator.py | 23 +- app/scodoc/gen_tables.py | 19 +- app/scodoc/html_sco_header.py | 126 +- app/scodoc/html_sidebar.py | 8 +- app/scodoc/sco_apogee_csv.py | 98 +- app/scodoc/sco_archives.py | 18 +- app/scodoc/sco_bulletins.py | 21 +- app/scodoc/sco_bulletins_standard.py | 10 +- app/scodoc/sco_cache.py | 10 +- app/scodoc/sco_codes_parcours.py | 61 +- app/scodoc/sco_edit_apc.py | 21 +- app/scodoc/sco_edit_formation.py | 6 +- app/scodoc/sco_edit_module.py | 801 +++++++------ app/scodoc/sco_edit_ue.py | 145 ++- app/scodoc/sco_excel.py | 4 +- app/scodoc/sco_formsemestre.py | 11 +- app/scodoc/sco_formsemestre_edit.py | 197 +-- app/scodoc/sco_formsemestre_inscriptions.py | 19 +- app/scodoc/sco_formsemestre_status.py | 87 +- app/scodoc/sco_formsemestre_validation.py | 28 +- app/scodoc/sco_groups.py | 193 ++- app/scodoc/sco_groups_edit.py | 11 +- app/scodoc/sco_import_etuds.py | 17 +- app/scodoc/sco_import_users.py | 12 +- app/scodoc/sco_inscr_passage.py | 11 +- app/scodoc/sco_logos.py | 8 +- app/scodoc/sco_moduleimpl_status.py | 6 +- app/scodoc/sco_page_etud.py | 9 +- app/scodoc/sco_parcours_dut.py | 12 +- app/scodoc/sco_pvjury.py | 56 +- app/scodoc/sco_pvpdf.py | 59 +- app/scodoc/sco_recapcomplet.py | 40 +- app/scodoc/sco_tag_module.py | 4 +- app/scodoc/sco_trombino.py | 6 +- app/scodoc/sco_undo_notes.py | 8 +- app/scodoc/sco_utils.py | 30 +- app/static/css/jury_but.css | 178 +++ app/static/css/releve-but.css | 4 +- app/static/css/scodoc.css | 89 +- ...artitionForm.js => edit_partition_form.js} | 0 app/static/js/edit_ue.js | 24 +- app/static/js/jury_but.js | 62 + app/static/js/module_edit.js | 8 + app/static/js/releve-but.js | 33 +- app/static/js/table_editor.js | 34 +- app/static/js/table_recap.js | 121 +- app/static/links/9.3.15 | 1 + app/static/links/README.md | 3 + app/templates/but/bulletin.html | 2 +- .../but/documentation_codes_jury.html | 266 +++++ .../but/formsemestre_validation_auto_but.html | 30 + app/templates/but/refcomp_assoc.html | 22 +- app/templates/but/refcomp_show.html | 2 +- app/templates/config_codes_decisions.html | 12 +- app/templates/confirm_dialog.html | 22 + app/templates/pn/form_mods.html | 18 +- app/templates/pn/form_modules_ue_coefs.html | 6 +- app/templates/pn/form_ues.html | 20 +- app/templates/pn/ue_infos.html | 2 +- app/templates/sco_page.html | 39 +- app/templates/scolar/affect_groups.html | 40 +- app/views/notes.py | 323 ++++- app/views/refcomp.py | 25 +- app/views/scolar.py | 37 +- bench.py | 0 .../versions/3c31bb0b27c9_fix_calais.py | 76 ++ .../versions/4311cc342dbd_validations_but.py | 128 ++ .../versions/6002d7d366e5_assoc_ue_niveau.py | 93 ++ ...a2771105c21c_parcours_inscriptions_casc.py | 318 +++++ .../versions/af77ca6a89d0_news_index.py | 41 + migrations/versions/c0c225192d61_coef_rcue.py | 28 + .../ee21c76c8183_fix_contrainte_refcomp.py | 37 + pylintrc | 0 sco_version.py | 8 +- tests/unit/test_refcomp.py | 44 +- tests/unit/test_sco_basic.py | 22 + 115 files changed, 6552 insertions(+), 1147 deletions(-) mode change 100644 => 100755 LICENSE mode change 100644 => 100755 README.md create mode 100644 app/but/apc_edit_ue.py create mode 100644 app/but/forms/jury_but_forms.py create mode 100644 app/but/jury_but.py create mode 100644 app/but/jury_but_pv.py create mode 100644 app/but/jury_but_recap.py create mode 100644 app/but/jury_but_validation_auto.py create mode 100644 app/but/jury_but_view.py create mode 100644 app/models/but_validations.py create mode 100644 app/static/css/jury_but.css rename app/static/js/{editPartitionForm.js => edit_partition_form.js} (100%) create mode 100644 app/static/js/jury_but.js create mode 100644 app/static/js/module_edit.js create mode 120000 app/static/links/9.3.15 create mode 100644 app/static/links/README.md create mode 100644 app/templates/but/documentation_codes_jury.html create mode 100644 app/templates/but/formsemestre_validation_auto_but.html create mode 100644 app/templates/confirm_dialog.html mode change 100644 => 100755 bench.py create mode 100644 migrations/versions/3c31bb0b27c9_fix_calais.py create mode 100644 migrations/versions/4311cc342dbd_validations_but.py create mode 100644 migrations/versions/6002d7d366e5_assoc_ue_niveau.py create mode 100644 migrations/versions/a2771105c21c_parcours_inscriptions_casc.py create mode 100644 migrations/versions/af77ca6a89d0_news_index.py create mode 100644 migrations/versions/c0c225192d61_coef_rcue.py create mode 100644 migrations/versions/ee21c76c8183_fix_contrainte_refcomp.py mode change 100644 => 100755 pylintrc mode change 100644 => 100755 sco_version.py diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/app/__init__.py b/app/__init__.py index a1862aaa..738ad097 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -205,8 +205,18 @@ def create_app(config_class=DevConfig): app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static") app.wsgi_app = ReverseProxied(app.wsgi_app) app.logger.setLevel(logging.DEBUG) + + # Evite de logguer toutes les requetes dans notre log + logging.getLogger("werkzeug").disabled = True + app.config.from_object(config_class) + # Vérifie/crée lien sym pour les URL statiques + link_filename = f"{app.root_path}/static/links/{sco_version.SCOVERSION}" + if not os.path.exists(link_filename): + app.logger.info(f"creating symlink {link_filename}") + os.symlink("..", link_filename) + db.init_app(app) migrate.init_app(app, db) login.init_app(app) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 1d33b04b..2b112836 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -212,7 +212,9 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) formsemestres = query.order_by(FormSemestre.date_debut) - return jsonify([formsemestre.to_dict() for formsemestre in formsemestres]) + return jsonify( + [formsemestre.to_dict(convert_parcours=True) for formsemestre in formsemestres] + ) @bp.route( diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index c086b627..e3d9a761 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -62,7 +62,7 @@ def formsemestre(formsemestre_id: int): formsemestre: FormSemestre = models.FormSemestre.query.filter_by( id=formsemestre_id ).first_or_404() - data = formsemestre.to_dict() + data = formsemestre.to_dict(convert_parcours=True) # Pour le moment on a besoin de fixer le departement # pour accéder aux préferences dept = Departement.query.get(formsemestre.dept_id) @@ -92,13 +92,9 @@ def formsemestre_apo(etape_apo: str): FormSemestreEtape.formsemestre_id == FormSemestre.id, ) - res = [formsemestre.to_dict() for formsemestre in formsemestres] - if len(res) == 0: - return error_response( - 404, message="Aucun formsemestre trouvé avec cette étape apogée" - ) - else: - return jsonify(res) + return jsonify( + [formsemestre.to_dict(convert_parcours=True) for formsemestre in formsemestres] + ) @bp.route("/formsemestre//bulletins", methods=["GET"]) diff --git a/app/but/apc_edit_ue.py b/app/but/apc_edit_ue.py new file mode 100644 index 00000000..dd8e60a8 --- /dev/null +++ b/app/but/apc_edit_ue.py @@ -0,0 +1,106 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +""" +Edition associations UE <-> Ref. Compétence +""" +from flask import g, url_for +from app import db, log +from app.models import Formation, UniteEns +from app.models.but_refcomp import ApcNiveau +from app.scodoc import sco_codes_parcours + + +def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str: + """Form. HTML pour associer une UE à un niveau de compétence""" + if ue.type != sco_codes_parcours.UE_STANDARD: + return "" + ref_comp = ue.formation.referentiel_competence + if ref_comp is None: + return f"""
+
Pas de référentiel de compétence associé à cette formation !
+ +
""" + annee = (ue.semestre_idx + 1) // 2 # 1, 2, 3 + niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee) + + # Les niveaux déjà associés à d'autres UE du même semestre + autres_ues = formation.ues.filter_by(semestre_idx=ue.semestre_idx) + niveaux_autres_ues = { + oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id + } + options = [] + if niveaux_by_parcours["TC"]: # TC pour Tronc Commun + options.append("""""") + for n in niveaux_by_parcours["TC"]: + if n.id in niveaux_autres_ues: + disabled = "disabled" + else: + disabled = "" + options.append( + f"""""" + ) + options.append("""""") + for parcour in ref_comp.parcours: + if len(niveaux_by_parcours[parcour.id]): + options.append(f"""""") + for n in niveaux_by_parcours[parcour.id]: + if n.id in niveaux_autres_ues: + disabled = "disabled" + else: + disabled = "" + options.append( + f"""""" + ) + options.append("""""") + options_str = "\n".join(options) + return f""" +
+
+ Niveau de compétence associé: + +
+
+ """ + + +def set_ue_niveau_competence(ue_id: int, niveau_id: int): + """Associe le niveau et l'UE""" + log(f"set_ue_niveau_competence( {ue_id}, {niveau_id} )") + ue = UniteEns.query.get_or_404(ue_id) + + autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx) + niveaux_autres_ues = { + oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id + } + if niveau_id in niveaux_autres_ues: + log( + f"set_ue_niveau_competence: denying association of {ue} to already associated {niveau_id}" + ) + return "", 409 # conflict + if niveau_id == "": + # suppression de l'association + ue.niveau_competence = None + else: + niveau = ApcNiveau.query.get_or_404(niveau_id) + ue.niveau_competence = niveau + db.session.add(ue) + db.session.commit() + return "", 204 diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index f9dbb870..3b90187b 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -14,6 +14,7 @@ from flask import url_for, g from app.comp.res_but import ResultatsSemestreBUT from app.models import FormSemestre, Identite +from app.models import but_validations from app.models.groups import GroupDescr from app.models.ues import UniteEns from app.scodoc import sco_bulletins, sco_utils as scu @@ -244,7 +245,7 @@ class BulletinBUT: f"{fmt_note(bonus_vect[ue.id])} sur {ue.acronyme}" for ue in res.ues if ue.type != UE_SPORT - and res.modimpls_in_ue(ue.id, etudid) + and res.modimpls_in_ue(ue, etudid) and ue.id in res.bonus_ues and bonus_vect[ue.id] > 0.0 ] @@ -274,6 +275,11 @@ class BulletinBUT: etat_inscription = etud.inscription_etat(formsemestre.id) nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT] published = (not formsemestre.bul_hide_xml) or force_publishing + if formsemestre.formation.referentiel_competence is None: + etud_ues_ids = {ue.id for ue in res.ues if res.modimpls_in_ue(ue, etud.id)} + else: + etud_ues_ids = res.etud_ues_ids(etud.id) + d = { "version": "0", "type": "BUT", @@ -318,9 +324,13 @@ class BulletinBUT: ects_tot = sum([ue.ects or 0 for ue in res.ues]) if res.ues else 0.0 ects_acquis = sum([d.get("ects", 0) for d in decisions_ues.values()]) semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot} - semestre_infos.update( - sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id) - ) + if sco_preferences.get_preference("bul_show_decision", formsemestre.id): + semestre_infos.update( + sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id) + ) + semestre_infos.update( + but_validations.dict_decision_jury(etud, formsemestre) + ) if etat_inscription == scu.INSCRIT: # moyenne des moyennes générales du semestre semestre_infos["notes"] = { @@ -365,10 +375,7 @@ class BulletinBUT: ) for ue in res.ues # si l'UE comporte des modules auxquels on est inscrit: - if ( - (ue.type == UE_SPORT) - or self.res.modimpls_in_ue(ue.id, etud.id) - ) + if ((ue.type == UE_SPORT) or ue.id in etud_ues_ids) }, "semestre": semestre_infos, }, diff --git a/app/but/forms/jury_but_forms.py b/app/but/forms/jury_but_forms.py new file mode 100644 index 00000000..0f371960 --- /dev/null +++ b/app/but/forms/jury_but_forms.py @@ -0,0 +1,18 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""ScoDoc 9.3 : Formulaires / jurys BUT +""" + + +from flask_wtf import FlaskForm +from wtforms import SubmitField + + +class FormSemestreValidationAutoBUTForm(FlaskForm): + "simple form de confirmation" + submit = SubmitField("Lancer le calcul") + cancel = SubmitField("Annuler") diff --git a/app/but/forms/refcomp_forms.py b/app/but/forms/refcomp_forms.py index ce16292d..ee67cba2 100644 --- a/app/but/forms/refcomp_forms.py +++ b/app/but/forms/refcomp_forms.py @@ -13,7 +13,9 @@ from wtforms import SelectField, SubmitField class FormationRefCompForm(FlaskForm): - referentiel_competence = SelectField("Référentiels déjà chargés") + referentiel_competence = SelectField( + "Choisir parmi les référentiels déjà chargés :" + ) submit = SubmitField("Valider") cancel = SubmitField("Annuler") @@ -23,7 +25,7 @@ class RefCompLoadForm(FlaskForm): "Choisir un référentiel de compétences officiel BUT" ) upload = FileField( - label="Ou bien sélectionner un fichier XML au format Orébut", + label="... ou bien sélectionner un fichier XML au format Orébut (réservé aux développeurs !)", validators=[ FileAllowed( [ diff --git a/app/but/import_refcomp.py b/app/but/import_refcomp.py index 0f97cd95..9acd1c9d 100644 --- a/app/but/import_refcomp.py +++ b/app/but/import_refcomp.py @@ -4,7 +4,6 @@ # See LICENSE ############################################################################## from xml.etree import ElementTree -from typing import TextIO import sqlalchemy @@ -57,13 +56,13 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None): try: c = ApcCompetence(**ApcCompetence.attr_from_xml(competence.attrib)) db.session.flush() - except sqlalchemy.exc.IntegrityError: + except sqlalchemy.exc.IntegrityError as exc: # ne devrait plus se produire car pas d'unicité de l'id: donc inutile db.session.rollback() raise ScoValueError( f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({competence.attrib["id"]}) """ - ) + ) from exc ref.competences.append(c) # --- SITUATIONS situations = competence.find("situations") diff --git a/app/but/jury_but.py b/app/but/jury_but.py new file mode 100644 index 00000000..397bd05f --- /dev/null +++ b/app/but/jury_but.py @@ -0,0 +1,1054 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Jury BUT: logique de gestion + +Utilisation: + 1) chargement page jury, pour un étudiant et un formsemestre BUT quelconque + - DecisionsProposeesAnnee(formsemestre) + cherche l'autre formsemestre de la même année scolaire (peut ne pas exister) + cherche les RCUEs de l'année (BUT1, 2, 3) + pour un redoublant, le RCUE peut considérer un formsemestre d'une année antérieure. + + on instancie des DecisionsProposees pour les + différents éléments (UEs, RCUEs, Année, Diplôme) + Cela donne + - les codes possibles (dans .codes) + - le code actuel si une décision existe déjà (dans code_valide) + - pour les UEs, le rcue s'il y en a un) + + 2) Validation pour l'utilisateur (form)) => enregistrement code + - on vérifie que le code soumis est bien dans les codes possibles + - on enregistre la décision (dans ScolarFormSemestreValidation pour les UE, + ApcValidationRCUE pour les RCUE, et ApcValidationAnnee pour les années) + - Si RCUE validé, on déclenche d'éventuelles validations: + ("La validation des deux UE du niveau d’une compétence emporte la validation + de l’ensemble des UE du niveau inférieur de cette même compétence.") + +Les jurys de semestre BUT impairs entrainent systématiquement la génération d'une +autorisation d'inscription dans le semestre pair suivant: `ScolarAutorisationInscription`. +Les jurys de semestres pairs non (S2, S4, S6): il y a une décision sur l'année (ETP) + - autorisation en S_2n+1 (S3 ou S5) si: ADM, ADJ, PASD, PAS1CN + - autorisation en S2n-1 (S1, S3 ou S5) si: RED + - rien si pour les autres codes d'année. + +Le formulaire permet de choisir des codes d'UE, RCUE et Année (ETP). +Mais normalement, les codes d'UE sont à choisir: les RCUE et l'année s'en déduisent. +Si l'utilisateur coche "décision manuelle", il peut alors choisir les codes RCUE et années. + +La soumission du formulaire: + - etud, formation + - UEs: [(formsemestre, ue, code), ...] + - RCUE: [(formsemestre, ue, code), ...] le formsemestre est celui d'indice pair du niveau + (S2, S4 ou S6), il sera regoupé avec celui impair de la même année ou de la suivante. + - Année: [(formsemestre, code)] + +DecisionsProposeesAnnee: + si 1/2 des rcue et aucun < 8 + pour S5 condition sur les UE de BUT1 et BUT2 + => charger les DecisionsProposeesRCUE + +DecisionsProposeesRCUE: les RCUEs pour cette année + validable, compensable, ajourné. Utilise classe RegroupementCoherentUE + +DecisionsProposeesUE: décisions de jury sur une UE du BUT + initialisation sans compensation (ue isolée), mais + DecisionsProposeesRCUE appelera .set_compensable() + si on a la possibilité de la compenser dans le RCUE. +""" +import html +from operator import attrgetter +import re +from typing import Union + +from flask import g, url_for + +from app import db +from app import log +from app.comp.res_but import ResultatsSemestreBUT +from app.comp import inscr_mod, res_sem +from app.models import formsemestre + +from app.models.but_refcomp import ( + ApcAnneeParcours, + ApcCompetence, + ApcNiveau, + ApcParcours, + ApcParcoursNiveauCompetence, +) +from app.models import Scolog, ScolarAutorisationInscription +from app.models.but_validations import ( + ApcValidationAnnee, + ApcValidationRCUE, + RegroupementCoherentUE, +) +from app.models.etudiants import Identite +from app.models.formations import Formation +from app.models.formsemestre import FormSemestre, FormSemestreInscription +from app.models.ues import UniteEns +from app.models.validations import ScolarFormSemestreValidation +from app.scodoc import sco_codes_parcours as sco_codes +from app.scodoc.sco_codes_parcours import RED, UE_STANDARD +from app.scodoc import sco_utils as scu +from app.scodoc.sco_exceptions import ScoException, ScoValueError + + +class NoRCUEError(ScoValueError): + """Erreur en cas de RCUE manquant""" + + def __init__(self, deca: "DecisionsProposeesAnnee", ue: UniteEns): + if all(u.niveau_competence for u in deca.ues_pair): + warning_pair = "" + else: + warning_pair = """
certaines UE du semestre pair ne sont pas associées à un niveau de compétence
""" + if all(u.niveau_competence for u in deca.ues_impair): + warning_impair = "" + else: + warning_impair = """
certaines UE du semestre impair ne sont pas associées à un niveau de compétence
""" + msg = ( + f"""

Pas de RCUE pour l'UE {ue.acronyme}

+ {warning_impair} + {warning_pair} +
UE {ue.acronyme}: niveau {html.escape(str(ue.niveau_competence))}
+
UEs impaires: {html.escape(', '.join(str(u.niveau_competence or "pas de niveau") + for u in deca.ues_impair))} +
+ """ + + deca.infos() + ) + super().__init__(msg) + + +class DecisionsProposees: + """Une décision de jury proposé, constituée d'une liste de codes et d'une explication. + Super-classe, spécialisée pour les UE, les RCUE, les années et le diplôme. + + validation : None ou une instance de d'une classe avec un champ code + ApcValidationRCUE, ApcValidationAnnee ou ScolarFormSemestreValidation + """ + + # Codes toujours proposés sauf si include_communs est faux: + codes_communs = [ + sco_codes.RAT, + sco_codes.DEF, + sco_codes.ABAN, + sco_codes.DEM, + sco_codes.UEBSL, + ] + + def __init__( + self, + etud: Identite = None, + code: Union[str, list[str]] = None, + explanation="", + code_valide=None, + include_communs=True, + ): + self.etud = etud + self.codes = [] + "Les codes attribuables par ce jury" + if include_communs: + self.codes = self.codes_communs.copy() + if isinstance(code, list): + self.codes = code + self.codes + elif code is not None: + self.codes = [code] + self.codes + self.validation = None + "Validation enregistrée" + self.code_valide: str = code_valide + "Code décision actuel enregistré" + self.explanation: str = explanation + "Explication à afficher à côté de la décision" + self.recorded = False + "true si la décision vient d'être enregistrée" + + def __repr__(self) -> str: + return f"""<{self.__class__.__name__} valid={self.code_valide + } codes={self.codes} explanation={self.explanation}""" + + +class DecisionsProposeesAnnee(DecisionsProposees): + """Décisions de jury sur une année (ETP) du BUT + + Le texte: + La poursuite d'études dans un semestre pair d’une même année est de droit + pour tout étudiant. La poursuite d’études dans un semestre impair est + possible si et seulement si l’étudiant a obtenu : + - la moyenne à plus de la moitié des regroupements cohérents d’UE; + - et une moyenne égale ou supérieure à 8 sur 20 à chaque RCUE. + La poursuite d'études dans le semestre 5 nécessite de plus la validation + de toutes les UE des semestres 1 et 2 dans les conditions de validation + des points 4.3 (moy_ue >= 10) et 4.4 (compensation rcue), ou par décision + de jury. + """ + + # Codes toujours proposés sauf si include_communs est faux: + codes_communs = [ + sco_codes.RAT, + sco_codes.ABAN, + sco_codes.ABL, + sco_codes.ATJ, + sco_codes.DEF, + sco_codes.DEM, + sco_codes.EXCLU, + ] + + def __init__( + self, + etud: Identite, + formsemestre: FormSemestre, + ): + super().__init__(etud=etud) + self.formsemestre_id = formsemestre.id + formsemestre_impair, formsemestre_pair = self.comp_formsemestres(formsemestre) + assert ( + (formsemestre_pair is None) + or (formsemestre_impair is None) + or ( + ((formsemestre_pair.semestre_id - formsemestre_impair.semestre_id) == 1) + and ( + formsemestre_pair.formation.referentiel_competence_id + == formsemestre_impair.formation.referentiel_competence_id + ) + ) + ) + + self.formsemestre_impair = formsemestre_impair + "le 1er semestre de l'année scolaire considérée (S1, S3, S5)" + self.formsemestre_pair = formsemestre_pair + "le second formsemestre de la même année scolaire (S2, S4, S6)" + formsemestre_last = formsemestre_pair or formsemestre_impair + "le formsemestre le plus avancé dans cette année" + + self.annee_but = (formsemestre_last.semestre_id + 1) // 2 + "le rang de l'année dans le BUT: 1, 2, 3" + assert self.annee_but in (1, 2, 3) + self.rcues_annee = [] + "RCUEs de l'année" + self.inscription_etat = etud.inscription_etat(formsemestre_last.id) + + if self.formsemestre_impair is not None: + self.validation = ApcValidationAnnee.query.filter_by( + etudid=self.etud.id, + formsemestre_id=formsemestre_impair.id, + ordre=self.annee_but, + ).first() + else: + self.validation = None + if self.validation is not None: + self.code_valide = self.validation.code + self.parcour = None + "Le parcours considéré (celui du semestre pair, ou à défaut impair)" + if self.formsemestre_pair is not None: + self.res_pair: ResultatsSemestreBUT = res_sem.load_formsemestre_results( + self.formsemestre_pair + ) + else: + self.res_pair = None + if self.formsemestre_impair is not None: + self.res_impair: ResultatsSemestreBUT = res_sem.load_formsemestre_results( + self.formsemestre_impair + ) + else: + self.res_impair = None + + self.ues_impair, self.ues_pair = self.compute_ues_annee() # pylint: disable=all + self.decisions_ues = { + ue.id: DecisionsProposeesUE( + etud, formsemestre_impair, ue, self.inscription_etat + ) + for ue in self.ues_impair + } + "{ue_id : DecisionsProposeesUE} pour toutes les UE de l'année" + self.decisions_ues.update( + { + ue.id: DecisionsProposeesUE( + etud, formsemestre_pair, ue, self.inscription_etat + ) + for ue in self.ues_pair + } + ) + self.rcues_annee = self.compute_rcues_annee() + + formation = ( + self.formsemestre_impair.formation + if self.formsemestre_impair + else self.formsemestre_pair.formation + ) + self.niveaux_competences = ApcNiveau.niveaux_annee_de_parcours( + self.parcour, self.annee_but, formation.referentiel_competence + ).all() # non triés + "liste des niveaux de compétences associés à cette année" + self.decisions_rcue_by_niveau = self.compute_decisions_niveaux() + "les décisions rcue associées aux niveau_id" + self.dec_rcue_by_ue = self._dec_rcue_by_ue() + "{ ue_id : DecisionsProposeesRCUE } pour toutes les UE associées à un niveau" + self.nb_competences = len(self.niveaux_competences) + "le nombre de niveaux de compétences à valider cette année" + rcues_avec_niveau = [d.rcue for d in self.decisions_rcue_by_niveau.values()] + self.nb_validables = len( + [rcue for rcue in rcues_avec_niveau if rcue.est_validable()] + ) + "le nombre de comp. validables (éventuellement par compensation)" + self.nb_rcues_under_8 = len( + [rcue for rcue in rcues_avec_niveau if not rcue.est_suffisant()] + ) + "le nb de comp. sous la barre de 8/20" + # année ADM si toutes RCUE validées (sinon PASD) et non DEM ou DEF + self.admis = (self.nb_validables == self.nb_competences) and ( + self.inscription_etat == scu.INSCRIT + ) + "vrai si l'année est réussie, tous niveaux validables" + self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2) + # Peut passer si plus de la moitié validables et tous > 8 + self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0) + # XXX TODO ajouter condition pour passage en S5 + + # Enfin calcule les codes des UE: + for dec_ue in self.decisions_ues.values(): + dec_ue.compute_codes() + + # Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR + expl_rcues = ( + f"{self.nb_validables} niveau validable(s) sur {self.nb_competences}" + ) + if self.admis: + self.codes = [sco_codes.ADM] + self.codes + self.explanation = expl_rcues + elif self.inscription_etat != scu.INSCRIT: + self.codes = [ + sco_codes.DEM + if self.inscription_etat == scu.DEMISSION + else sco_codes.DEF, + # propose aussi d'autres codes, au cas où... + sco_codes.DEM + if self.inscription_etat != scu.DEMISSION + else sco_codes.DEF, + sco_codes.ABAN, + sco_codes.ABL, + sco_codes.EXCLU, + ] + elif self.passage_de_droit: + self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes + self.explanation = expl_rcues + elif self.valide_moitie_rcue: # mais au moins 1 rcue insuffisante + self.codes = [ + sco_codes.RED, + sco_codes.NAR, + sco_codes.PAS1NCI, + sco_codes.ADJ, + ] + self.codes + self.explanation = expl_rcues + f" et {self.nb_rcues_under_8} < 8" + else: + self.codes = [ + sco_codes.RED, + sco_codes.NAR, + sco_codes.PAS1NCI, + sco_codes.ADJ, + ] + self.codes + self.explanation = ( + expl_rcues + + f""" et {self.nb_rcues_under_8} + niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8""" + ) + # + + def infos(self) -> str: + "informations, for debugging purpose" + return f"""DecisionsProposeesAnnee + + """ + + def annee_scolaire(self) -> int: + "L'année de début de l'année scolaire" + formsemestre = self.formsemestre_impair or self.formsemestre_pair + return formsemestre.annee_scolaire() + + def annee_scolaire_str(self) -> str: + "L'année scolaire, eg '2021 - 2022'" + formsemestre = self.formsemestre_impair or self.formsemestre_pair + return formsemestre.annee_scolaire_str().replace(" ", "") + + def comp_formsemestres( + self, formsemestre: FormSemestre + ) -> tuple[FormSemestre, FormSemestre]: + """les deux formsemestres de l'année scolaire à laquelle appartient formsemestre.""" + if not formsemestre.formation.is_apc(): # garde fou + return None, None + if formsemestre.semestre_id % 2 == 0: + other_semestre_id = formsemestre.semestre_id - 1 + else: + other_semestre_id = formsemestre.semestre_id + 1 + annee_scolaire = formsemestre.annee_scolaire() + other_formsemestre = None + for inscr in self.etud.formsemestre_inscriptions: + if ( + # Même spécialité BUT (tolère ainsi des variantes de formation) + ( + inscr.formsemestre.formation.referentiel_competence + == formsemestre.formation.referentiel_competence + ) + # L'autre semestre + and (inscr.formsemestre.semestre_id == other_semestre_id) + # de la même année scolaire: + and (inscr.formsemestre.annee_scolaire() == annee_scolaire) + ): + other_formsemestre = inscr.formsemestre + if formsemestre.semestre_id % 2 == 0: + return other_formsemestre, formsemestre + return formsemestre, other_formsemestre + + def compute_ues_annee(self) -> list[list[UniteEns], list[UniteEns]]: + """UEs à valider cette année pour cet étudiant, selon son parcours. + Ramène [ listes des UE du semestre impair, liste des UE du semestre pair ]. + """ + etudid = self.etud.id + ues_sems = [] + for (formsemestre, res) in ( + (self.formsemestre_impair, self.res_impair), + (self.formsemestre_pair, self.res_pair), + ): + if (formsemestre is None) or (not formsemestre.formation.is_apc()): + ues = [] + else: + formation: Formation = formsemestre.formation + # Parcour dans lequel l'étudiant est inscrit, et liste des UEs + if res.etuds_parcour_id[etudid] is None: + # pas de parcour: prend toutes les UEs (non bonus) + ues = [ue for ue in res.etud_ues(etudid) if ue.type == UE_STANDARD] + ues.sort(key=lambda u: u.numero) + else: + parcour = ApcParcours.query.get(res.etuds_parcour_id[etudid]) + if parcour is not None: + self.parcour = parcour + ues = ( + formation.query_ues_parcour(parcour) + .filter_by(semestre_idx=formsemestre.semestre_id) + .order_by(UniteEns.numero) + .all() + ) + ues_sems.append(ues) + return ues_sems + + def check_ues_ready_jury(self) -> list[str]: + """Vérifie que les toutes les UEs (hors bonus) de l'année sont + bien associées à des niveaux de compétences. + Renvoie liste vide si ok, sinon liste de message explicatifs + """ + messages = [] + for ue in self.ues_impair + self.ues_pair: + if ue.niveau_competence is None: + messages.append( + f"UE {ue.acronyme} non associée à un niveau de compétence" + ) + if ue.semestre_idx is None: + messages.append( + f"UE {ue.acronyme} n'a pas d'indice de semestre dans la formation" + ) + return messages + + def compute_rcues_annee(self) -> list[RegroupementCoherentUE]: + """Liste des regroupements d'UE à considérer cette année. + Pour le moment on ne considère pas de RCUE à cheval sur plusieurs années (redoublants). + Si on n'a pas les deux semestres, aucun RCUE. + Raises ScoValueError s'il y a des UE sans RCUE. + """ + if self.formsemestre_pair is None or self.formsemestre_impair is None: + return [] + rcues_annee = [] + ues_impair_sans_rcue = {ue.id for ue in self.ues_impair} + for ue_pair in self.ues_pair: + rcue = None + for ue_impair in self.ues_impair: + if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id: + rcue = RegroupementCoherentUE( + self.etud, + self.formsemestre_impair, + ue_impair, + self.formsemestre_pair, + ue_pair, + self.inscription_etat, + ) + ues_impair_sans_rcue.discard(ue_impair.id) + break + if rcue is None: + raise NoRCUEError(deca=self, ue=ue_pair) + rcues_annee.append(rcue) + if len(ues_impair_sans_rcue) > 0: + ue = UniteEns.query.get(ues_impair_sans_rcue.pop()) + raise NoRCUEError(deca=self, ue=ue) + return rcues_annee + + def compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]: + """Pour chaque niveau de compétence de cette année, construit + le DecisionsProposeesRCUE, + ou None s'il n'y en a pas + (ne devrait pas arriver car compute_rcues_annee vérifie déjà cela). + Return: { niveau_id : DecisionsProposeesRCUE } + """ + # Retrouve le RCUE associé à chaque niveau + rc_niveaux = [] + for niveau in self.niveaux_competences: + rcue = None + for rc in self.rcues_annee: + if rc.ue_1.niveau_competence_id == niveau.id: + rcue = rc + break + if rcue is not None: + dec_rcue = DecisionsProposeesRCUE(self, rcue, self.inscription_etat) + rc_niveaux.append((dec_rcue, niveau.id)) + # prévient les UE concernées :-) + self.decisions_ues[dec_rcue.rcue.ue_1.id].set_rcue(dec_rcue.rcue) + self.decisions_ues[dec_rcue.rcue.ue_2.id].set_rcue(dec_rcue.rcue) + # Ordonne par numéro d'UE + rc_niveaux.sort(key=lambda x: x[0].rcue.ue_1.numero) + decisions_rcue_by_niveau = {x[1]: x[0] for x in rc_niveaux} + return decisions_rcue_by_niveau + + def _dec_rcue_by_ue(self) -> dict[int, "DecisionsProposeesRCUE"]: + """construit dict { ue_id : DecisionsProposeesRCUE } + à partir de self.decisions_rcue_by_niveau""" + d = {} + for dec_rcue in self.decisions_rcue_by_niveau.values(): + d[dec_rcue.rcue.ue_1.id] = dec_rcue + d[dec_rcue.rcue.ue_2.id] = dec_rcue + return d + + def next_annee_semestre_id(self, code: str) -> int: + """L'indice du semestre dans lequel l'étudiant est autorisé à + poursuivre l'année suivante. None si aucun.""" + if self.formsemestre_pair is None: + return None # seulement sur année + if code == RED: + return self.formsemestre_pair.semestre_id - 1 + elif ( + code in sco_codes.BUT_CODES_PASSAGE + and self.formsemestre_pair.semestre_id < sco_codes.ParcoursBUT.NB_SEM + ): + return self.formsemestre_pair.semestre_id + 1 + return None + + def record_form(self, form: dict): + """Enregistre les codes de jury en base + form dict: + - 'code_ue_1896' : 'AJ' code pour l'UE id 1896 + - 'code_rcue_6" : 'ADM' code pour le RCUE du niveau 6 + - 'code_annee' : 'ADM' code pour l'année + + Si les code_rcue et le code_annee ne sont pas fournis, + et qu'il n'y en a pas déjà, enregistre ceux par défaut. + """ + for key in form: + code = form[key] + # Codes d'UE + m = re.match(r"^code_ue_(\d+)$", key) + if m: + ue_id = int(m.group(1)) + dec_ue = self.decisions_ues.get(ue_id) + if not dec_ue: + raise ScoValueError(f"UE invalide ue_id={ue_id}") + dec_ue.record(code) + else: + # Codes de RCUE + m = re.match(r"^code_rcue_(\d+)$", key) + if m: + niveau_id = int(m.group(1)) + dec_rcue = self.decisions_rcue_by_niveau.get(niveau_id) + if not dec_rcue: + raise ScoValueError(f"RCUE invalide niveau_id={niveau_id}") + dec_rcue.record(code) + elif key == "code_annee": + # Code annuel + self.record(code) + + self.record_all() + db.session.commit() + + def record(self, code: str, no_overwrite=False): + """Enregistre le code de l'année, et au besoin l'autorisation d'inscription. + Si no_overwrite, ne fait rien si un code est déjà enregistré. + """ + if code and not code in self.codes: + raise ScoValueError( + f"code annee {html.escape(code)} invalide pour formsemestre {html.escape(self.formsemestre)}" + ) + if code == self.code_valide or (self.code_valide is not None and no_overwrite): + self.recorded = True + return # no change + if self.validation: + db.session.delete(self.validation) + db.session.flush() + if code is None: + self.validation = None + else: + self.validation = ApcValidationAnnee( + etudid=self.etud.id, + formsemestre=self.formsemestre_impair, + ordre=self.annee_but, + annee_scolaire=self.annee_scolaire(), + code=code, + ) + Scolog.logdb( + method="jury_but", + etudid=self.etud.id, + msg=f"Validation année BUT{self.annee_but}: {code}", + ) + db.session.add(self.validation) + # --- Autorisation d'inscription dans semestre suivant ? + if self.formsemestre_pair is not None: + if code is None: + ScolarAutorisationInscription.delete_autorisation_etud( + etudid=self.etud.id, + origin_formsemestre_id=self.formsemestre_pair.id, + ) + else: + next_semestre_id = self.next_annee_semestre_id(code) + if next_semestre_id is not None: + ScolarAutorisationInscription.autorise_etud( + self.etud.id, + self.formsemestre_pair.formation.formation_code, + self.formsemestre_pair.id, + next_semestre_id, + ) + + self.recorded = True + + def record_all(self): + """Enregistre les codes qui n'ont pas été spécifiés par le formulaire, et sont donc en mode "automatique" """ + decisions = ( + list(self.decisions_ues.values()) + + list(self.decisions_rcue_by_niveau.values()) + + [self] + ) + for dec in decisions: + if not dec.recorded: + # rappel: le code par défaut est en tête + code = dec.codes[0] if dec.codes else None + # s'il n'y a pas de code, efface + dec.record(code, no_overwrite=True) + + def erase(self): + """Efface les décisions de jury de cet étudiant + pour cette année: décisions d'UE, de RCUE, d'année, + et autorisations d'inscription émises. + """ + for dec_ue in self.decisions_ues.values(): + dec_ue.erase() + for dec_rcue in self.decisions_rcue_by_niveau.values(): + dec_rcue.erase() + if self.formsemestre_impair: + ScolarAutorisationInscription.delete_autorisation_etud( + self.etud.id, self.formsemestre_impair.id + ) + if self.formsemestre_pair: + ScolarAutorisationInscription.delete_autorisation_etud( + self.etud.id, self.formsemestre_pair.id + ) + validations = ApcValidationAnnee.query.filter_by( + etudid=self.etud.id, + formsemestre_id=self.formsemestre_impair.id, + ordre=self.annee_but, + ) + for validation in validations: + db.session.delete(validation) + db.session.flush() + + def get_autorisations_passage(self) -> list[int]: + """Les liste des indices de semestres auxquels on est autorisé à + s'inscrire depuis cette année""" + formsemestre = self.formsemestre_pair or self.formsemestre_impair + if not formsemestre: + return [] + return [ + a.semestre_id + for a in ScolarAutorisationInscription.query.filter_by( + etudid=self.etud.id, + origin_formsemestre_id=formsemestre.id, + ) + ] + + def descr_niveaux_validation(self, line_sep: str = "\n") -> str: + """Description textuelle des niveaux validés (enregistrés) + pour PV jurys + """ + validations = [ + dec_rcue.descr_validation() + for dec_rcue in self.decisions_rcue_by_niveau.values() + ] + return line_sep.join(v for v in validations if v) + + def descr_ues_validation(self, line_sep: str = "\n") -> str: + """Description textuelle des UE validées (enregistrés) + pour PV jurys + """ + validations = [] + for res in (self.res_impair, self.res_pair): + if res: + dec_ues = [ + self.decisions_ues[ue.id] + for ue in res.ues + if ue.type == UE_STANDARD and ue.id in self.decisions_ues + ] + valids = [dec_ue.descr_validation() for dec_ue in dec_ues] + validations.append(", ".join(v for v in valids if v)) + return line_sep.join(validations) + + +class DecisionsProposeesRCUE(DecisionsProposees): + """Liste des codes de décisions que l'on peut proposer pour + le RCUE de cet étudiant dans cette année. + + ADM, CMP, ADJ, AJ, RAT, DEF, ABAN + """ + + codes_communs = [ + sco_codes.ADJ, + sco_codes.ATJ, + sco_codes.RAT, + sco_codes.DEF, + sco_codes.ABAN, + ] + + def __init__( + self, + dec_prop_annee: DecisionsProposeesAnnee, + rcue: RegroupementCoherentUE, + inscription_etat: str = scu.INSCRIT, + ): + super().__init__(etud=dec_prop_annee.etud) + self.rcue = rcue + if rcue is None: # RCUE non dispo, eg un seul semestre + self.codes = [] + return + self.inscription_etat = inscription_etat + "inscription: I, DEM, DEF" + self.parcour = dec_prop_annee.parcour + if inscription_etat != scu.INSCRIT: + self.validation = None # cache toute validation + self.explanation = "non incrit (dem. ou déf.)" + self.codes = [ + sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF + ] + return + self.validation = rcue.query_validations().first() + if self.validation is not None: + self.code_valide = self.validation.code + if rcue.est_compensable(): + self.codes.insert(0, sco_codes.CMP) + elif rcue.est_validable(): + self.codes.insert(0, sco_codes.ADM) + else: + self.codes.insert(0, sco_codes.AJ) + + def record(self, code: str, no_overwrite=False): + """Enregistre le code""" + if code and not code in self.codes: + raise ScoValueError( + f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}" + ) + if code == self.code_valide or (self.code_valide is not None and no_overwrite): + self.recorded = True + return # no change + parcours_id = self.parcour.id if self.parcour is not None else None + if self.validation: + db.session.delete(self.validation) + db.session.flush() + if code is None: + self.validation = None + else: + self.validation = ApcValidationRCUE( + etudid=self.etud.id, + formsemestre_id=self.rcue.formsemestre_2.id, + ue1_id=self.rcue.ue_1.id, + ue2_id=self.rcue.ue_2.id, + parcours_id=parcours_id, + code=code, + ) + Scolog.logdb( + method="jury_but", + etudid=self.etud.id, + msg=f"Validation RCUE {repr(self.rcue)}", + ) + db.session.add(self.validation) + self.recorded = True + + def erase(self): + """Efface la décision de jury de cet étudiant pour cet RCUE""" + # par prudence, on requete toutes les validations, en cas de doublons + validations = self.rcue.query_validations() + for validation in validations: + db.session.delete(validation) + db.session.flush() + + def descr_validation(self) -> str: + """Description validation niveau enregistrée, pour PV jury. + Si le niveau est validé, done son acronyme, sinon chaine vide. + """ + if self.code_valide in sco_codes.CODES_RCUE_VALIDES: + if ( + self.rcue and self.rcue.ue_1 and self.rcue.ue_1.niveau_competence + ): # prudence ! + niveau_titre = self.rcue.ue_1.niveau_competence.competence.titre or "" + ordre = self.rcue.ue_1.niveau_competence.ordre + else: + return "?" # oups ? + return f"{niveau_titre} niv. {ordre}" + return "" + + +class DecisionsProposeesUE(DecisionsProposees): + """Décisions de jury sur une UE du BUT + + Liste des codes de décisions que l'on peut proposer pour + cette UE d'un étudiant dans un semestre. + + Si DEF ou DEM ou ABAN ou ABL sur année BUT: seulement DEF, DEM, ABAN, ABL + + si moy_ue > 10, ADM + sinon si compensation dans RCUE: CMP + sinon: ADJ, AJ + + et proposer toujours: RAT, DEF, ABAN, DEM, UEBSL (codes_communs) + """ + + # Codes toujours proposés sauf si include_communs est faux: + codes_communs = [ + sco_codes.RAT, + sco_codes.DEF, + sco_codes.ABAN, + sco_codes.ATJ, + sco_codes.DEM, + sco_codes.UEBSL, + ] + + def __init__( + self, + etud: Identite, + formsemestre: FormSemestre, + ue: UniteEns, + inscription_etat: str = scu.INSCRIT, + ): + super().__init__(etud=etud) + self.formsemestre = formsemestre + self.ue: UniteEns = ue + self.rcue: RegroupementCoherentUE = None + "Le rcu auquel est rattaché cette UE, ou None" + self.inscription_etat = inscription_etat + "inscription: I, DEM, DEF" + if ue.type == sco_codes.UE_SPORT: + self.explanation = "UE bonus, pas de décision de jury" + self.codes = [] # aucun code proposé + return + if inscription_etat != scu.INSCRIT: + self.validation = None # cache toute validation + self.explanation = "non incrit (dem. ou déf.)" + self.codes = [ + sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF + ] + self.moy_ue = "-" + return + # Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non) + # mais ici on a restreint au formsemestre donc une seule (prend la première) + self.validation = ScolarFormSemestreValidation.query.filter_by( + etudid=self.etud.id, formsemestre_id=formsemestre.id, ue_id=ue.id + ).first() + if self.validation is not None: + self.code_valide = self.validation.code + + # Moyenne de l'UE ? + res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) + if not ue.id in res.etud_moy_ue: + self.explanation = "UE sans résultat" + return + if not etud.id in res.etud_moy_ue[ue.id]: + self.explanation = "Étudiant sans résultat dans cette UE" + return + self.moy_ue = res.etud_moy_ue[ue.id][etud.id] + + def set_rcue(self, rcue: RegroupementCoherentUE): + """Rattache cette UE à un RCUE. Cela peut modifier les codes + proposés (si compensation)""" + self.rcue = rcue + + def compute_codes(self): + """Calcul des .codes attribuables et de l'explanation associée""" + if self.inscription_etat != scu.INSCRIT: + return + if self.moy_ue > (sco_codes.ParcoursBUT.BARRE_MOY - sco_codes.NOTES_TOLERANCE): + self.codes.insert(0, sco_codes.ADM) + self.explanation = (f"Moyenne >= {sco_codes.ParcoursBUT.BARRE_MOY}/20",) + elif self.rcue and self.rcue.est_compensable(): + self.codes.insert(0, sco_codes.CMP) + self.explanation = "compensable dans le RCUE" + else: + # Échec à valider cette UE + self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes + self.explanation = "notes insuffisantes" + + def record(self, code: str, no_overwrite=False): + """Enregistre le code""" + if code and not code in self.codes: + raise ScoValueError( + f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}" + ) + if code == self.code_valide or (self.code_valide is not None and no_overwrite): + self.recorded = True + return # no change + if self.validation: + db.session.delete(self.validation) + db.session.flush() + if code is None: + self.validation = None + else: + self.validation = ScolarFormSemestreValidation( + etudid=self.etud.id, + formsemestre_id=self.formsemestre.id, + ue_id=self.ue.id, + code=code, + moy_ue=self.moy_ue, + ) + Scolog.logdb( + method="jury_but", + etudid=self.etud.id, + msg=f"Validation UE {self.ue.id}", + ) + db.session.add(self.validation) + self.recorded = True + + def erase(self): + """Efface la décision de jury de cet étudiant pour cette UE""" + # par prudence, on requete toutes les validations, en cas de doublons + validations = ScolarFormSemestreValidation.query.filter_by( + etudid=self.etud.id, formsemestre_id=self.formsemestre.id, ue_id=self.ue.id + ) + for validation in validations: + db.session.delete(validation) + db.session.flush() + + def descr_validation(self) -> str: + """Description validation niveau enregistrée, pour PV jury. + Si l'UE est validée, donne son acronyme, sinon chaine vide. + """ + if self.code_valide in sco_codes.CODES_UE_VALIDES: + return f"{self.ue.acronyme}" + return "" + + +class BUTCursusEtud: # WIP TODO + """Validation du cursus d'un étudiant""" + + def __init__(self, formsemestre: FormSemestre, etud: Identite): + if formsemestre.formation.referentiel_competence is None: + raise ScoException("BUTCursusEtud: pas de référentiel de compétences") + assert len(etud.formsemestre_inscriptions) > 0 + self.formsemestre = formsemestre + self.etud = etud + # + # La dernière inscription en date va donner le parcours (donc les compétences à valider) + self.last_inscription = sorted( + etud.formsemestre_inscriptions, key=attrgetter("formsemestre.date_debut") + )[-1] + + def est_diplomable(self) -> bool: + """Vrai si toutes les compétences sont validables""" + return all( + self.competence_validable(competence) + for competence in self.competences_du_parcours() + ) + + def est_diplome(self) -> bool: + """Vrai si BUT déjà validé""" + # vrai si la troisième année est validée + # On cherche les validations de 3ieme annee (ordre=3) avec le même référentiel + # de formation que nous. + return ( + ApcValidationAnnee.query.filter_by(etudid=self.etud.id, ordre=3) + .join(FormSemestre, FormSemestre.id == ApcValidationAnnee.formsemestre_id) + .join(Formation, FormSemestre.formation_id == Formation.id) + .filter( + Formation.referentiel_competence_id + == self.formsemestre.formation.referentiel_competence_id + ) + .count() + > 0 + ) + + def competences_du_parcours(self) -> list[ApcCompetence]: + """Construit liste des compétences du parcours, qui doivent être + validées pour obtenir le diplôme. + Le parcours est celui de la dernière inscription. + """ + parcour = self.last_inscription.parcour + query = self.formsemestre.formation.formation.query_competences_parcour(parcour) + if query is None: + return [] + return query.all() + + def competence_validee(self, competence: ApcCompetence) -> bool: + """Vrai si la compétence est validée, c'est à dire que tous ses + niveaux sont validés (ApcValidationRCUE). + """ + # XXX A REVOIR + validations = ( + ApcValidationRCUE.query.filter_by(etudid=self.etud.id) + .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id) + .join(ApcNiveau, ApcNiveau.id == UniteEns.niveau_competence_id) + .join(ApcCompetence, ApcCompetence.id == ApcNiveau.competence_id) + ) + + def competence_validable(self, competence: ApcCompetence): + """Vrai si la compétence est "validable" automatiquement, c'est à dire + que les conditions de notes sont satisfaites pour l'acquisition de + son niveau le plus élevé, qu'il ne manque que l'enregistrement de la décision. + + En vertu de la règle "La validation des deux UE du niveau d’une compétence + emporte la validation de l'ensemble des UE du niveau inférieur de cette + même compétence.", + il suffit de considérer le dernier niveau dans lequel l'étudiant est inscrit. + """ + pass + + def ues_emportees(self, niveau: ApcNiveau) -> list[tuple[FormSemestre, UniteEns]]: + """La liste des UE à valider si on valide ce niveau. + Ne liste que les UE qui ne sont pas déjà acquises. + + Selon la règle donnée par l'arrêté BUT: + * La validation des deux UE du niveau d’une compétence emporte la validation de + l'ensemble des UE du niveau inférieur de cette même compétence. + """ + pass diff --git a/app/but/jury_but_pv.py b/app/but/jury_but_pv.py new file mode 100644 index 00000000..095c5efc --- /dev/null +++ b/app/but/jury_but_pv.py @@ -0,0 +1,137 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Jury BUT: table synthèse résultats semestre / PV +""" +from flask import g, request, url_for + +from openpyxl.styles import Font, Border, Side, Alignment, PatternFill + +from app import log +from app.but import jury_but +from app.models.etudiants import Identite +from app.models.formsemestre import FormSemestre +from app.scodoc.gen_tables import GenTable +from app.scodoc import sco_excel +from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc import sco_preferences +from app.scodoc import sco_utils as scu + + +def _descr_cursus_but(etud: Identite) -> str: + "description de la liste des semestres BUT suivis" + # prend simplement tous les semestre de type APC, ce qui sera faux si + # l'étudiant change de spécialité au sein du même département + # (ce qui ne peut normalement pas se produire) + indices = sorted( + [ + ins.formsemestre.semestre_id + if ins.formsemestre.semestre_id is not None + else -1 + for ins in etud.formsemestre_inscriptions + if ins.formsemestre.formation.is_apc() + ] + ) + return ", ".join(f"S{indice}" for indice in indices) + + +def pvjury_table_but(formsemestre_id: int, format="html") -> list[dict]: + """Page récapitulant les décisions de jury BUT + formsemestre peut être pair ou impair + """ + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + assert formsemestre.formation.is_apc() + title = "Procès-verbal de jury BUT annuel" + + if format == "html": + line_sep = "
" + else: + line_sep = "\n" + # remplace pour le BUT la fonction sco_pvjury.pvjury_table + annee_but = (formsemestre.semestre_id + 1) // 2 + titles = { + "nom": "Nom", + "cursus": "Cursus", + "ues": "UE validées", + "niveaux": "Niveaux de compétences validés", + "decision_but": f"Décision BUT{annee_but}", + "diplome": "Résultat au diplôme", + "devenir": "Devenir", + "observations": "Observations", + } + rows = [] + for etudid in formsemestre.etuds_inscriptions: + etud: Identite = Identite.query.get(etudid) + try: + deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) + if deca.annee_but != annee_but: # wtf ? + log( + f"pvjury_table_but: inconsistent annee_but {deca.annee_but} != {annee_but}" + ) + continue + except ScoValueError: + deca = None + row = { + "nom": etud.etat_civil_pv(line_sep=line_sep), + "_nom_order": etud.sort_key, + "_nom_target_attrs": f'class="etudinfo" id="{etud.id}"', + "_nom_td_attrs": f'id="{etud.id}" class="etudinfo"', + "_nom_target": url_for( + "scolar.ficheEtud", + scodoc_dept=g.scodoc_dept, + etudid=etud.id, + ), + "cursus": _descr_cursus_but(etud), + "ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-", + "niveaux": deca.descr_niveaux_validation(line_sep=line_sep) + if deca + else "-", + "decision_but": deca.code_valide if deca else "", + "devenir": ", ".join([f"S{i}" for i in deca.get_autorisations_passage()]), + } + + rows.append(row) + + rows.sort(key=lambda x: x["_nom_order"]) + + # Style excel... passages à la ligne sur \n + xls_style_base = sco_excel.excel_make_style() + xls_style_base["alignment"] = Alignment(wrapText=True, vertical="top") + + tab = GenTable( + base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}", + caption=title, + columns_ids=titles.keys(), + html_caption=title, + html_class="pvjury_table_but table_leftalign", + html_title=f"""
{title} + + version excel
+ + """, + html_with_td_classes=True, + origin=f"Généré par {scu.sco_version.SCONAME} le {scu.timedate_human_repr()}", + page_title=title, + pdf_title=title, + preferences=sco_preferences.SemPreferences(), + rows=rows, + table_id="formation_table_recap", + titles=titles, + xls_columns_width={ + "nom": 32, + "cursus": 12, + "ues": 32, + "niveaux": 32, + "decision_but": 14, + "diplome": 17, + "devenir": 8, + "observations": 12, + }, + xls_style_base=xls_style_base, + ) + return tab.make_page(format=format, javascripts=["js/etud_info.js"], init_qtip=True) diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py new file mode 100644 index 00000000..7d47cbe5 --- /dev/null +++ b/app/but/jury_but_recap.py @@ -0,0 +1,424 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Jury BUT: table recap annuelle et liens saisie +""" + +import time +import numpy as np +from flask import g, url_for + +from app.but import jury_but +from app.but.jury_but import ( + DecisionsProposeesAnnee, + DecisionsProposeesRCUE, + DecisionsProposeesUE, +) +from app.comp.res_but import ResultatsSemestreBUT +from app.comp import res_sem +from app.models.etudiants import Identite +from app.models.formsemestre import FormSemestre + +from app.scodoc.sco_codes_parcours import ( + BUT_BARRE_RCUE, + BUT_BARRE_UE, + BUT_BARRE_UE8, + BUT_RCUE_SUFFISANT, +) +from app.scodoc import sco_formsemestre_status +from app.scodoc import html_sco_header +from app.scodoc import sco_utils as scu +from app.scodoc.sco_exceptions import ScoValueError + + +def formsemestre_saisie_jury_but( + formsemestre2: FormSemestre, + read_only: bool = False, + selected_etudid: int = None, + mode="jury", +) -> str: + """formsemestre est un semestre PAIR + Si readonly, ne montre pas le lien "saisir la décision" + + => page html complète + + Si mode == "recap", table recap des codes, sans liens de saisie. + """ + # Quick & Dirty + # pour chaque etud de res2 trié + # S1: UE1, ..., UEn + # S2: UE1, ..., UEn + # + # UE1_s1, UE1_s2, moy_rcue, UE2... , Nbrcue_validables, Nbrcue<8, passage_de_droit, valide_moitie_rcue + # + # Pour chaque etud de res2 trié + # DecisionsProposeesAnnee(etud, formsemestre2) + # Pour le 1er etud, faire un check_ues_ready_jury(self) -> page d'erreur + # -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc + if formsemestre2.semestre_id % 2 != 0: + raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs") + + if formsemestre2.formation.referentiel_competence is None: + raise ScoValueError( + """ +

Pas de référentiel de compétences associé à la formation !

+

Pour associer un référentiel, passer par le menu Semestre / + Voir la formation... et suivre le lien "associer à un référentiel + de compétences" + """ + ) + + rows, titles, column_ids = get_table_jury_but( + formsemestre2, read_only=read_only, mode=mode + ) + if not rows: + return ( + '

aucun étudiant !
' + ) + filename = scu.sanitize_filename( + f"""jury-but-{formsemestre2.titre_num()}-{time.strftime("%Y-%m-%d")}""" + ) + klass = "table_jury_but_bilan" if mode == "recap" else "" + table_html = build_table_jury_but_html( + filename, rows, titles, column_ids, selected_etudid=selected_etudid, klass=klass + ) + H = [ + html_sco_header.sco_header( + page_title=f"{formsemestre2.sem_modalite()}: jury BUT annuel", + no_side_bar=True, + init_qtip=True, + javascripts=["js/etud_info.js", "js/table_recap.js"], + ), + sco_formsemestre_status.formsemestre_status_head( + formsemestre_id=formsemestre2.id + ), + ] + if mode == "recap": + H.append( + f"""

Décisions de jury enregistrées pour les étudiants de ce semestre

+ + """ + ) + H.append( + f""" + + {table_html} + + + + {html_sco_header.sco_footer()} + """ + ) + return "\n".join(H) + + +def build_table_jury_but_html( + filename: str, rows, titles, column_ids, selected_etudid: int = None, klass="" +) -> str: + """assemble la table html""" + footer_rows = [] # inutilisé pour l'instant + H = [ + f"""
""" + ] + # header + H.append( + f""" + + {scu.gen_row(column_ids, titles, "th")} + + """ + ) + # body + H.append("") + for row in rows: + H.append(f"{scu.gen_row(column_ids, row, selected_etudid=selected_etudid)}\n") + H.append("\n") + # footer + H.append("") + idx_last = len(footer_rows) - 1 + for i, row in enumerate(footer_rows): + H.append(f'{scu.gen_row(column_ids, row, "th" if i == idx_last else "td")}\n') + H.append( + """ + +
+
+ """ + ) + return "".join(H) + + +class RowCollector: + """Une ligne de la table""" + + def __init__( + self, + cells: dict = None, + titles: dict = None, + convert_values=True, + column_classes: dict = None, + ): + self.titles = titles + self.row = cells or {} # col_id : str + self.column_classes = column_classes # col_id : str, css class + self.idx = 0 + self.last_etud_cell_idx = 0 + if convert_values: + self.fmt_note = scu.fmt_note + else: + self.fmt_note = lambda x: x + + def __setitem__(self, key, value): + self.row[key] = value + + def __getitem__(self, key): + return self.row[key] + + def get_row_dict(self): + "La ligne, comme un dict" + # create empty cells + for col_id in self.titles: + if col_id not in self.row: + self.row[col_id] = "" + klass = self.column_classes.get(col_id) + if klass: + self.row[f"_{col_id}_class"] = klass + return self.row + + def add_cell( + self, + col_id: str, + title: str, + content: str, + classes: str = "", + idx: int = None, + column_class="", + ): + """Add a row to our table. classes is a list of css class names""" + self.idx = idx if idx is not None else self.idx + self.row[col_id] = content + if classes: + self.row[f"_{col_id}_class"] = classes + f" c{self.idx}" + if not col_id in self.titles: + self.titles[col_id] = title + self.titles[f"_{col_id}_col_order"] = self.idx + if classes: + self.titles[f"_{col_id}_class"] = classes + self.column_classes[col_id] = column_class + self.idx += 1 + + def add_etud_cells(self, etud: Identite, formsemestre: FormSemestre): + "Les cells code, nom, prénom etc." + # --- Codes (seront cachés, mais exportés en excel) + self.add_cell("etudid", "etudid", etud.id, "codes") + self.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes") + # --- Identité étudiant (adapté de res_comon/get_table_recap, à factoriser XXX TODO) + self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail") + self.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail") + self["_nom_disp_order"] = etud.sort_key + self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") + self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court") + self["_nom_short_order"] = etud.sort_key + self["_nom_short_target"] = url_for( + "notes.formsemestre_bulletinetud", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre.id, + etudid=etud.id, + ) + self["_nom_short_target_attrs"] = f'class="etudinfo" id="{etud.id}"' + self["_nom_disp_target"] = self["_nom_short_target"] + self["_nom_disp_target_attrs"] = self["_nom_short_target_attrs"] + self.last_etud_cell_idx = self.idx + + def add_ue_cells(self, dec_ue: DecisionsProposeesUE): + "cell de moyenne d'UE" + col_id = f"moy_ue_{dec_ue.ue.id}" + note_class = "" + val = dec_ue.moy_ue + if isinstance(val, float): + if val < BUT_BARRE_UE: + note_class = " moy_inf" + elif val >= BUT_BARRE_UE: + note_class = " moy_ue_valid" + if val < BUT_BARRE_UE8: + note_class = " moy_ue_warning" # notes très basses + self.add_cell( + col_id, + dec_ue.ue.acronyme, + self.fmt_note(val), + "col_ue" + note_class, + column_class="col_ue", + ) + self.add_cell( + col_id + "_code", + dec_ue.ue.acronyme, + dec_ue.code_valide or "", + "col_ue_code recorded_code", + column_class="col_ue", + ) + + def add_rcue_cells(self, dec_rcue: DecisionsProposeesRCUE): + "2 cells: moyenne du RCUE, code enregistré" + rcue = dec_rcue.rcue + col_id = f"moy_rcue_{rcue.ue_1.niveau_competence_id}" # le niveau_id + note_class = "" + val = rcue.moy_rcue + if isinstance(val, float): + if val < BUT_BARRE_RCUE: + note_class = " moy_ue_inf" + elif val >= BUT_BARRE_RCUE: + note_class = " moy_ue_valid" + if val < BUT_RCUE_SUFFISANT: + note_class = " moy_ue_warning" # notes très basses + self.add_cell( + col_id, + f"
{rcue.ue_1.acronyme}
{rcue.ue_2.acronyme}
", + self.fmt_note(val), + "col_rcue" + note_class, + column_class="col_rcue", + ) + self.add_cell( + col_id + "_code", + f"
{rcue.ue_1.acronyme}
{rcue.ue_2.acronyme}
", + dec_rcue.code_valide or "", + "col_rcue_code recorded_code", + column_class="col_rcue", + ) + + def add_nb_rcues_cell(self, deca: DecisionsProposeesAnnee): + "cell avec nb niveaux validables / total" + klass = " " + if deca.nb_rcues_under_8 > 0: + klass += "moy_ue_warning" + elif deca.nb_validables < deca.nb_competences: + klass += "moy_ue_inf" + else: + klass += "moy_ue_valid" + self.add_cell( + "rcues_validables", + "RCUEs", + f"""{deca.nb_validables}/{deca.nb_competences}""" + + ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""), + "col_rcue col_rcues_validables" + klass, + ) + self["_rcues_validables_data"] = { + "etudid": deca.etud.id, + "nomprenom": deca.etud.nomprenom, + } + if len(deca.rcues_annee) > 0: + # permet un tri par nb de niveaux validables + moyenne gen indicative S_pair + if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen: + moy = deca.res_pair.etud_moy_gen[deca.etud.id] + if np.isnan(moy): + moy_gen_d = "x" + else: + moy_gen_d = f"{int(moy*1000):05}" + else: + moy_gen_d = "x" + self["_rcues_validables_order"] = f"{deca.nb_validables:04d}-{moy_gen_d}" + else: + # etudiants sans RCUE: pas de semestre impair, ... + # les classe à la fin + self[ + "_rcues_validables_order" + ] = f"{deca.nb_validables:04d}-00000-{deca.etud.sort_key}" + + +def get_table_jury_but( + formsemestre2: FormSemestre, read_only: bool = False, mode="jury" +) -> tuple[list[dict], list[str], list[str]]: + """Construit la table des résultats annuels pour le jury BUT""" + res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2) + titles = {} # column_id : title + column_classes = {} + rows = [] + for etudid in formsemestre2.etuds_inscriptions: + etud: Identite = Identite.query.get(etudid) + deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre2) + row = RowCollector(titles=titles, column_classes=column_classes) + row.add_etud_cells(etud, formsemestre2) + row.idx = 100 # laisse place pour les colonnes de groupes + # --- Nombre de niveaux + row.add_nb_rcues_cell(deca) + # --- Les RCUEs + for rcue in deca.rcues_annee: + dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id) + if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau + row.add_ue_cells(deca.decisions_ues[rcue.ue_1.id]) + row.add_ue_cells(deca.decisions_ues[rcue.ue_2.id]) + row.add_rcue_cells(dec_rcue) + # --- Le code annuel existant + row.add_cell( + "code_annee", + "Année", + f"""{deca.code_valide or ''}""", + "col_code_annee", + ) + # --- Le lien de saisie + if mode != "recap": + row.add_cell( + "lien_saisie", + "", + f""" + + {"voir" if read_only else ("modif." if deca.code_valide else "saisie")} + décision + """ + if deca.inscription_etat == scu.INSCRIT + else deca.inscription_etat, + "col_lien_saisie_but", + ) + rows.append(row) + rows_dict = [row.get_row_dict() for row in rows] + if len(rows_dict) > 0: + res2.recap_add_partitions(rows_dict, titles, col_idx=row.last_etud_cell_idx + 1) + column_ids = [title for title in titles if not title.startswith("_")] + column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000)) + rows_dict.sort(key=lambda row: row["_nom_disp_order"]) + return rows_dict, titles, column_ids diff --git a/app/but/jury_but_validation_auto.py b/app/but/jury_but_validation_auto.py new file mode 100644 index 00000000..99512168 --- /dev/null +++ b/app/but/jury_but_validation_auto.py @@ -0,0 +1,34 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Jury BUT: clacul des décisions de jury annuelles "automatiques" +""" + +from flask import g, url_for + +from app import db +from app.but import jury_but +from app.models.etudiants import Identite +from app.models.formsemestre import FormSemestre +from app.scodoc.sco_exceptions import ScoValueError + + +def formsemestre_validation_auto_but(formsemestre: FormSemestre) -> int: + """Calcul automatique des décisions de jury sur une année BUT. + Returns: nombre d'étudiants "admis" + """ + if not formsemestre.formation.is_apc(): + raise ScoValueError("fonction réservée aux formations BUT") + nb_admis = 0 + for etudid in formsemestre.etuds_inscriptions: + etud: Identite = Identite.query.get(etudid) + deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) + if deca.admis: # année réussie + deca.record_all() + nb_admis += 1 + + db.session.commit() + return nb_admis diff --git a/app/but/jury_but_view.py b/app/but/jury_but_view.py new file mode 100644 index 00000000..fa517120 --- /dev/null +++ b/app/but/jury_but_view.py @@ -0,0 +1,173 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Jury BUT: affichage/formulaire +""" +from flask import g, url_for +from app.models.etudiants import Identite + +from app.scodoc import sco_utils as scu +from app.but.jury_but import DecisionsProposeesAnnee, DecisionsProposeesUE +from app.models import FormSemestre, FormSemestreInscription, UniteEns +from app.scodoc.sco_exceptions import ScoValueError + + +def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str: + """Affichage des décisions annuelles BUT + Si pas read_only, menus sélection codes jury. + """ + H = [] + if deca.code_valide and not read_only: + erase_span = f"""effacer décisions""" + else: + erase_span = "" + + H.append( + f"""
+
+ Décision de jury pour l'année : { + _gen_but_select("code_annee", deca.codes, deca.code_valide, + disabled=True, klass="manual") + } + ({'non ' if deca.code_valide is None else ''}enregistrée) + {erase_span} +
+
{deca.explanation}
+
""" + ) + + H.append( + f""" +
Niveaux de compétences et unités d'enseignement :
+
+
+
S{1}
+
S{2}
+
RCUE
+ """ + ) + for niveau in deca.niveaux_competences: + H.append( + f"""
+
{niveau.competence.titre}
+
""" + ) + dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) + if dec_rcue is None: + break + # Semestre impair + H.append( + _gen_but_niveau_ue( + dec_rcue.rcue.ue_1, + dec_rcue.rcue.moy_ue_1, + deca.decisions_ues[dec_rcue.rcue.ue_1.id], + disabled=read_only, + ) + ) + # Semestre pair + H.append( + _gen_but_niveau_ue( + dec_rcue.rcue.ue_2, + dec_rcue.rcue.moy_ue_2, + deca.decisions_ues[dec_rcue.rcue.ue_2.id], + disabled=read_only, + ) + ) + # RCUE + H.append( + f"""
+
{scu.fmt_note(dec_rcue.rcue.moy_rcue)}
+
{ + _gen_but_select("code_rcue_"+str(niveau.id), + dec_rcue.codes, + dec_rcue.code_valide, + disabled=True, klass="manual" + ) + }
+
""" + ) + H.append("
") # but_annee + return "\n".join(H) + + +def _gen_but_select( + name: str, + codes: list[str], + code_valide: str, + disabled: bool = False, + klass: str = "", +) -> str: + "Le menu html select avec les codes" + h = "\n".join( + [ + f"""""" + for code in codes + ] + ) + return f""" + """ + + +def _gen_but_niveau_ue( + ue: UniteEns, moy_ue: float, dec_ue: DecisionsProposeesUE, disabled=False +): + return f"""
+
{ue.acronyme}
+
{scu.fmt_note(moy_ue)}
+
{ + _gen_but_select("code_ue_"+str(ue.id), + dec_ue.codes, + dec_ue.code_valide, disabled=disabled + ) + }
+
""" + + +# +def infos_fiche_etud_html(etudid: int) -> str: + """Section html pour fiche etudiant + provisoire pour BUT 2022 + """ + etud: Identite = Identite.query.get_or_404(etudid) + inscriptions = ( + FormSemestreInscription.query.join(FormSemestreInscription.formsemestre) + .filter( + FormSemestreInscription.etudid == etud.id, + ) + .order_by(FormSemestre.date_debut) + ) + formsemestres_but = [ + i.formsemestre for i in inscriptions if i.formsemestre.formation.is_apc() + ] + if len(formsemestres_but) == 0: + return "" + + # temporaire quick & dirty: affiche le dernier + try: + deca = DecisionsProposeesAnnee(etud, formsemestres_but[-1]) + if len(deca.rcues_annee) > 0: + return f"""
+ {show_etud(deca, read_only=True)} +
+ """ + except ScoValueError: + pass + + return "" diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 6cf0767f..88eb36fc 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -767,6 +767,21 @@ class BonusStMalo(BonusIUTRennes1): __doc__ = BonusIUTRennes1.__doc__ +class BonusLaRocheSurYon(BonusSportAdditif): + """Bonus IUT de La Roche-sur-Yon + + Si une note de bonus est saisie, l'étudiant est gratifié de 0,2 points + sur sa moyenne générale ou, en BUT, sur la moyenne de chaque UE. + """ + + name = "bonus_larochesuryon" + displayed_name = "IUT de La Roche-sur-Yon" + seuil_moy_gen = 0.0 + seuil_comptage = 0.0 + proportion_point = 1e10 # le moindre point sature le bonus + bonus_max = 0.2 # à 0.2 + + class BonusLaRochelle(BonusSportAdditif): """Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle. @@ -1023,6 +1038,54 @@ class BonusNantes(BonusSportAdditif): bonus_max = 0.5 # plafonnement à 0.5 points +class BonusOrleans(BonusSportAdditif): + """Calcul bonus modules optionnels (sport, culture), règle IUT d'Orléans +

Cadre général : + En reconnaissance de l'engagement des étudiants dans la vie associative, + sociale ou professionnelle, l’IUT d’Orléans accorde, sous conditions, + une bonification aux étudiants inscrits qui en font la demande en début + d’année universitaire. +

+

Cet engagement doit être régulier et correspondre à une activité réelle + et sérieuse qui bénéficie à toute la communauté étudiante de l’IUT, + de l’Université ou à l’ensemble de la collectivité.

+

Bonification : + Pour les DUT et LP, cette bonification interviendra sur la moyenne générale + des semestres pairs : +

  • du 2ème semestre pour les étudiants de 1ère année de DUT
  • +
  • du 4ème semestre pour les étudiants de 2nde année de DUT
  • +
  • du 6ème semestre pour les étudiants en LP
  • +
+ Pour le BUT, cette bonification interviendra sur la moyenne de chacune + des UE des semestre pairs : +
  • du 2ème semestre pour les étudiants de 1ère année de BUT
  • +
  • du 4ème semestre pour les étudiants de 2ème année de BUT
  • +
  • du 6ème semestre pour les étudiants de 3ème année de BUT
  • +
+ La bonification ne peut dépasser +0,5 points par année universitaire. +

+

Avant février 2020 : + Un bonus de 2,5% de la note de sport est accordé à la moyenne générale. +

+ """ + + name = "bonus_iutorleans" + displayed_name = "IUT d'Orléans" + bonus_max = 0.5 + seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés + proportion_point = 1 + classic_use_bonus_ues = False + + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + if self.formsemestre.date_debut > datetime.date(2020, 2, 1): + self.proportion_point = 1.0 + else: + self.proportion_point = 2.5 / 100.0 + return super().compute_bonus( + sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan + ) + + class BonusPoitiers(BonusSportAdditif): """Calcul bonus optionnels (sport, culture), règle IUT de Poitiers. diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index 8f8bd1a8..aba032fd 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -161,8 +161,11 @@ class ModuleImplResults: evals_notes = evals_notes.merge( eval_df, how="left", left_index=True, right_index=True ) - # Notes en attente: (on prend dans evals_notes pour ne pas avoir les dem.) - nb_att = sum(evals_notes[str(evaluation.id)] == scu.NOTES_ATTENTE) + # Notes en attente: (ne prend en compte que les inscrits, non démissionnaires) + nb_att = sum( + evals_notes[str(evaluation.id)][list(inscrits_module)] + == scu.NOTES_ATTENTE + ) self.evaluations_etat[evaluation.id] = EvaluationEtat( evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete ) diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 2d337a3d..5f432387 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -496,17 +496,26 @@ def compute_malus( """ ues_idx = [ue.id for ue in ues] malus = pd.DataFrame(index=modimpl_inscr_df.index, columns=ues_idx, dtype=float) + if len(sem_modimpl_moys.flat) == 0: # vide + return malus + if len(sem_modimpl_moys.shape) > 2: + # BUT: ne retient que la 1er composante du malus qui est scalaire + # au sens ou chaque note de malus n'affecte que la moyenne de l'UE + # de rattachement de son module. + sem_modimpl_moys_scalar = sem_modimpl_moys[:, :, 0] + else: # classic + sem_modimpl_moys_scalar = sem_modimpl_moys for ue in ues: if ue.type != UE_SPORT: modimpl_mask = np.array( [ (m.module.module_type == ModuleType.MALUS) - and (m.module.ue.id == ue.id) + and (m.module.ue.id == ue.id) # UE de rattachement for m in formsemestre.modimpls_sorted ] ) if len(modimpl_mask): - malus_moys = sem_modimpl_moys[:, modimpl_mask].sum(axis=1) + malus_moys = sem_modimpl_moys_scalar[:, modimpl_mask].sum(axis=1) malus[ue.id] = malus_moys malus.fillna(0.0, inplace=True) diff --git a/app/comp/res_but.py b/app/comp/res_but.py index d7fec786..0b58521a 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -6,6 +6,8 @@ """Résultats semestres BUT """ +from collections.abc import Generator +from re import U import time import numpy as np import pandas as pd @@ -28,6 +30,8 @@ class ResultatsSemestreBUT(NotesTableCompat): "modimpl_coefs_df", "modimpls_evals_poids", "sem_cube", + "etuds_parcour_id", # parcours de chaque étudiant + "ues_inscr_parcours_df", # inscriptions aux UE / parcours ) def __init__(self, formsemestre): @@ -35,7 +39,8 @@ class ResultatsSemestreBUT(NotesTableCompat): self.sem_cube = None """ndarray (etuds x modimpl x ue)""" - + self.etuds_parcour_id = None + """Parcours de chaque étudiant { etudid : parcour_id }""" if not self.load_cached(): t0 = time.time() self.compute() @@ -55,6 +60,7 @@ class ResultatsSemestreBUT(NotesTableCompat): self.modimpls_results, ) = moy_ue.notes_sem_load_cube(self.formsemestre) self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) + self.ues_inscr_parcours_df = self.load_ues_inscr_parcours() self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs( self.formsemestre, modimpls=self.formsemestre.modimpls_sorted ) @@ -108,6 +114,9 @@ class ResultatsSemestreBUT(NotesTableCompat): # Clippe toutes les moyennes d'UE dans [0,20] self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True) + # Nanifie les moyennes d'UE hors parcours pour chaque étudiant + self.etud_moy_ue *= self.ues_inscr_parcours_df + # Moyenne générale indicative: # (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte # donc la moyenne indicative) @@ -149,16 +158,24 @@ class ResultatsSemestreBUT(NotesTableCompat): """ return self.modimpl_coefs_df.loc[ue.id].sum() - def modimpls_in_ue(self, ue_id, etudid, with_bonus=True) -> list[ModuleImpl]: + def modimpls_in_ue(self, ue: UniteEns, etudid, with_bonus=True) -> list[ModuleImpl]: """Liste des modimpl ayant des coefs non nuls vers cette UE et auxquels l'étudiant est inscrit. Inclus modules bonus le cas échéant. """ # sert pour l'affichage ou non de l'UE sur le bulletin et la table recap - coefs = self.modimpl_coefs_df # row UE, cols modimpl + if ue.type == UE_SPORT: + return [ + modimpl + for modimpl in self.formsemestre.modimpls_sorted + if modimpl.module.ue.id == ue.id + and self.modimpl_inscr_df[modimpl.id][etudid] + ] + coefs = self.modimpl_coefs_df # row UE (sans bonus), cols modimpl modimpls = [ modimpl for modimpl in self.formsemestre.modimpls_sorted - if (coefs[modimpl.id][ue_id] != 0) + if modimpl.module.ue.type != UE_SPORT + and (coefs[modimpl.id][ue.id] != 0) and self.modimpl_inscr_df[modimpl.id][etudid] ] if not with_bonus: @@ -175,3 +192,50 @@ class ResultatsSemestreBUT(NotesTableCompat): i = self.modimpl_coefs_df.columns.get_loc(modimpl_id) j = self.modimpl_coefs_df.index.get_loc(ue_id) return self.sem_cube[:, i, j] + + def load_ues_inscr_parcours(self) -> pd.DataFrame: + """Chargement des inscriptions aux parcours et calcul de la + matrice d'inscriptions (etuds, ue). + S'il n'y pas de référentiel de compétence, donc pas de parcours, + on considère l'étudiant inscrit à toutes les ue. + La matrice avec ue ne comprend que les UE non bonus. + 1.0 si étudiant inscrit à l'UE, NaN sinon. + """ + etuds_parcour_id = { + inscr.etudid: inscr.parcour_id for inscr in self.formsemestre.inscriptions + } + self.etuds_parcour_id = etuds_parcour_id + ue_ids = [ue.id for ue in self.ues if ue.type != UE_SPORT] + # matrice de 1, inscrits par défaut à toutes les UE: + ues_inscr_parcours_df = pd.DataFrame( + 1.0, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float + ) + if self.formsemestre.formation.referentiel_competence is None: + return ues_inscr_parcours_df + + ue_by_parcours = {} # parcours_id : {ue_id:0|1} + for parcour in self.formsemestre.formation.referentiel_competence.parcours: + ue_by_parcours[parcour.id] = { + ue.id: 1.0 + for ue in self.formsemestre.formation.query_ues_parcour( + parcour + ).filter_by(semestre_idx=self.formsemestre.semestre_id) + } + for etudid in etuds_parcour_id: + parcour = etuds_parcour_id[etudid] + if parcour is not None: + ues_inscr_parcours_df.loc[etudid] = ue_by_parcours[ + etuds_parcour_id[etudid] + ] + return ues_inscr_parcours_df + + def etud_ues_ids(self, etudid: int) -> list[int]: + """Liste des id d'UE auxquelles l'étudiant est inscrit (sans bonus). + (surchargée ici pour prendre en compte les parcours) + """ + s = self.ues_inscr_parcours_df.loc[etudid] + return s.index[s.notna()] + + def etud_ues(self, etudid: int) -> Generator[UniteEns]: + """Liste des UE auxquelles l'étudiant est inscrit.""" + return (UniteEns.query.get(ue_id) for ue_id in self.etud_ues_ids(etudid)) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 645f067e..04bd92d1 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -112,6 +112,14 @@ class ResultatsSemestre(ResultatsCache): "dict { etudid : indice dans les inscrits }" return {e.id: idx for idx, e in enumerate(self.etuds)} + def etud_ues_ids(self, etudid: int) -> list[int]: + """Liste des UE auxquelles l'etudiant est inscrit, sans bonus + (surchargée en BUT pour prendre en compte les parcours) + """ + # Pour les formations classiques, etudid n'est pas utilisé + # car tous les étudiants sont inscrits à toutes les UE + return [ue.id for ue in self.ues if ue.type != UE_SPORT] + def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray: """Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue. Utile pour stats bottom tableau recap. @@ -179,7 +187,7 @@ class ResultatsSemestre(ResultatsCache): ues = sorted(list(ues), key=lambda x: x.numero or 0) return ues - def modimpls_in_ue(self, ue_id, etudid, with_bonus=True) -> list[ModuleImpl]: + def modimpls_in_ue(self, ue: UniteEns, etudid, with_bonus=True) -> list[ModuleImpl]: """Liste des modimpl de cette UE auxquels l'étudiant est inscrit. Utile en formations classiques, surchargée pour le BUT. Inclus modules bonus le cas échéant. @@ -189,7 +197,7 @@ class ResultatsSemestre(ResultatsCache): modimpls = [ modimpl for modimpl in self.formsemestre.modimpls_sorted - if modimpl.module.ue.id == ue_id + if modimpl.module.ue.id == ue.id and self.modimpl_inscr_df[modimpl.id][etudid] ] if not with_bonus: @@ -391,7 +399,7 @@ class ResultatsSemestre(ResultatsCache): # --- TABLEAU RECAP def get_table_recap( - self, convert_values=False, include_evaluations=False, modejury=False + self, convert_values=False, include_evaluations=False, mode_jury=False ): """Result: tuple avec - rows: liste de dicts { column_id : value } @@ -542,7 +550,7 @@ class ResultatsSemestre(ResultatsCache): titles_bot[ f"_{col_id}_target_attrs" ] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """ - if modejury: + if mode_jury: # pas d'autre colonnes de résultats continue # Bonus (sport) dans cette UE ? @@ -564,7 +572,7 @@ class ResultatsSemestre(ResultatsCache): # Les moyennes des modules (ou ressources et SAÉs) dans cette UE idx_malus = idx # place pour colonne malus à gauche des modules idx += 1 - for modimpl in self.modimpls_in_ue(ue.id, etudid, with_bonus=False): + for modimpl in self.modimpls_in_ue(ue, etudid, with_bonus=False): if ue_status["is_capitalized"]: val = "-c-" else: @@ -622,9 +630,10 @@ class ResultatsSemestre(ResultatsCache): f"_{col_id}_target_attrs" ] = f""" title="{modimpl.module.titre} ({nom_resp})" """ modimpl_ids.add(modimpl.id) + nb_ues_etud_parcours = len(self.etud_ues_ids(etudid)) ue_valid_txt = ( ue_valid_txt_html - ) = f"{nb_ues_validables}/{len(ues_sans_bonus)}" + ) = f"{nb_ues_validables}/{nb_ues_etud_parcours}" if nb_ues_warning: ue_valid_txt_html += " " + scu.EMO_WARNING add_cell( @@ -641,7 +650,17 @@ class ResultatsSemestre(ResultatsCache): elif nb_ues_validables < len(ues_sans_bonus): row["_ues_validables_class"] += " moy_inf" row["_ues_validables_order"] = nb_ues_validables # pour tri - if modejury: + if mode_jury: + dec_sem = self.validations.decisions_jury.get(etudid) + jury_code_sem = dec_sem["code"] if dec_sem else "" + idx = add_cell( + row, + "jury_code_sem", + "Jury", + jury_code_sem, + "jury_code_sem", + 1000, + ) idx = add_cell( row, "jury_link", @@ -651,11 +670,11 @@ class ResultatsSemestre(ResultatsCache): ) }">saisir décision""", "col_jury_link", - 1000, + idx, ) rows.append(row) - self._recap_add_partitions(rows, titles) + self.recap_add_partitions(rows, titles) self._recap_add_admissions(rows, titles) # tri par rang croissant @@ -762,7 +781,9 @@ class ResultatsSemestre(ResultatsCache): "apo": row_apo, } - def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict): + def _recap_etud_groups_infos( + self, etudid: int, row: dict, titles: dict + ): # XXX non utilisé """Table recap: ajoute à row les colonnes sur les groupes pour cet etud""" # dec = self.get_etud_decision_sem(etudid) # if dec: @@ -818,7 +839,7 @@ class ResultatsSemestre(ResultatsCache): else: row[f"_{cid}_class"] = "admission" - def _recap_add_partitions(self, rows: list[dict], titles: dict): + def recap_add_partitions(self, rows: list[dict], titles: dict, col_idx: int = None): """Ajoute les colonnes indiquant les groupes rows est une liste de dict avec une clé "etudid" Les colonnes ont la classe css "partition" @@ -827,7 +848,7 @@ class ResultatsSemestre(ResultatsCache): self.formsemestre.id ) first_partition = True - col_order = 10 + col_order = 10 if col_idx is None else col_idx for partition in partitions: cid = f"part_{partition['partition_id']}" rg_cid = cid + "_rg" # rang dans la partition diff --git a/app/comp/res_compat.py b/app/comp/res_compat.py index 5ac18ff4..d48c727d 100644 --- a/app/comp/res_compat.py +++ b/app/comp/res_compat.py @@ -54,6 +54,7 @@ class NotesTableCompat(ResultatsSemestre): self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}} self.expr_diagnostics = "" self.parcours = self.formsemestre.formation.get_parcours() + self._modimpls_dict_by_ue = {} # local cache def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]: """Liste des étudiants inscrits @@ -145,6 +146,10 @@ class NotesTableCompat(ResultatsSemestre): """Liste des modules pour une UE (ou toutes si ue_id==None), triés par numéros (selon le type de formation) """ + # cached ? + modimpls_dict = self._modimpls_dict_by_ue.get(ue_id) + if modimpls_dict: + return modimpls_dict modimpls_dict = [] for modimpl in self.formsemestre.modimpls_sorted: if (ue_id is None) or (modimpl.module.ue.id == ue_id): @@ -152,6 +157,7 @@ class NotesTableCompat(ResultatsSemestre): # compat ScoDoc < 9.2: ajoute matières d["mat"] = modimpl.module.matiere.to_dict() modimpls_dict.append(d) + self._modimpls_dict_by_ue[ue_id] = modimpls_dict return modimpls_dict def compute_rangs(self): diff --git a/app/decorators.py b/app/decorators.py index d6c6ed23..83441275 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -87,10 +87,10 @@ def permission_required(permission): def decorated_function(*args, **kwargs): scodoc_dept = getattr(g, "scodoc_dept", None) if not current_user.has_permission(permission, scodoc_dept): - abort(403) + return current_app.login_manager.unauthorized() return f(*args, **kwargs) - return login_required(decorated_function) + return decorated_function return decorator diff --git a/app/forms/main/config_apo.py b/app/forms/main/config_apo.py index 9a5e1198..946e6ff2 100644 --- a/app/forms/main/config_apo.py +++ b/app/forms/main/config_apo.py @@ -41,6 +41,7 @@ from app.scodoc import sco_codes_parcours def _build_code_field(code): return StringField( label=code, + default=code, description=sco_codes_parcours.CODES_EXPL[code], validators=[ validators.regexp( @@ -58,6 +59,8 @@ def _build_code_field(code): class CodesDecisionsForm(FlaskForm): "Formulaire code décisions Apogée" + ABAN = _build_code_field("ABAN") + ABL = _build_code_field("ABL") ADC = _build_code_field("ADC") ADJ = _build_code_field("ADJ") ADM = _build_code_field("ADM") @@ -68,8 +71,13 @@ class CodesDecisionsForm(FlaskForm): CMP = _build_code_field("CMP") DEF = _build_code_field("DEF") DEM = _build_code_field("DEM") + EXCLU = _build_code_field("EXCLU") NAR = _build_code_field("NAR") + PASD = _build_code_field("PASD") + PAS1NCI = _build_code_field("PAS1NCI") RAT = _build_code_field("RAT") + RED = _build_code_field("RED") + NOTES_FMT = StringField( label="Format notes exportées", description="""Format des notes. Par défaut %3.2f (deux chiffres après la virgule)""", diff --git a/app/forms/main/config_logos.py b/app/forms/main/config_logos.py index 0d0ac0d5..b35ac34e 100644 --- a/app/forms/main/config_logos.py +++ b/app/forms/main/config_logos.py @@ -43,7 +43,7 @@ from app.scodoc import sco_logos, html_sco_header from app.scodoc import sco_utils as scu from app.scodoc.sco_config_actions import LogoInsert - +from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_logos import find_logo @@ -108,6 +108,8 @@ def dept_key_to_id(dept_key): def logo_name_validator(message=None): def validate_logo_name(form, field): name = field.data if field.data else "" + if "." in name: + raise ValidationError(message) if not scu.is_valid_filename(name): raise ValidationError(message) @@ -199,9 +201,12 @@ class LogoForm(FlaskForm): def __init__(self, *args, **kwargs): kwargs["meta"] = {"csrf": False} super().__init__(*args, **kwargs) - self.logo = find_logo( + logo = find_logo( logoname=self.logo_id.data, dept_id=dept_key_to_id(self.dept_key.data) - ).select() + ) + if logo is None: + raise ScoValueError("logo introuvable") + self.logo = logo.select() self.description = None self.titre = None self.can_delete = True diff --git a/app/models/__init__.py b/app/models/__init__.py index d29b6bf3..c7a183ec 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,14 +1,25 @@ # -*- coding: UTF-8 -* """Modèles base de données ScoDoc -XXX version préliminaire ScoDoc8 #sco8 sans département """ +import sqlalchemy + CODE_STR_LEN = 16 # chaine pour les codes SHORT_STR_LEN = 32 # courtes chaine, eg acronymes APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs) GROUPNAME_STR_LEN = 64 +convention = { + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s", +} + +metadata_obj = sqlalchemy.MetaData(naming_convention=convention) + from app.models.raw_sql_init import create_database_functions from app.models.absences import Absence, AbsenceNotification, BilletAbsence @@ -65,5 +76,8 @@ from app.models.but_refcomp import ( ApcCompetence, ApcSituationPro, ApcAppCritique, + ApcParcours, ) +from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE + from app.models.config import ScoDocSiteConfig diff --git a/app/models/absences.py b/app/models/absences.py index 0dea5f18..881a81e3 100644 --- a/app/models/absences.py +++ b/app/models/absences.py @@ -11,7 +11,9 @@ class Absence(db.Model): __tablename__ = "absences" id = db.Column(db.Integer, primary_key=True) - etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True) + etudid = db.Column( + db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"), index=True + ) jour = db.Column(db.Date) estabs = db.Column(db.Boolean()) estjust = db.Column(db.Boolean()) @@ -59,7 +61,7 @@ class AbsenceNotification(db.Model): id = db.Column(db.Integer, primary_key=True) etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) notification_date = db.Column( db.DateTime(timezone=True), server_default=db.func.now() diff --git a/app/models/but_pn.py b/app/models/but_pn.py index 35afbe16..67993d02 100644 --- a/app/models/but_pn.py +++ b/app/models/but_pn.py @@ -1,12 +1,9 @@ """ScoDoc 9 models : Formation BUT 2021 + XXX inutilisé """ -from enum import unique -from typing import Any from app import db -from app.scodoc.sco_utils import ModuleType - class APCFormation(db.Model): """Formation par compétence""" diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 8b730faa..29428c05 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -7,12 +7,14 @@ """ from datetime import datetime +import flask_sqlalchemy from sqlalchemy.orm import class_mapper import sqlalchemy from app import db from app.scodoc.sco_utils import ModuleType +from app.scodoc.sco_exceptions import ScoValueError # from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns @@ -116,7 +118,7 @@ class ApcReferentielCompetences(db.Model, XMLModel): # self.formations = formations def __repr__(self): - return f"" + return f"" def to_dict(self): """Représentation complète du ref. de comp. @@ -139,6 +141,52 @@ class ApcReferentielCompetences(db.Model, XMLModel): "parcours": {x.code: x.to_dict() for x in self.parcours}, } + def get_niveaux_by_parcours(self, annee) -> dict: + """ + Construit la liste des niveaux de compétences pour chaque parcours + de ce référentiel. + Les niveaux sont groupés par parcours, en isolant les niveaux de tronc commun. + Le tronc commun n'est pas identifié comme tel dans les référentiels Orébut: + on cherche les niveaux qui sont présents dans tous les parcours et les range sous + la clé "TC" (toujours présente mais éventuellement liste vide si pas de tronc commun). + + résultat: + { + "TC" : [ ApcNiveau ], + parcour.id : [ ApcNiveau ] + } + """ + parcours = self.parcours.order_by(ApcParcours.numero).all() + niveaux_by_parcours = { + parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee, self) + for parcour in parcours + } + # Cherche tronc commun + niveaux_ids_tc = set.intersection( + *[ + {n.id for n in niveaux_by_parcours[parcour_id]} + for parcour_id in niveaux_by_parcours + ] + ) + # Enleve les niveaux du tronc commun + niveaux_by_parcours_no_tc = { + parcour.id: [ + niveau + for niveau in niveaux_by_parcours[parcour.id] + if niveau.id not in niveaux_ids_tc + ] + for parcour in parcours + } + # Niveaux du TC + niveaux_tc = [] + if len(parcours): + niveaux_parcours_1 = niveaux_by_parcours[parcours[0].id] + niveaux_tc = [ + niveau for niveau in niveaux_parcours_1 if niveau.id in niveaux_ids_tc + ] + niveaux_by_parcours_no_tc["TC"] = niveaux_tc + return niveaux_by_parcours_no_tc + class ApcCompetence(db.Model, XMLModel): "Compétence" @@ -204,9 +252,10 @@ class ApcCompetence(db.Model, XMLModel): self.niveaux = niveaux def __repr__(self): - return f"" + return f"" def to_dict(self): + "repr dict recursive sur situations, composantes, niveaux" return { "id_orebut": self.id_orebut, "titre": self.titre, @@ -220,6 +269,16 @@ class ApcCompetence(db.Model, XMLModel): "niveaux": {x.annee: x.to_dict() for x in self.niveaux}, } + def to_dict_bul(self) -> dict: + "dict court pour bulletins" + return { + "id_orebut": self.id_orebut, + "titre": self.titre, + "titre_long": self.titre_long, + "couleur": self.couleur, + "numero": self.numero, + } + class ApcSituationPro(db.Model, XMLModel): "Situation professionnelle" @@ -257,13 +316,20 @@ class ApcComposanteEssentielle(db.Model, XMLModel): class ApcNiveau(db.Model, XMLModel): + """Niveau de compétence + Chaque niveau peut être associé à deux UE, + des semestres impair et pair de la même année. + """ + + __tablename__ = "apc_niveau" + id = db.Column(db.Integer, primary_key=True) competence_id = db.Column( db.Integer, db.ForeignKey("apc_competence.id"), nullable=False ) libelle = db.Column(db.Text(), nullable=False) - annee = db.Column(db.Text(), nullable=False) # "BUT2" - # L'ordre est l'année d'apparition de ce niveau + annee = db.Column(db.Text(), nullable=False) # "BUT1", "BUT2", "BUT3" + # L'ordre est le niveau (1,2,3) ou (1,2) suivant la competence ordre = db.Column(db.Integer, nullable=False) # 1, 2, 3 app_critiques = db.relationship( "ApcAppCritique", @@ -271,6 +337,7 @@ class ApcNiveau(db.Model, XMLModel): lazy="dynamic", cascade="all, delete-orphan", ) + ues = db.relationship("UniteEns", back_populates="niveau_competence") def __init__(self, id, competence_id, libelle, annee, ordre, app_critiques): self.id = id @@ -281,9 +348,11 @@ class ApcNiveau(db.Model, XMLModel): self.app_critiques = app_critiques def __repr__(self): - return f"<{self.__class__.__name__} ordre={self.ordre}>" + return f"""<{self.__class__.__name__} ordre={self.ordre!r} annee={ + self.annee!r} {self.competence!r}>""" def to_dict(self): + "as a dict, recursif sur les AC" return { "libelle": self.libelle, "annee": self.annee, @@ -291,6 +360,64 @@ class ApcNiveau(db.Model, XMLModel): "app_critiques": {x.code: x.to_dict() for x in self.app_critiques}, } + def to_dict_bul(self): + "dict pour bulletins: indique la compétence, pas les ACs (pour l'instant ?)" + return { + "libelle": self.libelle, + "annee": self.annee, + "ordre": self.ordre, + "competence": self.competence.to_dict_bul(), + } + + @classmethod + def niveaux_annee_de_parcours( + cls, + parcour: "ApcParcours", + annee: int, + referentiel_competence: ApcReferentielCompetences = None, + ) -> flask_sqlalchemy.BaseQuery: + """Les niveaux de l'année du parcours + Si le parcour est None, tous les niveaux de l'année + """ + if annee not in {1, 2, 3}: + raise ValueError("annee invalide pour un parcours BUT") + if referentiel_competence is None: + raise ScoValueError( + "Pas de référentiel de compétences associé à la formation !" + ) + annee_formation = f"BUT{annee}" + if parcour is None: + return ApcNiveau.query.filter( + ApcNiveau.annee == annee_formation, + ApcCompetence.id == ApcNiveau.competence_id, + ApcCompetence.referentiel_id == referentiel_competence.id, + ) + else: + return ApcNiveau.query.filter( + ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id, + ApcParcours.id == ApcAnneeParcours.parcours_id, + ApcParcours.referentiel == parcour.referentiel, + ApcParcoursNiveauCompetence.competence_id == ApcCompetence.id, + ApcCompetence.id == ApcNiveau.competence_id, + ApcAnneeParcours.parcours == parcour, + ApcNiveau.annee == annee_formation, + ) + + +app_critiques_modules = db.Table( + "apc_modules_acs", + db.Column( + "module_id", + db.ForeignKey("notes_modules.id", ondelete="CASCADE"), + primary_key=True, + ), + db.Column( + "app_crit_id", + db.ForeignKey("apc_app_critique.id"), + primary_key=True, + ), +) + class ApcAppCritique(db.Model, XMLModel): "Apprentissage Critique BUT" @@ -299,12 +426,31 @@ class ApcAppCritique(db.Model, XMLModel): code = db.Column(db.Text(), nullable=False, index=True) libelle = db.Column(db.Text()) - modules = db.relationship( - "Module", - secondary="apc_modules_acs", - lazy="dynamic", - backref=db.backref("app_critiques", lazy="dynamic"), - ) + # modules = db.relationship( + # "Module", + # secondary="apc_modules_acs", + # lazy="dynamic", + # backref=db.backref("app_critiques", lazy="dynamic"), + # ) + + @classmethod + def app_critiques_ref_comp( + cls, + ref_comp: ApcReferentielCompetences, + annee: str, + competence: ApcCompetence = None, + ) -> flask_sqlalchemy.BaseQuery: + "Liste les AC de tous les parcours de ref_comp pour l'année indiquée" + assert annee in {"BUT1", "BUT2", "BUT3"} + query = cls.query.filter( + ApcAppCritique.niveau_id == ApcNiveau.id, + ApcNiveau.competence_id == ApcCompetence.id, + ApcNiveau.annee == annee, + ApcCompetence.referentiel_id == ref_comp.id, + ) + if competence is not None: + query = query.filter(ApcNiveau.competence == competence) + return query def __init__(self, id, niveau_id, code, libelle, modules): self.id = id @@ -320,18 +466,40 @@ class ApcAppCritique(db.Model, XMLModel): return self.code + " - " + self.titre def __repr__(self): - return f"<{self.__class__.__name__} {self.code}>" + return f"<{self.__class__.__name__} {self.code!r}>" def get_saes(self): """Liste des SAE associées""" return [m for m in self.modules if m.module_type == ModuleType.SAE] -ApcAppCritiqueModules = db.Table( - "apc_modules_acs", - db.Column("module_id", db.ForeignKey("notes_modules.id")), - db.Column("app_crit_id", db.ForeignKey("apc_app_critique.id")), +parcours_modules = db.Table( + "parcours_modules", + db.Column( + "parcours_id", db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True + ), + db.Column( + "module_id", + db.Integer, + db.ForeignKey("notes_modules.id", ondelete="CASCADE"), + primary_key=True, + ), ) +"""Association parcours <-> modules (many-to-many)""" + +parcours_formsemestre = db.Table( + "parcours_formsemestre", + db.Column( + "parcours_id", db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True + ), + db.Column( + "formsemestre_id", + db.Integer, + db.ForeignKey("notes_formsemestre.id", ondelete="CASCADE"), + primary_key=True, + ), +) +"""Association parcours <-> formsemestre (many-to-many)""" class ApcParcours(db.Model, XMLModel): @@ -358,7 +526,7 @@ class ApcParcours(db.Model, XMLModel): self.annes = annes def __repr__(self): - return f"<{self.__class__.__name__} {self.code}>" + return f"<{self.__class__.__name__} {self.code!r}>" def to_dict(self): return { @@ -375,6 +543,7 @@ class ApcAnneeParcours(db.Model, XMLModel): db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False ) ordre = db.Column(db.Integer) + "numéro de l'année: 1, 2, 3" def __init__(self, id, parcours_id, ordre): self.id = id @@ -382,7 +551,7 @@ class ApcAnneeParcours(db.Model, XMLModel): self.ordre = ordre def __repr__(self): - return f"<{self.__class__.__name__} ordre={self.ordre}>" + return f"<{self.__class__.__name__} ordre={self.ordre!r} parcours={self.parcours.code!r}>" def to_dict(self): return { @@ -420,6 +589,7 @@ class ApcParcoursNiveauCompetence(db.Model): "annee_parcours", passive_deletes=True, cascade="save-update, merge, delete, delete-orphan", + lazy="dynamic", ), ) annee_parcours = db.relationship( @@ -432,4 +602,4 @@ class ApcParcoursNiveauCompetence(db.Model): ) def __repr__(self): - return f"<{self.__class__.__name__} {self.competence} {self.annee_parcours}>" + return f"<{self.__class__.__name__} {self.competence!r}<->{self.annee_parcours!r} niveau={self.niveau!r}>" diff --git a/app/models/but_validations.py b/app/models/but_validations.py new file mode 100644 index 00000000..ebedadd7 --- /dev/null +++ b/app/models/but_validations.py @@ -0,0 +1,339 @@ +# -*- coding: UTF-8 -* + +"""Décisions de jury (validations) des RCUE et années du BUT +""" + +import flask_sqlalchemy +from sqlalchemy.sql import text +from typing import Union + +from app import db + +from app.models import CODE_STR_LEN +from app.models.but_refcomp import ApcNiveau +from app.models.etudiants import Identite +from app.models.ues import UniteEns +from app.models.formations import Formation +from app.models.formsemestre import FormSemestre +from app.scodoc import sco_codes_parcours as sco_codes +from app.scodoc import sco_utils as scu + + +class ApcValidationRCUE(db.Model): + """Validation des niveaux de compétences + + aka "regroupements cohérents d'UE" dans le jargon BUT. + + le formsemestre est celui du semestre PAIR du niveau de compétence + """ + + __tablename__ = "apc_validation_rcue" + # Assure unicité de la décision: + __table_args__ = ( + db.UniqueConstraint("etudid", "formsemestre_id", "ue1_id", "ue2_id"), + ) + + id = db.Column(db.Integer, primary_key=True) + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id", ondelete="CASCADE"), + index=True, + nullable=False, + ) + formsemestre_id = db.Column( + db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True + ) + # Les deux UE associées à ce niveau: + ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False) + ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False) + # optionnel, le parcours dans lequel se trouve la compétence: + parcours_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), nullable=True) + date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True) + + etud = db.relationship("Identite", backref="apc_validations_rcues") + formsemestre = db.relationship("FormSemestre", backref="apc_validations_rcues") + ue1 = db.relationship("UniteEns", foreign_keys=ue1_id) + ue2 = db.relationship("UniteEns", foreign_keys=ue2_id) + parcour = db.relationship("ApcParcours") + + def __repr__(self): + return f"<{self.__class__.__name__} {self.id} {self.etud} {self.ue1}/{self.ue2}:{self.code!r}>" + + def niveau(self) -> ApcNiveau: + """Le niveau de compétence associé à cet RCUE.""" + # Par convention, il est donné par la seconde UE + return self.ue2.niveau_competence + + def to_dict_bul(self) -> dict: + "Export dict pour bulletins" + return {"code": self.code, "niveau": self.niveau().to_dict_bul()} + + +# Attention: ce n'est pas un modèle mais une classe ordinaire: +class RegroupementCoherentUE: + """Le regroupement cohérent d'UE, dans la terminologie du BUT, est le couple d'UEs + de la même année (BUT1,2,3) liées au *même niveau de compétence*. + + La moyenne (10/20) au RCU déclenche la compensation des UE. + """ + + def __init__( + self, + etud: Identite, + formsemestre_1: FormSemestre, + ue_1: UniteEns, + formsemestre_2: FormSemestre, + ue_2: UniteEns, + inscription_etat: str, + ): + from app.comp import res_sem + from app.comp.res_but import ResultatsSemestreBUT + + # Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)... + if formsemestre_1.semestre_id > formsemestre_2.semestre_id: + (ue_1, formsemestre_1), (ue_2, formsemestre_2) = ( + ( + ue_2, + formsemestre_2, + ), + (ue_1, formsemestre_1), + ) + assert formsemestre_1.semestre_id % 2 == 1 + assert formsemestre_2.semestre_id % 2 == 0 + assert abs(formsemestre_1.semestre_id - formsemestre_2.semestre_id) == 1 + assert ue_1.niveau_competence_id == ue_2.niveau_competence_id + self.etud = etud + self.formsemestre_1 = formsemestre_1 + "semestre impair" + self.ue_1 = ue_1 + self.formsemestre_2 = formsemestre_2 + "semestre pair" + self.ue_2 = ue_2 + # Stocke les moyennes d'UE + if inscription_etat != scu.INSCRIT: + self.moy_rcue = None + self.moy_ue_1 = self.moy_ue_2 = "-" + self.moy_ue_1_val = self.moy_ue_2_val = 0.0 + return + res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_1) + if ue_1.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_1.id]: + self.moy_ue_1 = res.etud_moy_ue[ue_1.id][etud.id] + self.moy_ue_1_val = self.moy_ue_1 # toujours float, peut être NaN + else: + self.moy_ue_1 = None + self.moy_ue_1_val = 0.0 + res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_2) + if ue_2.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_2.id]: + self.moy_ue_2 = res.etud_moy_ue[ue_2.id][etud.id] + self.moy_ue_2_val = self.moy_ue_2 + else: + self.moy_ue_2 = None + self.moy_ue_2_val = 0.0 + # Calcul de la moyenne au RCUE + if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None): + # Moyenne RCUE (les pondérations par défaut sont 1.) + self.moy_rcue = ( + self.moy_ue_1 * ue_1.coef_rcue + self.moy_ue_2 * ue_2.coef_rcue + ) / (ue_1.coef_rcue + ue_2.coef_rcue) + else: + self.moy_rcue = None + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.ue_1.acronyme}({self.moy_ue_1}) {self.ue_2.acronyme}({self.moy_ue_2})>" + + def query_validations( + self, + ) -> flask_sqlalchemy.BaseQuery: # list[ApcValidationRCUE] + """Les validations de jury enregistrées pour ce RCUE""" + niveau = self.ue_2.niveau_competence + + return ( + ApcValidationRCUE.query.filter_by( + etudid=self.etud.id, + ) + .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id) + .join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id) + .filter(ApcNiveau.id == niveau.id) + ) + + def other_ue(self, ue: UniteEns) -> UniteEns: + """L'autre UE du regroupement. Si ue ne fait pas partie du regroupement, ValueError""" + if ue.id == self.ue_1.id: + return self.ue_2 + elif ue.id == self.ue_2.id: + return self.ue_1 + raise ValueError(f"ue {ue} hors RCUE {self}") + + def est_enregistre(self) -> bool: + """Vrai si ce RCUE, donc le niveau de compétences correspondant + a une décision jury enregistrée + """ + return self.query_validations().count() > 0 + + def est_compensable(self): + """Vrai si ce RCUE est validable par compensation + c'est à dire que sa moyenne est > 10 avec une UE < 10 + """ + return ( + (self.moy_rcue is not None) + and (self.moy_rcue > sco_codes.BUT_BARRE_RCUE) + and ( + (self.moy_ue_1_val < sco_codes.NOTES_BARRE_GEN) + or (self.moy_ue_2_val < sco_codes.NOTES_BARRE_GEN) + ) + ) + + def est_suffisant(self) -> bool: + """Vrai si ce RCUE est > 8""" + return (self.moy_rcue is not None) and ( + self.moy_rcue > sco_codes.BUT_RCUE_SUFFISANT + ) + + def est_validable(self) -> bool: + """Vrai si ce RCU satisfait les conditions pour être validé + Pour cela, il suffit que la moyenne des UE qui le constitue soit > 10 + """ + return (self.moy_rcue is not None) and ( + self.moy_rcue > sco_codes.BUT_BARRE_RCUE + ) + + def code_valide(self) -> Union[ApcValidationRCUE, None]: + "Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None" + validation = self.query_validations().first() + if (validation is not None) and ( + validation.code in sco_codes.CODES_RCUE_VALIDES + ): + return validation + return None + + +# unused +def find_rcues( + formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str +) -> list[RegroupementCoherentUE]: + """Les RCUE (niveau de compétence) à considérer pour cet étudiant dans + ce semestre pour cette UE. + + Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit. + En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs. + + Résultat: la liste peut être vide. + """ + if (ue.niveau_competence is None) or (ue.semestre_idx is None): + return [] + + if ue.semestre_idx % 2: # S1, S3, S5 + other_semestre_idx = ue.semestre_idx + 1 + else: + other_semestre_idx = ue.semestre_idx - 1 + + cursor = db.session.execute( + text( + """SELECT + ue.id, formsemestre.id + FROM + notes_ue ue, + notes_formsemestre_inscription inscr, + notes_formsemestre formsemestre + + WHERE + inscr.etudid = :etudid + AND inscr.formsemestre_id = formsemestre.id + + AND formsemestre.semestre_id = :other_semestre_idx + AND ue.formation_id = formsemestre.formation_id + AND ue.niveau_competence_id = :ue_niveau_competence_id + AND ue.semestre_idx = :other_semestre_idx + """ + ), + { + "etudid": etud.id, + "other_semestre_idx": other_semestre_idx, + "ue_niveau_competence_id": ue.niveau_competence_id, + }, + ) + rcues = [] + for ue_id, formsemestre_id in cursor: + other_ue = UniteEns.query.get(ue_id) + other_formsemestre = FormSemestre.query.get(formsemestre_id) + rcues.append( + RegroupementCoherentUE( + etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat + ) + ) + # safety check: 1 seul niveau de comp. concerné: + assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1 + return rcues + + +class ApcValidationAnnee(db.Model): + """Validation des années du BUT""" + + __tablename__ = "apc_validation_annee" + # Assure unicité de la décision: + __table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire"),) + id = db.Column(db.Integer, primary_key=True) + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id", ondelete="CASCADE"), + index=True, + nullable=False, + ) + ordre = db.Column(db.Integer, nullable=False) + "numéro de l'année: 1, 2, 3" + formsemestre_id = db.Column( + db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True + ) + "le semestre IMPAIR (le 1er) de l'année" + annee_scolaire = db.Column(db.Integer, nullable=False) # 2021 + date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True) + + etud = db.relationship("Identite", backref="apc_validations_annees") + formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees") + + def __repr__(self): + return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>" + + def to_dict_bul(self) -> dict: + "dict pour bulletins" + return { + "annee_scolaire": self.annee_scolaire, + "date": self.date.isoformat(), + "code": self.code, + "ordre": self.ordre, + } + + +def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict: + """ + Un dict avec les décisions de jury BUT enregistrées. + Ne reprend pas les décisions d'UE, non spécifiques au BUT. + """ + decisions = {} + # --- RCUEs: seulement sur semestres pairs XXX à améliorer + if formsemestre.semestre_id % 2 == 0: + # validations émises depuis ce formsemestre: + validations_rcues = ApcValidationRCUE.query.filter_by( + etudid=etud.id, formsemestre_id=formsemestre.id + ) + decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues] + else: + decisions["decision_rcue"] = [] + # --- Année: prend la validation pour l'année scolaire de ce semestre + validation = ( + ApcValidationAnnee.query.filter_by( + etudid=etud.id, + annee_scolaire=formsemestre.annee_scolaire(), + ) + .join(ApcValidationAnnee.formsemestre) + .join(FormSemestre.formation) + .filter(Formation.formation_code == formsemestre.formation.formation_code) + .first() + ) + if validation: + decisions["decision_annee"] = validation.to_dict_bul() + else: + decisions["decision_annee"] = None + return decisions diff --git a/app/models/config.py b/app/models/config.py index 53ac96e9..cb65d519 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -9,6 +9,8 @@ from app.comp import bonus_spo from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_codes_parcours import ( + ABAN, + ABL, ADC, ADJ, ADM, @@ -19,11 +21,17 @@ from app.scodoc.sco_codes_parcours import ( CMP, DEF, DEM, + EXCLU, NAR, + PASD, + PAS1NCI, RAT, + RED, ) CODES_SCODOC_TO_APO = { + ABAN: "ABAN", + ABL: "ABL", ADC: "ADMC", ADJ: "ADM", ADM: "ADM", @@ -34,8 +42,12 @@ CODES_SCODOC_TO_APO = { CMP: "COMP", DEF: "NAR", DEM: "NAR", + EXCLU: "EXC", NAR: "NAR", + PASD: "PASD", + PAS1NCI: "PAS1NCI", RAT: "ATT", + RED: "RED", "NOTES_FMT": "%3.2f", } @@ -161,9 +173,8 @@ class ScoDocSiteConfig(db.Model): @classmethod def get_code_apo(cls, code: str) -> str: """La représentation d'un code pour les exports Apogée. - Par exemple, à l'iUT du H., le code ADM est réprésenté par VAL + Par exemple, à l'IUT du H., le code ADM est réprésenté par VAL Les codes par défaut sont donnés dans sco_apogee_csv. - """ cfg = ScoDocSiteConfig.query.filter_by(name=code).first() if not cfg: @@ -172,6 +183,11 @@ class ScoDocSiteConfig(db.Model): code_apo = cfg.value return code_apo + @classmethod + def get_codes_apo_dict(cls) -> dict[str:str]: + "Un dict avec code jury : code exporté" + return {code: cls.get_code_apo(code) for code in CODES_SCODOC_TO_APO} + @classmethod def set_code_apo(cls, code: str, code_apo: str): """Enregistre nouvelle représentation du code""" diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 0bce6d47..30333a6b 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -60,7 +60,9 @@ class Identite(db.Model): admission = db.relationship("Admission", backref="identite", lazy="dynamic") def __repr__(self): - return f"" + return ( + f"" + ) @classmethod def from_request(cls, etudid=None, code_nip=None): @@ -133,8 +135,10 @@ class Identite(db.Model): def sort_key(self) -> tuple: "clé pour tris par ordre alphabétique" return ( - scu.suppress_accents(self.nom_usuel or self.nom or "").lower(), - scu.suppress_accents(self.prenom or "").lower(), + scu.sanitize_string( + self.nom_usuel or self.nom or "", remove_spaces=False + ).lower(), + scu.sanitize_string(self.prenom or "", remove_spaces=False).lower(), ) def get_first_email(self, field="email") -> str: @@ -201,6 +205,19 @@ class Identite(db.Model): d.update(adresse.to_dict(convert_nulls_to_str=True)) return d + def inscriptions(self) -> list["FormSemestreInscription"]: + "Liste des inscriptions à des formsemestres, triée, la plus récente en tête" + from app.models.formsemestre import FormSemestre, FormSemestreInscription + + return ( + FormSemestreInscription.query.join(FormSemestreInscription.formsemestre) + .filter( + FormSemestreInscription.etudid == self.id, + ) + .order_by(desc(FormSemestre.date_debut)) + .all() + ) + def inscription_courante(self): """La première inscription à un formsemestre _actuellement_ en cours. None s'il n'y en a pas (ou plus, ou pas encore). @@ -212,7 +229,7 @@ class Identite(db.Model): ] return r[0] if r else None - def inscriptions_courantes(self) -> list: # -> list[FormSemestreInscription]: + def inscriptions_courantes(self) -> list["FormSemestreInscription"]: """Liste des inscriptions à des semestres _courants_ (il est rare qu'il y en ai plus d'une, mais c'est possible). Triées par date de début de semestre décroissante (le plus récent en premier). @@ -240,18 +257,6 @@ class Identite(db.Model): ] return r[0] if r else None - def inscription_etat(self, formsemestre_id): - """État de l'inscription de cet étudiant au semestre: - False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF - """ - # voir si ce n'est pas trop lent: - ins = models.FormSemestreInscription.query.filter_by( - etudid=self.id, formsemestre_id=formsemestre_id - ).first() - if ins: - return ins.etat - return False - def inscription_descr(self) -> dict: """Description de l'état d'inscription""" inscription_courante = self.inscription_courante() @@ -290,6 +295,18 @@ class Identite(db.Model): "situation": situation, } + def inscription_etat(self, formsemestre_id): + """État de l'inscription de cet étudiant au semestre: + False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF + """ + # voir si ce n'est pas trop lent: + ins = models.FormSemestreInscription.query.filter_by( + etudid=self.id, formsemestre_id=formsemestre_id + ).first() + if ins: + return ins.etat + return False + def descr_situation_etud(self) -> str: """Chaîne décrivant la situation _actuelle_ de l'étudiant. Exemple: @@ -361,6 +378,15 @@ class Identite(db.Model): return situation + def etat_civil_pv(self, line_sep="\n") -> str: + """Présentation, pour PV jury + M. Pierre Dupont + n° 12345678 + né(e) le 7/06/1974 + à Paris + """ + return f"""{self.nomprenom}{line_sep}n°{self.code_nip or ""}{line_sep}né{self.e} le {self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""}{line_sep}à {self.lieu_naissance or ""}""" + def photo_html(self, title=None, size="small") -> str: """HTML img tag for the photo, either in small size (h90) or original size (size=="orig") @@ -434,7 +460,7 @@ class Adresse(db.Model): adresse_id = db.synonym("id") etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) email = db.Column(db.Text()) # mail institutionnel emailperso = db.Column(db.Text) # email personnel (exterieur) @@ -468,7 +494,7 @@ class Admission(db.Model): adm_id = db.synonym("id") etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) # Anciens champs de ScoDoc7, à revoir pour être plus générique et souple # notamment dans le cadre du bac 2021 @@ -513,21 +539,21 @@ class Admission(db.Model): def to_dict(self, no_nulls=False): """Représentation dictionnaire,""" - e = dict(self.__dict__) - e.pop("_sa_instance_state", None) + d = dict(self.__dict__) + d.pop("_sa_instance_state", None) if no_nulls: - for k in e: - if e[k] is None: + for k in d.keys(): + if d[k] is None: col_type = getattr( sqlalchemy.inspect(models.Admission).columns, "apb_groupe" ).expression.type if isinstance(col_type, sqlalchemy.Text): - e[k] = "" + d[k] = "" elif isinstance(col_type, sqlalchemy.Integer): - e[k] = 0 + d[k] = 0 elif isinstance(col_type, sqlalchemy.Boolean): - e[k] = False - return e + d[k] = False + return d # Suivi scolarité / débouchés @@ -538,7 +564,7 @@ class ItemSuivi(db.Model): itemsuivi_id = db.synonym("id") etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) item_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) situation = db.Column(db.Text) diff --git a/app/models/events.py b/app/models/events.py index b94549e7..4e566cbd 100644 --- a/app/models/events.py +++ b/app/models/events.py @@ -32,6 +32,21 @@ class Scolog(db.Model): authenticated_user = db.Column(db.Text) # login, sans contrainte # zope_remote_addr suppressed + @classmethod + def logdb( + cls, method: str = None, etudid: int = None, msg: str = None, commit=False + ): + """Add entry in student's log (replacement for old scolog.logdb)""" + entry = Scolog( + method=method, + msg=msg, + etudid=etudid, + authenticated_user=current_user.user_name, + ) + db.session.add(entry) + if commit: + db.session.commit() + class ScolarNews(db.Model): """Nouvelles pour page d'accueil""" diff --git a/app/models/formations.py b/app/models/formations.py index 43d3d680..d2970fce 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -1,15 +1,25 @@ """ScoDoc 9 models : Formations """ +import flask_sqlalchemy import app from app import db from app.comp import df_cache from app.models import SHORT_STR_LEN +from app.models.but_refcomp import ( + ApcAnneeParcours, + ApcCompetence, + ApcNiveau, + ApcParcours, + ApcParcoursNiveauCompetence, +) from app.models.modules import Module +from app.models.moduleimpls import ModuleImpl from app.models.ues import UniteEns from app.scodoc import sco_cache from app.scodoc import sco_codes_parcours from app.scodoc import sco_utils as scu +from app.scodoc.sco_codes_parcours import UE_STANDARD class Formation(db.Model): @@ -45,7 +55,11 @@ class Formation(db.Model): modules = db.relationship("Module", lazy="dynamic", backref="formation") def __repr__(self): - return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme}')>" + return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme!r}')>" + + def to_html(self) -> str: + "titre complet pour affichage" + return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}""" def to_dict(self): e = dict(self.__dict__) @@ -55,7 +69,10 @@ class Formation(db.Model): return e def get_parcours(self): - """get l'instance de TypeParcours de cette formation""" + """get l'instance de TypeParcours de cette formation + (le TypeParcours définit le genre de formation, à ne pas confondre + avec les parcours du BUT). + """ return sco_codes_parcours.get_parcours_from_code(self.type_parcours) def get_titre_version(self) -> str: @@ -97,6 +114,13 @@ class Formation(db.Model): else: keys = f"{self.id}.{semestre_idx}" df_cache.ModuleCoefsCache.delete_many(keys | {f"{self.id}"}) + # Invalidate aussi les poids de toutes les évals de la formation + for modimpl in ModuleImpl.query.filter( + ModuleImpl.module_id == Module.id, + Module.formation_id == self.id, + ): + modimpl.invalidate_evaluations_poids() + sco_cache.invalidate_formsemestre() def invalidate_cached_sems(self): @@ -148,6 +172,40 @@ class Formation(db.Model): if change: app.clear_scodoc_cache() + def query_ues_parcour(self, parcour: ApcParcours) -> flask_sqlalchemy.BaseQuery: + """Les UEs d'un parcours de la formation. + Exemple: pour avoir les UE du semestre 3, faire + `formation.query_ues_parcour(parcour).filter_by(semestre_idx=3)` + """ + return UniteEns.query.filter_by(formation=self).filter( + UniteEns.niveau_competence_id == ApcNiveau.id, + UniteEns.type == UE_STANDARD, + ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id, + ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id, + ApcAnneeParcours.parcours_id == parcour.id, + ) + + def query_competences_parcour( + self, parcour: ApcParcours + ) -> flask_sqlalchemy.BaseQuery: + """Les ApcCompetences d'un parcours de la formation. + None si pas de référentiel de compétences. + """ + if self.referentiel_competence_id is None: + return None + return ( + ApcCompetence.query.filter_by(referentiel_id=self.referentiel_competence_id) + .join( + ApcParcoursNiveauCompetence, + ApcParcoursNiveauCompetence.competence_id == ApcCompetence.id, + ) + .join( + ApcAnneeParcours, + ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id, + ) + .filter(ApcAnneeParcours.parcours_id == parcour.id) + ) + class Matiere(db.Model): """Matières: regroupe les modules d'une UE @@ -168,7 +226,7 @@ class Matiere(db.Model): def __repr__(self): return f"""<{self.__class__.__name__}(id={self.id}, ue_id={ - self.ue_id}, titre='{self.titre}')>""" + self.ue_id}, titre='{self.titre!r}')>""" def to_dict(self): """as a dict, with the same conversions as in ScoDoc7""" diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 16623bbf..bafb1c80 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -5,19 +5,31 @@ import datetime from functools import cached_property +from flask import flash import flask_sqlalchemy +from sqlalchemy.sql import text from app import db from app import log from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN +from app.models.but_refcomp import ( + ApcAnneeParcours, + ApcNiveau, + ApcParcours, + ApcParcoursNiveauCompetence, +) +from app.models.groups import GroupDescr, Partition import app.scodoc.sco_utils as scu -from app.models.ues import UniteEns +from app.models.but_refcomp import ApcParcours +from app.models.but_refcomp import parcours_formsemestre +from app.models.etudiants import Identite from app.models.modules import Module from app.models.moduleimpls import ModuleImpl -from app.models.etudiants import Identite +from app.models.ues import UniteEns + from app.scodoc import sco_codes_parcours from app.scodoc import sco_preferences from app.scodoc.sco_vdi import ApoEtapeVDI @@ -113,6 +125,14 @@ class FormSemestre(db.Model): # ne pas utiliser après migrate_scodoc7_dept_archives scodoc7_id = db.Column(db.Text(), nullable=True) + # BUT + parcours = db.relationship( + "ApcParcours", + secondary=parcours_formsemestre, + lazy="subquery", + backref=db.backref("formsemestres", lazy=True), + ) + def __init__(self, **kwargs): super(FormSemestre, self).__init__(**kwargs) if self.modalite is None: @@ -121,7 +141,7 @@ class FormSemestre(db.Model): def __repr__(self): return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>" - def to_dict(self): + def to_dict(self, convert_parcours=False): "dict (compatible ScoDoc7)" d = dict(self.__dict__) d.pop("_sa_instance_state", None) @@ -140,6 +160,8 @@ class FormSemestre(db.Model): d["date_fin"] = d["date_fin_iso"] = "" d["responsables"] = [u.id for u in self.responsables] d["titre_formation"] = self.titre_formation() + if convert_parcours: + d["parcours"] = [p.to_dict() for p in self.parcours] return d def to_dict_api(self): @@ -219,6 +241,22 @@ class FormSemestre(db.Model): sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT) return sem_ues.order_by(UniteEns.numero) + def query_ues_parcours_etud(self, etudid: int) -> flask_sqlalchemy.BaseQuery: + """UE que suit l'étudiant dans ce semestre BUT + en fonction du parcours dans lequel il est inscrit. + + Si voulez les UE d'un parcours, il est plus efficace de passer par + `formation.query_ues_parcour(parcour)`. + """ + return self.query_ues().filter( + FormSemestreInscription.etudid == etudid, + FormSemestreInscription.formsemestre == self, + UniteEns.niveau_competence_id == ApcNiveau.id, + ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id, + ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id, + ApcAnneeParcours.parcours_id == FormSemestreInscription.parcour_id, + ) + @cached_property def modimpls_sorted(self) -> list[ModuleImpl]: """Liste des modimpls du semestre (y compris bonus) @@ -245,6 +283,28 @@ class FormSemestre(db.Model): ) return modimpls + def modimpls_parcours(self, parcours: ApcParcours) -> list[ModuleImpl]: + """Liste des modimpls du semestre (sans les bonus (?)) dans le parcours donné. + - triée par type/numéro/code ?? + """ + cursor = db.session.execute( + text( + """ + SELECT modimpl.id + FROM notes_moduleimpl modimpl, notes_modules mod, + parcours_modules pm, parcours_formsemestre pf + WHERE modimpl.formsemestre_id = :formsemestre_id + AND modimpl.module_id = mod.id + AND pm.module_id = mod.id + AND pm.parcours_id = pf.parcours_id + AND pf.parcours_id = :parcours_id + AND pf.formsemestre_id = :formsemestre_id + """ + ), + {"formsemestre_id": self.id, "parcours_id": parcours.id}, + ) + return [ModuleImpl.query.get(modimpl_id) for modimpl_id in cursor] + def can_be_edited_by(self, user): """Vrai si user peut modifier ce semestre""" if not user.has_permission(Permission.ScoImplement): # pas chef @@ -311,6 +371,25 @@ class FormSemestre(db.Model): return "" return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape])) + def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]: + """Calcule la liste des regroupements cohérents d'UE impliquant ce + formsemestre. + Pour une année donnée: l'étudiant est inscrit dans ScoDoc soit dans le semestre + impair, soit pair, soit les deux (il est rare mais pas impossible d'avoir une + inscription seulement en semestre pair, par exemple suite à un transfert ou un + arrêt temporaire du cursus). + + 1. Déterminer l'*autre* formsemestre: semestre précédent ou suivant de la même + année, formation compatible (même référentiel de compétence) dans lequel + l'étudiant est inscrit. + + 2. Construire les couples d'UE (regroupements cohérents): apparier les UE qui + ont le même `ApcParcoursNiveauCompetence`. + """ + if not self.formation.is_apc(): + return [] + raise NotImplementedError() # XXX + def responsables_str(self, abbrev_prenom=True) -> str: """chaîne "J. Dupond, X. Martin" ou "Jacques Dupond, Xavier Martin" @@ -327,6 +406,11 @@ class FormSemestre(db.Model): "True si l'user est l'un des responsables du semestre" return user.id in [u.id for u in self.responsables] + def annee_scolaire(self) -> int: + """L'année de début de l'année scolaire. + Par exemple, 2022 si le semestre va de septebre 2022 à février 2023.""" + return scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month) + def annee_scolaire_str(self): "2021 - 2022" return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month) @@ -425,6 +509,19 @@ class FormSemestre(db.Model): etudid, self.date_debut.isoformat(), self.date_fin.isoformat() ) + def get_codes_apogee(self, category=None) -> set[str]: + """Les codes Apogée (codés en base comme "VRT1,VRT2") + category: None: tous, "etapes": étapes associées, "sem: code semestre", "annee": code annuel + """ + codes = set() + if category is None or category == "etapes": + codes |= {e.etape_apo for e in self.etapes if e} + if (category is None or category == "sem") and self.elt_sem_apo: + codes |= {x.strip() for x in self.elt_sem_apo.split(",") if x} + if (category is None or category == "annee") and self.elt_annee_apo: + codes |= {x.strip() for x in self.elt_annee_apo.split(",") if x} + return codes + def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]: """Liste des étudiants inscrits à ce semestre Si include_demdef, tous les étudiants, avec les démissionnaires @@ -449,6 +546,85 @@ class FormSemestre(db.Model): """Map { etudid : inscription } (incluant DEM et DEF)""" return {ins.etud.id: ins for ins in self.inscriptions} + def setup_parcours_groups(self) -> None: + """Vérifie et créee si besoin la partition et les groupes de parcours BUT.""" + if not self.formation.is_apc(): + return + partition = Partition.query.filter_by( + formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS + ).first() + if partition is None: + # Création de la partition de parcours + partition = Partition( + formsemestre_id=self.id, + partition_name=scu.PARTITION_PARCOURS, + numero=-1, + ) + db.session.add(partition) + db.session.flush() # pour avoir un id + flash(f"Partition Parcours créée.") + + for parcour in self.parcours: + if parcour.code: + group = GroupDescr.query.filter_by( + partition_id=partition.id, group_name=parcour.code + ).first() + if not group: + partition.groups.append(GroupDescr(group_name=parcour.code)) + db.session.commit() + + def update_inscriptions_parcours_from_groups(self) -> None: + """Met à jour les inscriptions dans les parcours du semestres en + fonction des groupes de parcours. + Les groupes de parcours sont ceux de la partition scu.PARTITION_PARCOURS + et leur nom est le code du parcours (eg "Cyber"). + """ + partition = Partition.query.filter_by( + formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS + ).first() + if partition is None: # pas de partition de parcours + return + + # Efface les inscriptions aux parcours: + db.session.execute( + text( + """UPDATE notes_formsemestre_inscription + SET parcour_id=NULL + WHERE formsemestre_id=:formsemestre_id + """ + ), + { + "formsemestre_id": self.id, + }, + ) + # Inscrit les étudiants des groupes de parcours: + for group in partition.groups: + query = ApcParcours.query.filter_by(code=group.group_name) + if query.count() != 1: + log( + f"""update_inscriptions_parcours_from_groups: { + query.count()} parcours with code {group.group_name}""" + ) + continue + parcour = query.first() + db.session.execute( + text( + """UPDATE notes_formsemestre_inscription ins + SET parcour_id=:parcour_id + FROM group_membership gm + WHERE formsemestre_id=:formsemestre_id + AND gm.etudid = ins.etudid + AND gm.group_id = :group_id + """ + ), + { + "formsemestre_id": self.id, + "parcour_id": parcour.id, + "group_id": group.id, + }, + ) + db.session.commit() + # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre notes_formsemestre_responsables = db.Table( @@ -607,7 +783,9 @@ class FormSemestreInscription(db.Model): id = db.Column(db.Integer, primary_key=True) formsemestre_inscription_id = db.synonym("id") - etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True) + etudid = db.Column( + db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"), index=True + ) formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), @@ -627,11 +805,16 @@ class FormSemestreInscription(db.Model): ) # I inscrit, D demission en cours de semestre, DEF si "defaillant" etat = db.Column(db.String(CODE_STR_LEN), index=True) - # etape apogee d'inscription (experimental 2020) + # Etape Apogée d'inscription (ajout 2020) etape = db.Column(db.String(APO_CODE_STR_LEN)) + # Parcours (pour les BUT) + parcour_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), index=True) + parcour = db.relationship(ApcParcours) def __repr__(self): - return f"<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={self.formsemestre_id} etat={self.etat}>" + return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={ + self.formsemestre_id} etat={self.etat} { + ('parcours='+str(self.parcour)) if self.parcour else ''}>""" class NotesSemSet(db.Model): diff --git a/app/models/groups.py b/app/models/groups.py index 4c64ad54..27b763d1 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -23,7 +23,7 @@ class Partition(db.Model): ) # "TD", "TP", ... (NULL for 'all') partition_name = db.Column(db.String(SHORT_STR_LEN)) - # numero = ordre de presentation) + # Numero = ordre de presentation) numero = db.Column(db.Integer) # Calculer le rang ? bul_show_rank = db.Column( @@ -33,6 +33,10 @@ class Partition(db.Model): show_in_lists = db.Column( db.Boolean(), nullable=False, default=True, server_default="true" ) + # Editable ? (faux pour les groupes de parcours) + groups_editable = db.Column( + db.Boolean(), nullable=False, default=True, server_default="true" + ) groups = db.relationship( "GroupDescr", backref=db.backref("partition", lazy=True), @@ -106,7 +110,7 @@ class GroupDescr(db.Model): group_membership = db.Table( "group_membership", - db.Column("etudid", db.Integer, db.ForeignKey("identite.id")), + db.Column("etudid", db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE")), db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")), db.UniqueConstraint("etudid", "group_id"), ) @@ -116,5 +120,5 @@ group_membership = db.Table( # __tablename__ = "group_membership" # __table_args__ = (db.UniqueConstraint("etudid", "group_id"),) # id = db.Column(db.Integer, primary_key=True) -# etudid = db.Column(db.Integer, db.ForeignKey("identite.id")) +# etudid = db.Column(db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE")) # group_id = db.Column(db.Integer, db.ForeignKey("group_descr.id")) diff --git a/app/models/modules.py b/app/models/modules.py index 67ff3de0..b3655772 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -3,6 +3,7 @@ from app import db from app.models import APO_CODE_STR_LEN +from app.models.but_refcomp import app_critiques_modules, parcours_modules from app.scodoc import sco_utils as scu from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_utils import ModuleType @@ -44,13 +45,27 @@ class Module(db.Model): lazy=True, backref=db.backref("modules", lazy=True), ) + # BUT + parcours = db.relationship( + "ApcParcours", + secondary=parcours_modules, + lazy="subquery", + backref=db.backref("modules", lazy=True), + ) + + app_critiques = db.relationship( + "ApcAppCritique", + secondary=app_critiques_modules, + lazy="subquery", + backref=db.backref("modules", lazy=True), + ) def __init__(self, **kwargs): self.ue_coefs = [] super(Module, self).__init__(**kwargs) def __repr__(self): - return f"" + return f"" def to_dict(self): e = dict(self.__dict__) @@ -160,6 +175,12 @@ class Module(db.Model): # Liste seulement les coefs définis: return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()] + def get_codes_apogee(self) -> set[str]: + """Les codes Apogée (codés en base comme "VRT1,VRT2")""" + if self.code_apogee: + return {x.strip() for x in self.code_apogee.split(",") if x} + return set() + class ModuleUECoef(db.Model): """Coefficients des modules vers les UE (APC, BUT) diff --git a/app/models/notes.py b/app/models/notes.py index a5e90be1..b3595126 100644 --- a/app/models/notes.py +++ b/app/models/notes.py @@ -17,7 +17,7 @@ class BulAppreciations(db.Model): date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), index=True, ) formsemestre_id = db.Column( @@ -36,7 +36,7 @@ class NotesNotes(db.Model): id = db.Column(db.Integer, primary_key=True) etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) evaluation_id = db.Column( db.Integer, db.ForeignKey("notes_evaluation.id"), index=True @@ -75,7 +75,7 @@ class NotesNotesLog(db.Model): etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) evaluation_id = db.Column( db.Integer, diff --git a/app/models/ues.py b/app/models/ues.py index 48d81a14..450482bc 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -40,8 +40,15 @@ class UniteEns(db.Model): # coef UE, utilise seulement si l'option use_ue_coefs est activée: coefficient = db.Column(db.Float) + # coef. pour le calcul de moyennes de RCUE. Par défaut, 1. + coef_rcue = db.Column(db.Float, nullable=False, default=1.0, server_default="1.0") + color = db.Column(db.Text()) + # BUT + niveau_competence_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id")) + niveau_competence = db.relationship("ApcNiveau", back_populates="ues") + # relations matieres = db.relationship("Matiere", lazy="dynamic", backref="ue") modules = db.relationship("Module", lazy="dynamic", backref="ue") @@ -113,3 +120,9 @@ class UniteEns(db.Model): (Module.module_type != scu.ModuleType.SAE), (Module.module_type != scu.ModuleType.RESSOURCE), ).all() + + def get_codes_apogee(self) -> set[str]: + """Les codes Apogée (codés en base comme "VRT1,VRT2")""" + if self.code_apogee: + return {x.strip() for x in self.code_apogee.split(",") if x} + return set() diff --git a/app/models/validations.py b/app/models/validations.py index 0bf487f3..42d7ba0d 100644 --- a/app/models/validations.py +++ b/app/models/validations.py @@ -6,6 +6,7 @@ from app import db from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN +from app.models.events import Scolog class ScolarFormSemestreValidation(db.Model): @@ -19,7 +20,7 @@ class ScolarFormSemestreValidation(db.Model): formsemestre_validation_id = db.synonym("id") etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), index=True, ) formsemestre_id = db.Column( @@ -36,7 +37,7 @@ class ScolarFormSemestreValidation(db.Model): # NULL pour les UE, True|False pour les semestres: assidu = db.Column(db.Boolean) event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) - # NULL sauf si compense un semestre: + # NULL sauf si compense un semestre: (pas utilisé pour BUT) compense_formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), @@ -54,7 +55,7 @@ class ScolarFormSemestreValidation(db.Model): ue = db.relationship("UniteEns", lazy="select", uselist=False) def __repr__(self): - return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue_id}, moy_ue={self.moy_ue})" + return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})" class ScolarAutorisationInscription(db.Model): @@ -66,10 +67,10 @@ class ScolarAutorisationInscription(db.Model): etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False) - # semestre ou on peut s'inscrire: + # Indice du semestre où on peut s'inscrire: semestre_id = db.Column(db.Integer) date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) origin_formsemestre_id = db.Column( @@ -77,6 +78,44 @@ class ScolarAutorisationInscription(db.Model): db.ForeignKey("notes_formsemestre.id"), ) + @classmethod + def autorise_etud( + cls, + etudid: int, + formation_code: str, + origin_formsemestre_id: int, + semestre_id: int, + ): + """Enregistre une autorisation, remplace celle émanant du même semestre si elle existe.""" + cls.delete_autorisation_etud(etudid, origin_formsemestre_id) + autorisation = cls( + etudid=etudid, + formation_code=formation_code, + origin_formsemestre_id=origin_formsemestre_id, + semestre_id=semestre_id, + ) + db.session.add(autorisation) + Scolog.logdb("autorise_etud", etudid=etudid, msg=f"passage vers S{semestre_id}") + + @classmethod + def delete_autorisation_etud( + cls, + etudid: int, + origin_formsemestre_id: int, + ): + """Efface les autorisations de cette étudiant venant du sem. origine""" + autorisations = cls.query.filter_by( + etudid=etudid, origin_formsemestre_id=origin_formsemestre_id + ) + for autorisation in autorisations: + db.session.delete(autorisation) + Scolog.logdb( + "autorise_etud", + etudid=etudid, + msg=f"annule passage vers S{autorisation.semestre_id}", + ) + db.session.flush() + class ScolarEvent(db.Model): """Evenement dans le parcours scolaire d'un étudiant""" @@ -86,7 +125,7 @@ class ScolarEvent(db.Model): event_id = db.synonym("id") etudid = db.Column( db.Integer, - db.ForeignKey("identite.id"), + db.ForeignKey("identite.id", ondelete="CASCADE"), ) event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) formsemestre_id = db.Column( diff --git a/app/scodoc/TrivialFormulator.py b/app/scodoc/TrivialFormulator.py index c8acefaf..ae700768 100644 --- a/app/scodoc/TrivialFormulator.py +++ b/app/scodoc/TrivialFormulator.py @@ -207,12 +207,16 @@ class TF(object): else: self.values[field] = 1 if field not in self.values: - if "default" in descr: # first: default in form description - self.values[field] = descr["default"] - else: # then: use initvalues dict - self.values[field] = self.initvalues.get(field, "") - if self.values[field] == None: - self.values[field] = "" + if (descr.get("input_type", None) == "checkbox") and self.submitted(): + # aucune case cochée + self.values[field] = [] + else: + if "default" in descr: # first: default in form description + self.values[field] = descr["default"] + else: # then: use initvalues dict + self.values[field] = self.initvalues.get(field, "") + if self.values[field] is None: + self.values[field] = "" # convert numbers, except ids if field.endswith("id") and self.values[field]: @@ -392,9 +396,7 @@ class TF(object): if self.top_buttons: R.append(buttons_markup + "

") R.append('') - idx = 0 - for idx in range(len(self.formdescription)): - (field, descr) = self.formdescription[idx] + for field, descr in self.formdescription: if descr.get("readonly", False): R.append(self._ReadOnlyElement(field, descr)) continue @@ -408,7 +410,7 @@ class TF(object): input_type = descr.get("input_type", "text") item_dom_id = descr.get("dom_id", "") if item_dom_id: - item_dom_attr = ' id="%s"' % item_dom_id + item_dom_attr = f' id="{item_dom_id}"' else: item_dom_attr = "" # choix du template @@ -523,7 +525,6 @@ class TF(object): else: checked = "" else: # boolcheckbox - # open('/tmp/toto','a').write('GenForm: values[%s] = %s (%s)\n' % (field, values[field], type(values[field]))) if values[field] == "True": v = True elif values[field] == "False": diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index 1c708ca5..0fab06ea 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -45,7 +45,7 @@ import random from collections import OrderedDict from xml.etree import ElementTree import json - +from openpyxl.utils import get_column_letter from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak from reportlab.platypus import Table, TableStyle, Image, KeepInFrame from reportlab.lib.colors import Color @@ -127,6 +127,8 @@ class GenTable(object): filename="table", # filename, without extension xls_sheet_name="feuille", xls_before_table=[], # liste de cellules a placer avant la table + xls_style_base=None, # style excel pour les cellules + xls_columns_width=None, # { col_id : largeur en "pixels excel" } pdf_title="", # au dessus du tableau en pdf pdf_table_style=None, pdf_col_widths=None, @@ -151,6 +153,8 @@ class GenTable(object): self.page_title = page_title self.pdf_link = pdf_link self.xls_link = xls_link + self.xls_style_base = xls_style_base + self.xls_columns_width = xls_columns_width or {} self.xml_link = xml_link # HTML parameters: if not table_id: # random id @@ -495,7 +499,8 @@ class GenTable(object): sheet = wb.create_sheet(sheet_name=self.xls_sheet_name) sheet.rows += self.xls_before_table style_bold = sco_excel.excel_make_style(bold=True) - style_base = sco_excel.excel_make_style() + style_base = self.xls_style_base or sco_excel.excel_make_style() + sheet.append_row(sheet.make_row(self.get_titles_list(), style_bold)) for line in self.get_data_list(xls_mode=True): sheet.append_row(sheet.make_row(line, style_base)) @@ -505,6 +510,16 @@ class GenTable(object): if self.origin: sheet.append_blank_row() # empty line sheet.append_single_cell_row(self.origin, style_base) + # Largeurs des colonnes + columns_ids = list(self.columns_ids) + for col_id, width in self.xls_columns_width.items(): + try: + idx = columns_ids.index(col_id) + col = get_column_letter(idx + 1) + sheet.set_column_dimension_width(col, width) + except ValueError: + pass + if wb is None: return sheet.generate() diff --git a/app/scodoc/html_sco_header.py b/app/scodoc/html_sco_header.py index 2cf0be40..0b2a3d58 100644 --- a/app/scodoc/html_sco_header.py +++ b/app/scodoc/html_sco_header.py @@ -59,35 +59,29 @@ BOOTSTRAP_MULTISELECT_CSS = [ def standard_html_header(): """Standard HTML header for pages outside depts""" # not used in ZScolar, see sco_header - return """ + return f"""ScoDoc: accueil - + - + -%s""" % ( - scu.SCO_ENCODING, - scu.CUSTOM_HTML_HEADER_CNX, - ) +{scu.CUSTOM_HTML_HEADER_CNX}""" def standard_html_footer(): """Le pied de page HTML de la page d'accueil.""" - return """ -

Problèmes et suggestions sur le logiciel: %s

+

Problèmes et suggestions sur le logiciel: {scu.SCO_USERS_LIST}

ScoDoc est un logiciel libre développé par Emmanuel Viennet.

-""" % ( - scu.SCO_USERS_LIST, - scu.SCO_USERS_LIST, - ) +""" -_HTML_BEGIN = """ +_HTML_BEGIN = f""" @@ -100,27 +94,27 @@ _HTML_BEGIN = """%(page_title)s - + - - - - + + + + - - - + + + - + - - + + - - + + """ @@ -138,9 +132,9 @@ def sco_header( # optional args page_title="", # page title no_side_bar=False, # hide sidebar - cssstyles=[], # additionals CSS sheets - javascripts=[], # additionals JS filenames to load - scripts=[], # script to put in page header + cssstyles=(), # additionals CSS sheets + javascripts=(), # additionals JS filenames to load + scripts=(), # script to put in page header bodyOnLoad="", # JS init_qtip=False, # include qTip init_google_maps=False, # Google maps @@ -148,6 +142,8 @@ def sco_header( titrebandeau="", # titre dans bandeau superieur head_message="", # message action (petit cadre jaune en haut) user_check=True, # verifie passwords temporaires + etudid=None, + formsemestre_id=None, ): "Main HTML page header for ScoDoc" from app.scodoc.sco_formsemestre_status import formsemestre_page_title @@ -191,7 +187,7 @@ def sco_header( # jQuery UI # can modify loaded theme here H.append( - '\n' + f'\n' ) if init_google_maps: # It may be necessary to add an API key: @@ -200,72 +196,65 @@ def sco_header( # Feuilles de style additionnelles: for cssstyle in cssstyles: H.append( - """\n""" - % cssstyle + f"""\n""" ) H.append( - """ - - - + f""" + + + - - + +""" - % params ) # jQuery H.append( - """ - """ + f""" + """ ) - H.append('') # qTip if init_qtip: H.append( - '' - ) - H.append( - '' + f""" + """ ) H.append( - '' + f""" + """ ) - - H.append('') if init_google_maps: H.append( - '' + f'' ) if init_datatables: H.append( - '' + f""" + """ ) - H.append('') # H.append( - # '' + # f'' # ) # JS additionels for js in javascripts: - H.append("""\n""" % js) + H.append(f"""\n""") H.append( - """ """ - % params ) # Scripts de la page: if scripts: @@ -281,25 +270,24 @@ def sco_header( H.append(scu.CUSTOM_HTML_HEADER) # if not no_side_bar: - H.append(html_sidebar.sidebar()) + H.append(html_sidebar.sidebar(etudid)) H.append("""
""") # En attendant le replacement complet de cette fonction, # inclusion ici des messages flask H.append(render_template("flashed_messages.html")) # # Barre menu semestre: - H.append(formsemestre_page_title()) + H.append(formsemestre_page_title(formsemestre_id)) # Avertissement si mot de passe à changer if user_check: if current_user.passwd_temp: H.append( - """
+ f"""
Attention !
Vous avez reçu un mot de passe temporaire.
- Vous devez le changer: cliquez ici + Vous devez le changer: cliquez ici
""" - % (scu.UsersURL, current_user.user_name) ) # if head_message: @@ -328,6 +316,6 @@ def html_sem_header( else: h = "" if with_h2: - return h + """

%s

""" % (title) + return h + f"""

{title}

""" else: return h diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index 5ab8eacf..c3468cfe 100644 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -73,7 +73,7 @@ def sidebar_common(): return "".join(H) -def sidebar(): +def sidebar(etudid: int = None): "Main HTML page sidebar" # rewritten from legacy DTML code from app.scodoc import sco_abs @@ -93,14 +93,14 @@ def sidebar(): """ ] # ---- Il y-a-t-il un etudiant selectionné ? - etudid = g.get("etudid", None) - if not etudid: + etudid = etudid if etudid is not None else g.get("etudid", None) + if etudid is None: if request.method == "GET": etudid = request.args.get("etudid", None) elif request.method == "POST": etudid = request.form.get("etudid", None) - if etudid: + if etudid is not None: etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] params.update(etud) params["fiche_url"] = url_for( diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index 7fe9501d..e309beda 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -258,11 +258,16 @@ class ApoEtud(dict): self["nom"] = nom self["prenom"] = prenom self["naissance"] = naissance - self.cols = cols # { col_id : value } colid = 'apoL_c0001' + self.cols = cols + "{ col_id : value } colid = 'apoL_c0001'" + self.col_elts = {} + "{'V1RT': {'R': 'ADM', 'J': '', 'B': 20, 'N': '12.14'}}" self.new_cols = {} # { col_id : value to record in csv } - self.etud = None # etud ScoDoc + self.etud: Identite = None + "etudiant ScoDoc associé" self.etat = None # ETUD_OK, ... - self.is_NAR = False # set to True si NARé dans un semestre + self.is_NAR = False + "True si NARé dans un semestre" self.log = [] self.has_logged_no_decision = False self.export_res_etape = export_res_etape # VET, ... @@ -276,7 +281,7 @@ class ApoEtud(dict): ) def __repr__(self): - return "ApoEtud( nom='%s', nip='%s' )" % (self["nom"], self["nip"]) + return f"""ApoEtud( nom='{self["nom"]}', nip='{self["nip"]}' )""" def lookup_scodoc(self, etape_formsemestre_ids): """Cherche l'étudiant ScoDoc associé à cet étudiant Apogée. @@ -284,6 +289,10 @@ class ApoEtud(dict): met .etud à None. Sinon, cherche le semestre, et met l'état à ETUD_OK ou ETUD_NON_INSCRIT. """ + + # futur: #WIP + # etud: Identite = Identite.query.filter_by(code_nip=self["nip"]).first() + # self.etud = etud etuds = sco_etud.get_etud_info(code_nip=self["nip"], filled=True) if not etuds: # pas dans ScoDoc @@ -291,13 +300,16 @@ class ApoEtud(dict): self.log.append("non inscrit dans ScoDoc") self.etat = ETUD_ORPHELIN else: + # futur: #WIP + # formsemestre_ids = { + # ins.formsemestre_id for ins in etud.formsemestre_inscriptions + # } + # in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids) self.etud = etuds[0] # cherche le semestre ScoDoc correspondant à l'un de ceux de l'etape: formsemestre_ids = {s["formsemestre_id"] for s in self.etud["sems"]} - self.in_formsemestre_ids = formsemestre_ids.intersection( - etape_formsemestre_ids - ) - if not self.in_formsemestre_ids: + in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids) + if not in_formsemestre_ids: self.log.append( "connu dans ScoDoc, mais pas inscrit dans un semestre de cette étape" ) @@ -305,7 +317,7 @@ class ApoEtud(dict): else: self.etat = ETUD_OK - def associate_sco(self, apo_data): + def associate_sco(self, apo_data: "ApoData"): """Recherche les valeurs des éléments Apogée pour cet étudiant Set .new_cols """ @@ -327,7 +339,7 @@ class ApoEtud(dict): cur_sem, autre_sem = self.etud_semestres_de_etape(apo_data) for sem in apo_data.sems_etape: el = self.search_elt_in_sem(code, sem, cur_sem, autre_sem) - if el != None: + if el is not None: sco_elts[code] = el break self.col_elts[code] = el @@ -338,15 +350,15 @@ class ApoEtud(dict): self.new_cols[col_id] = sco_elts[code][ apo_data.cols[col_id]["Type Rés."] ] - except KeyError: + except KeyError as exc: log( - "associate_sco: missing key, etud=%s\ncode='%s'\netape='%s'" - % (self, code, apo_data.etape_apogee) + f"associate_sco: missing key, etud={self}\ncode='{code}'\netape='{apo_data.etape_apogee}'" ) raise ScoValueError( - """L'élément %s n'a pas de résultat: peut-être une erreur dans les codes sur le programme pédagogique (vérifier qu'il est bien associé à une UE ou semestre)?""" - % code - ) + f"""L'élément {code} n'a pas de résultat: peut-être une erreur + dans les codes sur le programme pédagogique + (vérifier qu'il est bien associé à une UE ou semestre)?""" + ) from exc # recopie les 4 premieres colonnes (nom, ..., naissance): for col_id in apo_data.col_ids[:4]: self.new_cols[col_id] = self.cols[col_id] @@ -356,7 +368,7 @@ class ApoEtud(dict): # codes = set([apo_data.cols[col_id].code for col_id in apo_data.col_ids]) # return codes - set(sco_elts) - def search_elt_in_sem(self, code, sem, cur_sem, autre_sem): + def search_elt_in_sem(self, code, sem, cur_sem, autre_sem) -> dict: """ VET code jury etape ELP élément pédagogique: UE, module @@ -820,10 +832,8 @@ class ApoData(object): elts[col["Code"]] = ApoElt([col]) return elts # { code apo : ApoElt } - def apo_read_etuds(self, f): - """Lecture des etudiants (et resultats) du fichier CSV Apogée - -> liste de dicts - """ + def apo_read_etuds(self, f) -> list[ApoEtud]: + """Lecture des etudiants (et resultats) du fichier CSV Apogée""" L = [] while True: line = f.readline() @@ -958,36 +968,38 @@ class ApoData(object): """ codes_by_sem = {} for sem in self.sems_etape: + formsemestre: FormSemestre = FormSemestre.query.get_or_404( + sem["formsemestre_id"] + ) + # L'ensemble des codes apo associés aux éléments: + codes_semestre = formsemestre.get_codes_apogee() + codes_modules = set().union( + *[ + modimpl.module.get_codes_apogee() + for modimpl in formsemestre.modimpls + ] + ) + codes_ues = set().union( + *[ + ue.get_codes_apogee() + for ue in formsemestre.query_ues(with_sport=True) + ] + ) s = set() codes_by_sem[sem["formsemestre_id"]] = s for col_id in self.col_ids[4:]: code = self.cols[col_id]["Code"] # 'V1RT' - # associé à l'étape, l'année ou les semestre: - if ( - sco_formsemestre.sem_has_etape(sem, code) - or (code in {x.strip() for x in sem["elt_sem_apo"].split(",")}) - or (code in {x.strip() for x in sem["elt_annee_apo"].split(",")}) - ): + # associé à l'étape, l'année ou le semestre: + if code in codes_semestre: s.add(code) continue # associé à une UE: - formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) - nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - for ue in nt.get_ues_stat_dict(): - if ue["code_apogee"]: - codes = {x.strip() for x in ue["code_apogee"].split(",")} - if code in codes: - s.add(code) - continue + if code in codes_ues: + s.add(code) + continue # associé à un module: - modimpls = nt.get_modimpls_dict() - for modimpl in modimpls: - module = modimpl["module"] - if module["code_apogee"]: - codes = {x.strip() for x in module["code_apogee"].split(",")} - if code in codes: - s.add(code) - continue + if code in codes_modules: + s.add(code) # log('codes_by_sem=%s' % pprint.pformat(codes_by_sem)) return codes_by_sem diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index 773fc042..26ce9f8c 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -47,6 +47,7 @@ qui est une description (humaine, format libre) de l'archive. """ +import chardet import datetime import glob import json @@ -55,7 +56,7 @@ import os import re import shutil import time -import chardet +from typing import Union import flask from flask import g, request @@ -232,14 +233,17 @@ class BaseArchiver(object): os.mkdir(archive_id) # if exists, raises an OSError finally: scu.GSL.release() - self.store(archive_id, "_description.txt", description.encode("utf-8")) + self.store(archive_id, "_description.txt", description) return archive_id - def store(self, archive_id: str, filename: str, data: bytes): + def store(self, archive_id: str, filename: str, data: Union[str, bytes]): """Store data in archive, under given filename. Filename may be modified (sanitized): return used filename The file is created or replaced. + data may be str or bytes """ + if isinstance(data, str): + data = data.encode(scu.SCO_ENCODING) self.initialize() filename = scu.sanitize_filename(filename) log("storing %s (%d bytes) in %s" % (filename, len(data), archive_id)) @@ -350,19 +354,21 @@ def do_formsemestre_archive( html_sco_header.sco_footer(), ] ) - data = data.encode(scu.SCO_ENCODING) PVArchive.store(archive_id, "Tableau_moyennes.html", data) # Bulletins en JSON data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True) data_js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder) - data_js = data_js.encode(scu.SCO_ENCODING) if data: PVArchive.store(archive_id, "Bulletins.json", data_js) # Decisions de jury, en XLS data = sco_pvjury.formsemestre_pvjury(formsemestre_id, format="xls", publish=False) if data: - PVArchive.store(archive_id, "Decisions_Jury" + scu.XLSX_SUFFIX, data) + PVArchive.store( + archive_id, + "Decisions_Jury" + scu.XLSX_SUFFIX, + data, + ) # Classeur bulletins (PDF) data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( formsemestre_id, version=bulVersion diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index df790649..daf4a941 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -158,9 +158,24 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): I["server_name"] = request.url_root # Formation et parcours - I["formation"] = sco_formations.formation_list( - args={"formation_id": I["sem"]["formation_id"]} - )[0] + if I["sem"]["formation_id"]: + I["formation"] = sco_formations.formation_list( + args={"formation_id": I["sem"]["formation_id"]} + )[0] + else: # what's the fuck ? + I["formation"] = { + "acronyme": "?", + "code_specialite": "", + "dept_id": 1, + "formation_code": "?", + "formation_id": -1, + "id": -1, + "referentiel_competence_id": None, + "titre": "?", + "titre_officiel": "?", + "type_parcours": 0, + "version": 0, + } I["parcours"] = sco_codes_parcours.get_parcours_from_code( I["formation"]["type_parcours"] ) diff --git a/app/scodoc/sco_bulletins_standard.py b/app/scodoc/sco_bulletins_standard.py index e7c92ad7..e1e85372 100644 --- a/app/scodoc/sco_bulletins_standard.py +++ b/app/scodoc/sco_bulletins_standard.py @@ -439,15 +439,15 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): ects_txt = "-" t = { - "titre": ue["acronyme"] + " " + ue["titre"], + "titre": ue["acronyme"] + " " + (ue["titre"] or ""), "_titre_html": plusminus - + ue["acronyme"] + + (ue["acronyme"] or "") + " " - + ue["titre"] + + (ue["titre"] or "") + ' ' - + ue["ue_descr_txt"] + + (ue["ue_descr_txt"] or "") + "", - "_titre_help": ue["ue_descr_txt"], + "_titre_help": ue["ue_descr_txt"] or "", "_titre_colspan": 2, "module": ue_descr, "note": ue["moy_ue_txt"], diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index 8fcc6f7e..7975e3b2 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -67,6 +67,7 @@ class ScoDocCache: timeout = None # ttl, infinite by default prefix = "" + verbose = False # if true, verbose logging (debug) @classmethod def _get_key(cls, oid): @@ -87,7 +88,10 @@ class ScoDocCache: def set(cls, oid, value): """Store value""" key = cls._get_key(oid) - # log(f"CACHE key={key}, type={type(value)}, timeout={cls.timeout}") + if cls.verbose: + log( + f"{cls.__name__}.set key={key}, type={type(value).__name__}, timeout={cls.timeout}" + ) try: status = CACHE.set(key, value, timeout=cls.timeout) if not status: @@ -101,11 +105,15 @@ class ScoDocCache: @classmethod def delete(cls, oid): """Remove from cache""" + # if cls.verbose: + # log(f"{cls.__name__}.delete({oid})") CACHE.delete(cls._get_key(oid)) @classmethod def delete_many(cls, oids): """Remove multiple keys at once""" + if cls.verbose: + log(f"{cls.__name__}.delete_many({oids})") # delete_many seems bugged: # CACHE.delete_many([cls._get_key(oid) for oid in oids]) for oid in oids: diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index e77c711d..dc59aeb2 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -35,7 +35,7 @@ from app import log @enum.unique class CodesParcours(enum.IntEnum): - """Codes numériques de sparcours, enregistrés en base + """Codes numériques des parcours, enregistrés en base dans notes_formations.type_parcours Ne pas modifier. """ @@ -68,7 +68,8 @@ NOTES_TOLERANCE = 0.00499999999999 # si note >= (BARRE-TOLERANCE), considere ok # (permet d'eviter d'afficher 10.00 sous barre alors que la moyenne vaut 9.999) # Barre sur moyenne générale utilisée pour compensations semestres: -NOTES_BARRE_GEN_COMPENSATION = 10.0 - NOTES_TOLERANCE +NOTES_BARRE_GEN = 10.0 +NOTES_BARRE_GEN_COMPENSATION = NOTES_BARRE_GEN - NOTES_TOLERANCE # ---------------------------------------------------------------- # Types d'UE: @@ -114,6 +115,8 @@ UE_SEM_DEFAULT = 1000000 # indice semestre des UE sans modules # ------------------------------------------------------------------ # Codes proposés par ADIUT / Apogee +ABAN = "ABAN" +ABL = "ABL" ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10) ADJ = "ADJ" # admis par le jury @@ -122,10 +125,16 @@ ATJ = "ATJ" # pb assiduité: décision repoussée au semestre suivant ATB = "ATB" AJ = "AJ" CMP = "CMP" # utile pour UE seulement (indique UE acquise car semestre acquis) -NAR = "NAR" -RAT = "RAT" # en attente rattrapage, sera ATT dans Apogée DEF = "DEF" # défaillance (n'est pas un code jury dans scodoc mais un état, comme inscrit ou demission) DEM = "DEM" +EXCLU = "EXCLU" +JSD = "JSD" # jury tenu mais pas de code (Jury Sans Décision) +NAR = "NAR" +PASD = "PASD" +PAS1NCI = "PAS1NCI" +RAT = "RAT" # en attente rattrapage, sera ATT dans Apogée +RED = "RED" +UEBSL = "UEBSL" # UE blanchie # codes actions REDOANNEE = "REDOANNEE" # redouble annee (va en Sn-1) @@ -143,22 +152,34 @@ ALL = "ALL" # Explication des codes (de semestre ou d'UE) CODES_EXPL = { + ABAN: "Non évalué pour manque d’assiduité: non présentation des notes de l'étudiant au jury", + ABL: "Année blanche", ADC: "Validé par compensation", ADJ: "Validé par le Jury", ADM: "Validé", - AJ: "Ajourné", + AJ: "Ajourné (ou UE/BC de BUT en attente pour problème de moyenne)", ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)", ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)", ATT: "Décision en attente d'un autre semestre (faute d'atteindre la moyenne)", - CMP: "Code UE acquise car semestre acquis", - DEF: "Défaillant", - NAR: "Échec, non autorisé à redoubler", - RAT: "En attente d'un rattrapage", + CMP: """Code UE acquise car semestre acquis, ou, en BUT, acquise par + compensation UE avec l’UE de même compétence et de même année (ECTS acquis). + Utilisé aussi pour les blocs de compétences BUT (RCUE). + """, + DEF: "Défaillant, pas ou peu de notes par arrêt de la formation. Non évalué par manque assiduité.", DEM: "Démission", + EXCLU: "Exclusion: décision réservée à des décisions disciplinaires", + NAR: "Non admis, réorientation, non autorisé à redoubler", + PASD: """Année BUT: non admis, mais passage de droit: + Passage en Année Supérieure de Droit (+ de 50% des UE VAL et RCUE Ajourné(s) >=8) + """, + PAS1NCI: """Année BUT: Non admis, mais passage par décision de jury: + Passage en Année Supérieure avec au moins 1 Niveau de Compétence Insuffisant (RCUE<8) + """, + RAT: "En attente d'un rattrapage", + RED: "Année: Ajourné, mais autorisé à redoubler", + UEBSL: "UE blanchie", } -# Nota: ces explications sont personnalisables via le fichier -# de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py -# variable: CONFIG.CODES_EXP + # Les codes de semestres: CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT} @@ -167,7 +188,21 @@ CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente CODES_SEM_REO = {NAR: 1} # reorientation -CODES_UE_VALIDES = {ADM: True, CMP: True} # UE validée +CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True} # UE validée +CODES_RCUE_VALIDES = CODES_UE_VALIDES # Niveau RCUE validé +# Pour le BUT: +CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL} +CODES_RCUE = {ADM, AJ, CMP} +BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE +BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE +BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE +BUT_CODES_PASSAGE = { + ADM, + ADJ, + PASD, + PAS1NCI, + ATJ, +} def code_semestre_validant(code: str) -> bool: diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index c53094ef..7e71f51d 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -76,7 +76,7 @@ def html_edit_formation_apc( ues_by_sem[semestre_idx] = formation.ues.filter_by( semestre_idx=semestre_idx ).order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme) - ects = [ue.ects for ue in ues_by_sem[semestre_idx]] + ects = [ue.ects for ue in ues_by_sem[semestre_idx] if ue.type != UE_SPORT] if None in ects: ects_by_sem[semestre_idx] = 'manquant' else: @@ -127,27 +127,33 @@ def html_edit_formation_apc( formation=formation, titre=f"Ressources du S{semestre_idx}", create_element_msg="créer une nouvelle ressource", - matiere_parent=matiere_parent, + # matiere_parent=matiere_parent, modules=ressources_in_sem, module_type=ModuleType.RESSOURCE, editable=editable, tag_editable=tag_editable, icons=icons, scu=scu, - ), + semestre_id=semestre_idx, + ) + if ues_by_sem[semestre_idx].count() > 0 + else "", render_template( "pn/form_mods.html", formation=formation, titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}", create_element_msg="créer une nouvelle SAÉ", - matiere_parent=matiere_parent, + # matiere_parent=matiere_parent, modules=saes_in_sem, module_type=ModuleType.SAE, editable=editable, tag_editable=tag_editable, icons=icons, scu=scu, - ), + semestre_id=semestre_idx, + ) + if ues_by_sem[semestre_idx].count() > 0 + else "", render_template( "pn/form_mods.html", formation=formation, @@ -159,7 +165,10 @@ def html_edit_formation_apc( tag_editable=tag_editable, icons=icons, scu=scu, - ), + semestre_id=semestre_idx, + ) + if ues_by_sem[semestre_idx].count() > 0 + else """créer une UE pour pouvoir ajouter des modules""", ] return "\n".join(H) diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py index 606fc742..62fc2bbf 100644 --- a/app/scodoc/sco_edit_formation.py +++ b/app/scodoc/sco_edit_formation.py @@ -245,7 +245,11 @@ def formation_edit(formation_id=None, create=False): return ( "\n".join(H) + tf_error_message( - "Valeurs incorrectes: il existe déjà une formation avec même titre, acronyme et version." + f"""Valeurs incorrectes: il existe déjà une formation avec même titre, + acronyme et version. + """ ) + tf[1] + html_sco_header.sco_footer() diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index f99bb8a1..b94a6017 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -29,7 +29,7 @@ (portage from DTML) """ import flask -from flask import url_for, render_template +from flask import flash, url_for, render_template from flask import g, request from flask_login import current_user @@ -39,6 +39,7 @@ from app.models import APO_CODE_STR_LEN from app.models import Formation, Matiere, Module, UniteEns from app.models import FormSemestre, ModuleImpl from app.models import ScolarNews +from app.models.but_refcomp import ApcAppCritique, ApcParcours import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -121,235 +122,13 @@ def module_create( Sinon, donne le choix de l'UE de rattachement et utilise la première matière de cette UE (si elle n'existe pas, la crée). """ - if matiere_id: - matiere = Matiere.query.get_or_404(matiere_id) - ue = matiere.ue - formation = ue.formation - else: - formation = Formation.query.get_or_404(formation_id) - parcours = formation.get_parcours() - is_apc = parcours.APC_SAE - ues = formation.ues.order_by( - UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme - ).all() - # cherche le numero adéquat (pour placer le module en fin de liste) - modules = formation.modules.all() - if modules: - default_num = max([m.numero or 0 for m in modules]) + 10 - else: - default_num = 10 - - if is_apc and module_type is not None: - object_name = scu.MODULE_TYPE_NAMES[module_type] - else: - object_name = "Module" - H = [ - html_sco_header.sco_header(page_title=f"Création {object_name}"), - ] - if not matiere_id: - H += [ - f"""

Création {object_name} dans la formation {formation.acronyme} -

- """ - ] - else: - H += [ - f"""

Création {object_name} dans la matière {matiere.titre}, - (UE {ue.acronyme}), semestre {ue.semestre_idx}

- """ - ] - - H += [ - render_template( - "scodoc/help/modules.html", - is_apc=is_apc, - semestre_id=semestre_id, - ) - ] - - descr = [ - ( - "code", - { - "size": 10, - "explanation": "code du module, ressource ou SAÉ. Exemple M1203, R2.01, ou SAÉ 3.4. Ce code doit être unique dans la formation.", - "allow_null": False, - "validator": lambda val, field, formation_id=formation_id: check_module_code_unicity( - val, field, formation_id - ), - }, - ), - ( - "titre", - { - "size": 30, - "explanation": "nom du module. Exemple: Introduction à la démarche ergonomique", - }, - ), - ( - "abbrev", - { - "size": 20, - "explanation": "nom abrégé (pour les bulletins). Exemple: Intro. à l'ergonomie", - }, - ), - ] - - if is_apc: - module_types = scu.ModuleType # tous les types - else: - # ne propose pas SAE et Ressources: - module_types = set(scu.ModuleType) - { - scu.ModuleType.RESSOURCE, - scu.ModuleType.SAE, - } - - descr += [ - ( - "module_type", - { - "input_type": "menu", - "title": "Type", - "explanation": "", - "labels": [x.name.capitalize() for x in module_types], - "allowed_values": [str(int(x)) for x in module_types], - }, - ), - ( - "heures_cours", - { - "title": "Heures de cours", - "size": 4, - "type": "float", - "explanation": "nombre d'heures de cours (optionnel)", - }, - ), - ( - "heures_td", - { - "title": "Heures de TD", - "size": 4, - "type": "float", - "explanation": "nombre d'heures de Travaux Dirigés (optionnel)", - }, - ), - ( - "heures_tp", - { - "title": "Heures de TP", - "size": 4, - "type": "float", - "explanation": "nombre d'heures de Travaux Pratiques (optionnel)", - }, - ), - ] - if is_apc: - descr += [ - ( - "sep_ue_coefs", - { - "input_type": "separator", - "title": """ -
(les coefficients vers les UE se fixent sur la page dédiée) -
""", - }, - ), - ] - else: - descr += [ - ( - "coefficient", - { - "size": 4, - "type": "float", - "explanation": "coefficient dans la formation (PPN)", - "allow_null": False, - }, - ), - ] - - if matiere_id: - descr += [ - ("ue_id", {"default": ue.id, "input_type": "hidden"}), - ("matiere_id", {"default": matiere_id, "input_type": "hidden"}), - ] - else: - # choix de l'UE de rattachement - descr += [ - ( - "ue_id", - { - "input_type": "menu", - "type": "int", - "title": "UE de rattachement", - "explanation": "utilisée notamment pour les malus", - "labels": [ - f"S{u.semestre_idx if u.semestre_idx is not None else '.'} / {u.acronyme} {u.titre}" - for u in ues - ], - "allowed_values": [u.id for u in ues], - }, - ), - ] - - descr += [ - # ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }), - ("formation_id", {"default": formation.id, "input_type": "hidden"}), - ( - "code_apogee", - { - "title": "Code Apogée", - "size": 25, - "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", - "validator": lambda val, _: len(val) < APO_CODE_STR_LEN, - }, - ), - ( - "numero", - { - "size": 2, - "explanation": "numéro (1,2,3,4...) pour ordre d'affichage", - "type": "int", - "default": default_num, - }, - ), - ] - args = scu.get_request_args() - tf = TrivialFormulator( - request.base_url, - args, - descr, - submitlabel="Créer ce module", + return module_edit( + create=True, + matiere_id=matiere_id, + module_type=module_type, + semestre_id=semestre_id, + formation_id=formation_id, ) - if tf[0] == 0: - return "\n".join(H) + tf[1] + html_sco_header.sco_footer() - else: - if not matiere_id: - # formulaire avec choix UE de rattachement - ue = UniteEns.query.get(tf[2]["ue_id"]) - if ue is None: - raise ValueError("UE invalide") - matiere = ue.matieres.first() - if matiere: - tf[2]["matiere_id"] = matiere.id - else: - matiere_id = sco_edit_matiere.do_matiere_create( - {"ue_id": ue.id, "titre": ue.titre, "numero": 1}, - ) - tf[2]["matiere_id"] = matiere_id - - tf[2]["semestre_id"] = ue.semestre_idx - - _ = do_module_create(tf[2]) - - return flask.redirect( - url_for( - "notes.ue_table", - scodoc_dept=g.scodoc_dept, - formation_id=formation.id, - semestre_idx=tf[2]["semestre_id"], - ) - ) def can_delete_module(module): @@ -359,7 +138,6 @@ def can_delete_module(module): def do_module_delete(oid): "delete module" - module = Module.query.get_or_404(oid) mod = module_list({"module_id": oid})[0] # sco7 if module_is_locked(module.id): @@ -379,9 +157,14 @@ def do_module_delete(oid): # S'il y a des moduleimpls, on ne peut pas detruire le module ! mods = sco_moduleimpl.moduleimpl_list(module_id=oid) if mods: - err_page = f"""

Destruction du module impossible car il est utilisé dans des semestres existants !

-

Il faut d'abord supprimer le semestre (ou en retirer ce module). Mais il est peut être préférable de - laisser ce programme intact et d'en créer une nouvelle version pour la modifier sans affecter les semestres déjà en place. + err_page = f""" +

Destruction du module impossible car il est utilisé dans des + semestres existants !

+

Il faut d'abord supprimer le semestre (ou en retirer + ce module). + Mais il est peut être préférable de laisser ce programme intact et + d'en créer une nouvelle version pour la modifier sans affecter + les semestres déjà en place.

reprendre @@ -465,37 +248,65 @@ def do_module_edit(vals: dict) -> None: def check_module_code_unicity(code, field, formation_id, module_id=None): "true si code module unique dans la formation" - Mods = module_list(args={"code": code, "formation_id": formation_id}) + modules = module_list(args={"code": code, "formation_id": formation_id}) if module_id: # edition: supprime le module en cours - Mods = [m for m in Mods if m["module_id"] != module_id] + modules = [m for m in modules if m["module_id"] != module_id] - return len(Mods) == 0 + return len(modules) == 0 -def module_edit(module_id=None): - """Edit a module""" - from app.scodoc import sco_formations +def module_edit( + module_id=None, + create=False, + matiere_id=None, + module_type=None, + semestre_id=None, + formation_id=None, +): + """Formulaire édition ou création module. + Si create, création nouveau module. + Si matiere_id est spécifié, le module sera créé dans cette matière (cas normal). + Sinon, donne le choix de l'UE de rattachement et utilise la première matière + de cette UE (si elle n'existe pas, la crée). + """ from app.scodoc import sco_tag_module - if not module_id: - raise ScoValueError("invalid module !") - modules = module_list(args={"module_id": module_id}) - if not modules: - raise ScoValueError("invalid module !") - module = modules[0] - a_module = models.Module.query.get(module_id) - unlocked = not module_is_locked(module_id) - formation_id = module["formation_id"] - formation = sco_formations.formation_list(args={"formation_id": formation_id})[0] - parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"]) + # --- Détermination de la formation + orig_semestre_idx = semestre_id + ue = None + if create: + if matiere_id: + matiere = Matiere.query.get_or_404(matiere_id) + ue = matiere.ue + formation = ue.formation + orig_semestre_idx = ue.semestre_idx if semestre_id is None else semestre_id + else: + formation = Formation.query.get_or_404(formation_id) + module = None + unlocked = True + else: + if not module_id: + raise ValueError("missing module_id !") + module = models.Module.query.get_or_404(module_id) + ue = module.ue + module_dict = module.to_dict() + formation = module.formation + unlocked = not module_is_locked(module_id) + + parcours = sco_codes_parcours.get_parcours_from_code(formation.type_parcours) is_apc = parcours.APC_SAE # BUT - in_use = len(a_module.modimpls.all()) > 0 # il y a des modimpls + if not create: + orig_semestre_idx = module.ue.semestre_idx if is_apc else module.semestre_id + if orig_semestre_idx is None: + orig_semestre_idx = 1 + # il y a-t-il des modimpls ? + in_use = (module is not None) and (len(module.modimpls.all()) > 0) matieres = Matiere.query.filter( - Matiere.ue_id == UniteEns.id, UniteEns.formation_id == formation_id + Matiere.ue_id == UniteEns.id, UniteEns.formation_id == formation.id ).order_by(UniteEns.semestre_idx, UniteEns.numero, Matiere.numero) if in_use: # restreint aux matières du même semestre - matieres = matieres.filter(UniteEns.semestre_idx == a_module.ue.semestre_idx) + matieres = matieres.filter(UniteEns.semestre_idx == module.ue.semestre_idx) if is_apc: # ne conserve que la 1ere matière de chaque UE, @@ -503,7 +314,8 @@ def module_edit(module_id=None): matieres = [ mat for mat in matieres - if a_module.matiere.id == mat.id or mat.id == mat.ue.matieres.first().id + if ((module is not None) and (module.matiere.id == mat.id)) + or (mat.id == mat.ue.matieres.first().id) ] mat_names = [ "S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres @@ -511,64 +323,131 @@ def module_edit(module_id=None): else: mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres] - ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres] - module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"]) + if module: # edition + ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres] + module_dict["ue_matiere_id"] = "%s!%s" % ( + module_dict["ue_id"], + module_dict["matiere_id"], + ) semestres_indices = list(range(1, parcours.NB_SEM + 1)) + # Toutes les UEs de la formation (tout parcours): + ues = formation.ues.order_by( + UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme + ).all() + # L'UE de rattachement par défaut: 1ere du semestre + ue_default = ( + formation.ues.filter_by(semestre_idx=orig_semestre_idx) + .order_by(UniteEns.numero, UniteEns.acronyme) + .first() + ) + + # --- Titre de la page + if create: + if is_apc and module_type is not None: + object_name = scu.MODULE_TYPE_NAMES[module_type] + else: + object_name = "Module" + page_title = f"Création {object_name}" + if matiere_id: + title = f"""Création {object_name} dans la matière + {matiere.titre}, + (UE {ue.acronyme}), semestre {ue.semestre_idx} + """ + else: + title = f"""Création {object_name} dans la formation + {formation.acronyme}""" + else: + page_title = f"Modification du module {module.code or module.titre or ''}" + title = f"""Modification du module {module.code or ''} {module.titre or ''} + (formation {formation.acronyme}, version {formation.version}) + """ H = [ html_sco_header.sco_header( - page_title=f"Modification du module {a_module.code or a_module.titre or ''}", + page_title=page_title, cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"], javascripts=[ "libjs/jQuery-tagEditor/jquery.tag-editor.min.js", "libjs/jQuery-tagEditor/jquery.caret.min.js", "js/module_tag_editor.js", + "js/module_edit.js", ], ), - f"""

Modification du module {a_module.code or ''} {a_module.titre or ''}""", - """ (formation %(acronyme)s, version %(version)s)

""" % formation, + f"""

{title}

""", render_template( "scodoc/help/modules.html", is_apc=is_apc, + semestre_id=semestre_id, formsemestres=FormSemestre.query.filter( ModuleImpl.formsemestre_id == FormSemestre.id, ModuleImpl.module_id == module_id, ) .order_by(FormSemestre.date_debut) - .all(), + .all() + if not create + else None, ), ] if not unlocked: H.append( - """
Formation verrouillée, seuls certains éléments peuvent être modifiés
""" + """
Formation verrouillée, seuls + certains éléments peuvent être modifiés
""" ) if is_apc: module_types = scu.ModuleType # tous les types else: # ne propose pas SAE et Ressources, sauf si déjà de ce type... - module_types = ( - set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE} - ) | { - scu.ModuleType(a_module.module_type) - if a_module.module_type - else scu.ModuleType.STANDARD + module_types = set(scu.ModuleType) - { + scu.ModuleType.RESSOURCE, + scu.ModuleType.SAE, } + if module: + module_types |= { + scu.ModuleType(module.module_type) + if module.module_type + else scu.ModuleType.STANDARD + } + # Numéro du module + # cherche le numero adéquat (pour placer le module en fin de liste) + if module: + default_num = module.numero + else: + modules = formation.modules.all() + if modules: + default_num = max([m.numero or 0 for m in modules]) + 10 + else: + default_num = 10 descr = [ ( "code", { "size": 10, - "explanation": "code du module (issu du programme, exemple M1203 ou R2.01. Doit être unique dans la formation)", + "explanation": """code du module (issu du programme, exemple M1203, + R2.01, ou SAÉ 3.4. Doit être unique dans la formation)""", "allow_null": False, - "validator": lambda val, field, formation_id=formation_id: check_module_code_unicity( - val, field, formation_id, module_id=module_id + "validator": lambda val, field, formation_id=formation.id: check_module_code_unicity( + val, field, formation_id, module_id=module.id if module else None ), }, ), - ("titre", {"size": 30, "explanation": "nom du module"}), - ("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}), + ( + "titre", + { + "size": 30, + "explanation": """nom du module. Exemple: + Introduction à la démarche ergonomique""", + }, + ), + ( + "abbrev", + { + "size": 20, + "explanation": """nom abrégé (pour bulletins). + Exemple: Intro. à l'ergonomie""", + }, + ), ( "module_type", { @@ -583,50 +462,64 @@ def module_edit(module_id=None): ( "heures_cours", { - "title": "Heures CM :", + "title": "Heures cours :", "size": 4, "type": "float", - "explanation": "nombre d'heures de cours", + "explanation": "nombre d'heures de cours (optionnel)", }, ), ( "heures_td", { - "title": "Heures TD :", + "title": "Heures de TD :", "size": 4, "type": "float", - "explanation": "nombre d'heures de Travaux Dirigés", + "explanation": "nombre d'heures de Travaux Dirigés (optionnel)", }, ), ( "heures_tp", { - "title": "Heures TP :", + "title": "Heures de TP :", "size": 4, "type": "float", - "explanation": "nombre d'heures de Travaux Pratiques", + "explanation": "nombre d'heures de Travaux Pratiques (optionnel)", }, ), ] if is_apc: - coefs_lst = a_module.ue_coefs_list() - if coefs_lst: - coefs_descr_txt = ", ".join( - [f"{ue.acronyme}: {c}" for (ue, c) in coefs_lst] - ) + if module: + coefs_lst = module.ue_coefs_list() + if coefs_lst: + coefs_descr_txt = ", ".join( + [f"{ue.acronyme}: {c}" for (ue, c) in coefs_lst] + ) + else: + coefs_descr_txt = """non définis""" + descr += [ + ( + "ue_coefs", + { + "readonly": True, + "title": "Coefficients vers les UE ", + "default": coefs_descr_txt, + "explanation": """
(passer par la page d'édition de la + formation pour modifier les coefficients)""", + }, + ) + ] else: - coefs_descr_txt = """non définis""" - descr += [ - ( - "ue_coefs", - { - "readonly": True, - "title": "Coefficients vers les UE ", - "default": coefs_descr_txt, - "explanation": "
(passer par la page d'édition de la formation pour modifier les coefficients)", - }, - ) - ] + descr += [ + ( + "sep_ue_coefs", + { + "input_type": "separator", + "title": """ +
(les coefficients vers les UE se fixent sur la page dédiée) +
""", + }, + ), + ] else: # Module classique avec coef scalaire: descr += [ ( @@ -641,30 +534,72 @@ def module_edit(module_id=None): ), ] descr += [ - ("formation_id", {"input_type": "hidden"}), - ("ue_id", {"input_type": "hidden"}), - ("module_id", {"input_type": "hidden"}), ( - "ue_matiere_id", + "formation_id", { - "input_type": "menu", - "title": "Rattachement :" if is_apc else "Matière :", - "explanation": ( - "UE de rattachement" - + ( - " module utilisé, ne peut pas être changé de semestre" - if in_use - else "" - ) - ) - if is_apc - else "un module appartient à une seule matière.", - "labels": mat_names, - "allowed_values": ue_mat_ids, - "enabled": unlocked, + "input_type": "hidden", + "default": formation.id, + }, + ), + ( + "semestre_id", + { + "input_type": "hidden", + "default": orig_semestre_idx, }, ), ] + if module: + descr += [ + ("ue_id", {"input_type": "hidden"}), + ("module_id", {"input_type": "hidden"}), + ( + "ue_matiere_id", + { + "input_type": "menu", + "title": "Rattachement :" if is_apc else "Matière :", + "explanation": ( + "UE de rattachement, utilisée notamment pour les malus" + + ( + " (module utilisé, ne peut pas être changé de semestre)" + if in_use + else "" + ) + ) + if is_apc + else "un module appartient à une seule matière.", + "labels": mat_names, + "allowed_values": ue_mat_ids, + "enabled": unlocked, + }, + ), + ] + else: # Création + if matiere_id: + descr += [ + ("ue_id", {"default": ue.id, "input_type": "hidden"}), + ("matiere_id", {"default": matiere_id, "input_type": "hidden"}), + ] + else: + # choix de l'UE de rattachement + descr += [ + ( + "ue_id", + { + "input_type": "menu", + "type": "int", + "title": "UE de rattachement", + "explanation": "utilisée notamment pour les malus", + "labels": [ + f"S{u.semestre_idx if u.semestre_idx is not None else '.'} / {u.acronyme} {u.titre}" + for u in ues + ], + "allowed_values": [u.id for u in ues], + "default": ue_default.id if ue_default is not None else "", + }, + ), + ] + if is_apc: # le semestre du module est toujours celui de son UE descr += [ @@ -685,8 +620,7 @@ def module_edit(module_id=None): "input_type": "menu", "type": "int", "title": parcours.SESSION_NAME.capitalize(), - "explanation": "%s de début du module dans la formation standard" - % parcours.SESSION_NAME, + "explanation": f"{parcours.SESSION_NAME} de début du module dans la formation standard", "labels": [str(x) for x in semestres_indices], "allowed_values": semestres_indices, "enabled": unlocked, @@ -699,7 +633,7 @@ def module_edit(module_id=None): { "title": "Code Apogée", "size": 25, - "explanation": """(optionnel) code élément pédagogique Apogée ou liste de codes ELP + "explanation": """(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules (ce code est propre à chaque établissement, se rapprocher du référent Apogée). """, @@ -710,26 +644,123 @@ def module_edit(module_id=None): "numero", { "size": 2, - "explanation": "numéro (1,2,3,4...) pour ordre d'affichage", + "explanation": "numéro (1, 2, 3, 4, ...) pour ordre d'affichage", "type": "int", + "default": default_num, }, ), ] + + if is_apc: + # Choix des parcours + ref_comp = formation.referentiel_competence + if ref_comp: + descr += [ + ( + "parcours", + { + "input_type": "checkbox", + "vertical": True, + "dom_id": "tf_module_parcours", + "labels": [parcour.libelle for parcour in ref_comp.parcours] + + ["Tous (tronc commun)"], + "allowed_values": [ + str(parcour.id) for parcour in ref_comp.parcours + ] + + ["-1"], + "explanation": """Parcours dans lesquels est utilisé ce module.
+ Attention: si le module ne doit pas avoir les mêmes coefficients suivant le parcours, + il faut en créer plusieurs versions, car dans ScoDoc chaque module a ses coefficients.""", + }, + ) + ] + if module: + module_dict["parcours"] = [ + str(parcour.id) for parcour in module.parcours + ] + module_dict["app_critiques"] = [ + str(app_crit.id) for app_crit in module.app_critiques + ] + # Choix des Apprentissages Critiques + if ue is not None: + annee = f"BUT{(orig_semestre_idx+1)//2}" + app_critiques = ApcAppCritique.app_critiques_ref_comp(ref_comp, annee) + descr += ( + [ + ( + "app_critiques", + { + "title": "Apprentissages Critiques", + "input_type": "checkbox", + "vertical": True, + "dom_id": "tf_module_app_critiques", + "labels": [ + f"{app_crit.code}  {app_crit.libelle}" + for app_crit in app_critiques + ], + "allowed_values": [ + str(app_crit.id) for app_crit in app_critiques + ], + "html_data": [], + "explanation": """Apprentissages Critiques liés à ce module. + (si vous changez le semestre, revenez ensuite sur cette page + pour associer les AC.) + """, + }, + ) + ] + if (ue.niveau_competence is not None) + else [ + ( + "app_critiques", + { + "input_type": "separator", + "title": f"""{scu.EMO_WARNING } + L'UE {ue.acronyme} {ue.titre} + n'est pas associée à un niveau de compétences + """, + }, + ) + ] + ) + else: + descr += [ + ( + "parcours", + { + "input_type": "separator", + "title": f"""{scu.EMO_WARNING } + Pas de parcours: + associer un référentiel de compétence + """, + }, + ) + ] # force module semestre_idx to its UE - if a_module.ue.semestre_idx: - module["semestre_id"] = a_module.ue.semestre_idx - # Filet de sécurité si jamais l'UE n'a pas non plus de semestre: - if not module["semestre_id"]: - module["semestre_id"] = 1 + if module: + if module.ue.semestre_idx is None: + # Filet de sécurité si jamais l'UE n'a pas non plus de semestre: + module_dict["semestre_id"] = 1 + else: + module_dict["semestre_id"] = module.ue.semestre_idx + tf = TrivialFormulator( request.base_url, scu.get_request_args(), descr, - html_foot_markup="""
""".format( - module_id, ",".join(sco_tag_module.module_tag_list(module_id)) - ), - initvalues=module, - submitlabel="Modifier ce module", + html_foot_markup=f"""
+ """ + if not create + else "", + initvalues=module_dict if module else {}, + submitlabel="Modifier ce module" if module else "Créer ce module", + cancelbutton="Annuler", ) # if tf[0] == 0: @@ -739,39 +770,77 @@ def module_edit(module_id=None): url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=formation_id, - semestre_idx=module["semestre_id"], + formation_id=formation.id, + semestre_idx=orig_semestre_idx, ) ) else: - # l'UE de rattachement peut changer - tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") - x, y = tf[2]["ue_matiere_id"].split("!") - tf[2]["ue_id"] = int(x) - tf[2]["matiere_id"] = int(y) - old_ue_id = a_module.ue.id - new_ue_id = tf[2]["ue_id"] - if (old_ue_id != new_ue_id) and in_use: - new_ue = UniteEns.query.get_or_404(new_ue_id) - if new_ue.semestre_idx != a_module.ue.semestre_idx: - # pas changer de semestre un module utilisé ! - raise ScoValueError( - "Module utilisé: il ne peut pas être changé de semestre !" - ) - # En APC, force le semestre égal à celui de l'UE - if is_apc: - selected_ue = UniteEns.query.get(tf[2]["ue_id"]) - if selected_ue is None: - raise ValueError("UE invalide") - tf[2]["semestre_id"] = selected_ue.semestre_idx - # Check unicité code module dans la formation - do_module_edit(tf[2]) + if create: + if not matiere_id: + # formulaire avec choix UE de rattachement + ue = UniteEns.query.get(tf[2]["ue_id"]) + if ue is None: + raise ValueError("UE invalide") + matiere = ue.matieres.first() + if matiere: + tf[2]["matiere_id"] = matiere.id + else: + matiere_id = sco_edit_matiere.do_matiere_create( + {"ue_id": ue.id, "titre": ue.titre, "numero": 1}, + ) + tf[2]["matiere_id"] = matiere_id + + tf[2]["semestre_id"] = ue.semestre_idx + module_id = do_module_create(tf[2]) + module = Module.query.get(module_id) + else: # EDITION MODULE + # l'UE de rattachement peut changer + tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") + x, y = tf[2]["ue_matiere_id"].split("!") + tf[2]["ue_id"] = int(x) + tf[2]["matiere_id"] = int(y) + old_ue_id = module.ue.id + new_ue_id = tf[2]["ue_id"] + if (old_ue_id != new_ue_id) and in_use: + new_ue = UniteEns.query.get_or_404(new_ue_id) + if new_ue.semestre_idx != module.ue.semestre_idx: + # pas changer de semestre un module utilisé ! + raise ScoValueError( + "Module utilisé: il ne peut pas être changé de semestre !" + ) + # En APC, force le semestre égal à celui de l'UE + if is_apc: + selected_ue = UniteEns.query.get(tf[2]["ue_id"]) + if selected_ue is None: + raise ValueError("UE invalide") + tf[2]["semestre_id"] = selected_ue.semestre_idx + # Check unicité code module dans la formation + # ??? TODO + # + do_module_edit(tf[2]) + # Modifie les parcours + if ("parcours" in tf[2]) and formation.referentiel_competence: + if "-1" in tf[2]["parcours"]: # "tous" + module.parcours = formation.referentiel_competence.parcours.all() + else: + module.parcours = [ + ApcParcours.query.get(int(parcour_id_str)) + for parcour_id_str in tf[2]["parcours"] + ] + # Modifie les AC + if "app_critiques" in tf[2]: + module.app_critiques = [ + ApcAppCritique.query.get(int(ac_id_str)) + for ac_id_str in tf[2]["app_critiques"] + ] + db.session.add(module) + db.session.commit() return flask.redirect( url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=formation_id, - semestre_idx=tf[2]["semestre_id"], + formation_id=formation.id, + semestre_idx=tf[2]["semestre_id"] if is_apc else 1, ) ) @@ -846,13 +915,23 @@ def module_count_moduleimpls(module_id): return len(mods) -def formation_add_malus_modules(formation_id, titre=None, redirect=True): +def formation_add_malus_modules( + formation_id: int, semestre_id: int = None, titre=None, redirect=True +): """Création d'un module de "malus" dans chaque UE d'une formation""" formation = Formation.query.get_or_404(formation_id) - for ue in formation.ues: - ue_add_malus_module(ue, titre=titre) + nb = 0 + ues = formation.ues + if semestre_id is not None: + ues = ues.filter_by(semestre_idx=semestre_id) + for ue in ues: + if ue.type == sco_codes_parcours.UE_STANDARD: + if ue_add_malus_module(ue, titre=titre) != None: + nb += 1 + + flash(f"Modules de malus ajoutés dans {nb} UEs du S{semestre_id}") formation.invalidate_cached_sems() @@ -871,9 +950,9 @@ def ue_add_malus_module(ue: UniteEns, titre=None, code=None) -> int: """ modules_malus = [m for m in ue.modules if m.module_type == scu.ModuleType.MALUS] if len(modules_malus) > 0: - return modules_malus[0].id # déjà existant + return None # déjà existant - titre = titre or "" + titre = titre or f"Malus {ue.acronyme}" code = code or f"MALUS{ue.numero}" # Tout module doit avoir un semestre_id (indice 1, 2, ...) @@ -885,7 +964,7 @@ def ue_add_malus_module(ue: UniteEns, titre=None, code=None) -> int: # c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement # le semestre ? ou affecter le malus au semestre 1 ??? raise ScoValueError( - "Impossible d'ajouter un malus s'il n'y a pas d'autres modules" + "Impossible d'ajouter un malus si l'UE n'a pas de numéro de semestre et ne comporte pas d'autres modules" ) else: semestre_id = ue.semestre_idx @@ -899,7 +978,7 @@ def ue_add_malus_module(ue: UniteEns, titre=None, code=None) -> int: matiere = matieres_malus[0] else: if ue.matieres.count() > 0: - numero = max([mat.numero for mat in ue.matieres]) + 10 + numero = max([(mat.numero or 0) for mat in ue.matieres]) + 10 else: numero = 0 matiere = Matiere(ue_id=ue.id, titre=titre_matiere_malus, numero=numero) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 33ffc69c..def072c3 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -35,6 +35,7 @@ from flask_login import current_user from app import db from app import log +from app.but import apc_edit_ue from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import Formation, UniteEns, ModuleImpl, Module from app.models import ScolarNews @@ -51,7 +52,6 @@ from app.scodoc.sco_exceptions import ( ) from app.scodoc import html_sco_header -from app.scodoc import sco_cache from app.scodoc import sco_codes_parcours from app.scodoc import sco_edit_apc from app.scodoc import sco_edit_matiere @@ -77,6 +77,7 @@ _ueEditor = ndb.EditableTable( "is_external", "code_apogee", "coefficient", + "coef_rcue", "color", ), sortkey="numero", @@ -121,12 +122,7 @@ def do_ue_create(args): # create ue_id = _ueEditor.create(cnx, args) - # Invalidate cache: vire les poids de toutes les évals de la formation - for modimpl in ModuleImpl.query.filter( - ModuleImpl.module_id == Module.id, Module.formation_id == args["formation_id"] - ): - modimpl.invalidate_evaluations_poids() - formation = Formation.query.get(args["formation_id"]) + formation: Formation = Formation.query.get(args["formation_id"]) formation.invalidate_module_coefs() # news ue = UniteEns.query.get(ue_id) @@ -144,11 +140,10 @@ def do_ue_create(args): def do_ue_delete(ue_id, delete_validations=False, force=False): "delete UE and attached matieres (but not modules)" - from app.scodoc import sco_formations from app.scodoc import sco_parcours_dut ue = UniteEns.query.get_or_404(ue_id) - formation_id = ue.formation_id + formation = ue.formation semestre_idx = ue.semestre_idx if not ue.can_be_deleted(): raise ScoNonEmptyFormationObject( @@ -157,7 +152,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): dest_url=url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=formation_id, + formation_id=formation.id, semestre_idx=semestre_idx, ), ) @@ -181,7 +176,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): cancel_url=url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=formation_id, + formation_id=formation.id, semestre_idx=semestre_idx, ), parameters={"ue_id": ue.id, "dialog_confirmed": 1}, @@ -192,7 +187,11 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): "DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s", {"ue_id": ue.id}, ) - + # delete old formulas + ndb.SimpleQuery( + "DELETE FROM notes_formsemestre_ue_computation_expr WHERE ue_id=%(ue_id)s", + {"ue_id": ue.id}, + ) # delete all matiere in this UE mats = sco_edit_matiere.matiere_list({"ue_id": ue.id}) for mat in mats: @@ -207,13 +206,13 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): _ueEditor.delete(cnx, ue.id) # > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement # utilisé: acceptable de tout invalider): - sco_cache.invalidate_formsemestre() + formation.invalidate_module_coefs() + # -> invalide aussi .invalidate_formsemestre() # news - F = sco_formations.formation_list(args={"formation_id": formation_id})[0] ScolarNews.add( typ=ScolarNews.NEWS_FORM, - obj=formation_id, - text=f"Modification de la formation {F['acronyme']}", + obj=formation.id, + text=f"Modification de la formation {formation.acronyme}", max_frequency=10 * 60, ) # @@ -222,7 +221,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=formation_id, + formation_id=formation.id, semestre_idx=semestre_idx, ) ) @@ -248,13 +247,16 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No title = f"Modification de l'UE {ue.acronyme} {ue.titre}" initvalues = ue_dict submitlabel = "Modifier les valeurs" - can_change_semestre_id = (ue.modules.count() == 0) or (ue.semestre_idx is None) + can_change_semestre_id = ( + (ue.modules.count() == 0) or (ue.semestre_idx is None) + ) and ue.niveau_competence is None else: ue = None title = "Création d'une UE" initvalues = { "semestre_idx": default_semestre_idx, "color": ue_guess_color_default(formation_id, default_semestre_idx), + "coef_rcue": 1.0, } submitlabel = "Créer cette UE" can_change_semestre_id = True @@ -277,6 +279,11 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No

Note: sauf exception, l'UE n'a pas de coefficient associé. Seuls les modules ont des coefficients.

""", + f""" +

UE du semestre S{ue.semestre_idx}

+ """ + if is_apc and ue + else "", ] ue_types = parcours.ALLOWED_UE_TYPES @@ -308,8 +315,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No "type": "int", "allow_null": False, "title": parcours.SESSION_NAME.capitalize(), - "explanation": "%s de l'UE dans la formation" - % parcours.SESSION_NAME, + "explanation": f"{parcours.SESSION_NAME} de l'UE dans la formation", "labels": ["non spécifié"] + [str(x) for x in semestres_indices], "allowed_values": [""] + semestres_indices, }, @@ -339,22 +345,43 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No "allow_null": not is_apc, # ects requis en APC }, ), - ( - "coefficient", - { - "size": 4, - "type": "float", - "title": "Coefficient", - "explanation": """les coefficients d'UE ne sont utilisés que + ] + if is_apc: # coef pour la moyenne RCUE + form_descr.append( + ( + "coef_rcue", + { + "size": 4, + "type": "float", + "title": "Coef. RCUE", + "explanation": """pondération utilisée pour le calcul de la moyenne du RCUE. Laisser à 1, sauf si votre établissement a explicitement décidé de pondérations. + """, + "defaut": 1.0, + "allow_null": False, + "enabled": is_apc, + }, + ) + ) + else: # non APC, coef d'UE + form_descr.append( + ( + "coefficient", + { + "size": 4, + "type": "float", + "title": "Coefficient", + "explanation": """les coefficients d'UE ne sont utilisés que lorsque l'option Utiliser les coefficients d'UE pour calculer la moyenne générale est activée. Par défaut, le coefficient d'une UE est simplement la somme des coefficients des modules dans lesquels l'étudiant a des notes. Jamais utilisé en BUT. """, - "enabled": not is_apc, - }, - ), + "enabled": not is_apc, + }, + ) + ) + form_descr += [ ( "ue_code", { @@ -410,8 +437,12 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No form_descr, initvalues=initvalues, submitlabel=submitlabel, + cancelbutton="Revenir à la formation", ) if tf[0] == 0: + niveau_competence_div = "" + if ue and is_apc: + niveau_competence_div = apc_edit_ue.form_ue_choix_niveau(formation, ue) if ue and ue.modules.count() and ue.semestre_idx is not None: modules_div = f"""
{ue.modules.count()} modules sont rattachés @@ -420,7 +451,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
    """ for m in ue.modules: modules_div += f"""
  • {m.code} {m.titre}
  • """ + "notes.module_edit",scodoc_dept=g.scodoc_dept, module_id=m.id)}">{m.code} {m.titre or "sans titre"}""" modules_div += """
""" else: modules_div = "" @@ -429,12 +460,13 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No return ( "\n".join(H) + tf[1] + + niveau_competence_div + modules_div + bonus_div + ue_div + html_sco_header.sco_footer() ) - else: + elif tf[2]: if create: if not tf[2]["ue_code"]: del tf[2]["ue_code"] @@ -467,14 +499,26 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No else: do_ue_edit(tf[2]) flash("UE modifiée") - return flask.redirect( - url_for( - "notes.ue_table", - scodoc_dept=g.scodoc_dept, - formation_id=formation_id, - semestre_idx=tf[2]["semestre_idx"], - ) + + if tf[2]: + dest_semestre_idx = tf[2]["semestre_idx"] + elif ue: + dest_semestre_idx = ue.semestre_idx + elif default_semestre_idx: + dest_semestre_idx = default_semestre_idx + elif "semestre_idx" in request.form: + dest_semestre_idx = request.form["semestre_idx"] + else: + dest_semestre_idx = 1 + + return flask.redirect( + url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=formation_id, + semestre_idx=dest_semestre_idx, ) + ) def _add_ue_semestre_id(ues: list[dict], is_apc): @@ -646,9 +690,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list ], page_title=f"Programme {formation.acronyme}", ), - f"""

Formation {formation.titre} ({formation.acronyme}) - [version {formation.version}] code {formation.formation_code} - {lockicon} + f"""

{formation.to_html()} {lockicon}

""", ] @@ -711,7 +753,8 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); else: descr_refcomp = f"""Référentiel de compétences: + scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}" + class="stdlink"> {formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}  """ msg_refcomp = "changer" @@ -727,7 +770,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); f"""
  • éditer les coefficients des ressources et SAÉs + }">Éditer les coefficients des ressources et SAÉs
  • """ @@ -816,6 +859,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); ) }">Créer une nouvelle version (non verrouillée) + """ ) H.append( @@ -914,7 +958,7 @@ def _html_select_semestre_idx(formation_id, semestre_ids, semestre_idx): def _ue_table_ues( parcours, - ues, + ues: list[dict], editable, tag_editable, has_perm_change, @@ -923,7 +967,7 @@ def _ue_table_ues( arrow_none, delete_icon, delete_disabled_icon, -): +) -> str: """Édition de programme: liste des UEs (avec leurs matières et modules). Pour les formations classiques (non APC/BUT) """ @@ -951,9 +995,9 @@ def _ue_table_ues( if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT: lab = "Pas d'indication de semestre:" else: - lab = "Semestre %s:" % ue["semestre_id"] + lab = f"""Semestre {ue["semestre_id"]}:""" H.append( - '
    %s
    ' % lab + f'
    {lab}
    ' ) H.append('
      ') H.append('
    • ') @@ -1304,8 +1348,9 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False): formation = Formation.query.get(ue["formation_id"]) if not dont_invalidate_cache: - # Invalide les semestres utilisant cette formation: - formation.invalidate_cached_sems() + # Invalide les semestres utilisant cette formation + # ainsi que les poids et coefs + formation.invalidate_module_coefs() # essai edition en ligne: diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 5d1b2d72..d6247207 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -59,7 +59,7 @@ class COLORS(Enum): LIGHT_YELLOW = "FFFFFF99" -# Un style est enregistré comme un dictionnaire qui précise la valeur d'un attributdans la liste suivante: +# Un style est enregistré comme un dictionnaire qui précise la valeur d'un attribut dans la liste suivante: # font, border, number_format, fill,... # (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles) @@ -288,7 +288,7 @@ class ScoExcelSheet: value -- contenu de la cellule (texte, numérique, booléen ou date) style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié """ - # adapatation des valeurs si nécessaire + # adaptation des valeurs si nécessaire if value is None: value = "" elif value is True: diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py index 11792057..32c2d9e3 100644 --- a/app/scodoc/sco_formsemestre.py +++ b/app/scodoc/sco_formsemestre.py @@ -141,11 +141,18 @@ def do_formsemestre_list(*a, **kw): def _formsemestre_enrich(sem): - """Ajoute champs souvent utiles: titre + annee et dateord (pour tris)""" + """Ajoute champs souvent utiles: titre + annee et dateord (pour tris). + XXX obsolete: préférer formsemestre.to_dict() ou, mieux, les méthodes de FormSemestre. + """ # imports ici pour eviter refs circulaires from app.scodoc import sco_formsemestre_edit - F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] + formations = sco_formations.formation_list( + args={"formation_id": sem["formation_id"]} + ) + if not formations: + raise ScoValueError("pas de formation pour ce semestre !") + F = formations[0] parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) # 'S1', 'S2', ... ou '' pour les monosemestres if sem["semestre_id"] != NO_SEMESTRE_ID: diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 3ccba772..c4e32946 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -39,23 +39,21 @@ from app.models import Module, ModuleImpl, Evaluation, EvaluationUEPoids, UniteE from app.models import ScolarNews from app.models.formations import Formation from app.models.formsemestre import FormSemestre +from app.models.but_refcomp import ApcParcours import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app.scodoc import sco_cache from app.scodoc import sco_groups from app import log -from app.scodoc.TrivialFormulator import TrivialFormulator, TF +from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc import html_sco_header from app.scodoc import sco_codes_parcours from app.scodoc import sco_compute_moy -from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_module -from app.scodoc import sco_edit_ue from app.scodoc import sco_etud -from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db from app.scodoc import sco_formations from app.scodoc import sco_formsemestre @@ -119,12 +117,12 @@ def formsemestre_editwithmodules(formsemestre_id): vals = scu.get_request_args() if not vals.get("tf_submitted", False): H.append( - """

      Seuls les modules cochés font partie de ce semestre. + """

      Seuls les modules cochés font partie de ce semestre. Pour les retirer, les décocher et appuyer sur le bouton "modifier".

      -

      Attention : s'il y a déjà des évaluations dans un module, +

      Attention : s'il y a déjà des évaluations dans un module, il ne peut pas être supprimé !

      -

      Les modules ont toujours un responsable. +

      Les modules ont toujours un responsable. Par défaut, c'est le directeur des études.

      Un semestre ne peut comporter qu'une seule UE "bonus sport/culture"

      @@ -153,7 +151,7 @@ def do_formsemestre_createwithmodules(edit=False): formsemestre = FormSemestre.query.get_or_404(formsemestre_id) if not current_user.has_permission(Permission.ScoImplement): if not edit: - # il faut ScoImplement pour creer un semestre + # il faut ScoImplement pour créer un semestre raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération") else: if not sem["resp_can_edit"] or current_user.id not in sem["responsables"]: @@ -175,6 +173,7 @@ def do_formsemestre_createwithmodules(edit=False): formation = Formation.query.get(formation_id) if formation is None: raise ScoValueError("Formation inexistante !") + is_apc = formation.is_apc() if not edit: initvalues = {"titre": _default_sem_title(formation)} semestre_id = int(vals["semestre_id"]) @@ -210,12 +209,12 @@ def do_formsemestre_createwithmodules(edit=False): if NB_SEM == 1: semestre_id_list = [-1] else: - if edit and formation.is_apc(): + if edit and is_apc: # en APC, ne permet pas de changer de semestre semestre_id_list = [formsemestre.semestre_id] else: semestre_id_list = list(range(1, NB_SEM + 1)) - if not formation.is_apc(): + if not is_apc: # propose "pas de semestre" seulement en classique semestre_id_list.insert(0, -1) @@ -226,7 +225,7 @@ def do_formsemestre_createwithmodules(edit=False): else: semestre_id_labels.append(f"S{sid}") # Liste des modules dans cette formation - if formation.is_apc(): + if is_apc: modules = formation.modules.order_by(Module.module_type, Module.numero) else: modules = ( @@ -318,10 +317,10 @@ def do_formsemestre_createwithmodules(edit=False): { "size": 40, "title": "Nom de ce semestre", - "explanation": """n'indiquez pas les dates, ni le semestre, ni la modalité dans + "explanation": f"""n'indiquez pas les dates, ni le semestre, ni la modalité dans le titre: ils seront automatiquement ajoutés """ - % _default_sem_title(formation), + value="remettre titre par défaut" onClick="document.tf.titre.value='{ + _default_sem_title(formation)}';"/>""", }, ), ( @@ -343,11 +342,9 @@ def do_formsemestre_createwithmodules(edit=False): "allowed_values": semestre_id_list, "labels": semestre_id_labels, "explanation": "en BUT, on ne peut pas modifier le semestre après création" - if formation.is_apc() - else "", - "attributes": ['onchange="change_semestre_id();"'] - if formation.is_apc() + if is_apc else "", + "attributes": ['onchange="change_semestre_id();"'] if is_apc else "", }, ), ) @@ -386,7 +383,7 @@ def do_formsemestre_createwithmodules(edit=False): mf = mf_manual for n in range(1, scu.EDIT_NB_ETAPES + 1): - mf["title"] = "Etape Apogée (%d)" % n + mf["title"] = f"Etape Apogée ({n})" modform.append(("etape_apo" + str(n), mf.copy())) modform.append( ( @@ -443,15 +440,19 @@ def do_formsemestre_createwithmodules(edit=False): ) ) if edit: - formtit = ( - """ -

      Modifier les coefficients des UE capitalisées

      -

      Sélectionner les modules, leurs responsables et les étudiants à inscrire:

      + formtit = f""" +

      Modifier les coefficients des UE capitalisées

      +

      Sélectionner les modules, leurs responsables et les étudiants + à inscrire:

      """ - % formsemestre_id - ) else: - formtit = """

      Sélectionner les modules et leurs responsables

      Si vous avez des parcours (options), ne sélectionnez que les modules du tronc commun.

      """ + formtit = """

      Sélectionner les modules et leurs responsables

      +

      Si vous avez des parcours (options), dans un premier + ne sélectionnez que les modules du tronc commun, puis après inscriptions, + revenez ajouter les modules de parcours en sélectionnant les groupes d'étudiants + à y inscrire. +

      """ modform += [ ( @@ -531,12 +532,53 @@ def do_formsemestre_createwithmodules(edit=False): "explanation": "empêcher le calcul des moyennes d'UE et générale.", }, ), + ] + # Choix des parcours + if is_apc: + ref_comp = formation.referentiel_competence + if ref_comp: + modform += [ + ( + "parcours", + { + "input_type": "checkbox", + "vertical": True, + "dom_id": "tf_module_parcours", + "labels": [parcour.libelle for parcour in ref_comp.parcours], + "allowed_values": [ + str(parcour.id) for parcour in ref_comp.parcours + ], + "explanation": """Parcours proposés dans ce semestre. + S'il s'agit d'un semestre de "tronc commun", ne pas indiquer de parcours.""", + }, + ) + ] + if edit: + sem["parcours"] = [str(parcour.id) for parcour in formsemestre.parcours] + else: + modform += [ + ( + "parcours", + { + "input_type": "separator", + "title": f"""{scu.EMO_WARNING } + Pas de parcours: + vérifier la formation + """, + }, + ) + ] + + # Choix des modules + modform += [ ( "sep", { "input_type": "separator", "title": "", - "template": "
    %s" % formtit, + "template": f"
    {formtit}", }, ), ] @@ -544,8 +586,8 @@ def do_formsemestre_createwithmodules(edit=False): nbmod = 0 for semestre_id in semestre_ids: - if formation.is_apc(): - # pour restreindre l'édition aux module du semestre sélectionné + if is_apc: + # pour restreindre l'édition aux modules du semestre sélectionné tr_class = f'class="sem{semestre_id}"' else: tr_class = "" @@ -560,7 +602,7 @@ def do_formsemestre_createwithmodules(edit=False): "sep", { "input_type": "separator", - "title": "Semestre %s" % semestre_id, + "title": f"Semestre {semestre_id}", "template": templ_sep, }, ) @@ -568,13 +610,13 @@ def do_formsemestre_createwithmodules(edit=False): for mod in mods: if mod["semestre_id"] == semestre_id and ( (not edit) # creation => tous modules - or (not formation.is_apc()) # pas BUT, on peut mixer les semestres + or (not is_apc) # pas BUT, on peut mixer les semestres or (semestre_id == formsemestre.semestre_id) # module du semestre or (mod["module_id"] in module_ids_set) # module déjà présent ): nbmod += 1 if edit: - select_name = "%s!group_id" % mod["module_id"] + select_name = f"{mod['module_id']}!group_id" def opt_selected(gid): if gid == vals.get(select_name): @@ -603,13 +645,16 @@ def do_formsemestre_createwithmodules(edit=False): group["group_name"], ) fcg += "" - itemtemplate = ( - f"""" - ) + itemtemplate = f""" + + + + """ else: - itemtemplate = f"""""" + itemtemplate = f""" + + + """ modform.append( ( "MI" + str(mod["module_id"]), @@ -742,7 +787,8 @@ def do_formsemestre_createwithmodules(edit=False): for module_id in tf[2]["tf-checked"]: mod_resp_id = User.get_user_id_from_nomplogin(tf[2][module_id]) if mod_resp_id is None: - # Si un module n'a pas de responsable (ou inconnu), l'affecte au 1er directeur des etudes: + # Si un module n'a pas de responsable (ou inconnu), + # l'affecte au 1er directeur des etudes: mod_resp_id = tf[2]["responsable_id"] tf[2][module_id] = mod_resp_id @@ -763,7 +809,7 @@ def do_formsemestre_createwithmodules(edit=False): module_ids_checked = [int(x[2:]) for x in tf[2]["tf-checked"]] _formsemestre_check_ue_bonus_unicity(module_ids_checked) if not edit: - if formation.is_apc(): + if is_apc: _formsemestre_check_module_list( module_ids_checked, tf[2]["semestre_id"] ) @@ -777,14 +823,6 @@ def do_formsemestre_createwithmodules(edit=False): "responsable_id": tf[2][f"MI{module_id}"], } _ = sco_moduleimpl.do_moduleimpl_create(modargs) - flash("Nouveau semestre créé") - return flask.redirect( - url_for( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - ) - ) else: # Modification du semestre: # on doit creer les modules nouvellement selectionnés @@ -794,7 +832,7 @@ def do_formsemestre_createwithmodules(edit=False): module_ids_tocreate = [ x for x in module_ids_checked if not x in module_ids_existing ] - if formation.is_apc(): + if is_apc: _formsemestre_check_module_list( module_ids_tocreate, tf[2]["semestre_id"] ) @@ -868,27 +906,48 @@ def do_formsemestre_createwithmodules(edit=False): modargs, formsemestre_id=formsemestre_id ) mod = sco_edit_module.module_list({"module_id": module_id})[0] - - if msg: - msg_html = ( - '
    Attention !
    • ' - + "
    • ".join(msg) - + "
    " - ) - if ok: - msg_html += "

    Modification effectuée

    " - else: - msg_html += "

    Modification effectuée (mais modules cités non supprimés)

    " - msg_html += ( - 'retour au tableau de bord' - % formsemestre_id - ) - return msg_html + # --- Association des parcours + formsemestre = FormSemestre.query.get(formsemestre_id) + if "parcours" in tf[2]: + formsemestre.parcours = [ + ApcParcours.query.get(int(parcour_id_str)) + for parcour_id_str in tf[2]["parcours"] + ] + db.session.add(formsemestre) + db.session.commit() + # --- Crée ou met à jour les groupes de parcours BUT + formsemestre.setup_parcours_groups() + # --- Fin + if edit: + if msg: + msg_html = ( + '
    Attention !
    • ' + + "
    • ".join(msg) + + "
    " + ) + if ok: + msg_html += "

    Modification effectuée

    " else: - return flask.redirect( - "formsemestre_status?formsemestre_id=%s&head_message=Semestre modifié" - % formsemestre_id - ) + msg_html += "

    Modification effectuée (mais modules cités non supprimés)

    " + msg_html += ( + 'retour au tableau de bord' + % formsemestre_id + ) + return msg_html + else: + return flask.redirect( + "formsemestre_status?formsemestre_id=%s&head_message=Semestre modifié" + % formsemestre_id + ) + else: + flash("Nouveau semestre créé") + return flask.redirect( + url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) def _formsemestre_check_module_list(module_ids, semestre_idx): diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py index 11563c0d..3f95add7 100644 --- a/app/scodoc/sco_formsemestre_inscriptions.py +++ b/app/scodoc/sco_formsemestre_inscriptions.py @@ -35,6 +35,7 @@ from flask import url_for, g, request from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre +from app.models.groups import GroupDescr, Partition import app.scodoc.sco_utils as scu from app import log from app.scodoc.scolog import logdb @@ -257,14 +258,14 @@ def do_formsemestre_inscription_with_modules( """Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS (donc sauf le sport) """ + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) # inscription au semestre args = {"formsemestre_id": formsemestre_id, "etudid": etudid} if etat is not None: args["etat"] = etat do_formsemestre_inscription_create(args, method=method) log( - "do_formsemestre_inscription_with_modules: etudid=%s formsemestre_id=%s" - % (etudid, formsemestre_id) + f"do_formsemestre_inscription_with_modules: etudid={etudid} formsemestre_id={formsemestre_id}" ) # inscriptions aux groupes # 1- inscrit au groupe 'tous' @@ -275,10 +276,16 @@ def do_formsemestre_inscription_with_modules( # 2- inscrit aux groupes for group_id in group_ids: if group_id and not group_id in gdone: - sco_groups.set_group(etudid, group_id) - gdone[group_id] = 1 + group = GroupDescr.query.get_or_404(group_id) + if group.partition.groups_editable: + sco_groups.set_group(etudid, group_id) + gdone[group_id] = 1 + else: + log( + f"do_formsemestre_inscription_with_modules: group {group:r} belongs to non editable partition" + ) - # inscription a tous les modules de ce semestre + # Inscription à tous les modules de ce semestre modimpls = sco_moduleimpl.moduleimpl_withmodule_list( formsemestre_id=formsemestre_id ) @@ -288,6 +295,8 @@ def do_formsemestre_inscription_with_modules( {"moduleimpl_id": mod["moduleimpl_id"], "etudid": etudid}, formsemestre_id=formsemestre_id, ) + # Mise à jour des inscriptions aux parcours: + formsemestre.update_inscriptions_parcours_from_groups() def formsemestre_inscription_with_modules_etud( diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 299318ed..ec4fb6da 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -40,6 +40,7 @@ from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import Module from app.models.formsemestre import FormSemestre +from app.models.moduleimpls import ModuleImpl import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType import app.scodoc.notesdb as ndb @@ -148,7 +149,10 @@ def formsemestre_status_menubar(sem): { "title": "Voir la formation %(acronyme)s (v%(version)s)" % F, "endpoint": "notes.ue_table", - "args": {"formation_id": sem["formation_id"]}, + "args": { + "formation_id": sem["formation_id"], + "semestre_idx": sem["semestre_id"], + }, "enabled": True, "helpmsg": "Tableau de bord du semestre", }, @@ -325,7 +329,7 @@ def formsemestre_status_menubar(sem): }, { "title": "Créer/modifier les partitions...", - "endpoint": "scolar.editPartitionForm", + "endpoint": "scolar.edit_partition_form", "args": {"formsemestre_id": formsemestre_id}, "enabled": sco_groups.sco_permissions_check.can_change_groups( formsemestre_id @@ -345,7 +349,7 @@ def formsemestre_status_menubar(sem): "title": "%s" % partition["partition_name"], "endpoint": "scolar.affect_groups", "args": {"partition_id": partition["partition_id"]}, - "enabled": enabled, + "enabled": enabled and partition["groups_editable"], } ) menuGroupes.append( @@ -406,10 +410,9 @@ def formsemestre_status_menubar(sem): }, { "title": "Saisie des décisions du jury", - "endpoint": "notes.formsemestre_recapcomplet", + "endpoint": "notes.formsemestre_saisie_jury", "args": { "formsemestre_id": formsemestre_id, - "modejury": 1, }, "enabled": sco_permissions_check.can_validate_sem(formsemestre_id), }, @@ -499,20 +502,24 @@ def retreive_formsemestre_from_request() -> int: # Element HTML decrivant un semestre (barre de menu et infos) -def formsemestre_page_title(): +def formsemestre_page_title(formsemestre_id=None): """Element HTML decrivant un semestre (barre de menu et infos) Cherche dans la requete si un semestre est défini (formsemestre_id ou moduleimpl ou evaluation ou group) """ - formsemestre_id = retreive_formsemestre_from_request() + formsemestre_id = ( + formsemestre_id + if formsemestre_id is not None + else retreive_formsemestre_from_request() + ) # if not formsemestre_id: return "" try: formsemestre_id = int(formsemestre_id) - formsemestre = FormSemestre.query.get(formsemestre_id) - except: - log("can't find formsemestre_id %s" % formsemestre_id) + except ValueError: + log(f"formsemestre_id: invalid type {formsemestre_id:r}") return "" + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) h = render_template( "formsemestre_page_title.html", @@ -578,7 +585,9 @@ def fill_formsemestre(sem): # Description du semestre sous forme de table exportable -def formsemestre_description_table(formsemestre_id, with_evals=False): +def formsemestre_description_table( + formsemestre_id, with_evals=False, with_parcours=False +): """Description du semestre sous forme de table exportable Liste des modules et de leurs coefficients """ @@ -618,7 +627,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False): ue_info["Coef._class"] = "ue_coef" R.append(ue_info) - ModInscrits = sco_moduleimpl.do_moduleimpl_inscription_list( + mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( moduleimpl_id=M["moduleimpl_id"] ) enseignants = ", ".join( @@ -629,7 +638,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False): "Code": M["module"]["code"] or "", "Module": M["module"]["abbrev"] or M["module"]["titre"], "_Module_class": "scotext", - "Inscrits": len(ModInscrits), + "Inscrits": len(mod_inscrits), "Responsable": sco_users.user_info(M["responsable_id"])["nomprenom"], "_Responsable_class": "scotext", "Enseignants": enseignants, @@ -648,10 +657,15 @@ def formsemestre_description_table(formsemestre_id, with_evals=False): moduleimpl_id=M["moduleimpl_id"], ), } - R.append(l) if M["module"]["coefficient"]: sum_coef += M["module"]["coefficient"] + if with_parcours: + module = Module.query.get(M["module_id"]) + l["parcours"] = ", ".join(sorted([pa.code for pa in module.parcours])) + + R.append(l) + if with_evals: # Ajoute lignes pour evaluations evals = nt.get_mod_evaluation_etat_list(M["moduleimpl_id"]) @@ -676,7 +690,10 @@ def formsemestre_description_table(formsemestre_id, with_evals=False): sums = {"_css_row_class": "moyenne sortbottom", "ects": sum_ects, "Coef.": sum_coef} R.append(sums) - columns_ids = ["UE", "Code", "Module", "Coef."] + columns_ids = ["UE", "Code", "Module"] + if with_parcours: + columns_ids += ["parcours"] + columns_ids += ["Coef."] if sco_preferences.get_preference("bul_show_ects", formsemestre_id): columns_ids += ["ects"] columns_ids += ["Inscrits", "Responsable", "Enseignants"] @@ -696,6 +713,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False): titles["description"] = "" titles["coefficient"] = "Coef. éval." titles["evalcomplete_str"] = "Complète" + titles["parcours"] = "Parcours" titles["publish_incomplete_str"] = "Toujours Utilisée" title = "%s %s" % (parcours.SESSION_NAME.capitalize(), formsemestre.titre_mois()) @@ -720,21 +738,26 @@ def formsemestre_description_table(formsemestre_id, with_evals=False): ) -def formsemestre_description(formsemestre_id, format="html", with_evals=False): +def formsemestre_description( + formsemestre_id, format="html", with_evals=False, with_parcours=False +): """Description du semestre sous forme de table exportable Liste des modules et de leurs coefficients """ with_evals = int(with_evals) - tab = formsemestre_description_table(formsemestre_id, with_evals=with_evals) - tab.html_before_table = """ - - + + indiquer les évaluations + indiquer les parcours BUT + """ return tab.make_page(format=format) @@ -854,7 +877,7 @@ def _make_listes_sem(sem, with_absences=True): H.append( f"""

    )""" ) H.append("") + if sem.parcours: + H.append( + f""" +

    + + + """ + ) evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id) H.append( - '' - % (modimpl["moduleimpl_id"], mod_descr, mod.abbrev or mod.titre) + % (modimpl["moduleimpl_id"], mod_descr, mod.abbrev or mod.titre or "") ) H.append('' % len(mod_inscrits)) H.append( diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index f4896a8d..2af209c2 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -111,7 +111,7 @@ def formsemestre_validation_etud_form( url_tableau = url_for( "notes.formsemestre_recapcomplet", scodoc_dept=g.scodoc_dept, - modejury=1, + mode_jury=1, formsemestre_id=formsemestre_id, selected_etudid=etudid, # va a la bonne ligne ) @@ -581,19 +581,27 @@ def formsemestre_recap_parcours_table( else: pm = plusminus % sem["formsemestre_id"] - H.append( - '' - % (bgcolor, num_sem, pm) + inscr = formsemestre.etuds_inscriptions.get(etudid) + parcours_name = ( + f' {inscr.parcour.code}' + if (inscr and inscr.parcour) + else "" ) - H.append('' % sem) H.append( - '' - % (a_url, sem["formsemestre_id"], etudid, sem["titreannee"]) + f""" + + + + """ ) - if decision_sem: + if nt.is_apc: + H.append('') + elif decision_sem: H.append('' % decision_sem["code"]) else: - H.append('') + H.append("") H.append('' % ass) # abs # acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé) ues = nt.get_ues_stat_dict(filter_sport=True) @@ -973,7 +981,7 @@ def do_formsemestre_validation_auto(formsemestre_id): H.append("") H.append( f"""continuer""" ) H.append(html_sco_header.sco_footer()) diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 79ed91ca..f17e017e 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -76,10 +76,12 @@ partitionEditor = ndb.EditableTable( "numero", "bul_show_rank", "show_in_lists", + "editable", ), input_formators={ "bul_show_rank": bool, "show_in_lists": bool, + "editable": bool, }, ) @@ -105,14 +107,19 @@ def get_group(group_id: int): return r[0] -def group_delete(group, force=False): +def group_delete(group_id: int): """Delete a group.""" # if not group['group_name'] and not force: # raise ValueError('cannot suppress this group') # remove memberships: - ndb.SimpleQuery("DELETE FROM group_membership WHERE group_id=%(group_id)s", group) + ndb.SimpleQuery( + "DELETE FROM group_membership WHERE group_id=%(group_id)s", + {"group_id": group_id}, + ) # delete group: - ndb.SimpleQuery("DELETE FROM group_descr WHERE id=%(group_id)s", group) + ndb.SimpleQuery( + "DELETE FROM group_descr WHERE id=%(group_id)s", {"group_id": group_id} + ) def get_partition(partition_id): @@ -264,6 +271,17 @@ def get_group_members(group_id, etat=None): return r +def check_group_name(group_name, partition, raiser=False): + """If groupe name exists in partition : if raiser -> Raise ScoValueError else-> return true""" + exists = group_name in [g["group_name"] for g in get_partition_groups(partition)] + if exists: + if raiser: + raise ScoValueError("Le nom de groupe existe déjà dans la partition") + else: + return True + return False + + # obsolete: sco_groups_view.DisplayedGroupsInfos # def get_groups_members(group_ids, etat=None): # """Liste les étudiants d'une liste de groupes @@ -621,10 +639,12 @@ def comp_origin(etud, cur_sem): return "" # parcours normal, ne le signale pas -def set_group(etudid, group_id): +def set_group(etudid: int, group_id: int) -> bool: """Inscrit l'étudiant au groupe. Return True if ok, False si deja inscrit. - Warning: don't check if group_id exists (the caller should check). + Warning: + - don't check if group_id exists (the caller should check). + - don't check if group's partition is editable """ cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) @@ -686,7 +706,12 @@ def change_etud_group_in_partition(etudid, group_id, partition=None): % (formsemestre_id, partition["partition_name"], group["group_name"]), ) cnx.commit() - # 4- invalidate cache + + # 5- Update parcours + formsemestre = FormSemestre.query.get(formsemestre_id) + formsemestre.update_inscriptions_parcours_from_groups() + + # 6- invalidate cache sco_cache.invalidate_formsemestre( formsemestre_id=formsemestre_id ) # > change etud group @@ -698,14 +723,28 @@ def setGroups( groupsToCreate="", # name and members of new groups groupsToDelete="", # groups to delete ): - """Affect groups (Ajax request) + """Affect groups (Ajax request): renvoie du XML groupsLists: lignes de la forme "group_id;etudid;...\n" groupsToCreate: lignes "group_name;etudid;...\n" groupsToDelete: group_id;group_id;... + + Ne peux pas modifier les groupes des partitions non éditables. """ from app.scodoc import sco_formsemestre + def xml_error(msg, code=404): + data = ( + f'Error: {msg}' + ) + response = make_response(data, code) + response.headers["Content-Type"] = scu.XML_MIMETYPE + return response + partition = get_partition(partition_id) + if not partition["groups_editable"]: + msg = "setGroups: partition non editable" + log(msg) + return xml_error(msg, code=403) formsemestre_id = partition["formsemestre_id"] if not sco_permissions_check.can_change_groups(formsemestre_id): raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") @@ -727,8 +766,8 @@ def setGroups( continue try: group_id = int(group_id) - except ValueError as exc: - log("setGroups: ignoring invalid group_id={group_id}") + except ValueError: + log(f"setGroups: ignoring invalid group_id={group_id}") continue group = get_group(group_id) # Anciens membres du groupe: @@ -778,6 +817,10 @@ def setGroups( for etudid in fs[1:-1]: change_etud_group_in_partition(etudid, group_id, partition) + # Update parcours + formsemestre = FormSemestre.query.get(formsemestre_id) + formsemestre.update_inscriptions_parcours_from_groups() + data = ( 'Groupes enregistrés' ) @@ -798,15 +841,15 @@ def create_group(partition_id, group_name="", default=False) -> int: if not group_name and not default: raise ValueError("invalid group name: ()") # checkGroupName(group_name) - if group_name in [g["group_name"] for g in get_partition_groups(partition)]: - raise ValueError( - "group_name %s already exists in partition" % group_name + if check_group_name(group_name, partition): + raise ScoValueError( + f"group_name {group_name} already exists in partition" ) # XXX FIX: incorrect error handling (in AJAX) cnx = ndb.GetDBConnexion() group_id = groupEditor.create( cnx, {"partition_id": partition_id, "group_name": group_name} ) - log("create_group: created group_id=%s" % group_id) + log("create_group: created group_id={group_id}") # return group_id @@ -817,21 +860,18 @@ def delete_group(group_id, partition_id=None): affectation aux groupes) partition_id est optionnel et ne sert que pour verifier que le groupe est bien dans cette partition. + S'il s'agit d'un groupe de parcours, affecte l'inscription des étudiants aux parcours. """ - group = get_group(group_id) + group = GroupDescr.query.get_or_404(group_id) if partition_id: - if partition_id != group["partition_id"]: + if partition_id != group.partition_id: raise ValueError("inconsistent partition/group") - else: - partition_id = group["partition_id"] - partition = get_partition(partition_id) - if not sco_permissions_check.can_change_groups(partition["formsemestre_id"]): + if not sco_permissions_check.can_change_groups(group.partition.formsemestre_id): raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") - log( - "delete_group: group_id=%s group_name=%s partition_name=%s" - % (group_id, group["group_name"], partition["partition_name"]) - ) - group_delete(group) + log(f"delete_group: group={group} partition={group.partition}") + formsemestre = group.partition.formsemestre + group_delete(group.id) + formsemestre.update_inscriptions_parcours_from_groups() def partition_create( @@ -881,7 +921,7 @@ def partition_create( if redirect: return flask.redirect( url_for( - "scolar.editPartitionForm", + "scolar.edit_partition_form", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) @@ -900,11 +940,12 @@ def get_arrow_icons_tags(): return arrow_up, arrow_down, arrow_none -def editPartitionForm(formsemestre_id=None): +def edit_partition_form(formsemestre_id=None): """Form to create/suppress partitions""" # ad-hoc form if not sco_permissions_check.can_change_groups(formsemestre_id): raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) partitions = get_partitions_list(formsemestre_id) arrow_up, arrow_down, arrow_none = get_arrow_icons_tags() suppricon = scu.icontag( @@ -914,7 +955,7 @@ def editPartitionForm(formsemestre_id=None): H = [ html_sco_header.sco_header( page_title="Partitions...", - javascripts=["js/editPartitionForm.js"], + javascripts=["js/edit_partition_form.js"], ), # limite à SHORT_STR_LEN r""" + {% include 'bul_foot.html' %} diff --git a/app/templates/but/documentation_codes_jury.html b/app/templates/but/documentation_codes_jury.html new file mode 100644 index 00000000..dbaf51ca --- /dev/null +++ b/app/templates/but/documentation_codes_jury.html @@ -0,0 +1,266 @@ +
    +

    Ci-dessous la signification de chaque code est expliquée, + ainsi que la correspondance avec certains codes préconisés par + l'AMUE et l'ADIUT pour Apogée. + + On distingue les codes ScoDoc (utilisés ci-dessus et dans les différentes + tables générées par ScoDoc) et leur transcription vers Apogée lors des exports + (transcription paramétrable par votre administrateur ScoDoc). +

    +
    Codes d'année
    +
    +
    %(label)s%(elem)s""" - + fcg - + "
    %(label)s%(elem)s{fcg}
    %(label)s%(elem)s
    %(label)s%(elem)s
    Parcours: {', '.join(parcours.code for parcours in sem.parcours)}
    Evaluations: %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides' + '
    Évaluations: %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides' % evals ) if evals["last_modif"]: @@ -1175,7 +1206,7 @@ def formsemestre_tableau_modules( ) H.append( '%s%s%s%s%(mois_debut)s%s{num_sem}{pm}{sem['mois_debut']}{formsemestre.titre_annee()}{parcours_name}BUT%sen coursen cours%s
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ScoDoc{{nom_univ}}AMUESignification
    ADM{{codes["ADM"]}}Admis
    ADJ{{codes["ADJ"]}}Admis par décision jury
    PASD{{codes["PASD"]}}PASDNon admis, mais passage de droit
    PAS1NCI{{codes["PAS1NCI"]}}PAS1NCINon admis, mais passage par décision de jury (Passage en Année + Supérieure avec au moins 1 Niveau de Compétence Insuffisant (RCUE<8)) +
    RED{{codes["RED"]}}REDAjourné, mais autorisé à redoubler
    NAR{{codes["NAR"]}}REONon admis, réorientation
    DEM{{codes["DEM"]}}Démission
    ABAN{{codes["ABAN"]}}ABANABANdon constaté (sans lettre de démission)
    ATJ{{codes["ATJ"]}}ndNon validé pour une autre raison, voir règlement local
    RAT{{codes["RAT"]}}En attente d’un rattrapage
    EXCLU{{codes["EXCLU"]}}EXCEXClusion, décision réservée à des décisions disciplinaires
    DEF{{codes["DEF"]}}(défaillance) Non évalué par manque assiduité
    ABL{{codes["ABL"]}}ABLAnnée Blanche
    + + +
    Codes RCUE (niveaux de compétences annuels)
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ScoDoc{{nom_univ}}AMUESignification
    ADM + {{codes["ADM"]}}VAL + Acquis +
    CMP{{codes["CMP"]}}Acquis par compensation annuelle
    ADJ{{codes["ADJ"]}}CODJAcquis par décision du jury
    AJ{{codes["AJ"]}}AJAttente pour problème de moyenne
    ATJ{{codes["ATJ"]}}ndNon validé pour une autre raison, voir règlement local
    RAT{{codes["RAT"]}}En attente d’un rattrapage
    DEF{{codes["DEF"]}}Défaillant
    ABAN{{codes["ABAN"]}}Non évalué pour manque assiduité
    +
    + +
    Codes des Unités d'Enseignement (UE)
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ScoDoc{{nom_univ}}AMUESignification
    ADM{{codes["ADM"]}}VALAcquis (ECTS acquis)
    CMP{{codes["CMP"]}}COMPAcquis par compensation UE compensée avec l’UE de même compétence et de même année (ECTS acquis) +
    ADJ{{codes["ADJ"]}}Acquis par décision de jury (ECTS acquis)
    AJ{{codes["AJ"]}}AJAttente pour problème de moyenne
    ATJ{{codes["ATJ"]}}ndNon validé pour une autre raison, voir règlement local
    RAT{{codes["RAT"]}}En attente d’un rattrapage
    DEF{{codes["DEF"]}}ABANDéfaillant Pas ou peu de notes par arrêt de la formation
    ABAN{{codes["ABAN"]}}ABANNon évalué pour manque d’assiduité Non présentation des notes de l’étudiant au jury
    DEM{{codes["DEM"]}}Démission
    UEBSL{{codes["UEBSL"]}}UEBSLUE blanchie
    +
    + +
    Rappels de l'arrêté BUT (extraits)
    +
    +
      +
    • Au sein de chaque regroupement cohérent d’UE, la compensation est intégrale. + Si une UE n’a pas été acquise en raison d’une moyenne inférieure à 10, + cette UE sera acquise par compensation si et seulement si l’étudiant + a obtenu la moyenne au regroupement cohérent auquel l’UE appartient.
    • +
    • La poursuite d'études dans un semestre pair d’une même année est de droit + pour tout étudiant. + La poursuite d’études dans un semestre impair est possible + si et seulement si l’étudiant a obtenu : +
        +
      • la moyenne à plus de la moitié des regroupements cohérents d’UE
      • +
      • et une moyenne égale ou supérieure à 8 sur 20 à chaque regroupement cohérent d’UE.
      • +
      +
    • +
    • La poursuite d'études dans le semestre 5 nécessite de plus la validation de toutes les UE des + semestres 1 et 2 dans les conditions de validation des points 4.3 et 4.4, ou par décision de jury.
    • +
    + Textes de référence: + + +
    + \ No newline at end of file diff --git a/app/templates/but/formsemestre_validation_auto_but.html b/app/templates/but/formsemestre_validation_auto_but.html new file mode 100644 index 00000000..c9397fc0 --- /dev/null +++ b/app/templates/but/formsemestre_validation_auto_but.html @@ -0,0 +1,30 @@ +{# -*- mode: jinja-html -*- #} +{% extends "sco_page.html" %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block styles %} +{{super()}} +{% endblock %} + +{% block app_content %} + +

    Calcul automatique des décisions de jury annuelle BUT

    +
      +
    • Seuls les étudiants qui valident l'année seront affectés: + tous les niveaux de compétences (RCUE) validables + (moyenne annuelle au dessus de 10); +
    • +
    • l'assiduité n'est pas prise en compte;
    • +
    +

    + Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure ! +

    + + +
    +
    + {{ wtf.quick_form(form) }} +
    +
    + +{% endblock %} \ No newline at end of file diff --git a/app/templates/but/refcomp_assoc.html b/app/templates/but/refcomp_assoc.html index 9f8335be..9ba07dee 100644 --- a/app/templates/but/refcomp_assoc.html +++ b/app/templates/but/refcomp_assoc.html @@ -6,11 +6,25 @@

    Associer un référentiel de compétences

    Association d'un référentiel de compétence à la formation - {{formation.titre}} ({{formation.acronyme}}) + {{formation.titre}} ({{formation.acronyme}})
    -
    -
    - {{ wtf.quick_form(form) }} +
    + + Référentiel actuellement associé: + {% if formation.referentiel_competence is not none %} + {{ formation.referentiel_competence.specialite_long }} + supprimer + {% else %} + aucun + {% endif %} +
    +
    + {{ wtf.quick_form(form) }} +
    diff --git a/app/templates/but/refcomp_show.html b/app/templates/but/refcomp_show.html index 0e4ffef6..62b3a015 100644 --- a/app/templates/but/refcomp_show.html +++ b/app/templates/but/refcomp_show.html @@ -10,7 +10,7 @@ - +
    Référentiel chargé le {{ref.scodoc_date_loaded.strftime("%d/%m/%Y à %H:%M") if ref.scodoc_date_loaded else ""}} à diff --git a/app/templates/config_codes_decisions.html b/app/templates/config_codes_decisions.html index 0c2f32b2..5f92aa8d 100644 --- a/app/templates/config_codes_decisions.html +++ b/app/templates/config_codes_decisions.html @@ -6,12 +6,12 @@
    -

    Ces codes (ADM, AJ, ...) sont utilisés pour représenter les décisions de jury -et les validations de semestres ou d'UE. les valeurs indiquées ici sont utilisées -dans les exports Apogée. -

    -

    Ne les modifier que si vous savez ce que vous faites ! -

    +

    Ces codes (ADM, AJ, ...) sont utilisés pour représenter les décisions de jury + et les validations de semestres ou d'UE. + Les valeurs indiquées ici sont utilisées dans les exports Apogée. +

    +

    Ne les modifier que si vous savez ce que vous faites ! +

    diff --git a/app/templates/confirm_dialog.html b/app/templates/confirm_dialog.html new file mode 100644 index 00000000..8067f6c7 --- /dev/null +++ b/app/templates/confirm_dialog.html @@ -0,0 +1,22 @@ +{# -*- mode: jinja-html -*- #} +{% extends 'base.html' %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block app_content %} + +

    {{ title }}

    + +
    + {{ explanation }} +
    +
    + + + {% if cancel_url %} + + {% endif %} + +
    + +{% endblock %} \ No newline at end of file diff --git a/app/templates/pn/form_mods.html b/app/templates/pn/form_mods.html index 10927bc8..480cf267 100644 --- a/app/templates/pn/form_mods.html +++ b/app/templates/pn/form_mods.html @@ -84,17 +84,29 @@ url_for("notes.module_create", scodoc_dept=g.scodoc_dept, module_type=module_type|int, - matiere_id=matiere_parent.id + matiere_id=matiere_parent.id, + semestre_id=semestre_id, )}}" {% else %}"{{ url_for("notes.module_create", scodoc_dept=g.scodoc_dept, module_type=module_type|int, - formation_id=formation.id + formation_id=formation.id, + semestre_id=semestre_id, )}}" - {% endif %} + {% endif %} >{{create_element_msg}} + + {% if module_type==scu.ModuleType.STANDARD %} +
  • ajouter un module de malus dans chaque UE du S{{semestre_id}} +
  • + {% endif %} {% endif %} {% endif %} diff --git a/app/templates/pn/form_modules_ue_coefs.html b/app/templates/pn/form_modules_ue_coefs.html index f022eccd..75ed3de6 100644 --- a/app/templates/pn/form_modules_ue_coefs.html +++ b/app/templates/pn/form_modules_ue_coefs.html @@ -2,10 +2,12 @@

    {% if not read_only %}Édition des c{% else %}C{%endif%}oefficients des modules vers les UEs

    {% if not read_only %} - Double-cliquer pour changer une valeur. +

    Double-cliquer pour changer une valeur. Les valeurs sont automatiquement enregistrées au fur et à mesure. +

    {% endif %} - +

    Chaque ligne représente une ressource ou SAÉ, et chaque colonne une Unité d'Enseignement (UE). +

    Semestre: