forked from ScoDoc/ScoDoc
API evaluation: create avec poids, /delete + tests unitaires + corrections
This commit is contained in:
parent
94f857665c
commit
1d3726a4cd
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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
69
app/api/moduleimpl.py
Normal 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)
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -502,15 +502,16 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
|
|||||||
"""
|
"""
|
||||||
]
|
]
|
||||||
# news
|
# news
|
||||||
ScolarNews.add(
|
if nb_suppress:
|
||||||
typ=ScolarNews.NEWS_NOTE,
|
ScolarNews.add(
|
||||||
obj=evaluation.moduleimpl.id,
|
typ=ScolarNews.NEWS_NOTE,
|
||||||
text=f"""Suppression des notes d'une évaluation dans
|
obj=evaluation.moduleimpl.id,
|
||||||
<a class="stdlink" href="{status_url}"
|
text=f"""Suppression des notes d'une évaluation dans
|
||||||
>{evaluation.moduleimpl.module.titre or 'module sans titre'}</a>
|
<a class="stdlink" href="{status_url}"
|
||||||
""",
|
>{evaluation.moduleimpl.module.titre or 'module sans titre'}</a>
|
||||||
url=status_url,
|
""",
|
||||||
)
|
url=status_url,
|
||||||
|
)
|
||||||
|
|
||||||
return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer()
|
return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer()
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
|
)
|
||||||
|
@ -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
|
||||||
fuser -v "$PORT"/tcp
|
START_SERVER=0
|
||||||
echo Server already running on port "$PORT"
|
shift
|
||||||
echo You may want to try: fuser -k "$PORT"/tcp
|
echo "Using existing scodoc server on port $PORT"
|
||||||
echo aborting tests
|
else
|
||||||
exit 1
|
START_SERVER=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
tests/api/start_api_server.sh -p "$PORT" &> "$SERVER_LOG" &
|
if [ "$START_SERVER" -eq 1 ]
|
||||||
pid=$!
|
then
|
||||||
echo "ScoDoc test server logs are in $SERVER_LOG"
|
# ------- Check pas de serveur déjà lancé
|
||||||
# Wait for server setup
|
if nc -z localhost "$PORT"
|
||||||
echo -n "Waiting for server"
|
then
|
||||||
while ! nc -z localhost "$PORT"; do
|
fuser -v "$PORT"/tcp
|
||||||
echo -n .
|
echo Server already running on port "$PORT"
|
||||||
sleep 1
|
echo You may want to try: fuser -k "$PORT"/tcp
|
||||||
done
|
echo aborting tests
|
||||||
echo
|
exit 1
|
||||||
echo Server PID "$pid" running on port "$PORT"
|
fi
|
||||||
# ------------------
|
|
||||||
|
tests/api/start_api_server.sh -p "$PORT" &> "$SERVER_LOG" &
|
||||||
|
pid=$!
|
||||||
|
echo "ScoDoc test server logs are in $SERVER_LOG"
|
||||||
|
# Wait for server setup
|
||||||
|
echo -n "Waiting for server"
|
||||||
|
while ! nc -z localhost "$PORT"; do
|
||||||
|
echo -n .
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user