API evaluation: create avec poids, /delete + tests unitaires + corrections

This commit is contained in:
Emmanuel Viennet 2023-08-26 16:34:56 +02:00
parent 94f857665c
commit 1d3726a4cd
17 changed files with 304 additions and 124 deletions

View File

@ -20,5 +20,5 @@ ignored-classes=Permission,
# supports qualified module names, as well as Unix pattern matching. # supports qualified module names, as well as Unix pattern matching.
ignored-modules=entreprises ignored-modules=entreprises
good-names=d,e,f,i,j,k,nt,t,u,ue,v,x,y,z,H,F good-names=d,e,f,i,j,k,n,nt,t,u,ue,v,x,y,z,H,F

View File

@ -5,7 +5,7 @@ from flask import Blueprint
from flask import request, g from flask import request, g
from app import db from app import db
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoException from app.scodoc.sco_exceptions import AccessDenied, ScoException
api_bp = Blueprint("api", __name__) api_bp = Blueprint("api", __name__)
api_web_bp = Blueprint("apiweb", __name__) api_web_bp = Blueprint("apiweb", __name__)
@ -15,12 +15,24 @@ API_CLIENT_ERROR = 400 # erreur dans les paramètres fournis par le client
@api_bp.errorhandler(ScoException) @api_bp.errorhandler(ScoException)
@api_web_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"
return scu.json_error(404, message=str(e)) return scu.json_error(404, message=str(e))
@api_bp.errorhandler(AccessDenied)
@api_web_bp.errorhandler(AccessDenied)
def permission_denied_error_handler(exc):
"""
Renvoie message d'erreur pour l'erreur 403
"""
return scu.json_error(
403, f"operation non autorisee ({exc.args[0] if exc.args else ''})"
)
def requested_format(default_format="json", allowed_formats=None): def requested_format(default_format="json", allowed_formats=None):
"""Extract required format from query string. """Extract required format from query string.
* default value is json. A list of allowed formats may be provided * default value is json. A list of allowed formats may be provided
@ -65,6 +77,7 @@ from app.api import (
jury, jury,
justificatifs, justificatifs,
logos, logos,
moduleimpl,
partitions, partitions,
semset, semset,
users, users,

View File

@ -12,7 +12,7 @@ from flask_json import as_json
from flask_login import current_user, login_required from flask_login import current_user, login_required
import app import app
from app import db from app import log, db
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.models import Evaluation, ModuleImpl, FormSemestre from app.models import Evaluation, ModuleImpl, FormSemestre
@ -28,7 +28,7 @@ import app.scodoc.sco_utils as scu
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def evaluation(evaluation_id: int): def get_evaluation(evaluation_id: int):
"""Description d'une évaluation. """Description d'une évaluation.
{ {
@ -203,13 +203,7 @@ def evaluation_create(moduleimpl_id: int):
"visibulletin" : boolean , //default true "visibulletin" : boolean , //default true
"publish_incomplete" : boolean , //default false "publish_incomplete" : boolean , //default false
"coefficient" : float, // si non spécifié, 1.0 "coefficient" : float, // si non spécifié, 1.0
"poids" : [ { "poids" : { ue_id : poids } // optionnel
"ue_id": int,
"poids": float
},
...
] // si non spécifié, tous les poids à 1.0
}
} }
Result: l'évaluation créée. Result: l'évaluation créée.
""" """
@ -220,8 +214,6 @@ def evaluation_create(moduleimpl_id: int):
try: try:
evaluation = Evaluation.create(moduleimpl=moduleimpl, **data) evaluation = Evaluation.create(moduleimpl=moduleimpl, **data)
except AccessDenied:
return scu.json_error(403, "opération non autorisée (2)")
except ValueError: except ValueError:
return scu.json_error(400, "paramètre incorrect") return scu.json_error(400, "paramètre incorrect")
except ScoValueError as exc: except ScoValueError as exc:
@ -231,6 +223,27 @@ def evaluation_create(moduleimpl_id: int):
db.session.add(evaluation) db.session.add(evaluation)
db.session.commit() db.session.commit()
# Les poids vers les UEs:
poids = data.get("poids")
if poids is not None:
if not isinstance(poids, dict):
log("API error: canceling evaluation creation")
db.session.delete(evaluation)
db.session.commit()
return scu.json_error(
400, "paramètre de type incorrect (poids must be a dict)"
)
try:
evaluation.set_ue_poids_dict(data["poids"])
except ScoValueError as exc:
log("API error: canceling evaluation creation")
db.session.delete(evaluation)
db.session.commit()
return scu.json_error(
400,
f"erreur enregistrement des poids ({exc.args[0] if exc.args else ''})",
)
db.session.commit()
return evaluation.to_dict_api() return evaluation.to_dict_api()
@ -241,4 +254,24 @@ def evaluation_create(moduleimpl_id: int):
@permission_required(Permission.ScoEnsView) # permission gérée dans la fonction @permission_required(Permission.ScoEnsView) # permission gérée dans la fonction
@as_json @as_json
def evaluation_delete(evaluation_id: int): def evaluation_delete(evaluation_id: int):
pass """Suppression d'une évaluation.
Efface aussi toutes ses notes
"""
query = Evaluation.query.filter_by(id=evaluation_id)
if g.scodoc_dept:
query = (
query.join(ModuleImpl)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
)
evaluation = query.first_or_404()
dept = evaluation.moduleimpl.formsemestre.departement
app.set_sco_dept(dept.acronym)
if not evaluation.moduleimpl.can_edit_evaluation(current_user):
raise AccessDenied("evaluation_delete")
sco_saisie_notes.evaluation_suppress_alln(
evaluation_id=evaluation_id, dialog_confirmed=True
)
sco_evaluation_db.do_evaluation_delete(evaluation_id)
return "ok"

