Skip to content
Snippets Groups Projects
Commit eb04984c authored by Emmanuel Viennet's avatar Emmanuel Viennet
Browse files

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

parent fdeaafe6
No related branches found
No related tags found
No related merge requests found
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,29 +8,23 @@ Documentation utilisateur: <https://scodoc.org> ...@@ -9,29 +8,23 @@ 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
### État actuel (26 jan 22) - le prochain jalon est 9.4. Voir branches sur gitea.
- 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
...@@ -40,6 +33,7 @@ les fichiers locaux (archives, photos, configurations, logs) sous ...@@ -40,6 +33,7 @@ les fichiers locaux (archives, photos, configurations, logs) sous
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`. 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 Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configuration sous
`/opt/scodoc-data/config`. `/opt/scodoc-data/config`.
...@@ -117,14 +111,14 @@ Ou avec couverture (`pip install pytest-cov`) ...@@ -117,14 +111,14 @@ 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
...@@ -133,8 +127,8 @@ normalement, par exemple: ...@@ -133,8 +127,8 @@ 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
...@@ -182,8 +176,6 @@ puis ...@@ -182,8 +176,6 @@ 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
...@@ -192,4 +184,3 @@ upgrade de scodoc9). ...@@ -192,4 +184,3 @@ 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`.
...@@ -257,9 +257,9 @@ def dept_formsemestres_courants(acronym: str): ...@@ -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): ...@@ -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
......
...@@ -76,9 +76,9 @@ def etudiants_courants(long=False): ...@@ -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(
......
...@@ -22,6 +22,44 @@ from app.scodoc.sco_permissions import Permission ...@@ -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): ...@@ -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")
......
...@@ -398,7 +398,7 @@ def etat_evals(formsemestre_id: int): ...@@ -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
......
...@@ -51,7 +51,7 @@ class Evaluation(db.Model): ...@@ -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): ...@@ -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): ...@@ -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:
......
...@@ -93,7 +93,7 @@ def do_evaluation_list(args, sortkey=None): ...@@ -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)
......
...@@ -5,14 +5,15 @@ Démarche générale: ...@@ -5,14 +5,15 @@ 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: ...@@ -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
......
...@@ -115,7 +115,7 @@ class Sample: ...@@ -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:
......
...@@ -46,26 +46,17 @@ def test_evaluations(api_headers): ...@@ -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
......
...@@ -545,27 +545,17 @@ FORMSEMESTRE_ETUS_GROUPS_FIELDS = { ...@@ -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 = {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment