1
0
forked from ScoDoc/ScoDoc

Restreint accès aux bulletins PDF si formsemestre.bul_hide_xml (sémantique changée) + WIP tests unitaires API

This commit is contained in:
Emmanuel Viennet 2024-03-19 18:22:02 +01:00
parent 763f60fb3d
commit 9c6db169f3
6 changed files with 90 additions and 51 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

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

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

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