View File

@ -21,8 +21,6 @@ from app.models import (
ApcNiveau, ApcNiveau,
ApcParcours, ApcParcours,
Formation, Formation,
FormSemestre,
ModuleImpl,
UniteEns, UniteEns,
) )
from app.scodoc import sco_formations from app.scodoc import sco_formations
@ -249,54 +247,6 @@ def referentiel_competences(formation_id: int):
return formation.referentiel_competence.to_dict() return formation.referentiel_competence.to_dict()
@bp.route("/moduleimpl/<int:moduleimpl_id>")
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def moduleimpl(moduleimpl_id: int):
"""
Retourne un moduleimpl en fonction de son id
moduleimpl_id : l'id d'un moduleimpl
Exemple de résultat :
{
"id": 1,
"formsemestre_id": 1,
"module_id": 1,
"responsable_id": 2,
"moduleimpl_id": 1,
"ens": [],
"module": {
"heures_tp": 0,
"code_apogee": "",
"titre": "Initiation aux réseaux informatiques",
"coefficient": 1,
"module_type": 2,
"id": 1,
"ects": null,
"abbrev": "Init aux réseaux informatiques",
"ue_id": 1,
"code": "R101",
"formation_id": 1,
"heures_cours": 0,
"matiere_id": 1,
"heures_td": 0,
"semestre_id": 1,
"numero": 10,
"module_id": 1
}
}
"""
query = ModuleImpl.query.filter_by(id=moduleimpl_id)
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
modimpl: ModuleImpl = query.first_or_404()
return modimpl.to_dict(convert_objects=True)
@bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"]) @bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
@api_web_bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"]) @api_web_bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
@login_required @login_required

View File

@ -212,7 +212,7 @@ def bulletins(formsemestre_id: int, version: str = "long"):
@as_json @as_json
def formsemestre_programme(formsemestre_id: int): def formsemestre_programme(formsemestre_id: int):
""" """
Retourne la liste des Ues, ressources et SAE d'un semestre Retourne la liste des UEs, ressources et SAEs d'un semestre
formsemestre_id : l'id d'un formsemestre formsemestre_id : l'id d'un formsemestre

69
app/api/moduleimpl.py Normal file
View File

