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). (c) Emmanuel Viennet 1999 - 2022 (voir LICENCE.txt).
@ -9,39 +8,34 @@ Documentation utilisateur: <https://scodoc.org>
## Version ScoDoc 9 ## Version ScoDoc 9
La version ScoDoc 9 est parue en septembre 2021. La version ScoDoc 9 est parue en septembre 2021. Elle représente une évolution
Elle représente une évolution majeure du projet, maintenant basé sur majeure du projet, maintenant basé sur Flask (au lieu de Zope) et sur **python
Flask (au lieu de Zope) et sur **python 3.9+**. 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, 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). 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 ### Lignes de commandes
Voir [https://scodoc.org/GuideConfig](le guide de configuration). Voir [https://scodoc.org/GuideConfig](le guide de configuration).
## Organisation des fichiers ## Organisation des fichiers
L'installation comporte les fichiers de l'application, sous `/opt/scodoc/`, et L'installation comporte les fichiers de l'application, sous `/opt/scodoc/`, et
les fichiers locaux (archives, photos, configurations, logs) sous les fichiers locaux (archives, photos, configurations, logs) sous
`/opt/scodoc-data`. Par ailleurs, il y a évidemment les bases de données `/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 ### 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`. `/opt/scodoc-data/config`.
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé. 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)). 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 sudo su
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez 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: # Et donner ce répertoire à l'utilisateur scodoc:
chown -R scodoc.scodoc /opt/scodoc chown -R scodoc.scodoc /opt/scodoc
Il faut ensuite installer l'environnement et le fichier de configuration: Il faut ensuite installer l'environnement et le fichier de configuration:
# Le plus simple est de piquer le virtualenv configuré par l'installeur: # 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 flask db upgrade
Cette commande n'est nécessaire que la première fois (le contenu de la base 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. 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: scripts de tests:
Lancer au préalable: 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/* pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
#### Utilisation des tests unitaires pour initialiser la base de dev #### 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 On peut aussi utiliser les tests unitaires pour mettre la base de données de
utilisée par les tests: 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 export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer (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 pytest tests/unit/test_sco_basic.py
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
un utilisateur: utilisateur:
flask user-password admin flask user-password admin
@ -178,12 +172,10 @@ Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bie
pip install snakeviz pip install snakeviz
puis puis
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
# Paquet Debian 11 # Paquet Debian 11
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus 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). upgrade de scodoc9).
La préparation d'une release se fait à l'aide du script 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() dept = Departement.query.filter_by(acronym=acronym).first_or_404()
faked_date = request.args.get("faked_date") date_courante = request.args.get("date_courante")
if faked_date: if date_courante:
test_date = datetime.fromisoformat(faked_date) test_date = datetime.fromisoformat(date_courante)
else: else:
test_date = app.db.func.now() test_date = app.db.func.now()
# Les semestres en cours de ce département # 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 # Le département, spécifié par un id ou un acronyme
dept = Departement.query.get_or_404(dept_id) dept = Departement.query.get_or_404(dept_id)
faked_date = request.args.get("faked_date") date_courante = request.args.get("date_courante")
if faked_date: if date_courante:
test_date = datetime.fromisoformat(faked_date) test_date = datetime.fromisoformat(date_courante)
else: else:
test_date = app.db.func.now() test_date = app.db.func.now()
# Les semestres en cours de ce département # 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) allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
faked_date = request.args.get("faked_date") date_courante = request.args.get("date_courante")
if faked_date: if date_courante:
test_date = datetime.fromisoformat(faked_date) test_date = datetime.fromisoformat(date_courante)
else: else:
test_date = app.db.func.now() test_date = app.db.func.now()
etuds = Identite.query.filter( etuds = Identite.query.filter(

View File

@ -22,6 +22,44 @@ from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu 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") @bp.route("/moduleimpl/<int:moduleimpl_id>/evaluations")
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/evaluations") @api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/evaluations")
@login_required @login_required
@ -33,39 +71,16 @@ def evaluations(moduleimpl_id: int):
moduleimpl_id : l'id d'un moduleimpl moduleimpl_id : l'id d'un moduleimpl
Exemple de résultat : Exemple de résultat : voir /evaluation
[
{
"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
},
...
]
""" """
query = Evaluation.query.filter_by(id=moduleimpl_id) query = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id)
if g.scodoc_dept: if g.scodoc_dept:
query = ( query = (
query.join(ModuleImpl) query.join(ModuleImpl)
.join(FormSemestre) .join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id) .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") @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: for evaluation_id in modimpl_results.evaluations_etat:
eval_etat = modimpl_results.evaluations_etat[evaluation_id] eval_etat = modimpl_results.evaluations_etat[evaluation_id]
evaluation = Evaluation.query.get_or_404(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["etat"] = eval_etat.to_dict()
eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module 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 ''}">""" self.description[:16] if self.description else ''}">"""
def to_dict(self) -> dict: def to_dict(self) -> dict:
"Représentation dict, pour json" "Représentation dict (riche, compat ScoDoc 7)"
e = dict(self.__dict__) e = dict(self.__dict__)
e.pop("_sa_instance_state", None) e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators # ScoDoc7 output_formators
@ -71,6 +71,34 @@ class Evaluation(db.Model):
e["poids"] = self.get_ue_poids_dict() # { ue_id : poids } e["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
return evaluation_enrich_dict(e) 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): def from_dict(self, data):
"""Set evaluation attributes from given dict values.""" """Set evaluation attributes from given dict values."""
check_evaluation_args(data) 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) heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"]) e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"]) 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"] heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
d = ndb.TimeDuration(heure_debut, heure_fin) d = ndb.TimeDuration(heure_debut, heure_fin)
if d is not None: if d is not None:

View File

@ -93,7 +93,7 @@ def do_evaluation_list(args, sortkey=None):
# Attention: transformation fonction ScoDoc7 en SQLAlchemy # Attention: transformation fonction ScoDoc7 en SQLAlchemy
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
evals = _evaluationEditor.list(cnx, args, sortkey=sortkey) 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: for e in evals:
evaluation_enrich_dict(e) evaluation_enrich_dict(e)

View File

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

View File

@ -115,7 +115,7 @@ class Sample:
pp(self.result, indent=4) pp(self.result, indent=4)
def dump(self, file): 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") file.write(f"#### {self.method} {self.url}\n")
if len(self.content) > 0: if len(self.content) > 0:

View File

@ -46,26 +46,17 @@ def test_evaluations(api_headers):
for eval in list_eval: for eval in list_eval:
assert verify_fields(eval, EVALUATIONS_FIELDS) is True assert verify_fields(eval, EVALUATIONS_FIELDS) is True
assert isinstance(eval["id"], int) 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["note_max"], float)
assert isinstance(eval["visibulletin"], bool) assert isinstance(eval["visi_bulletin"], bool)
assert isinstance(eval["evaluation_type"], int) assert isinstance(eval["evaluation_type"], int)
assert isinstance(eval["moduleimpl_id"], int) assert isinstance(eval["moduleimpl_id"], int)
assert isinstance(eval["heure_debut"], str)
assert eval["description"] is None or isinstance(eval["description"], str) assert eval["description"] is None or isinstance(eval["description"], str)
assert isinstance(eval["coefficient"], float) assert isinstance(eval["coefficient"], float)
assert isinstance(eval["publish_incomplete"], bool) assert isinstance(eval["publish_incomplete"], bool)
assert isinstance(eval["numero"], int) 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_debut"] is None or isinstance(eval["date_debut"], str)
assert eval["date_fin"] is None or isinstance(eval["date_fin"], str) assert eval["date_fin"] is None or isinstance(eval["date_fin"], str)
assert isinstance(eval["poids"], dict) 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 assert eval["moduleimpl_id"] == moduleimpl_id

View File

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