API: modification format evaluations, et ajout route /evaluation.

This commit is contained in:
Emmanuel Viennet 2022-11-01 11:19:28 +01:00
parent fdeaafe622
commit eb04984c2e
11 changed files with 125 additions and 106 deletions

View File

@ -1,5 +1,4 @@
i
# ScoDoc - Gestion de la scolarité - Version ScoDoc 9
# ScoDoc - Gestion de la scolarité - Version ScoDoc 9
(c) Emmanuel Viennet 1999 - 2022 (voir LICENCE.txt).
@ -9,39 +8,34 @@ Documentation utilisateur: <https://scodoc.org>
## Version ScoDoc 9
La version ScoDoc 9 est parue en septembre 2021.
Elle représente une évolution majeure du projet, maintenant basé sur
Flask (au lieu de Zope) et sur **python 3.9+**.
La version ScoDoc 9 est parue en septembre 2021. Elle représente une évolution
majeure du projet, maintenant basé sur Flask (au lieu de Zope) et sur **python
3.9+**.
La version 9.0 s'efforce de reproduire presque à l'identique le fonctionnement
La version 9.0 s'efforce de reproduire presque à l'identique le fonctionnement
de ScoDoc7, avec des composants logiciels différents (Debian 11, Python 3,
Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
### État actuel (nov 22)
- 9.3.x est en production
- le prochain jalon est 9.4. Voir branches sur gitea.
### État actuel (26 jan 22)
- 9.1.5x (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
- ancien module "Entreprises" (obsolète) et ajoute la gestion du BUT.
- 9.2 (branche dev92) est la version de développement.
### Lignes de commandes
Voir [https://scodoc.org/GuideConfig](le guide de configuration).
## Organisation des fichiers
L'installation comporte les fichiers de l'application, sous `/opt/scodoc/`, et
les fichiers locaux (archives, photos, configurations, logs) sous
`/opt/scodoc-data`. Par ailleurs, il y a évidemment les bases de données
postgresql et la configuration du système Linux.
postgresql et la configuration du système Linux.
### Fichiers locaux
Sous `/opt/scodoc-data`, fichiers et répertoires appartenant à l'utilisateur `scodoc`.
Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configuration sous
Sous `/opt/scodoc-data`, fichiers et répertoires appartenant à l'utilisateur `scodoc`.
Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configuration sous
`/opt/scodoc-data/config`.
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
@ -62,7 +56,7 @@ Principaux contenus:
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian11)).
Puis remplacer `/opt/scodoc` par un clone du git.
Puis remplacer `/opt/scodoc` par un clone du git.
sudo su
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
@ -76,7 +70,7 @@ Puis remplacer `/opt/scodoc` par un clone du git.
# Et donner ce répertoire à l'utilisateur scodoc:
chown -R scodoc.scodoc /opt/scodoc
Il faut ensuite installer l'environnement et le fichier de configuration:
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
@ -100,10 +94,10 @@ Avant le premier lancement, créer cette base ainsi:
flask db upgrade
Cette commande n'est nécessaire que la première fois (le contenu de la base
est effacé au début de chaque test, mais son schéma reste) et aussi si des
est effacé au début de chaque test, mais son schéma reste) et aussi si des
migrations (changements de schéma) ont eu lieu dans le code.
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
scripts de tests:
Lancer au préalable:
@ -117,24 +111,24 @@ Ou avec couverture (`pip install pytest-cov`)
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
#### Utilisation des tests unitaires pour initialiser la base de dev
On peut aussi utiliser les tests unitaires pour mettre la base
de données de développement dans un état connu, par exemple pour éviter de
recréer à la main étudiants et semestres quand on développe.
Il suffit de positionner une variable d'environnement indiquant la BD
utilisée par les tests:
On peut aussi utiliser les tests unitaires pour mettre la base de données de
développement dans un état connu, par exemple pour éviter de recréer à la main
étudiants et semestres quand on développe.
Il suffit de positionner une variable d'environnement indiquant la BD utilisée
par les tests:
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
normalement, par exemple:
normalement, par exemple:
pytest tests/unit/test_sco_basic.py
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins)
un utilisateur:
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
utilisateur:
flask user-password admin
@ -178,12 +172,10 @@ Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bie
pip install snakeviz
puis
puis
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
# Paquet Debian 11
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
@ -191,5 +183,4 @@ important est `postinst`qui se charge de configurer le système (install ou
upgrade de scodoc9).
La préparation d'une release se fait à l'aide du script
`tools/build_release.sh`.
`tools/build_release.sh`.

View File

