Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into entreprises

This commit is contained in:
Arthur ZHU 2022-08-19 08:01:10 +02:00
commit 809b5a992d
21 changed files with 575 additions and 244 deletions

View File

@ -4,11 +4,13 @@
from flask import Blueprint from flask import Blueprint
from flask import request from flask import request
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoException
api_bp = Blueprint("api", __name__) api_bp = Blueprint("api", __name__)
api_web_bp = Blueprint("apiweb", __name__) api_web_bp = Blueprint("apiweb", __name__)
@api_bp.errorhandler(ScoException)
@api_bp.errorhandler(404) @api_bp.errorhandler(404)
def api_error_handler(e): def api_error_handler(e):
"erreurs API => json" "erreurs API => json"

View File

@ -218,7 +218,6 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
methods=["GET"], methods=["GET"],
defaults={"version": "long", "pdf": False}, defaults={"version": "long", "pdf": False},
) )
# Version PDF non testée
@bp.route( @bp.route(
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf", "/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
methods=["GET"], methods=["GET"],
@ -254,6 +253,16 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
methods=["GET"], methods=["GET"],
defaults={"version": "short", "pdf": True}, defaults={"version": "short", "pdf": True},
) )
@bp.route(
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
methods=["GET"],
defaults={"version": "long", "pdf": True},
)
@bp.route(
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
methods=["GET"],
defaults={"version": "long", "pdf": True},
)
@api_web_bp.route( @api_web_bp.route(
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin", "/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin",
methods=["GET"], methods=["GET"],
@ -269,7 +278,6 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
methods=["GET"], methods=["GET"],
defaults={"version": "long", "pdf": False}, defaults={"version": "long", "pdf": False},
) )
# Version PDF non testée
@api_web_bp.route( @api_web_bp.route(
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf", "/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
methods=["GET"], methods=["GET"],

View File

@ -251,55 +251,52 @@ def formsemestre_programme(formsemestre_id: int):
@bp.route( @bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants", "/formsemestre/<int:formsemestre_id>/etudiants",
defaults={"etat": None}, defaults={"with_query": False},
) )
@bp.route( @bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/actifs", "/formsemestre/<int:formsemestre_id>/etudiants/query",
defaults={"etat": scu.INSCRIT}, defaults={"with_query": True},
)
@bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/demissionnaires",
defaults={"etat": scu.DEMISSION},
)
@bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/defaillants",
defaults={"etat": scu.DEF},
) )
@api_web_bp.route( @api_web_bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants", "/formsemestre/<int:formsemestre_id>/etudiants",
defaults={"etat": None}, defaults={"with_query": False},
) )
@api_web_bp.route( @api_web_bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/actifs", "/formsemestre/<int:formsemestre_id>/etudiants/query",
defaults={"etat": scu.INSCRIT}, defaults={"with_query": True},
)
@api_web_bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/demissionnaires",
defaults={"etat": scu.DEMISSION},
)
@api_web_bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/defaillants",
defaults={"etat": scu.DEF},
) )
@login_required @login_required
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def formsemestre_etudiants(formsemestre_id: int, etat: str = None): def formsemestre_etudiants(formsemestre_id: int, with_query: bool = False):
"""Etudiants d'un formsemestre.""" """Etudiants d'un formsemestre."""
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id) query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
if etat is None: if with_query:
etat = request.args.get("etat")
if etat is not None:
etat = {
"actifs": scu.INSCRIT,
"demissionnaires": scu.DEMISSION,
"defaillants": scu.DEF,
}.get(etat, etat)
inscriptions = [
ins for ins in formsemestre.inscriptions if ins.etat == etat
]
else:
inscriptions = formsemestre.inscriptions inscriptions = formsemestre.inscriptions
else: else:
inscriptions = [ins for ins in formsemestre.inscriptions if ins.etat == etat] inscriptions = formsemestre.inscriptions
etuds = [ins.etud.to_dict_short() for ins in inscriptions] etuds = [ins.etud.to_dict_short() for ins in inscriptions]
# Ajout des groupes de chaque étudiants # Ajout des groupes de chaque étudiants
# XXX A REVOIR: trop inefficace ! # XXX A REVOIR: trop inefficace !
for etud in etuds: for etud in etuds:
etud["groups"] = sco_groups.get_etud_groups(etud["id"], formsemestre_id) etud["groups"] = sco_groups.get_etud_groups(
etud["id"], formsemestre_id, exclude_default=True
)
return jsonify(etuds) return jsonify(etuds)

View File

@ -15,6 +15,7 @@ import app
from app import db, log from app import db, log
from app.api import api_bp as bp, api_web_bp from app.api import api_bp as bp, api_web_bp
from app.decorators import scodoc, permission_required from app.decorators import scodoc, permission_required
from app.scodoc.sco_exceptions import ScoException
from app.scodoc.sco_utils import json_error from app.scodoc.sco_utils import json_error
from app.but import jury_but_recap from app.but import jury_but_recap
from app.models import FormSemestre, FormSemestreInscription, Identite from app.models import FormSemestre, FormSemestreInscription, Identite
@ -30,6 +31,9 @@ def decisions_jury(formsemestre_id: int):
"""Décisions du jury des étudiants du formsemestre.""" """Décisions du jury des étudiants du formsemestre."""
# APC, pair: # APC, pair:
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
if formsemestre.formation.is_apc():
app.set_sco_dept(formsemestre.departement.acronym) app.set_sco_dept(formsemestre.departement.acronym)
rows = jury_but_recap.get_jury_but_results(formsemestre) rows = jury_but_recap.get_jury_but_results(formsemestre)
return jsonify(rows) return jsonify(rows)
else:
raise ScoException("non implemente")

View File

@ -136,20 +136,22 @@ def etud_in_group(group_id: int):
def etud_in_group_query(group_id: int): def etud_in_group_query(group_id: int):
"""Etudiants du groupe, filtrés par état""" """Etudiants du groupe, filtrés par état"""
etat = request.args.get("etat") etat = request.args.get("etat")
if etat not in {scu.INSCRIT, scu.DEMISSION, scu.DEF}: if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}:
return json_error(404, "etat: valeur invalide") return json_error(404, "etat: valeur invalide")
query = GroupDescr.query.filter_by(id=group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept: if g.scodoc_dept:
query = ( query = (
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
) )
group = query.first_or_404() # just tro ckeck that group exists in accessible dept group = query.first_or_404() # just to ckeck that group exists in accessible dept
query = (
Identite.query.join(FormSemestreInscription) query = Identite.query.join(FormSemestreInscription).filter_by(
.filter_by(formsemestre_id=group.partition.formsemestre_id, etat=etat) formsemestre_id=group.partition.formsemestre_id
.join(group_membership)
.filter_by(group_id=group_id)
) )
if etat is not None:
query = query.filter_by(etat=etat)
query = query.join(group_membership).filter_by(group_id=group_id)
return jsonify([etud.to_dict_short() for etud in query]) return jsonify([etud.to_dict_short() for etud in query])

View File

@ -29,12 +29,8 @@ def get_etud(etudid=None, nip=None, ine=None) -> models.Identite:
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView) allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
if etudid is not None: if etudid is not None:
etud: Identite = Identite.query.get(etudid) query: Identite = Identite.query.filter_by(id=etudid)
if (None in allowed_depts) or etud.departement.acronym in allowed_depts: elif nip is not None:
return etud
return None # accès interdit => pas d'étudiant
if nip is not None:
query = Identite.query.filter_by(code_nip=nip) query = Identite.query.filter_by(code_nip=nip)
elif ine is not None: elif ine is not None:
query = Identite.query.filter_by(code_ine=ine) query = Identite.query.filter_by(code_ine=ine)
@ -45,7 +41,7 @@ def get_etud(etudid=None, nip=None, ine=None) -> models.Identite:
) )
if None not in allowed_depts: if None not in allowed_depts:
# restreint aux départements autorisés: # restreint aux départements autorisés:
etuds = etuds.join(Departement).filter( query = query.join(Departement).filter(
or_(Departement.acronym == acronym for acronym in allowed_depts) or_(Departement.acronym == acronym for acronym in allowed_depts)
) )
return query.join(Admission).order_by(desc(Admission.annee)).first() return query.join(Admission).order_by(desc(Admission.annee)).first()

