diff --git a/app/api/etudiants.py b/app/api/etudiants.py index afeb8aba..a41634f9 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -55,8 +55,8 @@ def etudiants_courant(long=False): @bp.route("/etudiant/etudid/", methods=["GET"]) -@bp.route("/etudiant/nip/", methods=["GET"]) -@bp.route("/etudiant/ine/", methods=["GET"]) +@bp.route("/etudiant/nip/", methods=["GET"]) +@bp.route("/etudiant/ine/", methods=["GET"]) @token_auth.login_required @token_permission_required(Permission.APIView) def etudiant(etudid: int = None, nip: int = None, ine: int = None): @@ -109,8 +109,8 @@ def etudiant(etudid: int = None, nip: int = None, ine: int = None): @bp.route("/etudiant/etudid//formsemestres") -@bp.route("/etudiant/nip//formsemestres") -@bp.route("/etudiant/ine//formsemestres") +@bp.route("/etudiant/nip//formsemestres") +@bp.route("/etudiant/ine//formsemestres") @token_auth.login_required @token_permission_required(Permission.APIView) def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None): @@ -175,12 +175,12 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) defaults={"version": "long"}, ) @bp.route( - "/etudiant/nip//formsemestre//bulletin", + "/etudiant/nip//formsemestre//bulletin", methods=["GET"], defaults={"version": "long"}, ) @bp.route( - "/etudiant/ine//formsemestre//bulletin", + "/etudiant/ine//formsemestre//bulletin", methods=["GET"], defaults={"version": "long"}, ) @@ -190,12 +190,12 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) defaults={"version": "short"}, ) @bp.route( - "/etudiant/nip//formsemestre//bulletin/short", + "/etudiant/nip//formsemestre//bulletin/short", methods=["GET"], defaults={"version": "short"}, ) @bp.route( - "/etudiant/ine//formsemestre//bulletin/short", + "/etudiant/ine//formsemestre//bulletin/short", methods=["GET"], defaults={"version": "short"}, ) @@ -408,10 +408,12 @@ def etudiant_bulletin_semestre( methods=["GET"], ) @bp.route( - "/etudiant/nip//formsemestre//groups", methods=["GET"] + "/etudiant/nip//formsemestre//groups", + methods=["GET"], ) @bp.route( - "/etudiant/ine//formsemestre//groups", methods=["GET"] + "/etudiant/ine//formsemestre//groups", + methods=["GET"], ) @token_auth.login_required @token_permission_required(Permission.APIView) diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 8e834b23..80d8e02c 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -413,8 +413,15 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"): return R -def etud_add_group_infos(etud, formsemestre_id, sep=" "): - """Add informations on partitions and group memberships to etud (a dict with an etudid)""" +def etud_add_group_infos(etud, formsemestre_id, sep=" ", only_to_show=False): + """Add informations on partitions and group memberships to etud + (a dict with an etudid) + If only_to_show, restrict to partions such that show_in_lists is True. + + etud['partitions'] = { partition_id : group + partition_name } + etud['groupes'] = "TDB, Gr2, TPB1" + etud['partitionsgroupes'] = "Groupes TD:TDB, Groupes TP:Gr2 (...)" + """ etud[ "partitions" ] = collections.OrderedDict() # partition_id : group + partition_name @@ -423,11 +430,14 @@ def etud_add_group_infos(etud, formsemestre_id, sep=" "): return etud infos = ndb.SimpleDictFetch( - """SELECT p.partition_name, g.*, g.id AS group_id + """SELECT p.partition_name, p.show_in_lists, g.*, g.id AS group_id FROM group_descr g, partition p, group_membership gm WHERE gm.etudid=%(etudid)s and gm.group_id = g.id and g.partition_id = p.id and p.formsemestre_id = %(formsemestre_id)s + """ + + (" and (p.show_in_lists is True) " if only_to_show else "") + + """ ORDER BY p.numero """, {"etudid": etud["etudid"], "formsemestre_id": formsemestre_id}, diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index a030f514..2631ef45 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -153,14 +153,14 @@ def ficheEtud(etudid=None): try: # pour les bookmarks avec d'anciens ids... etudid = int(etudid) except ValueError: - raise ScoValueError("id invalide !") + raise ScoValueError("id invalide !") from ValueError # la sidebar est differente s'il y a ou pas un etudid # voir html_sidebar.sidebar() g.etudid = etudid args = make_etud_args(etudid=etudid) etuds = sco_etud.etudident_list(cnx, args) if not etuds: - log("ficheEtud: etudid=%s request.args=%s" % (etudid, request.args)) + log(f"ficheEtud: etudid={etudid!r} request.args={request.args!r}") raise ScoValueError("Etudiant inexistant !") etud = etuds[0] etudid = etud["etudid"] @@ -173,7 +173,7 @@ def ficheEtud(etudid=None): if info["lieu_naissance"]: info["info_naissance"] += " à " + info["lieu_naissance"] if info["dept_naissance"]: - info["info_naissance"] += " (%s)" % info["dept_naissance"] + info["info_naissance"] += f" ({info['dept_naissance']})" info["etudfoto"] = sco_photos.etud_photo_html(etud) if ( (not info["domicile"]) @@ -205,7 +205,7 @@ def ficheEtud(etudid=None): ) else: info["emaillink"] = "(pas d'adresse e-mail)" - # champs dependant des permissions + # Champ dépendant des permissions: if authuser.has_permission(Permission.ScoEtudChangeAdr): info["modifadresse"] = ( 'modifier adresse' @@ -216,9 +216,10 @@ def ficheEtud(etudid=None): # Groupes: sco_groups.etud_add_group_infos( - info, info["cursem"]["formsemestre_id"] if info["cursem"] else None + info, + info["cursem"]["formsemestre_id"] if info["cursem"] else None, + only_to_show=True, ) - # Parcours de l'étudiant if info["sems"]: info["last_formsemestre_id"] = info["sems"][0]["formsemestre_id"] @@ -235,15 +236,28 @@ def ficheEtud(etudid=None): ) grlink = '%s' % descr["situation"] else: - group = sco_groups.get_etud_main_group(etudid, sem["formsemestre_id"]) - if group["partition_name"]: - gr_name = group["group_name"] - else: - gr_name = "tous" - grlink = ( - 'groupe %s' - % (group["group_id"], gr_name) + e = {"etudid": etudid} + sco_groups.etud_add_group_infos( + e, + sem["formsemestre_id"], + only_to_show=True, ) + + grlinks = [] + for partition in e["partitions"].values(): + if partition["partition_name"]: + gr_name = partition["group_name"] + else: + gr_name = "tous" + + grlinks.append( + f"""{gr_name} + """ + ) + grlink = ", ".join(grlinks) # infos ajoutées au semestre dans le parcours (groupe, menu) menu = _menuScolarite(authuser, sem, etudid) if menu: @@ -423,9 +437,11 @@ def ficheEtud(etudid=None): # if info["groupes"].strip(): - info["groupes_row"] = ( - 'Groupe :%(groupes)s' % info - ) + info[ + "groupes_row" + ] = f""" + Groupes :{info['groupes']} + """ else: info["groupes_row"] = "" info["menus_etud"] = menus_etud(etudid) diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 179014cf..6a22c195 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -84,12 +84,15 @@ def formsemestre_recapcomplet( selected_etudid: etudid sélectionné (pour scroller au bon endroit) """ formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - + file_formats = {"csv", "json", "xls", "xlsx", "xlsall", "xml"} + supported_formats = file_formats | {"html"} + if tabformat not in supported_formats: + raise ScoValueError(f"Format non supporté: {tabformat}") + is_file = tabformat in file_formats modejury = int(modejury) - xml_with_decisions = int(xml_with_decisions) force_publishing = int(force_publishing) - is_file = tabformat in {"csv", "json", "xls", "xlsx", "xlsall", "xml"} + data = _do_formsemestre_recapcomplet( formsemestre_id, format=tabformat, @@ -128,6 +131,7 @@ def formsemestre_recapcomplet( for (format, label) in ( ("html", "Tableau"), ("evals", "Avec toutes les évaluations"), + ("xlsx", "Excel non formatté"), ("xml", "Bulletins XML (obsolète)"), ("json", "Bulletins JSON"), ): diff --git a/sco_version.py b/sco_version.py index fe5771ed..8c3e5f8b 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.2.17" +SCOVERSION = "9.2.18" SCONAME = "ScoDoc" diff --git a/tests/api/test_api_departements.py b/tests/api/test_api_departements.py index bc0b5c4a..d0337530 100644 --- a/tests/api/test_api_departements.py +++ b/tests/api/test_api_departements.py @@ -20,15 +20,18 @@ 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, DEPARTEMENT_FIELDS +from tests.api.tools_test_api import ( + verify_fields, + DEPARTEMENT_FIELDS, + verify_occurences_ids_etus, +) def test_departements(api_headers): - """ " - Routes: /departements_ids, /departement, /departement//formsemestres_ids - """ - # --- Liste des ids + Routes: /departements_ids, /departement, /departement//formsemestres_ids + """ + # --- departement_ids : liste des ids r = requests.get( API_URL + "/departements_ids", headers=api_headers, @@ -40,8 +43,17 @@ def test_departements(api_headers): assert len(departements_ids) > 0 assert all(isinstance(x, int) for x in departements_ids) + all_unique = True + for id in departements_ids: + if departements_ids.count(id) > 1: + all_unique = False + + assert all_unique is True + dept_id = departements_ids[0] - # --- Infos sur un département, accès par id + + # --- departement + # Infos sur un département, accès par id r = requests.get( f"{API_URL}/departement/{dept_id}", headers=api_headers, @@ -49,8 +61,7 @@ def test_departements(api_headers): ) 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 + # Infos sur un département, accès par acronyme4 r = requests.get( f"{API_URL}/departement/{dept_a['acronym']}", headers=api_headers, @@ -58,20 +69,76 @@ def test_departements(api_headers): ) assert r.status_code == 200 dept_b = r.json() - assert dept_a == dept_b - # Liste des formsemestres + assert dept_a == dept_b + assert verify_fields(dept_a, DEPARTEMENT_FIELDS) is True + assert isinstance(dept_a["id"], int) + assert isinstance(dept_a["acronym"], str) + assert dept_a["description"] is None or isinstance(dept_a["description"], str) + assert isinstance(dept_a["visible"], bool) + assert dept_a["date_creation"] is None or isinstance(dept_a["date_creation"], str) + + # --- departements : Liste des départements + r = requests.get( + API_URL + "/departements", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + + # --- formsemestre_ids : listes des ids de formsemestres du département 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 + dept_ids_a = r.json() + + r = requests.get( + f"{API_URL}/departement/{dept_a['id']}/formsemestres_ids", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + dept_ids_b = r.json() + + assert dept_ids_a == dept_ids_b + assert isinstance(dept_ids_a, list) + assert all(isinstance(id, int) for id in dept_ids_a) + assert len(dept_ids_a) > 0 + assert dept_id in dept_ids_a + + # Les erreurs + id_inexistant = 50000 + r = requests.get( + f"{API_URL}/departement/{id_inexistant}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 404 + + r = requests.get( + f"{API_URL}/departement/{id_inexistant}/formsemestres_ids", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 404 + + acronym_inexistant = "AAAAAAAAAAAAAAAAAAA" + r = requests.get( + f"{API_URL}/departement/{acronym_inexistant}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 404 + + r = requests.get( + f"{API_URL}/departement/{acronym_inexistant}/formsemestres_ids", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 404 def test_list_etudiants(api_headers): @@ -83,33 +150,46 @@ def test_list_etudiants(api_headers): verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - etud = r.json()[0] - assert verify_fields(etud, fields) is True - assert isinstance(etud["id"], int) + etud_a = r.json()[0] - # 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 + r = requests.get( + API_URL + "/departement/1/etudiants", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + etud_b = r.json()[0] + + assert etud_a == etud_b + assert verify_fields(etud_a, fields) is True + assert isinstance(etud_a["id"], int) + assert etud_a["nip"] is None or isinstance(etud_a["nip"], str) + assert etud_a["ine"] is None or isinstance(etud_a["ine"], str) + assert etud_a["nom"] is None or isinstance(etud_a["nom"], str) + assert etud_a["nom_usuel"] is None or isinstance(etud_a["nom_usuel"], str) + assert etud_a["prenom"] is None or isinstance(etud_a["prenom"], str) + assert isinstance(etud_a["civilite"], str) + assert len(etud_a["civilite"]) == 1 + + all_unique = verify_occurences_ids_etus(r.text) + assert all_unique is True + + # Les erreurs + id_inexistant = 50000 + r = requests.get( + f"{API_URL}/departement/{id_inexistant}/etudiants", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 404 + + acronym_inexistant = "AAAAAAAAAAAAAAAAAAA" + r = requests.get( + f"{API_URL}/departement/{acronym_inexistant}/etudiants", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 404 # liste_semestres_courant @@ -151,6 +231,7 @@ def test_semestres_courant(api_headers): assert r.status_code == 200 dept = r.json() assert dept["id"] == dept_id + # Accès via acronyme r = requests.get( f"{API_URL}/departement/{dept['acronym']}/formsemestres_courants", diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py index b641c757..5d915ad9 100644 --- a/tests/api/test_api_etudiants.py +++ b/tests/api/test_api_etudiants.py @@ -20,7 +20,7 @@ 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, verify_occurences_ids_etus from tests.api.tools_test_api import ETUD_FIELDS, FSEM_FIELDS @@ -47,13 +47,7 @@ def test_etudiants_courant(api_headers): assert isinstance(etud["prenom"], str) assert isinstance(etud["civilite"], str) - all_unique = True - list_ids = [etu["id"] for etu in etudiants] - - for i in range(len(etudiants) - 1): - if etudiants.count(list_ids[i]) > 1: - all_unique = False - + all_unique = verify_occurences_ids_etus(r.text) assert all_unique is True ########## Version long ################ @@ -72,7 +66,7 @@ def test_etudiants_courant(api_headers): def test_etudiant(api_headers): """ - Route: + Routes : /etudiant/etudid/, /etudiant/nip/, /etudiant/ine/ """ ######### Test etudid ######### diff --git a/tests/api/tools_test_api.py b/tests/api/tools_test_api.py index 5aefb87b..1d0c429d 100644 --- a/tests/api/tools_test_api.py +++ b/tests/api/tools_test_api.py @@ -1,5 +1,6 @@ """Utilitaires pour les tests de l'API """ +import json def verify_fields(json_response: dict, expected_fields: set) -> bool: @@ -14,6 +15,25 @@ 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): + list_etu = json.loads(json_response) + + list_ids = [etu["id"] for etu in list_etu] + list_nip = [etu["nip"] for etu in list_etu] + list_ine = [etu["ine"] for etu in list_etu] + + for id in list_ids: + if list_ids.count(id) > 1: + return False + for nip in list_nip: + if list_nip.count(nip) > 1: + return False + for ine in list_ine: + if list_ine.count(ine) > 1: + return False + return True + + DEPARTEMENT_FIELDS = [ "id", "acronym", diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index ad1198f8..bfe4800e 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 +from app.models import Departement, Formation, FormSemestre, Identite from app import db from app.scodoc import ( sco_cache, @@ -95,15 +95,18 @@ def create_users(dept: Departement) -> tuple: return user, other -def create_fake_etud(dept: Departement) -> models.Identite: - """Créé un faux étudiant et l'insère dans la base""" +def create_fake_etud(dept: Departement) -> Identite: + """Créé un faux étudiant et l'insère dans la base.""" civilite = random.choice(("M", "F", "X")) nom, prenom = nomprenom(civilite) - etud = models.Identite(civilite=civilite, nom=nom, prenom=prenom, dept_id=dept.id) + etud: Identite = Identite( + civilite=civilite, nom=nom, prenom=prenom, dept_id=dept.id + ) db.session.add(etud) db.session.commit() - etud.code_nip = etud.id - etud.code_ine = etud.id + # créé un étudiant sur deux avec un NIP et INE alphanumérique + etud.code_nip = f"{etud.id}" if (etud.id % 2) else f"NIP{etud.id}" + etud.code_ine = f"INE{etud.id}" if (etud.id % 2) else f"{etud.id}" db.session.add(etud) db.session.commit() adresse = models.Adresse(