From eb04984c2e9bc80f436e65d75b58a9f755986aff Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Nov 2022 11:19:28 +0100 Subject: [PATCH] API: modification format evaluations, et ajout route /evaluation. --- README.md | 63 +++++++++++++---------------- app/api/departements.py | 12 +++--- app/api/etudiants.py | 6 +-- app/api/evaluations.py | 67 +++++++++++++++++++------------ app/api/formsemestres.py | 2 +- app/models/evaluations.py | 32 ++++++++++++++- app/scodoc/sco_evaluation_db.py | 2 +- tests/api/README.md | 10 +++-- tests/api/make_samples.py | 2 +- tests/api/test_api_evaluations.py | 11 +---- tests/api/tools_test_api.py | 24 ++++------- 11 files changed, 125 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 603ac93d..115ef229 100644 --- a/README.md +++ b/README.md @@ -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: ## 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`. diff --git a/app/api/departements.py b/app/api/departements.py index b498cd66..066197d3 100644 --- a/app/api/departements.py +++ b/app/api/departements.py @@ -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 diff --git a/app/api/etudiants.py b/app/api/etudiants.py index f8728f44..fcafae68 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -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( diff --git a/app/api/evaluations.py b/app/api/evaluations.py index 5fe479f2..a8fb2c26 100644 --- a/app/api/evaluations.py +++ b/app/api/evaluations.py @@ -22,6 +22,44 @@ from app.scodoc.sco_permissions import Permission import app.scodoc.sco_utils as scu +@bp.route("/evaluation/") +@api_web_bp.route("/evaluation/") +@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//evaluations") @api_web_bp.route("/moduleimpl//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//notes") diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index e80da766..38794d62 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -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 diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 04ed7dce..71d54d82 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -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: diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py index 7aec9088..09873bbe 100644 --- a/app/scodoc/sco_evaluation_db.py +++ b/app/scodoc/sco_evaluation_db.py @@ -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) diff --git a/tests/api/README.md b/tests/api/README.md index c8ae7883..483da78a 100644 --- a/tests/api/README.md +++ b/tests/api/README.md @@ -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 diff --git a/tests/api/make_samples.py b/tests/api/make_samples.py index 57a7b477..576f1263 100644 --- a/tests/api/make_samples.py +++ b/tests/api/make_samples.py @@ -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: diff --git a/tests/api/test_api_evaluations.py b/tests/api/test_api_evaluations.py index 59ff3c05..383f5fe5 100644 --- a/tests/api/test_api_evaluations.py +++ b/tests/api/test_api_evaluations.py @@ -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 diff --git a/tests/api/tools_test_api.py b/tests/api/tools_test_api.py index 794095fe..e3b1158a 100644 --- a/tests/api/tools_test_api.py +++ b/tests/api/tools_test_api.py @@ -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 = {