@ -0,0 +1,69 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""
ScoDoc 9 API : accès aux moduleimpl
"""
from flask import g
from flask_json import as_json
from flask_login import login_required
from app.api import api_bp as bp, api_web_bp
from app.decorators import scodoc, permission_required
from app.models import (
FormSemestre,
ModuleImpl,
)
from app.scodoc.sco_permissions import Permission
@bp.route("/moduleimpl/<int:moduleimpl_id>")
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def moduleimpl(moduleimpl_id: int):
"""
Retourne un moduleimpl en fonction de son id
moduleimpl_id : l'id d'un moduleimpl
Exemple de résultat :
{
"id": 1,
"formsemestre_id": 1,
"module_id": 1,
"responsable_id": 2,
"moduleimpl_id": 1,
"ens": [],
"module": {
"heures_tp": 0,
"code_apogee": "",
"titre": "Initiation aux réseaux informatiques",
"coefficient": 1,
"module_type": 2,
"id": 1,
"ects": null,
"abbrev": "Init aux réseaux informatiques",
"ue_id": 1,
"code": "R101",
"formation_id": 1,
"heures_cours": 0,
"matiere_id": 1,
"heures_td": 0,
"semestre_id": 1,
"numero": 10,
"module_id": 1
}
}
"""
query = ModuleImpl.query.filter_by(id=moduleimpl_id)
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
modimpl: ModuleImpl = query.first_or_404()
return modimpl.to_dict(convert_objects=True)

View File

@ -53,8 +53,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.store() self.store()
t2 = time.time() t2 = time.time()
log( log(
f"""ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id f"""+++ ResultatsSemestreBUT: cached [{formsemestre.id
} ({(t1-t0):g}s +{(t2-t1):g}s)""" }] ({(t1-t0):g}s +{(t2-t1):g}s) +++"""
) )
def compute(self): def compute(self):

View File

@ -50,8 +50,8 @@ class ResultatsSemestreClassic(NotesTableCompat):
self.store() self.store()
t2 = time.time() t2 = time.time()
log( log(
f"""ResultatsSemestreClassic: cached formsemestre_id={ f"""+++ ResultatsSemestreClassic: cached formsemestre_id={
formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)""" formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s) +++"""
) )
# recalculé (aussi rapide que de les cacher) # recalculé (aussi rapide que de les cacher)
self.moy_min = self.etud_moy_gen.min() self.moy_min = self.etud_moy_gen.min()

View File

@ -9,7 +9,7 @@ from flask import g, url_for
from flask_login import current_user from flask_login import current_user
import sqlalchemy as sa import sqlalchemy as sa
from app import db from app import db, log
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.events import ScolarNews from app.models.events import ScolarNews
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
@ -79,7 +79,9 @@ class Evaluation(db.Model):
numero=None, numero=None,
**kw, # ceci pour absorber les éventuel arguments excedentaires **kw, # ceci pour absorber les éventuel arguments excedentaires
): ):
"""Create an evaluation. Check permission and all arguments.""" """Create an evaluation. Check permission and all arguments.
Ne crée pas les poids vers les UEs.
"""
if not moduleimpl.can_edit_evaluation(current_user): if not moduleimpl.can_edit_evaluation(current_user):
raise AccessDenied( raise AccessDenied(
f"Modification évaluation impossible pour {current_user.get_nomplogin()}" f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
@ -100,11 +102,12 @@ class Evaluation(db.Model):
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl.id, moduleimpl_id=moduleimpl.id,
) )
log(f"created evaluation in {moduleimpl.module.titre_str()}")
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
obj=moduleimpl.id, obj=moduleimpl.id,
text=f"""Création d'une évaluation dans <a href="{url}">{ text=f"""Création d'une évaluation dans <a href="{url}">{
moduleimpl.module.titre or '(module sans titre)'}</a>""", moduleimpl.module.titre_str()}</a>""",
url=url, url=url,
) )
return evaluation return evaluation
@ -275,17 +278,17 @@ class Evaluation(db.Model):
return f"{dt.hour}h" return f"{dt.hour}h"
if self.date_fin is None: if self.date_fin is None:
return ( return f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut())}"
)
if self.date_debut.date() == self.date_fin.date(): # même jour if self.date_debut.date() == self.date_fin.date(): # même jour
if self.date_debut.time() == self.date_fin.time(): if self.date_debut.time() == self.date_fin.time():
return f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut())}" return (
f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
)
return f"""le {self.date_debut.strftime('%d/%m/%Y')} de { return f"""le {self.date_debut.strftime('%d/%m/%Y')} de {
_h(self.date_debut())} à {_h(self.date_fin())}""" _h(self.date_debut)} à {_h(self.date_fin)}"""
# évaluation sur plus d'une journée # évaluation sur plus d'une journée
return f"""du {self.date_debut.strftime('%d/%m/%Y')} à { return f"""du {self.date_debut.strftime('%d/%m/%Y')} à {
_h(self.date_debut())} au {self.date_fin.strftime('%d/%m/%Y')} à {_h(self.date_fin())}""" _h(self.date_debut)} au {self.date_fin.strftime('%d/%m/%Y')} à {_h(self.date_fin)}"""
def heure_debut(self) -> str: def heure_debut(self) -> str:
"""L'heure de début (sans la date), en ISO. """L'heure de début (sans la date), en ISO.
@ -356,6 +359,8 @@ class Evaluation(db.Model):
L = [] L = []
for ue_id, poids in ue_poids_dict.items(): for ue_id, poids in ue_poids_dict.items():
ue = db.session.get(UniteEns, ue_id) ue = db.session.get(UniteEns, ue_id)
if ue is None:
raise ScoValueError("poids vers une UE inexistante")
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids) ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
L.append(ue_poids) L.append(ue_poids)
db.session.add(ue_poids) db.session.add(ue_poids)