View File

@ -124,8 +124,8 @@ def user_create():
return jsonify(user.to_dict()) return jsonify(user.to_dict())
@bp.route("/user/edit/<int:uid>", methods=["POST"]) @bp.route("/user/<int:uid>/edit", methods=["POST"])
@api_web_bp.route("/user/edit/<int:uid>", methods=["POST"]) @api_web_bp.route("/user/<int:uid>/edit", methods=["POST"])
@login_required @login_required
@scodoc @scodoc
@permission_required(Permission.ScoUsersAdmin) @permission_required(Permission.ScoUsersAdmin)
@ -306,8 +306,8 @@ def role_permission_remove(role_name: str, perm_name: str):
return jsonify(role.to_dict()) return jsonify(role.to_dict())
@bp.route("/role/<string:role_name>/create", methods=["POST"]) @bp.route("/role/create/<string:role_name>", methods=["POST"])
@api_web_bp.route("/role/<string:role_name>/create", methods=["POST"]) @api_web_bp.route("/role/create/<string:role_name>", methods=["POST"])
@login_required @login_required
@scodoc @scodoc
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)

View File

@ -63,13 +63,13 @@ from operator import attrgetter
import re import re
from typing import Union from typing import Union
import numpy as np
from flask import g, url_for from flask import g, url_for
from app import db from app import db
from app import log from app import log
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.comp import inscr_mod, res_sem from app.comp import res_sem
from app.models import formsemestre
from app.models.but_refcomp import ( from app.models.but_refcomp import (
ApcAnneeParcours, ApcAnneeParcours,
@ -917,7 +917,7 @@ class DecisionsProposeesUE(DecisionsProposees):
self.codes = [ self.codes = [
sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF
] ]
self.moy_ue = "-" self.moy_ue = np.NaN
return return
# Moyenne de l'UE ? # Moyenne de l'UE ?

View File

@ -448,6 +448,9 @@ def get_jury_but_table(
def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]: def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]:
"""Liste des résultats jury BUT sous forme de dict, pour API""" """Liste des résultats jury BUT sous forme de dict, pour API"""
if formsemestre.formation.referentiel_competence is None:
# pas de ref. comp., donc pas de decisions de jury (ne lance pas d'exception)
return []
dpv = sco_pvjury.dict_pvjury(formsemestre.id) dpv = sco_pvjury.dict_pvjury(formsemestre.id)
rows = [] rows = []
for etudid in formsemestre.etuds_inscriptions: for etudid in formsemestre.etuds_inscriptions:
@ -484,12 +487,16 @@ def get_jury_but_etud_result(
rcue_dict = { rcue_dict = {
"ue_1": { "ue_1": {
"ue_id": rcue.ue_1.id, "ue_id": rcue.ue_1.id,
"moy": None if np.isnan(dec_ue1.moy_ue) else dec_ue1.moy_ue, "moy": None
if (dec_ue1.moy_ue is None or np.isnan(dec_ue1.moy_ue))
else dec_ue1.moy_ue,
"code": dec_ue1.code_valide, "code": dec_ue1.code_valide,
}, },
"ue_2": { "ue_2": {
"ue_id": rcue.ue_2.id, "ue_id": rcue.ue_2.id,
"moy": None if np.isnan(dec_ue2.moy_ue) else dec_ue2.moy_ue, "moy": None
if (dec_ue2.moy_ue is None or np.isnan(dec_ue2.moy_ue))
else dec_ue2.moy_ue,
"code": dec_ue2.code_valide, "code": dec_ue2.code_valide,
}, },
"moy": rcue.moy_rcue, "moy": rcue.moy_rcue,

View File

@ -221,10 +221,7 @@ def compute_ue_moys_apc(
modimpl_mask: np.array, modimpl_mask: np.array,
) -> pd.DataFrame: ) -> pd.DataFrame:
"""Calcul de la moyenne d'UE en mode APC (BUT). """Calcul de la moyenne d'UE en mode APC (BUT).
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR La moyenne d'UE est un nombre (note/20), ou NaN si pas de notes disponibles
NI non inscrit à (au moins un) module de cette UE
NA pas de notes disponibles
ERR erreur dans une formule utilisateurs (pas gérées ici).
sem_cube: notes moyennes aux modules sem_cube: notes moyennes aux modules
ndarray (etuds x modimpls x UEs) ndarray (etuds x modimpls x UEs)

View File

@ -50,6 +50,8 @@ main {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 32px; gap: 32px;
row-gap: 4px;
margin-right: 16px;
} }
main h2 { main h2 {
@ -184,8 +186,16 @@ body.editionActivated .filtres>div>div>div>div {
} }
/*****************************/ /*****************************/
/* Zone Choix */ /* Zone Partitions */
/*****************************/ /*****************************/
#zonePartitions {
width: 100%;
}
.filtres {
display: table;
}
.filtres>div { .filtres>div {
background: #ddd; background: #ddd;
padding: 8px; padding: 8px;
@ -226,6 +236,9 @@ body:not(.editionActivated) .filtres>div>div>div>div:active {
background: rgba(0, 153, 204, 0.5); background: rgba(0, 153, 204, 0.5);
} }
/*****************************/
/* Zone Etudiants */
/*****************************/
#zoneChoix .etudiants>div { #zoneChoix .etudiants>div {
background: #FFF; background: #FFF;
border: 1px solid #aaa; border: 1px solid #aaa;

View File

@ -1,18 +1,16 @@
{# -*- mode: jinja-html -*- #} {# -*- mode: jinja-html -*- #}
<h1>{% if not read_only %}Édition des p{% else %}P{%endif%}artitions</h1> <h1>{% if not read_only %}Édition des p{% else %}P{%endif%}artitions</h1>
<div>
<label class="edition">
<input type="checkbox" autocomplete="off">
Edition des partitions - tout s'enregistre automatiquement dès qu'il y a modification
</label>
</div>
<main> <main>
<div class="wait"></div> <div class="wait"></div>
<section id="zoneChoix"> <section id="zonePartitions">
<h2>Choix</h2> <h2>Partitions et groupes</h2>
<div>
<label class="edition">
<input type="checkbox" autocomplete="off">
Modifier les partitions et groupes - tout s'enregistre automatiquement dès qu'il y a modification
</label>
<div class="filtres"> <div class="filtres">
<div class="partitions"> <div class="partitions">
<h3>Afficher les partitions</h3> <h3>Afficher les partitions</h3>
@ -26,8 +24,14 @@
<div></div> <div></div>
</div> </div>
</div> </div>
</div>
</section>
<section id="zoneChoix">
<h2>Etudiants</h2>
<div class="etudiants"></div> <div class="etudiants"></div>
</section> </section>
<section id="zoneGroupes"> <section id="zoneGroupes">
<h2>Groupes</h2> <h2>Groupes</h2>
<div class="groupes"></div> <div class="groupes"></div>
@ -78,7 +82,12 @@
arrayPartitions.forEach((partition) => { arrayPartitions.forEach((partition) => {
// Filtres // Filtres
if (partition.groups_editable) {
outputPartitions += `<div data-idpartition="${partition.id}"><span class="editing move">||</span><span>${partition.partition_name}</span><span class="editing modif">✏️</span><span class="editing suppr"></span></div>`; outputPartitions += `<div data-idpartition="${partition.id}"><span class="editing move">||</span><span>${partition.partition_name}</span><span class="editing modif">✏️</span><span class="editing suppr"></span></div>`;
} else {
outputPartitions += `<div data-idpartition="${partition.id}"><span>${partition.partition_name}</span></div>`;
}
outputMasques += `<div data-idpartition="${partition.id}"><div data-idpartition="${partition.id}" data-idgroupe=aucun>Non affectés - ${partition.partition_name}</div>`; outputMasques += `<div data-idpartition="${partition.id}"><div data-idpartition="${partition.id}" data-idgroupe=aucun>Non affectés - ${partition.partition_name}</div>`;
// Groupes // Groupes
@ -96,9 +105,13 @@
let output = ""; let output = "";
arrayGroups.forEach((groupe) => { arrayGroups.forEach((groupe) => {
/***************/ /***************/
outputMasques += `<div data-idgroupe="${groupe.id}"><span class="editing move">||</span><span>${groupe.group_name}</span><span class="editing modif">✏️</span><span class="editing suppr"></span></div>`; // patch JMP (renommage du champ name dans l API) if (partition.groups_editable) {
outputMasques += `<div data-idgroupe="${groupe.id}"><span class="editing move">||</span><span>${groupe.group_name}</span><span class="editing modif">✏️</span><span class="editing suppr"></span></div>`;
} else {
outputMasques += `<div data-idgroupe="${groupe.id}"><span>${groupe.group_name}</span></div>`;
}
/***************/ /***************/
output += templateGroupe_zoneGroupes(groupe.id, groupe.group_name); // patch JMP (renommage du champ name dans l API) output += templateGroupe_zoneGroupes(groupe.id, groupe.group_name);
}) })
return output; return output;
})()} })()}
@ -144,7 +157,7 @@
if (!affected) { if (!affected) {
document.querySelector(`#zoneGroupes [data-idpartition="${partition.id}"]>[data-idgroupe="aucun"]>.etudiants`).innerHTML += templateEtudiant_zoneGroupes(etudiant); document.querySelector(`#zoneGroupes [data-idpartition="${partition.id}"]>[data-idgroupe="aucun"]>.etudiants`).innerHTML += templateEtudiant_zoneGroupes(etudiant);
} }
return `<label title="Aucun groupe"><input type=radio name="${partition.id}-${etudiant.etudid}" value="aucun" ${(!affected) ? "checked" : ""}><span class=aucun></span></label>` + output; return `<label title="Aucun groupe"><input type=radio name="${partition.id}-${etudiant.etudid}" value="aucun" ${(!affected) ? "checked" : ""}><span class=aucun> - </span></label>` + output;
})()} })()}
</div>`; </div>`;
}) })
@ -171,9 +184,6 @@
/******************************/ /******************************/
function input() { function input() {
document.querySelector("body").classList.toggle("editionActivated"); document.querySelector("body").classList.toggle("editionActivated");
/*if (event.currentTarget.checked == false) {
go();
}*/
} }
function processEvents() { function processEvents() {
/*--------------------*/ /*--------------------*/
@ -193,7 +203,7 @@
/*--------------------*/ /*--------------------*/
/* Changement groupe */ /* Changement groupe */
/*--------------------*/ /*--------------------*/
document.querySelectorAll("#zoneChoix label").forEach(btn => { btn.addEventListener("mousedown", (event) => { event.preventDefault() }) }); document.querySelectorAll("label").forEach(btn => { btn.addEventListener("mousedown", (event) => { event.preventDefault() }) });
document.querySelectorAll(".etudiants input").forEach(input => { input.addEventListener("input", assignment) }) document.querySelectorAll(".etudiants input").forEach(input => { input.addEventListener("input", assignment) })
} }
@ -223,6 +233,7 @@
} }
if (!this.dataset.idgroupe) { if (!this.dataset.idgroupe) {
// Partitions
let groupesSelected = []; let groupesSelected = [];
this.parentElement.querySelectorAll(":not(.unselect)").forEach(e => { this.parentElement.querySelectorAll(":not(.unselect)").forEach(e => {
groupesSelected.push(e.dataset.idpartition); groupesSelected.push(e.dataset.idpartition);
@ -238,6 +249,7 @@
} }
}) })
} else { } else {
// Groupes
let groupesSelected = {}; let groupesSelected = {};
this.parentElement.parentElement.querySelectorAll("[data-idgroupe]:not(.unselect)").forEach(e => { this.parentElement.parentElement.querySelectorAll("[data-idgroupe]:not(.unselect)").forEach(e => {
@ -350,6 +362,8 @@
div.querySelector(".move").addEventListener("mousedown", moveStart); div.querySelector(".move").addEventListener("mousedown", moveStart);
this.parentElement.insertBefore(div, this); this.parentElement.insertBefore(div, this);
div.querySelector(".modif").click();
// Save // Save
fetch(url, fetch(url,
{ {
@ -380,12 +394,12 @@
div.querySelector("div").addEventListener("click", filtre); div.querySelector("div").addEventListener("click", filtre);
div.querySelector(".ajoutGroupe").addEventListener("click", addPartition); div.querySelector(".ajoutGroupe").addEventListener("click", addPartition);
document.querySelector("#zoneChoix .masques>div").appendChild(div); document.querySelector("#zonePartitions .masques>div").appendChild(div);
// Ajout de la zone pour chaque étudiant // Ajout de la zone pour chaque étudiant
let outputGroupes = ""; let outputGroupes = "";
document.querySelectorAll(`#zoneChoix .grpPartitions`).forEach(e => { document.querySelectorAll(`#zonePartitions .grpPartitions`).forEach(e => {
let etudid = e.previousElementSibling.dataset.etudid; let etudid = e.previousElementSibling.dataset.etudid;
// Préparation pour la section suivante // Préparation pour la section suivante
@ -447,10 +461,17 @@
/* Edition du texte */ /* Edition du texte */
/********************/ /********************/
function editText() { function editText() {
this.previousElementSibling.classList.add("editingText"); let e = this.previousElementSibling;
this.previousElementSibling.setAttribute("contenteditable", "true"); e.classList.add("editingText");
this.previousElementSibling.focus(); e.setAttribute("contenteditable", "true");
this.previousElementSibling.addEventListener("keydown", writing); e.addEventListener("keydown", writing);
// On sélectionne la zone
const range = document.createRange();
const selection = window.getSelection();
selection.removeAllRanges();
range.selectNodeContents(e);
selection.addRange(range);
} }
function writing(event) { function writing(event) {
@ -613,12 +634,12 @@
let formsemestre_id = params.get('formsemestre_id'); let formsemestre_id = params.get('formsemestre_id');
var url = `/ScoDoc/{{formsemestre.departement.acronym}}/api/formsemestre/${formsemestre_id}/partitions/order`; var url = `/ScoDoc/{{formsemestre.departement.acronym}}/api/formsemestre/${formsemestre_id}/partitions/order`;
document.querySelectorAll(`#zoneChoix .masques>div`).forEach(parent => { document.querySelectorAll(`#zonePartitions .masques>div`).forEach(parent => {
positions.forEach(position => { positions.forEach(position => {
parent.append(parent.querySelector(`[data-idpartition="${position}"]`)) parent.append(parent.querySelector(`[data-idpartition="${position}"]`))
}) })
}) })
document.querySelectorAll(`#zoneChoix .grpPartitions`).forEach(parent => { document.querySelectorAll(`#zonePartitions .grpPartitions`).forEach(parent => {
positions.forEach(position => { positions.forEach(position => {
parent.append(parent.querySelector(`[data-idpartition="${position}"]`)) parent.append(parent.querySelector(`[data-idpartition="${position}"]`))
}) })

View File

@ -72,6 +72,7 @@ from app.decorators import (
) )
from app.models import FormSemestre, GroupDescr from app.models import FormSemestre, GroupDescr
from app.models.absences import BilletAbsence from app.models.absences import BilletAbsence
from app.models.etudiants import Identite
from app.views import absences_bp as bp from app.views import absences_bp as bp
# --------------- # ---------------
@ -1099,7 +1100,7 @@ def AddBilletAbsence(
begin, begin,
end, end,
description, description,
etudid=False, etudid=None,
code_nip=None, code_nip=None,
code_ine=None, code_ine=None,
justified=True, justified=True,
@ -1114,7 +1115,7 @@ def AddBilletAbsence(
end = str(end) end = str(end)
code_nip = str(code_nip) if code_nip else None code_nip = str(code_nip) if code_nip else None
etud = api.tools.get_etud(etudid=None, nip=None, ine=None) etud = api.tools.get_etud(etudid=etudid, nip=code_nip, ine=code_ine)
# check dates # check dates
begin_date = dateutil.parser.isoparse(begin) # may raises ValueError begin_date = dateutil.parser.isoparse(begin) # may raises ValueError
end_date = dateutil.parser.isoparse(end) end_date = dateutil.parser.isoparse(end)
@ -1212,9 +1213,12 @@ def billets_etud(etudid=False):
@scodoc @scodoc
@permission_required_compat_scodoc7(Permission.ScoView) @permission_required_compat_scodoc7(Permission.ScoView)
@scodoc7func @scodoc7func
def XMLgetBilletsEtud(etudid=False): def XMLgetBilletsEtud(etudid=False, code_nip=False):
"""Liste billets pour un etudiant""" """Liste billets pour un etudiant"""
log("Warning: called deprecated XMLgetBilletsEtud") log("Warning: called deprecated XMLgetBilletsEtud")
if etudid is False:
etud = Identite.query.filter_by(code_nip=str(code_nip)).first_or_404()
etudid = etud.id
table = sco_abs_billets.table_billets_etud(etudid) table = sco_abs_billets.table_billets_etud(etudid)
if table: if table:
return table.make_page(format="xml") return table.make_page(format="xml")

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.3.25" SCOVERSION = "9.3.26"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"
@ -10,6 +10,8 @@ SCONEWS = """
<ul> <ul>
<li>ScoDoc 9.3</li> <li>ScoDoc 9.3</li>
<ul> <ul>
<li>Nouvelle API REST pour connecter ScoDoc à d'autres applications<li>
<li>Module de gestion des relations avec les entreprises</li>
<li>Prise en charge des parcours BUT</li> <li>Prise en charge des parcours BUT</li>
<li>Association des UEs aux compétences du référentiel</li> <li>Association des UEs aux compétences du référentiel</li>
<li>Jury BUT1</li> <li>Jury BUT1</li>
@ -21,7 +23,6 @@ SCONEWS = """
<ul> <ul>
<li>Tableau récap. complet pour BUT et autres formations.</li> <li>Tableau récap. complet pour BUT et autres formations.</li>
<li>Tableau état évaluations</li> <li>Tableau état évaluations</li>
<li>Version alpha du module "relations entreprises"</li>
<li>Export des trombinoscope en document docx</li> <li>Export des trombinoscope en document docx</li>
<li>Très nombreux correctifs</li> <li>Très nombreux correctifs</li>
</ul> </ul>

View File

@ -4,143 +4,97 @@
"""Exemple utilisation API ScoDoc 9 avec jeton obtenu par basic authentication """Exemple utilisation API ScoDoc 9 avec jeton obtenu par basic authentication
Usage:
Utilisation: créer les variables d'environnement: (indiquer les valeurs cd /opt/scodoc/tests/api
pour le serveur ScoDoc que vous voulez interroger) python -i exemple-api-basic.py
export SCODOC_URL="https://scodoc.xxx.net/"
export SCODOC_USER="xxx"
export SCODOC_PASSWD="xxx"
export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valide
(on peut aussi placer ces valeurs dans un fichier .env du répertoire tests/api).
Travail en cours. Pour utiliser l'API, (sur une base quelconque):
```
cd /opt/scodoc/tests/api
python -i exemple-api-basic.p
>>> admin_h = get_auth_headers("admin", "xxx")
>>> GET("/etudiant/etudid/14806", headers=admin_h)
```
Créer éventuellement un fichier `.env` dans /opt/scodoc/tests/api
avec la config du client API:
```
SCODOC_URL = "http://localhost:5000/"
API_USER = "admin"
API_PASSWORD = "test"
```
""" """
from dotenv import load_dotenv
import json
import os
import requests
import urllib3
from pprint import pprint as pp from pprint import pprint as pp
import sys
import urllib3
from setup_test_api import (
API_PASSWORD,
API_URL,
API_USER,
APIError,
CHECK_CERTIFICATE,
get_auth_headers,
GET,
POST_JSON,
SCODOC_URL,
)
# --- Lecture configuration (variables d'env ou .env)
try:
BASEDIR = os.path.abspath(os.path.dirname(__file__))
except NameError:
BASEDIR = "."
load_dotenv(os.path.join(BASEDIR, ".env")) if not CHECK_CERTIFICATE:
CHK_CERT = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) urllib3.disable_warnings()
SCODOC_URL = os.environ.get("SCODOC_URL") or "http://localhost:5000"
API_URL = SCODOC_URL + "/ScoDoc/api"
# Admin:
SCODOC_USER = os.environ["SCODOC_USER"]
SCODOC_PASSWORD = os.environ["SCODOC_PASSWORD"]
# Lecteur
SCODOC_USER_API_LECTEUR = os.environ["SCODOC_USER_API_LECTEUR"]
SCODOC_PASSWORD_API_LECTEUR = os.environ["SCODOC_PASSWORD_API_LECTEUR"]
print(f"SCODOC_URL={SCODOC_URL}") print(f"SCODOC_URL={SCODOC_URL}")
print(f"API URL={API_URL}") print(f"API URL={API_URL}")
# ---
if not CHK_CERT:
urllib3.disable_warnings()
HEADERS = get_auth_headers(API_USER, API_PASSWORD)
class ScoError(Exception): departements = GET("/departements", headers=HEADERS)
pass pp(departements)
def GET(path: str, headers={}, errmsg=None, dept=None):
"""Get and returns as JSON"""
if dept:
url = SCODOC_URL + f"/ScoDoc/{dept}/api" + path
else:
url = API_URL + path
r = requests.get(url, headers=headers or HEADERS, verify=CHK_CERT)
if r.status_code != 200:
raise ScoError(errmsg or f"""erreur status={r.status_code} !\n{r.text}""")
return r.json() # decode la reponse JSON
def POST(path: str, data: dict = {}, headers={}, errmsg=None):
"""Post"""
r = requests.post(
API_URL + path,
data=data,
headers=headers or HEADERS,
verify=CHK_CERT,
)
if r.status_code != 200:
raise ScoError(errmsg or f"erreur status={r.status_code} !\n{r.text}")
return r.json() # decode la reponse JSON
def POST_JSON(path: str, data: dict = {}, headers={}, errmsg=None):
"""Post"""
r = requests.post(
API_URL + path,
json=data,
headers=headers or HEADERS,
verify=CHK_CERT,
)
if r.status_code != 200:
raise ScoError(errmsg or f"erreur status={r.status_code} !\n{r.text}")
return r.json() # decode la reponse JSON
def GET_TOKEN(user, password):
"Obtention du jeton (token)"
r = requests.post(API_URL + "/tokens", auth=(user, password))
assert r.status_code == 200
token = r.json()["token"]
return {"Authorization": f"Bearer {token}"}
HEADERS = GET_TOKEN(SCODOC_USER, SCODOC_PASSWORD)
HEADERS_USER = GET_TOKEN(SCODOC_USER_API_LECTEUR, SCODOC_PASSWORD_API_LECTEUR)
r = requests.get(API_URL + "/departements", headers=HEADERS, verify=CHK_CERT)
if r.status_code != 200:
raise ScoError("erreur de connexion: vérifier adresse et identifiants")
pp(r.json())
# Liste de tous les étudiants en cours (de tous les depts) # Liste de tous les étudiants en cours (de tous les depts)
r = requests.get(API_URL + "/etudiants/courant", headers=HEADERS, verify=CHK_CERT) etuds = GET("/etudiants/courants", headers=HEADERS)
if r.status_code != 200:
raise ScoError("erreur de connexion: vérifier adresse et identifiants")
print(f"{len(r.json())} étudiants courants") print(f"{len(etuds)} étudiants courants")
raise Exception("arret en mode interactif")
# ---------------- DIVERS ESSAIS EN MODE INTERACTIF
# ---------------- A ADAPTER A VOS BESOINS
# Bulletin d'un BUT # Bulletin d'un BUT
formsemestre_id = 1063 # A adapter formsemestre_id = 1063 # A adapter
etudid = 16450 etudid = 16450
bul = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin") bul = GET(
f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin",
headers=HEADERS,
)
# d'un DUT # d'un DUT
formsemestre_id = 1062 # A adapter formsemestre_id = 1062 # A adapter
etudid = 16309 etudid = 16309
bul_dut = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin") bul_dut = GET(
f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin",
headers=HEADERS,
)
# Infos sur un étudiant # Infos sur un étudiant
etudid = 3561 etudid = 3561
code_nip = "11303314" code_nip = "11303314"
etud = GET(f"/etudiant/etudid/{etudid}") etud = GET(f"/etudiant/etudid/{etudid}", headers=HEADERS)
print(etud) print(etud)
etud = GET(f"/etudiant/nip/{code_nip}") etud = GET(f"/etudiant/nip/{code_nip}", headers=HEADERS)
print(etud) print(etud)
sems = GET(f"/etudiant/etudid/{etudid}/formsemestres") sems = GET(f"/etudiant/etudid/{etudid}/formsemestres", headers=HEADERS)
print("\n".join([s["titre_num"] for s in sems])) print("\n".join([s["titre_num"] for s in sems]))
sems = GET(f"/etudiant/nip/{code_nip}/formsemestres") sems = GET(f"/etudiant/nip/{code_nip}/formsemestres", headers=HEADERS)
print("\n".join([s["titre_num"] for s in sems])) print("\n".join([s["titre_num"] for s in sems]))
# Evaluation # Evaluation
@ -148,35 +102,72 @@ evals = GET("/evaluations/1")
# Partitions d'un BUT # Partitions d'un BUT
formsemestre_id = 1063 # A adapter formsemestre_id = 1063 # A adapter
partitions = GET(f"/formsemestre/{formsemestre_id}/partitions") partitions = GET(f"/formsemestre/{formsemestre_id}/partitions", headers=HEADERS)
print(partitions) print(partitions)
pid = partitions[1]["id"] pid = partitions[1]["id"]
partition = GET(f"/partition/{pid}") partition = GET(f"/partition/{pid}", headers=HEADERS)
print(partition) print(partition)
group_id = partition["groups"][0]["id"] group_id = partition["groups"][0]["id"]
etuds = GET(f"/group/{group_id}/etudiants") etuds = GET(f"/group/{group_id}/etudiants", headers=HEADERS)
print(f"{len(etuds)} étudiants") print(f"{len(etuds)} étudiants")
pp(etuds[1]) pp(etuds[1])
etuds_dem = GET(f"/group/{group_id}/etudiants/query?etat=D") etuds_dem = GET(f"/group/{group_id}/etudiants/query?etat=D", headers=HEADERS)
print(f"{len(etuds_dem)} étudiants") print(f"{len(etuds_dem)} étudiants")
etudid = 16650 etudid = 16650
group_id = 5315 group_id = 5315
POST(f"/group/{group_id}/set_etudiant/{etudid}") POST(f"/group/{group_id}/set_etudiant/{etudid}", headers=HEADERS)
POST_JSON(f"/partition/{pid}/group/create", data={"group_name": "Omega10"}) POST_JSON(
partitions = GET(f"/formsemestre/{formsemestre_id}/partitions") f"/partition/{pid}/group/create", data={"group_name": "Omega10"}, headers=HEADERS
)
partitions = GET(f"/formsemestre/{formsemestre_id}/partitions", headers=HEADERS)
pp(partitions) pp(partitions)
POST_JSON(f"/group/5559/delete") POST_JSON(f"/group/5559/delete", headers=HEADERS)
POST_JSON(f"/group/5327/edit", data={"group_name": "TDXXX"}) POST_JSON(f"/group/5327/edit", data={"group_name": "TDXXX"}, headers=HEADERS)
# --------- XXX à passer en dans les tests unitaires # --------- Toutes les bulletins, un à un, et les décisions de jury d'un semestre
formsemestre_id = 911
etuds = GET(f"/formsemestre/{formsemestre_id}/etudiants", headers=admin_h)
etudid = 16450
bul = GET(
f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin",
headers=HEADERS,
)
for etud in etuds:
bul = GET(
f"/etudiant/etudid/{etud['id']}/formsemestre/{formsemestre_id}/bulletin",
headers=HEADERS,
)
sys.stdout.write(".")
sys.stdout.flush()
print("")
decisions = GET(f"/formsemestre/{formsemestre_id}/decisions_jury", headers=HEADERS)
# Decisions de jury des _tous_ les formsemestre, un à un, en partant de l'id le plus élevé
formsemestres = GET("/formsemestres/query", headers=HEADERS)
formsemestres.sort(key=lambda s: s["id"], reverse=1)
print(f"###### Testing {len(formsemestres)} formsemestres...")
for formsemestre in formsemestres:
print(formsemestre["session_id"])
try:
decisions = GET(
f"/formsemestre/{formsemestre['id']}/decisions_jury", headers=HEADERS
)
except APIError as exc:
if exc.payload.get("message") != "non implemente":
raise
decisions = []
print(f"{len(decisions)} decisions")
# --------- A été passé dans les tests unitaires:
# 0- Prend un étudiant au hasard dans le semestre # 0- Prend un étudiant au hasard dans le semestre
etud = GET(f"/formsemestre/{formsemestre_id}/etudiants")[10] etud = GET(f"/formsemestre/{formsemestre_id}/etudiants", headers=HEADERS)[10]
etudid = etud["id"] etudid = etud["id"]
# 1- Crée une partition, puis la change de nom # 1- Crée une partition, puis la change de nom
@ -188,30 +179,43 @@ partition_id = js["id"]
POST_JSON( POST_JSON(
f"/partition/{partition_id}/edit", f"/partition/{partition_id}/edit",
data={"partition_name": "PART1", "show_in_lists": True}, data={"partition_name": "PART1", "show_in_lists": True},
headers=HEADERS,
) )
# 2- Crée un groupe # 2- Crée un groupe
js = POST_JSON(f"/partition/{partition_id}/group/create", data={"group_name": "G1"}) js = POST_JSON(
f"/partition/{partition_id}/group/create",
data={"group_name": "G1"},
headers=HEADERS,
)
group_1 = js["id"] group_1 = js["id"]
# 3- Crée deux autres groupes # 3- Crée deux autres groupes
js = POST_JSON(f"/partition/{partition_id}/group/create", data={"group_name": "G2"}) js = POST_JSON(
js = POST_JSON(f"/partition/{partition_id}/group/create", data={"group_name": "G3"}) f"/partition/{partition_id}/group/create",
data={"group_name": "G2"},
headers=HEADERS,
)
js = POST_JSON(
f"/partition/{partition_id}/group/create",
data={"group_name": "G3"},
headers=HEADERS,
)
# 4- Affecte étudiant au groupe G1 # 4- Affecte étudiant au groupe G1
POST_JSON(f"/group/{group_1}/set_etudiant/{etudid}") POST_JSON(f"/group/{group_1}/set_etudiant/{etudid}", headers=HEADERS)
# 5- retire du groupe # 5- retire du groupe
POST_JSON(f"/group/{group_1}/remove_etudiant/{etudid}") POST_JSON(f"/group/{group_1}/remove_etudiant/{etudid}", headers=HEADERS)
# 6- affecte au groupe G2 # 6- affecte au groupe G2
partition = GET(f"/partition/{partition_id}") partition = GET(f"/partition/{partition_id}")
assert len(partition["groups"]) == 3 assert len(partition["groups"]) == 3
group_2 = [g for g in partition["groups"].values() if g["group_name"] == "G2"][0]["id"] group_2 = [g for g in partition["groups"].values() if g["group_name"] == "G2"][0]["id"]
POST_JSON(f"/group/{group_2}/set_etudiant/{etudid}") POST_JSON(f"/group/{group_2}/set_etudiant/{etudid}", headers=HEADERS)
# 7- Membres du groupe # 7- Membres du groupe
etuds_g2 = GET(f"/group/{group_2}/etudiants") etuds_g2 = GET(f"/group/{group_2}/etudiants", headers=HEADERS)
assert len(etuds_g2) == 1 assert len(etuds_g2) == 1
assert etuds_g2[0]["id"] == etudid assert etuds_g2[0]["id"] == etudid
@ -221,9 +225,13 @@ group_3 = [g for g in partition["groups"].values() if g["group_name"] == "G3"][0
POST_JSON( POST_JSON(
f"/partition/{partition_id}/groups/order", f"/partition/{partition_id}/groups/order",
data=[group_2, group_1, group_3], data=[group_2, group_1, group_3],
headers=HEADERS,
) )
new_groups = [g["id"] for g in GET(f"/partition/{partition_id}")["groups"].values()] new_groups = [
g["id"]
for g in GET(f"/partition/{partition_id}", headers=HEADERS)["groups"].values()
]
assert new_groups == [group_2, group_1, group_3] assert new_groups == [group_2, group_1, group_3]
# 9- Suppression # 9- Suppression
@ -248,23 +256,25 @@ POST_JSON(f"/partition/{partition_id}/delete")
POST_JSON( POST_JSON(
"/partition/2264/groups/order", "/partition/2264/groups/order",
data=[5563, 5562, 5561, 5560, 5558, 5557, 5316, 5315], data=[5563, 5562, 5561, 5560, 5558, 5557, 5316, 5315],
headers=HEADERS,
) )
POST_JSON( POST_JSON(
"/formsemestre/1063/partitions/order", "/formsemestre/1063/partitions/order",
data=[2264, 2263, 2265, 2266, 2267, 2372, 2378], data=[2264, 2263, 2265, 2266, 2267, 2372, 2378],
headers=HEADERS,
) )
GET(f"/partition/2264") GET(f"/partition/2264", headers=HEADERS)
# Recherche de formsemestres # Recherche de formsemestres
sems = GET(f"/formsemestres/query?etape_apo=V1RT&annee_scolaire=2021") sems = GET(f"/formsemestres/query?etape_apo=V1RT&annee_scolaire=2021", headers=HEADERS)
# Table récap: # Table récap:
pp(GET(f"/formsemestre/1063/resultats")[0]) pp(GET(f"/formsemestre/1063/resultats", headers=HEADERS)[0])
pp(GET(f"/formsemestre/880/resultats")[0]) pp(GET(f"/formsemestre/880/resultats", headers=HEADERS)[0])
# # sems est une liste de semestres (dictionnaires) # # sems est une liste de semestres (dictionnaires)
# for sem in sems: # for sem in sems:

177
tests/api/make_samples.py Normal file
View File

@ -0,0 +1,177 @@
#!/usr/bin/env python3
# -*- mode: python -*-
# -*- coding: utf-8 -*-
"""Construction des fichiers exemples pour la documentation.
Usage:
cd /opt/scodoc/tests/api
python make_samples.py
doit être exécutée immédiatement apres une initialisation de la base pour test API! (car dépendant des identifiants générés lors de la création des objets)
cd /opt/scodoc/tests/api
tools/create_database.sh --drop SCODOC_TEST_API && flask db upgrade &&flask sco-db-init --erase && flask init-test-database
Créer éventuellement un fichier `.env` dans /opt/scodoc/tests/api
avec la config du client API:
```
SCODOC_URL = "http://localhost:5000/"
```
Cet utilitaire prend en donnée le fichier de nom `samples.csv` contenant la description des exemples (séparés par une tabulation (\t), une ligne par exemple)
* Le nom de l'exemple donne le nom du fichier généré (nom_exemple => nom_exemple.json.md). plusieurs lignes peuvent partager le même nom. dans ce cas le fichier contiendra chacun des exemples
* l'url utilisée
* la permission nécessaire (par défaut ScoView)
* la méthode GET,POST à utiliser (si commence par #, la ligne est ignorée)
* les arguments éventuel (en cas de POST): une chaîne de caractère selon json
Implémentation:
Le code complète une structure de données (Samples) qui est un dictionnaire de set (indicé par le nom des exemple.
Chacun des éléments du set est un exemple (Sample)
Quand la structure est complète, on génére tous les fichiers textes
- nom de l exemple
- un ou plusieurs exemples avec pour chaucn
- l url utilisée
- les arguments éventuels
- le résultat
Le tout mis en forme au format markdown et rangé dans le répertoire DATA_DIR (/tmp/samples) qui est créé ou écrasé si déjà existant
TODO: ajouter un argument au script permettant de ne générer qu'un seul fichier (exemple: `python make_samples.py nom_exemple`)
"""
import os
import shutil
from collections import defaultdict
from pprint import pprint as pp
from pprint import pformat as pf
import urllib3
import json
from setup_test_api import (
API_PASSWORD,
API_URL,
API_USER,
APIError,
CHECK_CERTIFICATE,
get_auth_headers,
GET,
POST_JSON,
SCODOC_URL,
)
DATA_DIR = "/tmp/samples/"
class Sample:
def __init__(self, url, method="GET", permission="ScoView", content=None):
self.content = content
self.permission = permission
self.url = url
self.method = method
self.result = None
if permission == "ScoView":
HEADERS = get_auth_headers("test", "test")
elif permission == "ScoSuperAdmin":
HEADERS = get_auth_headers("admin_api", "admin_api")
elif permission == "ScoUsersAdmin":
HEADERS = get_auth_headers("admin_api", "admin_api")
else:
raise Exception(f"Bad permission : {permission}")
if self.method == "GET":
self.result = GET(self.url, HEADERS)
elif self.method == "POST":
if self.content == "":
self.result = POST_JSON(self.url, headers=HEADERS)
else:
HEADERS["Content-Type"] = "application/json ; charset=utf-8"
self.result = POST_JSON(self.url, json.loads(self.content), HEADERS)
elif self.method[0] != "#":
raise Exception(f"Bad method : {self.method}")
else: # method begin with # => comment
print(" pass")
self.shorten()
file = open(f"sample_TEST.json.md", "tw")
self.dump(file)
file.close()
def _shorten(self, item):
if isinstance(item, list):
return [self._shorten(child) for child in item[:2]]
return item
def shorten(self):
self.result = self._shorten(self.result)
def pp(self):
print(f"------ url: {self.url}")
print(f"method: {self.method}")
print(f"content: {self.content}")
print(f"permission: {self.permission}")
pp(self.result, indent=4)
def dump(self, file):
file.write(f"#### {self.method} {self.url}\n")
if len(self.content) > 0:
file.write(f"> `Content-Type: application/json`\n")
file.write(f"> \n")
file.write(f"> `{self.content}`\n\n")
file.write("```json\n")
file.write(json.dumps(self.result, indent=4))
file.write("\n```\n\n")
class Samples:
def __init__(self):
self.entries = defaultdict(lambda: set())
def add_sample(self, entry, url, method="GET", permission="ScoView", content=None):
show_content = "" if content == "" else f": '{content}'"
print(f"{entry:50} {method:5} {url:50} {show_content}")
sample = Sample(url, method, permission, content)
self.entries[entry].add(sample)
def pp(self):
for entry, samples in self.entries.items():
print(f"=== {entry}")
for sample in samples:
sample.pp()
def dump(self):
for entry, samples in self.entries.items():
file = open(f"{DATA_DIR}sample_{entry}.json.md", "tw")
file.write(f"### {entry}\n\n")
for sample in samples:
sample.dump(file)
file.close()
def make_samples():
if os.path.exists(DATA_DIR):
if not os.path.isdir(DATA_DIR):
raise f"{DATA_DIR} existe déjà et n'est pas un répertoire"
else:
# DATA_DIR existe déjà - effacer et recréer
shutil.rmtree(DATA_DIR)
os.mkdir(DATA_DIR)
else:
os.mkdir("/tmp/samples")
samples = Samples()
# samples.pp()
with open("samples.csv") as f:
L = [x[:-1].split("\t") for x in f]
for line in L[1:]:
entry_name = line[0]
url = line[1]
permission = line[2] if line[2] != "" else "ScoView"
method = line[3] if line[3] != "" else "GET"
content = line[4]
samples.add_sample(entry_name, url, method, permission, content)
samples.dump()
return samples
if not CHECK_CERTIFICATE:
urllib3.disable_warnings()
make_samples()

77
tests/api/samples.csv Normal file
View File

@ -0,0 +1,77 @@
reference url permission method content
departements /departements GET
departements-ids /departements_ids GET
departement /departement/TAPI GET
departement /departement/id/1 GET
departement-etudiants /departement/TAPI/etudiants GET
departement-etudiants /departement/id/1/etudiants GET
departement-formsemestres_ids /departement/TAPI/formsemestres_ids GET
departement-formsemestres_ids /departement/id/1/formsemestres_ids GET
departement-formsemestres-courants /departement/TAPI/formsemestres_courants GET
departement-formsemestres-courants /departement/id/1/formsemestres_courants GET
departement-create /departement/create ScoSuperAdmin POST {"acronym": "NEWONE" , "visible": true}
departement-edit /departement/NEWONE/edit ScoSuperAdmin POST {"visible": false}
departement-delete /departement/NEWONE/delete ScoSuperAdmin POST
etudiants-courants /etudiants/courants GET
etudiants-courants /etudiants/courants/long GET
etudiant /etudiant/etudid/11 GET
etudiant /etudiant/nip/11 GET
etudiant /etudiant/ine/INE11 GET
etudiants-clef /etudiants/etudid/11 GET
etudiants-clef /etudiants/ine/INE11 GET
etudiants-clef /etudiants/nip/11 GET
etudiant-formsemestres /etudiant/etudid/11/formsemestres GET
etudiant-formsemestres /etudiant/ine/INE11/formsemestres GET
etudiant_formsemestres /etudiant/nip/11/formsemestres GET
etudiant-formsemestre-bulletin /etudiant/etudid/11/formsemestre/1/bulletin GET
etudiant-formsemestre-bulletin /etudiant/ine/INE11/formsemestre/1/bulletin GET
etudiant-formsemestre-bulletin /etudiant/nip/11/formsemestre/1/bulletin GET
etudiant-formsemestre-groups /etudiant/etudid/11/formsemestre/1/groups GET
formations /formations GET
formations_ids /formations_ids GET
formation /formation/1 GET
formation-export /formation/1/export GET
formation-export /formation/1/export_with_ids GET
formation-referentiel_competences /formation/1/referentiel_competences GET
moduleimpl /moduleimpl/1 GET
formsemestre /formsemestre/1 GET
formsemestres-query /formsemestres/query?annee_scolaire=2022&etape_apo=A2 GET
formsemestre-bulletins /formsemestre/1/bulletins GET
formsemestre-programme /formsemestre/1/programme GET
formsemestre-etudiants /formsemestre/1/etudiants GET
formsemestre-etudiants-query /formsemestre/1/etudiants/query?etat=D GET
formsemestre-etat_evals /formsemestre/1/etat_evals GET
formsemestre-resultats /formsemestre/1/resultats GET
formsemestre-decisions_jury /formsemestre/1/decisions_jury GET
formsemestre-partitions /formsemestre/1/partitions GET
partition /partition/1 GET
group-etudiants /group/1/etudiants GET
group-etudiants-query /group/1/etudiants/query?etat=D GET
moduleimpl-evaluations /moduleimpl/1/evaluations GET
evaluation-notes /evaluation/1/notes GET
user /user/1 GET
users-query /users/query?starts_with=u_ GET
permissions /permissions GET
roles /roles GET
role /role/Observateur GET
group-set_etudiant /group/1/set_etudiant/10 ScoSuperAdmin POST
group-remove_etudiant /group/1/remove_etudiant/10 ScoSuperAdmin POST
partition-group-create /partition/1/group/create ScoSuperAdmin POST {"group_name": "NEW_GROUP"}
group-edit /group/2/edit ScoSuperAdmin POST {"group_name": "NEW_GROUP2"}
group-delete /group/2/delete ScoSuperAdmin POST
formsemestre-partition-create /formsemestre/1/partition/create ScoSuperAdmin POST {"partition_name": "PART"}
formsemestre-partitions-order /formsemestre/1/partitions/order ScoSuperAdmin POST [ 1 ]
partition-edit /partition/1/edit ScoSuperAdmin POST {"partition_name":"P2BIS", "numero":3,"bul_show_rank":true,"show_in_lists":false, "groups_editable":true}
partition-remove_etudiant /partition/2/remove_etudiant/10 ScoSuperAdmin POST
partition-groups-order /partition/1/groups/order ScoSuperAdmin POST [ 1 ]
partition-delete /partition/2/delete ScoSuperAdmin POST
user-create /user/create ScoSuperAdmin POST {"user_name": "alain", "dept": null, "nom": "alain", "prenom": "bruno", "active": true }
user-edit /user/10/edit ScoSuperAdmin POST { "dept": "TAPI", "nom": "alain2", "prenom": "bruno2", "active": false }
user-role-add /user/10/role/Observateur/add ScoSuperAdmin POST
user-role-remove /user/10/role/Observateur/remove ScoSuperAdmin POST
role-create /role/create/customRole ScoSuperAdmin POST {"permissions": ["ScoView", "ScoUsersView"]}
role-remove_permission /role/customRole/remove_permission/ScoUsersView ScoSuperAdmin POST
role-add_permission /role/customRole/add_permission/ScoUsersView ScoSuperAdmin POST
role-edit /role/customRole/edit ScoSuperAdmin POST { "name" : "LaveurDeVitres", "permissions" : [ "ScoView", "APIView" ] }
role-edit /role/customRole/edit ScoSuperAdmin POST { "name" : "LaveurDeVitres", "permissions" : [ "ScoView", "APIView" ] }
role-delete /role/customRole/delete ScoSuperAdmin POST
Can't render this file because it contains an unexpected character in line 12 and column 60.

View File

@ -18,10 +18,15 @@ import requests
from dotenv import load_dotenv from dotenv import load_dotenv
import pytest import pytest
# --- Lecture configuration (variables d'env ou .env)
try:
BASEDIR = os.path.abspath(os.path.dirname(__file__))
except NameError:
BASEDIR = "/opt/scodoc/tests/api" BASEDIR = "/opt/scodoc/tests/api"
load_dotenv(os.path.join(BASEDIR, ".env")) load_dotenv(os.path.join(BASEDIR, ".env"))
CHECK_CERTIFICATE = bool(os.environ.get("CHECK_CERTIFICATE", False)) CHECK_CERTIFICATE = bool(os.environ.get("CHECK_CERTIFICATE", False))
SCODOC_URL = os.environ["SCODOC_URL"] SCODOC_URL = os.environ["SCODOC_URL"] or "http://localhost:5000"
API_URL = SCODOC_URL + "/ScoDoc/api" API_URL = SCODOC_URL + "/ScoDoc/api"
API_USER = os.environ.get("API_USER", "test") API_USER = os.environ.get("API_USER", "test")
API_PASSWORD = os.environ.get("API_PASSWD", "test") API_PASSWORD = os.environ.get("API_PASSWD", "test")
@ -33,7 +38,9 @@ print(f"API URL={API_URL}")
class APIError(Exception): class APIError(Exception):
pass def __init__(self, message: str = "", payload=None):
self.message = message
self.payload = payload or {}
def get_auth_headers(user, password) -> dict: def get_auth_headers(user, password) -> dict:
@ -65,7 +72,7 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None):
url = API_URL + path url = API_URL + path
r = requests.get(url, headers=headers or {}, verify=CHECK_CERTIFICATE) r = requests.get(url, headers=headers or {}, verify=CHECK_CERTIFICATE)
if r.status_code != 200: if r.status_code != 200:
raise APIError(errmsg or f"""erreur status={r.status_code} !\n{r.text}""") raise APIError(errmsg or f"""erreur status={r.status_code} !""", r.json())
return r.json() # decode la reponse JSON return r.json() # decode la reponse JSON
@ -78,5 +85,5 @@ def POST_JSON(path: str, data: dict = {}, headers: dict = None, errmsg=None):
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
if r.status_code != 200: if r.status_code != 200:
raise APIError(errmsg or f"erreur status={r.status_code} !\n{r.text}") raise APIError(errmsg or f"erreur status={r.status_code} !", r.json())
return r.json() # decode la reponse JSON return r.json() # decode la reponse JSON

View File

@ -515,22 +515,28 @@ def test_formsemestre_etudiants(api_headers):
assert isinstance(group["group_id"], int) assert isinstance(group["group_id"], int)
assert group["group_name"] is None or isinstance(group["group_name"], int) assert group["group_name"] is None or isinstance(group["group_name"], int)
## Avec query:
etuds_query = GET(
f"/formsemestre/{formsemestre_id}/etudiants/query", headers=api_headers
)
assert etuds_query == etuds
### actifs ### actifs
etuds_actifs = GET( etuds_actifs = GET(
f"/formsemestre/{formsemestre_id}/etudiants/actifs", headers=api_headers f"/formsemestre/{formsemestre_id}/etudiants/query?etat=I", headers=api_headers
) )
assert isinstance(etuds_actifs, list) assert isinstance(etuds_actifs, list)
### démissionnaires ### démissionnaires
etuds_dem = GET( etuds_dem = GET(
f"/formsemestre/{formsemestre_id}/etudiants/demissionnaires", f"/formsemestre/{formsemestre_id}/etudiants/query?etat=D",
headers=api_headers, headers=api_headers,
) )
assert isinstance(etuds_dem, list) assert isinstance(etuds_dem, list)
### défaillants ### défaillants
etuds_def = GET( etuds_def = GET(
f"/formsemestre/{formsemestre_id}/etudiants/defaillants", headers=api_headers f"/formsemestre/{formsemestre_id}/etudiants/query?etat=DEF", headers=api_headers
) )
assert isinstance(etuds_def, list) assert isinstance(etuds_def, list)

View File

@ -134,10 +134,10 @@ def test_etud_in_group(api_headers):
- /group/<int:group_id>/etudiants/query?etat=<string:etat> - /group/<int:group_id>/etudiants/query?etat=<string:etat>
""" """
group_id = 1 group_id = 1
etudiants = GET(f"/group/{group_id}/etudiants", headers=api_headers) etuds = GET(f"/group/{group_id}/etudiants", headers=api_headers)
assert isinstance(etudiants, list) assert isinstance(etuds, list)
for etud in etudiants: for etud in etuds:
assert verify_fields(etud, PARTITION_GROUPS_ETUD_FIELDS) assert verify_fields(etud, PARTITION_GROUPS_ETUD_FIELDS)
assert isinstance(etud["id"], int) assert isinstance(etud["id"], int)
assert isinstance(etud["dept_id"], int) assert isinstance(etud["dept_id"], int)
@ -148,12 +148,14 @@ def test_etud_in_group(api_headers):
assert isinstance(etud["code_nip"], str) assert isinstance(etud["code_nip"], str)
assert isinstance(etud["code_ine"], str) assert isinstance(etud["code_ine"], str)
# query sans filtre:
etuds_query = GET(f"/group/{group_id}/etudiants/query", headers=api_headers)
assert etuds_query == etuds
etat = "I" etat = "I"
etudiants = GET( etuds = GET(f"/group/{group_id}/etudiants/query?etat={etat}", headers=api_headers)
f"/group/{group_id}/etudiants/query?etat={etat}", headers=api_headers assert isinstance(etuds, list)
) for etud in etuds:
assert isinstance(etudiants, list)
for etud in etudiants:
assert verify_fields(etud, PARTITION_GROUPS_ETUD_FIELDS) assert verify_fields(etud, PARTITION_GROUPS_ETUD_FIELDS)
assert isinstance(etud["id"], int) assert isinstance(etud["id"], int)
assert isinstance(etud["dept_id"], int) assert isinstance(etud["dept_id"], int)

View File

@ -94,7 +94,7 @@ def test_edit_users(api_admin_headers):
def test_roles(api_admin_headers): def test_roles(api_admin_headers):
""" """
Routes: /user/create Routes: /user/create
/user/edit/<int:uid> /user/<int:uid>/edit
""" """
admin_h = api_admin_headers admin_h = api_admin_headers
user = POST_JSON( user = POST_JSON(
@ -105,7 +105,7 @@ def test_roles(api_admin_headers):
uid = user["id"] uid = user["id"]
ans = POST_JSON(f"/user/{uid}/role/Secr/add", headers=admin_h) ans = POST_JSON(f"/user/{uid}/role/Secr/add", headers=admin_h)
assert ans["user_name"] == "test_roles" assert ans["user_name"] == "test_roles"
role = POST_JSON("/role/Test_X/create", headers=admin_h) role = POST_JSON("/role/create/Test_X", headers=admin_h)
assert role["role_name"] == "Test_X" assert role["role_name"] == "Test_X"
assert role["permissions"] == [] assert role["permissions"] == []
role = GET("/role/Test_X", headers=admin_h) role = GET("/role/Test_X", headers=admin_h)