From 763f60fb3d1f5649b47116044dd828d6c2f1cbfb Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 19 Mar 2024 09:34:03 +0100 Subject: [PATCH 1/2] =?UTF-8?q?Fix:=20/etud=5Finfo=5Fhtml=20si=20pas=20de?= =?UTF-8?q?=20donn=C3=A9es=20admission?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/formations.py | 3 +- app/but/import_refcomp.py | 10 +++-- app/scodoc/sco_page_etud.py | 88 ++++++++++++++++++++----------------- sco_version.py | 2 +- 4 files changed, 57 insertions(+), 46 deletions(-) diff --git a/app/api/formations.py b/app/api/formations.py index 7ff02a07..37bf6ccf 100644 --- a/app/api/formations.py +++ b/app/api/formations.py @@ -52,7 +52,8 @@ def formations(): @as_json def formations_ids(): """ - Retourne la liste de toutes les id de formations (tous départements) + Retourne la liste de toutes les id de formations + (tous départements, ou du département indiqué dans la route) Exemple de résultat : [ 17, 99, 32 ] """ diff --git a/app/but/import_refcomp.py b/app/but/import_refcomp.py index b203c413..8a11c2d8 100644 --- a/app/but/import_refcomp.py +++ b/app/but/import_refcomp.py @@ -23,9 +23,12 @@ from app.models.but_refcomp import ( from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError -def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None): +def orebut_import_refcomp( + xml_data: str, dept_id: int, orig_filename=None +) -> ApcReferentielCompetences: """Importation XML Orébut peut lever TypeError ou ScoFormatError + L'objet créé est ajouté et commité. Résultat: instance de ApcReferentielCompetences """ # Vérifie que le même fichier n'a pas déjà été chargé: @@ -41,7 +44,7 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None): try: root = ElementTree.XML(xml_data) except ElementTree.ParseError as exc: - raise ScoFormatError(f"fichier XML Orébut invalide (2): {exc.args}") + raise ScoFormatError(f"fichier XML Orébut invalide (2): {exc.args}") from exc if root.tag != "referentiel_competence": raise ScoFormatError("élément racine 'referentiel_competence' manquant") args = ApcReferentielCompetences.attr_from_xml(root.attrib) @@ -60,7 +63,8 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None): # 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"]}) + 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) diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index c5b21b7b..ad52e813 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -332,28 +332,29 @@ def fiche_etud(etudid=None): ) # fiche admission - infos_admission = _infos_admission(etud, restrict_etud_data) - has_adm_notes = any( - infos_admission[k] for k in ("math", "physique", "anglais", "francais") - ) - has_bac_info = any( - infos_admission[k] - for k in ( - "bac_specialite", - "annee_bac", - "rapporteur", - "commentaire", - "classement", - "type_admission", - "rap", + if etud.admission: + infos_admission = _infos_admission(etud, restrict_etud_data) + has_adm_notes = any( + infos_admission[k] for k in ("math", "physique", "anglais", "francais") ) - ) - if has_bac_info or has_adm_notes: - adm_tmpl = """ -
Informations admission
-""" - if has_adm_notes: - adm_tmpl += """ + has_bac_info = any( + infos_admission[k] + for k in ( + "bac_specialite", + "annee_bac", + "rapporteur", + "commentaire", + "classement", + "type_admission", + "rap", + ) + ) + if has_bac_info or has_adm_notes: + adm_tmpl = """ +
Informations admission
+ """ + if has_adm_notes: + adm_tmpl += """ @@ -364,24 +365,26 @@ def fiche_etud(etudid=None):
BacAnnéeRg MathPhysiqueAnglaisFrançais
%(math)s%(physique)s%(anglais)s%(francais)s
-""" - adm_tmpl += """ -
Bac %(bac_specialite)s obtenu en %(annee_bac)s
-
%(info_lycee)s
""" - if infos_admission["type_admission"] or infos_admission["classement"]: - adm_tmpl += """
""" - if infos_admission["type_admission"]: - adm_tmpl += """Voie d'admission: %(type_admission)s """ - if infos_admission["classement"]: - adm_tmpl += """Rang admission: %(classement)s""" - if infos_admission["type_admission"] or infos_admission["classement"]: - adm_tmpl += "
" - if infos_admission["rap"]: - adm_tmpl += """
%(rap)s
""" - adm_tmpl += """""" + """ + adm_tmpl += """ +
Bac %(bac_specialite)s obtenu en %(annee_bac)s
+
%(info_lycee)s
""" + if infos_admission["type_admission"] or infos_admission["classement"]: + adm_tmpl += """
""" + if infos_admission["type_admission"]: + adm_tmpl += """Voie d'admission: %(type_admission)s """ + if infos_admission["classement"]: + adm_tmpl += """Rang admission: %(classement)s""" + if infos_admission["type_admission"] or infos_admission["classement"]: + adm_tmpl += "
" + if infos_admission["rap"]: + adm_tmpl += """
%(rap)s
""" + adm_tmpl += """""" + else: + adm_tmpl = "" # pas de boite "info admission" + info["adm_data"] = adm_tmpl % infos_admission else: - adm_tmpl = "" # pas de boite "info admission" - info["adm_data"] = adm_tmpl % infos_admission + info["adm_data"] = "" # Fichiers archivés: info["fichiers_archive_htm"] = ( @@ -654,7 +657,7 @@ def _format_adresse(adresse: Adresse | None) -> dict: def _infos_admission(etud: Identite, restrict_etud_data: bool) -> dict: - """dict with adminission data, restricted or not""" + """dict with admission data, restricted or not""" # info sur rapporteur et son commentaire rap = "" if not restrict_etud_data: @@ -799,8 +802,11 @@ def etud_info_html(etudid, with_photo="1", debug=False): code_cursus, _ = sco_report.get_code_cursus_etud( etud, formsemestres=etud.get_formsemestres(), prefix="S", separator=", " ) - bac = sco_bac.Baccalaureat(etud.admission.bac, etud.admission.specialite) - bac_abbrev = bac.abbrev() + if etud.admission: + bac = sco_bac.Baccalaureat(etud.admission.bac, etud.admission.specialite) + bac_abbrev = bac.abbrev() + else: + bac_abbrev = "-" H = f"""
Date: Tue, 19 Mar 2024 18:22:02 +0100 Subject: [PATCH 2/2] =?UTF-8?q?Restreint=20acc=C3=A8s=20aux=20bulletins=20?= =?UTF-8?q?PDF=20si=20formsemestre.bul=5Fhide=5Fxml=20(s=C3=A9mantique=20c?= =?UTF-8?q?hang=C3=A9e)=20+=20WIP=20tests=20unitaires=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/etudiants.py | 11 +++++-- app/api/formsemestres.py | 52 ++++++++++++++++++++++++++----- app/models/formsemestre.py | 9 ++++-- app/scodoc/sco_bulletins_json.py | 6 ++-- tests/api/setup_test_api.py | 10 +++--- tests/api/test_api_etudiants.py | 53 +++++++++++++------------------- 6 files changed, 90 insertions(+), 51 deletions(-) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 10fb562b..9b21db43 100755 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -414,9 +414,16 @@ def bulletin( if version == "pdf": version = "long" pdf = True - if version not in scu.BULLETINS_VERSIONS_BUT: + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + if version not in ( + scu.BULLETINS_VERSIONS_BUT + if formsemestre.formation.is_apc() + else scu.BULLETINS_VERSIONS + ): return json_error(404, "version invalide") - formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() + if formsemestre.bul_hide_xml and pdf: + return json_error(403, "bulletin non disponible") + # note: la version json est réduite si bul_hide_xml dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() if g.scodoc_dept and dept.acronym != g.scodoc_dept: return json_error(404, "formsemestre inexistant") diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 4c9c4024..1c3ddac2 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -12,7 +12,7 @@ from operator import attrgetter, itemgetter from flask import g, make_response, request from flask_json import as_json from flask_login import current_user, login_required - +import sqlalchemy as sa import app from app import db from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR @@ -171,6 +171,44 @@ def formsemestres_query(): ] +@bp.route("/formsemestre//edit", methods=["POST"]) +@api_web_bp.route("/formsemestre//edit", methods=["POST"]) +@scodoc +@permission_required(Permission.EditFormSemestre) +@as_json +def formsemestre_edit(formsemestre_id: int): + """Modifie les champs d'un formsemestre.""" + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + args = request.get_json(force=True) # may raise 400 Bad Request + editable_keys = { + "semestre_id", + "titre", + "date_debut", + "date_fin", + "edt_id", + "etat", + "modalite", + "gestion_compensation", + "bul_hide_xml", + "block_moyennes", + "block_moyenne_generale", + "mode_calcul_moyennes", + "gestion_semestrielle", + "bul_bgcolor", + "resp_can_edit", + "resp_can_change_ens", + "ens_can_edit_eval", + "elt_sem_apo", + "elt_annee_apo", + } + formsemestre.from_dict({k: v for (k, v) in args.items() if k in editable_keys}) + try: + db.session.commit() + except sa.exc.StatementError as exc: + return json_error(404, f"invalid argument(s): {exc.args[0]}") + return formsemestre.to_dict_api() + + @bp.route("/formsemestre//bulletins") @bp.route("/formsemestre//bulletins/") @api_web_bp.route("/formsemestre//bulletins") @@ -468,13 +506,13 @@ def etat_evals(formsemestre_id: int): date_mediane = notes_sorted[len(notes_sorted) // 2].date eval_dict["saisie_notes"] = { - "datetime_debut": date_debut.isoformat() - if date_debut is not None - else None, + "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, + "datetime_mediane": ( + date_mediane.isoformat() if date_mediane is not None else None + ), } list_eval.append(eval_dict) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index ba08fed4..00bac5c2 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -25,6 +25,7 @@ from sqlalchemy import func import app.scodoc.sco_utils as scu from app import db, log from app.auth.models import User +from app import models from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN from app.models.but_refcomp import ( ApcParcours, @@ -54,7 +55,7 @@ from app.scodoc.sco_vdi import ApoEtapeVDI GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes -class FormSemestre(db.Model): +class FormSemestre(models.ScoDocModel): """Mise en oeuvre d'un semestre de formation""" __tablename__ = "notes_formsemestre" @@ -84,7 +85,7 @@ class FormSemestre(db.Model): bul_hide_xml = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) - "ne publie pas le bulletin XML ou JSON" + "ne publie pas le bulletin sur l'API" block_moyennes = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) @@ -191,7 +192,8 @@ class FormSemestre(db.Model): def get_formsemestre( cls, formsemestre_id: int | str, dept_id: int = None ) -> "FormSemestre": - """FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant""" + """FormSemestre ou 404, cherche uniquement dans le département spécifié + ou le courant (g.scodoc_dept)""" if not isinstance(formsemestre_id, int): try: formsemestre_id = int(formsemestre_id) @@ -251,6 +253,7 @@ class FormSemestre(db.Model): d.pop("_sa_instance_state", None) d.pop("groups_auto_assignment_data", None) d["annee_scolaire"] = self.annee_scolaire() + d["bul_hide_xml"] = self.bul_hide_xml if self.date_debut: d["date_debut"] = self.date_debut.strftime("%d/%m/%Y") d["date_debut_iso"] = self.date_debut.isoformat() diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index a7848b39..42e2a910 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -114,10 +114,8 @@ def formsemestre_bulletinetud_published_dict( if etudid not in nt.identdict: abort(404, "etudiant non inscrit dans ce semestre") d = {"type": "classic", "version": "0"} - if (not sem["bul_hide_xml"]) or force_publishing: - published = True - else: - published = False + + published = (not formsemestre.bul_hide_xml) or force_publishing if xml_nodate: docdate = "" else: diff --git a/tests/api/setup_test_api.py b/tests/api/setup_test_api.py index f564b9d1..65ac47b5 100644 --- a/tests/api/setup_test_api.py +++ b/tests/api/setup_test_api.py @@ -79,10 +79,11 @@ if pytest: return get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN) -def GET(path: str, headers: dict = None, errmsg=None, dept=None): - """Get and returns as JSON +def GET(path: str, headers: dict = None, errmsg=None, dept=None, raw=False): + """Get and optionaly returns as JSON Special case for non json result (image or pdf): return Content-Disposition string (inline or attachment) + If raw, return a requests.Response """ if dept: url = SCODOC_URL + f"/ScoDoc/{dept}/api" + path @@ -101,10 +102,11 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None): raise APIError( errmsg or f"""erreur status={reply.status_code} !""", reply.json() ) - + if raw: + return reply if reply.headers.get("Content-Type", None) == "application/json": return reply.json() # decode la reponse JSON - elif reply.headers.get("Content-Type", None) in [ + if reply.headers.get("Content-Type", None) in [ "image/jpg", "image/png", "application/pdf", diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py index 58d380e4..4ee134b4 100644 --- a/tests/api/test_api_etudiants.py +++ b/tests/api/test_api_etudiants.py @@ -823,16 +823,13 @@ def test_etudiant_bulletin_semestre(api_headers): assert r.content[:4] == b"%PDF" ######## Bulletin BUT format intermédiaire en pdf ######### - r = requests.get( - API_URL - + "/etudiant/ine/" - + str(INE) - + "/formsemestre/1/bulletin/selectedevals/pdf", + r = GET( + f"/etudiant/ine/{INE}/formsemestre/1/bulletin/selectedevals/pdf", headers=api_headers, - verify=CHECK_CERTIFICATE, - timeout=scu.SCO_TEST_API_TIMEOUT, + raw=True, # get response, do not convert to json ) assert r.status_code == 200 + assert r.headers.get("Content-Type", None) == "application/pdf" assert r.content[:4] == b"%PDF" ################### LONG + PDF ##################### @@ -869,37 +866,17 @@ def test_etudiant_bulletin_semestre(api_headers): ################### SHORT ##################### ######### Test etudid ######### - r = requests.get( - API_URL + "/etudiant/etudid/" + str(ETUDID) + "/formsemestre/1/bulletin/short", - headers=api_headers, - verify=CHECK_CERTIFICATE, - timeout=scu.SCO_TEST_API_TIMEOUT, + bul = GET( + f"/etudiant/etudid/{ETUDID}/formsemestre/1/bulletin/short", headers=api_headers ) - assert r.status_code == 200 - bul = r.json() assert len(bul) == 14 # HARDCODED ######### Test code nip ######### - - r = requests.get( - API_URL + "/etudiant/nip/" + str(NIP) + "/formsemestre/1/bulletin/short", - headers=api_headers, - verify=CHECK_CERTIFICATE, - timeout=scu.SCO_TEST_API_TIMEOUT, - ) - assert r.status_code == 200 - bul = r.json() + bul = GET(f"/etudiant/nip/{NIP}/formsemestre/1/bulletin/short", headers=api_headers) assert len(bul) == 14 # HARDCODED ######### Test code ine ######### - r = requests.get( - API_URL + "/etudiant/ine/" + str(INE) + "/formsemestre/1/bulletin/short", - headers=api_headers, - verify=CHECK_CERTIFICATE, - timeout=scu.SCO_TEST_API_TIMEOUT, - ) - assert r.status_code == 200 - bul = r.json() + bul = GET(f"/etudiant/ine/{INE}/formsemestre/1/bulletin/short", headers=api_headers) assert len(bul) == 14 # HARDCODED ################### SHORT + PDF ##################### @@ -941,6 +918,20 @@ def test_etudiant_bulletin_semestre(api_headers): ) assert r.status_code == 404 + ### -------- Modifie publication bulletins + admin_header = get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN) + formsemestre = POST_JSON( + f"/formsemestre/{1}/edit", {"bul_hide_xml": True}, headers=admin_header + ) + assert formsemestre["bul_hide_xml"] is True + # La forme utilisée par la passerelle: + bul = GET(f"/etudiant/nip/{NIP}/formsemestre/1/bulletin", headers=api_headers) + assert len(bul) == 9 # version raccourcie, longueur HARDCODED + # TODO forme utilisée par la passerelle pour les PDF + # /ScoDoc/api/etudiant/nip/12345/formsemestre/123/bulletin/long/pdf/nosi + # TODO voir forme utilisée par ScoDoc en interne: + # formsemestre_bulletinetud?formsemestre_id=1263&etudid=16387 + def test_etudiant_groups(api_headers): """