View File

@ -153,6 +153,10 @@ class Module(db.Model):
""" """
return scu.ModuleType.get_abbrev(self.module_type) return scu.ModuleType.get_abbrev(self.module_type)
def titre_str(self) -> str:
"Identifiant du module à afficher : abbrev ou titre ou code"
return self.abbrev or self.titre or self.code
def sort_key_apc(self) -> tuple: def sort_key_apc(self) -> tuple:
"""Clé de tri pour avoir """Clé de tri pour avoir
présentation par type (res, sae), parcours, type, numéro présentation par type (res, sae), parcours, type, numéro

View File

@ -57,8 +57,10 @@ def _pe_view_sem_recap_form(formsemestre_id):
poursuites d'études. poursuites d'études.
<br> <br>
De nombreux aspects sont paramétrables: De nombreux aspects sont paramétrables:
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener"> <a href="https://scodoc.org/AvisPoursuiteEtudes"
voir la documentation</a>. target="_blank" rel="noopener noreferrer">
voir la documentation
</a>.
</p> </p>
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form" <form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form"
enctype="multipart/form-data"> enctype="multipart/form-data">

View File

@ -273,9 +273,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
if formsemestre_id is None: if formsemestre_id is None:
# clear all caches # clear all caches
log( log(f"--- invalidate_formsemestre: clearing all caches. pdfonly={pdfonly}---")
f"----- invalidate_formsemestre: clearing all caches. pdfonly={pdfonly}-----"
)
formsemestre_ids = [ formsemestre_ids = [
formsemestre.id formsemestre.id
for formsemestre in FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id) for formsemestre in FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id)
@ -285,7 +283,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
formsemestre_id formsemestre_id
] + sco_cursus.list_formsemestre_utilisateurs_uecap(formsemestre_id) ] + sco_cursus.list_formsemestre_utilisateurs_uecap(formsemestre_id)
log( log(
f"----- invalidate_formsemestre: clearing {formsemestre_ids}. pdfonly={pdfonly} -----" f"--- invalidate_formsemestre: clearing {formsemestre_ids}. pdfonly={pdfonly} ---"
) )
if not pdfonly: if not pdfonly:

View File

@ -133,7 +133,7 @@ def do_evaluation_delete(evaluation_id):
raise ScoValueError( raise ScoValueError(
"Impossible de supprimer cette évaluation: il reste des notes" "Impossible de supprimer cette évaluation: il reste des notes"
) )
log(f"deleting evaluation {evaluation}")
db.session.delete(evaluation) db.session.delete(evaluation)
db.session.commit() db.session.commit()

View File

