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":
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")

View File

@ -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 ]
"""

View File

@ -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/<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/<string:version>")
@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
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)

View File

@ -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)

View File

@ -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()

View File

@ -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:

View File

@ -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 = """<!-- Donnees admission -->
<div class="fichetitre">Informations admission</div>
"""
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 = """<!-- Donnees admission -->
<div class="fichetitre">Informations admission</div>
"""
if has_adm_notes:
adm_tmpl += """
<table>
<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>
@ -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>
</tr>
</table>
"""
adm_tmpl += """
<div>Bac %(bac_specialite)s obtenu en %(annee_bac)s </div>
<div class="info_lycee">%(info_lycee)s</div>"""
if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += """<div class="vadmission">"""
if infos_admission["type_admission"]:
adm_tmpl += """<span>Voie d'admission: <span class="etud_type_admission">%(type_admission)s</span></span> """
if infos_admission["classement"]:
adm_tmpl += """<span>Rang admission: <span class="etud_type_admission">%(classement)s</span></span>"""
if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += "</div>"
if infos_admission["rap"]:
adm_tmpl += """<div class="note_rapporteur">%(rap)s</div>"""
adm_tmpl += """</div>"""
"""
adm_tmpl += """
<div>Bac %(bac_specialite)s obtenu en %(annee_bac)s </div>
<div class="info_lycee">%(info_lycee)s</div>"""
if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += """<div class="vadmission">"""
if infos_admission["type_admission"]:
adm_tmpl += """<span>Voie d'admission: <span class="etud_type_admission">%(type_admission)s</span></span> """
if infos_admission["classement"]:
adm_tmpl += """<span>Rang admission: <span class="etud_type_admission">%(classement)s</span></span>"""
if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += "</div>"
if infos_admission["rap"]:
adm_tmpl += """<div class="note_rapporteur">%(rap)s</div>"""
adm_tmpl += """</div>"""
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"""<div class="etud_info_div">
<div class="eid_left">
<div class="eid_nom"><div><a class="stdlink" target="_blank" href="{

View File

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

View File

@ -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",

View File

@ -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):
"""