Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into iziram-main96

This commit is contained in:
Emmanuel Viennet 2024-03-19 20:59:28 +01:00
commit 2e5d94f048
10 changed files with 147 additions and 97 deletions

View File

@ -414,9 +414,16 @@ def bulletin(
if version == "pdf": if version == "pdf":
version = "long" version = "long"
pdf = True 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") 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() dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
if g.scodoc_dept and dept.acronym != g.scodoc_dept: if g.scodoc_dept and dept.acronym != g.scodoc_dept:
return json_error(404, "formsemestre inexistant") return json_error(404, "formsemestre inexistant")

View File

@ -52,7 +52,8 @@ def formations():
@as_json @as_json
def formations_ids(): 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 ] Exemple de résultat : [ 17, 99, 32 ]
""" """

View File

@ -12,7 +12,7 @@ from operator import attrgetter, itemgetter
from flask import g, make_response, request from flask import g, make_response, request
from flask_json import as_json from flask_json import as_json
from flask_login import current_user, login_required from flask_login import current_user, login_required
import sqlalchemy as sa
import app import app
from app import db from app import db
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
@ -171,6 +171,44 @@ def formsemestres_query():
] ]
@bp.route("/formsemestre/<int:formsemestre_id>/edit", methods=["POST"])
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/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/<int:formsemestre_id>/bulletins") @bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>") @bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins") @api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
@ -468,13 +506,13 @@ def etat_evals(formsemestre_id: int):
date_mediane = notes_sorted[len(notes_sorted) // 2].date date_mediane = notes_sorted[len(notes_sorted) // 2].date
eval_dict["saisie_notes"] = { eval_dict["saisie_notes"] = {
"datetime_debut": date_debut.isoformat() "datetime_debut": (
if date_debut is not None date_debut.isoformat() if date_debut is not None else None
else None, ),
"datetime_fin": date_fin.isoformat() if date_fin is not None else None, "datetime_fin": date_fin.isoformat() if date_fin is not None else None,
"datetime_mediane": date_mediane.isoformat() "datetime_mediane": (
if date_mediane is not None date_mediane.isoformat() if date_mediane is not None else None
else None, ),
} }
list_eval.append(eval_dict) list_eval.append(eval_dict)

View File

@ -23,9 +23,12 @@ from app.models.but_refcomp import (
from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError 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 """Importation XML Orébut
peut lever TypeError ou ScoFormatError peut lever TypeError ou ScoFormatError
L'objet créé est ajouté et commité.
Résultat: instance de ApcReferentielCompetences Résultat: instance de ApcReferentielCompetences
""" """
# Vérifie que le même fichier n'a pas déjà été chargé: # 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: try:
root = ElementTree.XML(xml_data) root = ElementTree.XML(xml_data)
except ElementTree.ParseError as exc: 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": if root.tag != "referentiel_competence":
raise ScoFormatError("élément racine 'referentiel_competence' manquant") raise ScoFormatError("élément racine 'referentiel_competence' manquant")
args = ApcReferentielCompetences.attr_from_xml(root.attrib) 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 # ne devrait plus se produire car pas d'unicité de l'id: donc inutile
db.session.rollback() db.session.rollback()
raise ScoValueError( 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 ) from exc
ref.competences.append(c) ref.competences.append(c)

View File

@ -25,6 +25,7 @@ from sqlalchemy import func
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import db, log from app import db, log
from app.auth.models import User 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 import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
from app.models.but_refcomp import ( from app.models.but_refcomp import (
ApcParcours, ApcParcours,
@ -54,7 +55,7 @@ from app.scodoc.sco_vdi import ApoEtapeVDI
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
class FormSemestre(db.Model): class FormSemestre(models.ScoDocModel):
"""Mise en oeuvre d'un semestre de formation""" """Mise en oeuvre d'un semestre de formation"""
__tablename__ = "notes_formsemestre" __tablename__ = "notes_formsemestre"
@ -84,7 +85,7 @@ class FormSemestre(db.Model):
bul_hide_xml = db.Column( bul_hide_xml = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" 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( block_moyennes = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" db.Boolean(), nullable=False, default=False, server_default="false"
) )
@ -191,7 +192,8 @@ class FormSemestre(db.Model):
def get_formsemestre( def get_formsemestre(
cls, formsemestre_id: int | str, dept_id: int = None cls, formsemestre_id: int | str, dept_id: int = None
) -> "FormSemestre": ) -> "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): if not isinstance(formsemestre_id, int):
try: try:
formsemestre_id = int(formsemestre_id) formsemestre_id = int(formsemestre_id)
@ -251,6 +253,7 @@ class FormSemestre(db.Model):
d.pop("_sa_instance_state", None) d.pop("_sa_instance_state", None)
d.pop("groups_auto_assignment_data", None) d.pop("groups_auto_assignment_data", None)
d["annee_scolaire"] = self.annee_scolaire() d["annee_scolaire"] = self.annee_scolaire()
d["bul_hide_xml"] = self.bul_hide_xml
if self.date_debut: if self.date_debut:
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y") d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
d["date_debut_iso"] = self.date_debut.isoformat() d["date_debut_iso"] = self.date_debut.isoformat()

View File

@ -114,10 +114,8 @@ def formsemestre_bulletinetud_published_dict(
if etudid not in nt.identdict: if etudid not in nt.identdict:
abort(404, "etudiant non inscrit dans ce semestre") abort(404, "etudiant non inscrit dans ce semestre")
d = {"type": "classic", "version": "0"} d = {"type": "classic", "version": "0"}
if (not sem["bul_hide_xml"]) or force_publishing:
published = True published = (not formsemestre.bul_hide_xml) or force_publishing
else:
published = False
if xml_nodate: if xml_nodate:
docdate = "" docdate = ""
else: else:

View File

@ -332,28 +332,29 @@ def fiche_etud(etudid=None):
) )
# fiche admission # fiche admission
infos_admission = _infos_admission(etud, restrict_etud_data) if etud.admission:
has_adm_notes = any( infos_admission = _infos_admission(etud, restrict_etud_data)
infos_admission[k] for k in ("math", "physique", "anglais", "francais") 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",
) )
) has_bac_info = any(
if has_bac_info or has_adm_notes: infos_admission[k]
adm_tmpl = """<!-- Donnees admission --> for k in (
<div class="fichetitre">Informations admission</div> "bac_specialite",
""" "annee_bac",
if has_adm_notes: "rapporteur",
adm_tmpl += """ "commentaire",
"classement",
"type_admission",
"rap",
)
)
if has_bac_info or has_adm_notes:
adm_tmpl = """<!-- Donnees admission -->
<div class="fichetitre">Informations admission</div>
"""
if has_adm_notes:
adm_tmpl += """
<table> <table>
<tr><th>Bac</th><th>Année</th><th>Rg</th> <tr><th>Bac</th><th>Année</th><th>Rg</th>
<th>Math</th><th>Physique</th><th>Anglais</th><th>Français</th></tr> <th>Math</th><th>Physique</th><th>Anglais</th><th>Français</th></tr>
@ -364,24 +365,26 @@ def fiche_etud(etudid=None):
<td>%(math)s</td><td>%(physique)s</td><td>%(anglais)s</td><td>%(francais)s</td> <td>%(math)s</td><td>%(physique)s</td><td>%(anglais)s</td><td>%(francais)s</td>
</tr> </tr>
</table> </table>
""" """
adm_tmpl += """ adm_tmpl += """
<div>Bac %(bac_specialite)s obtenu en %(annee_bac)s </div> <div>Bac %(bac_specialite)s obtenu en %(annee_bac)s </div>
<div class="info_lycee">%(info_lycee)s</div>""" <div class="info_lycee">%(info_lycee)s</div>"""
if infos_admission["type_admission"] or infos_admission["classement"]: if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += """<div class="vadmission">""" adm_tmpl += """<div class="vadmission">"""
if infos_admission["type_admission"]: if infos_admission["type_admission"]:
adm_tmpl += """<span>Voie d'admission: <span class="etud_type_admission">%(type_admission)s</span></span> """ adm_tmpl += """<span>Voie d'admission: <span class="etud_type_admission">%(type_admission)s</span></span> """
if infos_admission["classement"]: if infos_admission["classement"]:
adm_tmpl += """<span>Rang admission: <span class="etud_type_admission">%(classement)s</span></span>""" adm_tmpl += """<span>Rang admission: <span class="etud_type_admission">%(classement)s</span></span>"""
if infos_admission["type_admission"] or infos_admission["classement"]: if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += "</div>" adm_tmpl += "</div>"
if infos_admission["rap"]: if infos_admission["rap"]:
adm_tmpl += """<div class="note_rapporteur">%(rap)s</div>""" adm_tmpl += """<div class="note_rapporteur">%(rap)s</div>"""
adm_tmpl += """</div>""" adm_tmpl += """</div>"""
else:
adm_tmpl = "" # pas de boite "info admission"
info["adm_data"] = adm_tmpl % infos_admission
else: else:
adm_tmpl = "" # pas de boite "info admission" info["adm_data"] = ""
info["adm_data"] = adm_tmpl % infos_admission
# Fichiers archivés: # Fichiers archivés:
info["fichiers_archive_htm"] = ( 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: 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 # info sur rapporteur et son commentaire
rap = "" rap = ""
if not restrict_etud_data: 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( code_cursus, _ = sco_report.get_code_cursus_etud(
etud, formsemestres=etud.get_formsemestres(), prefix="S", separator=", " etud, formsemestres=etud.get_formsemestres(), prefix="S", separator=", "
) )
bac = sco_bac.Baccalaureat(etud.admission.bac, etud.admission.specialite) if etud.admission:
bac_abbrev = bac.abbrev() bac = sco_bac.Baccalaureat(etud.admission.bac, etud.admission.specialite)
bac_abbrev = bac.abbrev()
else:
bac_abbrev = "-"
H = f"""<div class="etud_info_div"> H = f"""<div class="etud_info_div">
<div class="eid_left"> <div class="eid_left">
<div class="eid_nom"><div><a class="stdlink" target="_blank" href="{ <div class="eid_nom"><div><a class="stdlink" target="_blank" href="{

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.6.951" SCOVERSION = "9.6.952"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -79,10 +79,11 @@ if pytest:
return get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN) return get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN)
def GET(path: str, headers: dict = None, errmsg=None, dept=None): def GET(path: str, headers: dict = None, errmsg=None, dept=None, raw=False):
"""Get and returns as JSON """Get and optionaly returns as JSON
Special case for non json result (image or pdf): Special case for non json result (image or pdf):
return Content-Disposition string (inline or attachment) return Content-Disposition string (inline or attachment)
If raw, return a requests.Response
""" """
if dept: if dept:
url = SCODOC_URL + f"/ScoDoc/{dept}/api" + path 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( raise APIError(
errmsg or f"""erreur status={reply.status_code} !""", reply.json() errmsg or f"""erreur status={reply.status_code} !""", reply.json()
) )
if raw:
return reply
if reply.headers.get("Content-Type", None) == "application/json": if reply.headers.get("Content-Type", None) == "application/json":
return reply.json() # decode la reponse 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/jpg",
"image/png", "image/png",
"application/pdf", "application/pdf",

View File

@ -823,16 +823,13 @@ def test_etudiant_bulletin_semestre(api_headers):
assert r.content[:4] == b"%PDF" assert r.content[:4] == b"%PDF"
######## Bulletin BUT format intermédiaire en pdf ######### ######## Bulletin BUT format intermédiaire en pdf #########
r = requests.get( r = GET(
API_URL f"/etudiant/ine/{INE}/formsemestre/1/bulletin/selectedevals/pdf",
+ "/etudiant/ine/"
+ str(INE)
+ "/formsemestre/1/bulletin/selectedevals/pdf",
headers=api_headers, headers=api_headers,
verify=CHECK_CERTIFICATE, raw=True, # get response, do not convert to json
timeout=scu.SCO_TEST_API_TIMEOUT,
) )
assert r.status_code == 200 assert r.status_code == 200
assert r.headers.get("Content-Type", None) == "application/pdf"
assert r.content[:4] == b"%PDF" assert r.content[:4] == b"%PDF"
################### LONG + PDF ##################### ################### LONG + PDF #####################
@ -869,37 +866,17 @@ def test_etudiant_bulletin_semestre(api_headers):
################### SHORT ##################### ################### SHORT #####################
######### Test etudid ######### ######### Test etudid #########
r = requests.get( bul = GET(
API_URL + "/etudiant/etudid/" + str(ETUDID) + "/formsemestre/1/bulletin/short", f"/etudiant/etudid/{ETUDID}/formsemestre/1/bulletin/short", headers=api_headers
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
) )
assert r.status_code == 200
bul = r.json()
assert len(bul) == 14 # HARDCODED assert len(bul) == 14 # HARDCODED
######### Test code nip ######### ######### Test code nip #########
bul = GET(f"/etudiant/nip/{NIP}/formsemestre/1/bulletin/short", headers=api_headers)
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()
assert len(bul) == 14 # HARDCODED assert len(bul) == 14 # HARDCODED
######### Test code ine ######### ######### Test code ine #########
r = requests.get( bul = GET(f"/etudiant/ine/{INE}/formsemestre/1/bulletin/short", headers=api_headers)
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()
assert len(bul) == 14 # HARDCODED assert len(bul) == 14 # HARDCODED
################### SHORT + PDF ##################### ################### SHORT + PDF #####################
@ -941,6 +918,20 @@ def test_etudiant_bulletin_semestre(api_headers):
) )
assert r.status_code == 404 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): def test_etudiant_groups(api_headers):
""" """