@ -257,9 +257,9 @@ def dept_formsemestres_courants(acronym: str):
]
"""
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
faked_date = request.args.get("faked_date")
if faked_date:
test_date = datetime.fromisoformat(faked_date)
date_courante = request.args.get("date_courante")
if date_courante:
test_date = datetime.fromisoformat(date_courante)
else:
test_date = app.db.func.now()
# Les semestres en cours de ce département
@ -281,9 +281,9 @@ def dept_formsemestres_courants_by_id(dept_id: int):
"""
# Le département, spécifié par un id ou un acronyme
dept = Departement.query.get_or_404(dept_id)
faked_date = request.args.get("faked_date")
if faked_date:
test_date = datetime.fromisoformat(faked_date)
date_courante = request.args.get("date_courante")
if date_courante:
test_date = datetime.fromisoformat(date_courante)
else:
test_date = app.db.func.now()
# Les semestres en cours de ce département

View File

@ -76,9 +76,9 @@ def etudiants_courants(long=False):
"""
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
faked_date = request.args.get("faked_date")
if faked_date:
test_date = datetime.fromisoformat(faked_date)
date_courante = request.args.get("date_courante")
if date_courante:
test_date = datetime.fromisoformat(date_courante)
else:
test_date = app.db.func.now()
etuds = Identite.query.filter(

View File

@ -22,6 +22,44 @@ from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu
@bp.route("/evaluation/<int:evaluation_id>")
@api_web_bp.route("/evaluation/<int:evaluation_id>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def evaluation(evaluation_id: int):
"""Description d'une évaluation.
{
'coefficient': 1.0,
'date_debut': '2016-01-04T08:30:00',
'date_fin': '2016-01-04T12:30:00',
'description': 'TP NI9219 Température',
'evaluation_type': 0,
'id': 15797,
'moduleimpl_id': 1234,
'note_max': 20.0,
'numero': 3,
'poids': {
'UE1.1': 1.0,
'UE1.2': 1.0,
'UE1.3': 1.0
},
'publish_incomplete': False,
'visi_bulletin': True
}
"""
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)
)
e = query.first_or_404()
return jsonify(e.to_dict_api())
@bp.route("/moduleimpl/<int:moduleimpl_id>/evaluations")
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/evaluations")
@login_required
@ -33,39 +71,16 @@ def evaluations(moduleimpl_id: int):
moduleimpl_id : l'id d'un moduleimpl
Exemple de résultat :
[
{
"moduleimpl_id": 1,
"jour": "20/04/2022",
"heure_debut": "08h00",
"description": "eval1",
"coefficient": 1.0,
"publish_incomplete": false,
"numero": 0,
"id": 1,
"heure_fin": "09h00",
"note_max": 20.0,
"visibulletin": true,
"evaluation_type": 0,
"evaluation_id": 1,
"jouriso": "2022-04-20",
"duree": "1h",
"descrheure": " de 08h00 à 09h00",
"matin": 1,
"apresmidi": 0
},
...
]
Exemple de résultat : voir /evaluation
"""
query = Evaluation.query.filter_by(id=moduleimpl_id)
query = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id)
if g.scodoc_dept:
query = (
query.join(ModuleImpl)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
)
return jsonify([d.to_dict() for d in query])
return jsonify([e.to_dict_api() for e in query])
@bp.route("/evaluation/<int:evaluation_id>/notes")

View File

@ -398,7 +398,7 @@ def etat_evals(formsemestre_id: int):
for evaluation_id in modimpl_results.evaluations_etat:
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
evaluation = Evaluation.query.get_or_404(evaluation_id)
eval_dict = evaluation.to_dict()
eval_dict = evaluation.to_dict_api()
eval_dict["etat"] = eval_etat.to_dict()
eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module

View File