@ -502,6 +502,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
""" """
] ]
# news # news
if nb_suppress:
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
obj=evaluation.moduleimpl.id, obj=evaluation.moduleimpl.id,

View File

@ -57,7 +57,7 @@
</div> </div>
{% endfor %} {% endfor %}
<div class="link"> <div class="link">
<a class="stdlink" target="_blank" href="{{ <a class="stdlink" target="_blank" rel="noopener noreferrer" href="{{
url_for('notes.refcomp_show', url_for('notes.refcomp_show',
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id ) scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id )
}}">référentiel de compétences</a> }}">référentiel de compétences</a>

View File

@ -108,6 +108,12 @@ def test_evaluation_create(api_admin_headers):
Test /moduleimpl/<int:moduleimpl_id>/evaluation/create Test /moduleimpl/<int:moduleimpl_id>/evaluation/create
""" """
moduleimpl_id = 20 moduleimpl_id = 20
# Nombre d'évaluations initial
evaluations = GET(
f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers
)
nb_evals = len(evaluations)
#
e = POST_JSON( e = POST_JSON(
f"/moduleimpl/{moduleimpl_id}/evaluation/create", f"/moduleimpl/{moduleimpl_id}/evaluation/create",
{"description": "eval test"}, {"description": "eval test"},
@ -123,6 +129,11 @@ def test_evaluation_create(api_admin_headers):
assert e["visibulletin"] is True assert e["visibulletin"] is True
assert e["publish_incomplete"] is False assert e["publish_incomplete"] is False
assert e["coefficient"] == 1.0 assert e["coefficient"] == 1.0
new_nb_evals = len(
GET(f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers)
)
assert new_nb_evals == nb_evals + 1
nb_evals = new_nb_evals
# Avec une erreur # Avec une erreur
check_failure_post( check_failure_post(
@ -131,7 +142,10 @@ def test_evaluation_create(api_admin_headers):
{"evaluation_type": 666}, {"evaluation_type": 666},
err="paramètre de type incorrect (invalid evaluation_type value)", err="paramètre de type incorrect (invalid evaluation_type value)",
) )
new_nb_evals = len(
GET(f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers)
)
assert new_nb_evals == nb_evals # inchangé
# Avec plein de valeurs # Avec plein de valeurs
data = { data = {
"coefficient": 12.0, "coefficient": 12.0,
@ -148,11 +162,72 @@ def test_evaluation_create(api_admin_headers):
data, data,
api_admin_headers, api_admin_headers,
) )
e2 = GET(f"/evaluation/{e['id']}", headers=api_admin_headers) e_ret = GET(f"/evaluation/{e['id']}", headers=api_admin_headers)
for k, v in data.items(): for k, v in data.items():
assert e2[k] == v, f"received '{e2[k]}'" assert e_ret[k] == v, f"received '{e_ret[k]}'"
# Avec des poids APC
# TODO nb_evals = len(
# - tester creation UE externe GET(f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers)
# - tester création base test et test API )
data.update(
{
"description": "eval test apc erreur",
"poids": {"666": 666.0}, # poids erroné: UE inexistante
}
)
check_failure_post(
f"/moduleimpl/{moduleimpl_id}/evaluation/create",
api_admin_headers,
data,
)
new_nb_evals = len(
GET(f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers)
)
assert new_nb_evals == nb_evals # inchangé
# Avec des poids absurdes
data.update({"description": "eval test apc erreur 2", "poids": "nimporte quoi"})
check_failure_post(
f"/moduleimpl/{moduleimpl_id}/evaluation/create",
api_admin_headers,
data,
)
new_nb_evals = len(
GET(f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers)
)
assert new_nb_evals == nb_evals # inchangé
# Avec de bons poids
# pour cela il nous faut les UEs de ce formsemestre
# sachant que l'on a moduleimpl
modimpl = GET(f"/moduleimpl/{moduleimpl_id}", headers=api_admin_headers)
formation = GET(
f"/formsemestre/{modimpl['formsemestre_id']}/programme",
headers=api_admin_headers,
)
ues = formation["ues"]
assert len(ues)
ue_ids = [ue["id"] for ue in ues]
poids = {ue_id: float(i) + 0.5 for i, ue_id in enumerate(ue_ids)}
data.update({"description": "eval avec poids", "poids": poids})
e = POST_JSON(
f"/moduleimpl/{moduleimpl_id}/evaluation/create",
data,
api_admin_headers,
)
assert e["poids"]
e_ret = GET(f"/evaluation/{e['id']}", headers=api_admin_headers)
assert e_ret["poids"] == e["poids"]
new_nb_evals = len(
GET(f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers)
)
assert new_nb_evals == nb_evals + 1
nb_evals = new_nb_evals
# Delete
ans = POST_JSON(
f"/evaluation/{e_ret['id']}/delete",
headers=api_admin_headers,
)
assert ans == "ok"
assert nb_evals - 1 == len(
GET(f"/moduleimpl/{moduleimpl_id}/evaluations", headers=api_admin_headers)
)

View File

@ -4,6 +4,21 @@
# Ce script lance un serveur scodoc sur le port 5555 # Ce script lance un serveur scodoc sur le port 5555
# attend qu'il soit initialisé puis lance les tests client API. # attend qu'il soit initialisé puis lance les tests client API.
# #
# On peut aussi le lancer avec l'option --dont-start-server
# auquel cas il utilise un serveur existant, qui doit avoir été lancé
# par ailleurs, par exemple via le script:
# tests/api/start_api_server.sh -p 5555
#
# Toutes les autres options sont passées telles qu'elles à pytest
#
# Exemples:
# - lancer tous les tests API: tools/test_api.sh
# - lancer tous les tests, en mode debug (arrêt pdb sur le 1er):
# tools/test_api.sh -x --pdb tests/api
# - lancer un module de test, en utilisant un server dev existant:
# tools/test_api.sh --dont-start-server -x --pdb tests/api/test_api_evaluations.py
#
#
# E. Viennet, Fev 2023 # E. Viennet, Fev 2023
cd /opt/scodoc cd /opt/scodoc
@ -16,28 +31,40 @@ SERVER_LOG=/tmp/test_api_server.log
export SCODOC_URL="http://localhost:${PORT}" export SCODOC_URL="http://localhost:${PORT}"
# ------- Check pas de serveur déjà lancé if [ "$1" = "--dont-start-server" ]
if nc -z localhost "$PORT"
then then
START_SERVER=0
shift
echo "Using existing scodoc server on port $PORT"
else
START_SERVER=1
fi
if [ "$START_SERVER" -eq 1 ]
then
# ------- Check pas de serveur déjà lancé
if nc -z localhost "$PORT"
then
fuser -v "$PORT"/tcp fuser -v "$PORT"/tcp
echo Server already running on port "$PORT" echo Server already running on port "$PORT"
echo You may want to try: fuser -k "$PORT"/tcp echo You may want to try: fuser -k "$PORT"/tcp
echo aborting tests echo aborting tests
exit 1 exit 1
fi fi
tests/api/start_api_server.sh -p "$PORT" &> "$SERVER_LOG" & tests/api/start_api_server.sh -p "$PORT" &> "$SERVER_LOG" &
pid=$! pid=$!
echo "ScoDoc test server logs are in $SERVER_LOG" echo "ScoDoc test server logs are in $SERVER_LOG"
# Wait for server setup # Wait for server setup
echo -n "Waiting for server" echo -n "Waiting for server"
while ! nc -z localhost "$PORT"; do while ! nc -z localhost "$PORT"; do
echo -n . echo -n .
sleep 1 sleep 1
done done
echo echo
echo Server PID "$pid" running on port "$PORT" echo Server PID "$pid" running on port "$PORT"
# ------------------ # ------------------
fi
if [ "$#" -eq 0 ] if [ "$#" -eq 0 ]
then then
@ -49,9 +76,12 @@ else
fi fi
# ------------------ # ------------------
echo "Killing server" if [ "$START_SERVER" -eq 1 ]
kill "$pid" then
fuser -k "$PORT"/tcp echo "Killing test server"
kill "$pid"
fuser -k "$PORT"/tcp
fi