@ -51,7 +51,7 @@ class Evaluation(db.Model):
self.description[:16] if self.description else ''}">"""
def to_dict(self) -> dict:
"Représentation dict, pour json"
"Représentation dict (riche, compat ScoDoc 7)"
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators
@ -71,6 +71,34 @@ class Evaluation(db.Model):
e["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
return evaluation_enrich_dict(e)
def to_dict_api(self) -> dict:
"Représentation dict pour API JSON"
if self.jour is None:
date_debut = None
date_fin = None
else:
date_debut = datetime.datetime.combine(
self.jour, self.heure_debut or datetime.time(0, 0)
).isoformat()
date_fin = datetime.datetime.combine(
self.jour, self.heure_fin or datetime.time(0, 0)
).isoformat()
return {
"coefficient": self.coefficient,
"date_debut": date_debut,
"date_fin": date_fin,
"description": self.description,
"evaluation_type": self.evaluation_type,
"id": self.id,
"moduleimpl_id": self.moduleimpl_id,
"note_max": self.note_max,
"numero": self.numero,
"poids": self.get_ue_poids_dict(),
"publish_incomplete": self.publish_incomplete,
"visi_bulletin": self.visibulletin,
}
def from_dict(self, data):
"""Set evaluation attributes from given dict values."""
check_evaluation_args(data)
@ -227,7 +255,7 @@ def evaluation_enrich_dict(e: dict):
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
e["jouriso"] = ndb.DateDMYtoISO(e["jour"])
e["jour_iso"] = ndb.DateDMYtoISO(e["jour"])
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
d = ndb.TimeDuration(heure_debut, heure_fin)
if d is not None:

View File

@ -93,7 +93,7 @@ def do_evaluation_list(args, sortkey=None):
# Attention: transformation fonction ScoDoc7 en SQLAlchemy
cnx = ndb.GetDBConnexion()
evals = _evaluationEditor.list(cnx, args, sortkey=sortkey)
# calcule duree (chaine de car.) de chaque evaluation et ajoute jouriso, matin, apresmidi
# calcule duree (chaine de car.) de chaque evaluation et ajoute jour_iso, matin, apresmidi
for e in evals:
evaluation_enrich_dict(e)

View File

@ -1,18 +1,19 @@
# Tests unitaires de l'API ScoDoc
Démarche générale:
Démarche générale:
1. On génère une base SQL de test: voir
`tools/fakedatabase/create_test_api_database.py`
1. modifier /opt/scodoc/.env pour indiquer
1. modifier /opt/scodoc/.env pour indiquer
```
FLASK_ENV=test_api
FLASK_DEBUG=1
```
2. En tant qu'utilisateur scodoc, lancer:
```
tools/create_database.sh --drop SCODOC_TEST_API
flask db upgrade
@ -25,17 +26,20 @@ Démarche générale:
```
2. On lance le serveur ScoDoc sur cette base
```
flask run --host 0.0.0.0
```
3. On lance les tests unitaires API
```
pytest tests/api/test_api_departements.py
```
Rappel: pour interroger l'API, il fait avoir un utilisateur avec (au moins) la permission
ScoView dans tous les départements. Pour en créer un:
```
flask user-create lecteur_api LecteurAPI @all
flask user-password lecteur_api

View File

@ -115,7 +115,7 @@ class Sample:
pp(self.result, indent=4)
def dump(self, file):
self.url = self.url.replace("?faked_date=2022-07-20", "")
self.url = self.url.replace("?date_courante=2022-07-20", "")
file.write(f"#### {self.method} {self.url}\n")
if len(self.content) > 0:

View File

@ -46,26 +46,17 @@ def test_evaluations(api_headers):
for eval in list_eval:
assert verify_fields(eval, EVALUATIONS_FIELDS) is True
assert isinstance(eval["id"], int)
assert isinstance(eval["jour"], str)
assert isinstance(eval["heure_fin"], str)
assert isinstance(eval["note_max"], float)
assert isinstance(eval["visibulletin"], bool)
assert isinstance(eval["visi_bulletin"], bool)
assert isinstance(eval["evaluation_type"], int)
assert isinstance(eval["moduleimpl_id"], int)
assert isinstance(eval["heure_debut"], str)
assert eval["description"] is None or isinstance(eval["description"], str)
assert isinstance(eval["coefficient"], float)
assert isinstance(eval["publish_incomplete"], bool)
assert isinstance(eval["numero"], int)
assert isinstance(eval["evaluation_id"], int)
assert eval["date_debut"] is None or isinstance(eval["date_debut"], str)
assert eval["date_fin"] is None or isinstance(eval["date_fin"], str)
assert isinstance(eval["poids"], dict)
assert eval["jouriso"] is None or isinstance(eval["jouriso"], str)
assert isinstance(eval["duree"], str)
assert isinstance(eval["descrheure"], str)
assert isinstance(eval["matin"], int)
assert isinstance(eval["apresmidi"], int)
assert eval["moduleimpl_id"] == moduleimpl_id

View File

@ -545,27 +545,17 @@ FORMSEMESTRE_ETUS_GROUPS_FIELDS = {
}
EVALUATIONS_FIELDS = {
"id",
"jour",
"heure_fin",
"note_max",
"visibulletin",
"evaluation_type",
"moduleimpl_id",
"heure_debut",
"description",
"coefficient",
"publish_incomplete",
"numero",
"evaluation_id",
"date_debut",
"date_fin",
"description",
"evaluation_type",
"id",
"note_max",
"numero",
"poids",
"jouriso",
"duree",
"descrheure",
"matin",
"apresmidi",
"publish_incomplete",
"visi_bulletin",
}
EVALUATION_FIELDS = {