From 5268ea4f135244de7ee285fb17fe062f82efd216 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 25 Oct 2021 15:49:56 +0200 Subject: [PATCH 01/16] =?UTF-8?q?Detecte=20et=20supprime=20doublons=20dans?= =?UTF-8?q?=20les=20pr=C3=A9f=C3=A9rences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_preferences.py | 11 +++++++++-- sco_version.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index 2935bdb3..92b00a67 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -1949,6 +1949,15 @@ class BasePreferences(object): "name": name, }, ) + if len(pdb) > 1: + # suppress buggy duplicates (may come from corrupted database for ice ages) + log( + f"**oups** detected duplicated preference !\n({self.dept_id}, {formsemestre_id}, {name}, {value})" + ) + for obj in pdb[1:]: + self._editor.delete(cnx, obj["id"]) + pdb = [pdb[0]] + if not pdb: # crée préférence log("create pref sem=%s %s=%s" % (formsemestre_id, name, value)) @@ -1962,10 +1971,8 @@ class BasePreferences(object): }, ) modif = True - log("create pref sem=%s %s=%s" % (formsemestre_id, name, value)) else: # edit existing value - existing_value = pdb[0]["value"] # old stored value if ( (existing_value != value) diff --git a/sco_version.py b/sco_version.py index b464e1d1..120cecdf 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.0.55" +SCOVERSION = "9.0.56" SCONAME = "ScoDoc" From c29199eff44c98dbdac9bd397001e93aef59e84c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 25 Oct 2021 15:51:11 +0200 Subject: [PATCH 02/16] Fix: get_etud_dept recherche par INE --- app/views/scodoc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 2ccac928..1722aca2 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -110,7 +110,7 @@ def get_etud_dept(): # il peut y avoir plusieurs réponses si l'étudiant est passé par plusieurs départements etuds = Identite.query.filter_by(code_nip=request.args["code_nip"]).all() elif "code_ine" in request.args: - etuds = Identite.query.filter_by(code_nip=request.args["code_ine"]).all() + etuds = Identite.query.filter_by(code_ine=request.args["code_ine"]).all() else: raise BadRequest( "missing argument (expected one among: etudid, code_nip or code_ine)" From 0da60384a188f8e68f49adbcc6168ca53cf23388 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 26 Oct 2021 00:13:42 +0200 Subject: [PATCH 03/16] Modification authentification ScoDoc7 API POST --- app/decorators.py | 5 ++++- app/views/absences.py | 10 +++++----- app/views/notes.py | 6 +++--- app/views/scolar.py | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/decorators.py b/app/decorators.py index df67751a..a688cb17 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -52,7 +52,10 @@ def scodoc(func): def scodoc_function(*args, **kwargs): # interdit les POST si pas loggué if request.method == "POST" and not current_user.is_authenticated: - current_app.logger.info("POST by non authenticated user") + current_app.logger.info( + "POST by non authenticated user (request.form=%s)", + str(request.form)[:2048], + ) return redirect( url_for( "auth.login", diff --git a/app/views/absences.py b/app/views/absences.py index 21ed46f6..c6fe5a1c 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -1047,8 +1047,8 @@ def EtatAbsencesDate(group_ids=[], date=None): # list of groups to display # ----- Gestion des "billets d'absence": signalement par les etudiants eux mêmes (à travers le portail) @bp.route("/AddBilletAbsence", methods=["GET", "POST"]) # API ScoDoc 7 compat -@scodoc @permission_required_compat_scodoc7(Permission.ScoAbsAddBillet) +@scodoc @scodoc7func def AddBilletAbsence( begin, @@ -1105,7 +1105,7 @@ def AddBilletAbsence( return billet_id -@bp.route("/AddBilletAbsenceForm") +@bp.route("/AddBilletAbsenceForm", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoAbsAddBillet) @scodoc7func @@ -1238,8 +1238,8 @@ def listeBilletsEtud(etudid=False, format="html"): @bp.route( "/XMLgetBilletsEtud", methods=["GET", "POST"] ) # pour compat anciens clients PHP -@scodoc @permission_required_compat_scodoc7(Permission.ScoView) +@scodoc @scodoc7func def XMLgetBilletsEtud(etudid=False): """Liste billets pour un etudiant""" @@ -1464,8 +1464,8 @@ def ProcessBilletAbsenceForm(billet_id): # @bp.route("/essai_api7") -# @scodoc # @permission_required_compat_scodoc7(Permission.ScoView) +# @scodoc # @scodoc7func # def essai_api7(x="xxx"): # "un essai" @@ -1474,8 +1474,8 @@ def ProcessBilletAbsenceForm(billet_id): @bp.route("/XMLgetAbsEtud", methods=["GET", "POST"]) # pour compat anciens clients PHP -@scodoc @permission_required_compat_scodoc7(Permission.ScoView) +@scodoc @scodoc7func def XMLgetAbsEtud(beg_date="", end_date=""): """returns list of absences in date interval""" diff --git a/app/views/notes.py b/app/views/notes.py index 90d70c46..c549377b 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -266,8 +266,8 @@ sco_publish( @bp.route( "formsemestre_bulletinetud", methods=["GET", "POST"] ) # POST pour compat anciens clients PHP (deprecated) -@scodoc @permission_required_compat_scodoc7(Permission.ScoView) +@scodoc @scodoc7func def formsemestre_bulletinetud( etudid=None, @@ -642,8 +642,8 @@ sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.ScoChangeFormatio @bp.route( "/formsemestre_list", methods=["GET", "POST"] ) # pour compat anciens clients PHP -@scodoc @permission_required_compat_scodoc7(Permission.ScoView) +@scodoc @scodoc7func def formsemestre_list( format="json", @@ -669,8 +669,8 @@ def formsemestre_list( @bp.route( "/XMLgetFormsemestres", methods=["GET", "POST"] ) # pour compat anciens clients PHP -@scodoc @permission_required_compat_scodoc7(Permission.ScoView) +@scodoc @scodoc7func def XMLgetFormsemestres(etape_apo=None, formsemestre_id=None): """List all formsemestres matching etape, XML format diff --git a/app/views/scolar.py b/app/views/scolar.py index 38f63f06..40c45a19 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -358,8 +358,8 @@ def search_etud_by_name(): @bp.route( "/Notes/XMLgetEtudInfos", methods=["GET", "POST"] ) # pour compat anciens clients PHP -@scodoc @permission_required_compat_scodoc7(Permission.ScoView) +@scodoc @scodoc7func def etud_info(etudid=None, format="xml"): "Donne les informations sur un etudiant" From 668210aaefa0e64b1ef248d9dd8a7ed5c1d633c3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 26 Oct 2021 10:22:55 +0200 Subject: [PATCH 04/16] Fix: suppression de modules avec tags (cascade manquante) --- app/models/formations.py | 10 ++++-- .../75cf18659984_cascade_tags_modules.py | 34 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 migrations/versions/75cf18659984_cascade_tags_modules.py diff --git a/app/models/formations.py b/app/models/formations.py index bb822911..c4f81e52 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -137,6 +137,12 @@ class NotesTag(db.Model): # Association tag <-> module notes_modules_tags = db.Table( "notes_modules_tags", - db.Column("tag_id", db.Integer, db.ForeignKey("notes_tags.id")), - db.Column("module_id", db.Integer, db.ForeignKey("notes_modules.id")), + db.Column( + "tag_id", + db.Integer, + db.ForeignKey("notes_tags.id", ondelete="CASCADE"), + ), + db.Column( + "module_id", db.Integer, db.ForeignKey("notes_modules.id", ondelete="CASCADE") + ), ) diff --git a/migrations/versions/75cf18659984_cascade_tags_modules.py b/migrations/versions/75cf18659984_cascade_tags_modules.py new file mode 100644 index 00000000..1a498b07 --- /dev/null +++ b/migrations/versions/75cf18659984_cascade_tags_modules.py @@ -0,0 +1,34 @@ +"""cascade tags modules + +Revision ID: 75cf18659984 +Revises: d74b4e16fb3c +Create Date: 2021-10-26 10:17:15.547905 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '75cf18659984' +down_revision = 'd74b4e16fb3c' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('notes_modules_tags_tag_id_fkey', 'notes_modules_tags', type_='foreignkey') + op.drop_constraint('notes_modules_tags_module_id_fkey', 'notes_modules_tags', type_='foreignkey') + op.create_foreign_key(None, 'notes_modules_tags', 'notes_tags', ['tag_id'], ['id'], ondelete='CASCADE') + op.create_foreign_key(None, 'notes_modules_tags', 'notes_modules', ['module_id'], ['id'], ondelete='CASCADE') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'notes_modules_tags', type_='foreignkey') + op.drop_constraint(None, 'notes_modules_tags', type_='foreignkey') + op.create_foreign_key('notes_modules_tags_module_id_fkey', 'notes_modules_tags', 'notes_modules', ['module_id'], ['id']) + op.create_foreign_key('notes_modules_tags_tag_id_fkey', 'notes_modules_tags', 'notes_tags', ['tag_id'], ['id']) + # ### end Alembic commands ### From 461f14631b72e76128988dbaca5bb1221e9393dd Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Tue, 26 Oct 2021 19:27:51 +0200 Subject: [PATCH 05/16] fix: bcc as list in Message build --- app/scodoc/sco_bulletins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 0a88f648..98f3a917 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -987,7 +987,7 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr): bcc = copy_addr.strip() else: bcc = "" - msg = Message(subject, sender=sender, recipients=recipients, bcc=bcc) + msg = Message(subject, sender=sender, recipients=recipients, bcc=[bcc]) msg.body = hea # Attach pdf From d2f41b6a21cb58b7891f9d8a4bf6a1df2a402f19 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 28 Oct 2021 00:52:23 +0200 Subject: [PATCH 06/16] API scodoc7, exemple/test usage, progres sur l'API scodoc9 --- app/api/__init__.py | 1 + app/api/auth.py | 15 ++++ app/api/sco_api.py | 4 +- app/auth/models.py | 3 + app/decorators.py | 6 +- app/views/absences.py | 8 +- app/views/notes.py | 8 +- app/views/scolar.py | 2 +- misc/example-api-1.py | 133 --------------------------- tests/api/exemple-api-basic.py | 149 +++++++++++++++++++++++++++++++ tests/api/exemple-api-scodoc7.py | 144 +++++++++++++++++++++++++++++ 11 files changed, 327 insertions(+), 146 deletions(-) delete mode 100644 misc/example-api-1.py create mode 100644 tests/api/exemple-api-basic.py create mode 100644 tests/api/exemple-api-scodoc7.py diff --git a/app/api/__init__.py b/app/api/__init__.py index 34ebbc77..956c1b46 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -6,3 +6,4 @@ from flask import Blueprint bp = Blueprint("api", __name__) from app.api import sco_api +from app.api import tokens diff --git a/app/api/auth.py b/app/api/auth.py index 24348aab..bb8464e0 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -33,6 +33,7 @@ token_auth = HTTPTokenAuth() @basic_auth.verify_password def verify_password(username, password): + # breakpoint() user = User.query.filter_by(user_name=username).first() if user and user.check_password(password): return user @@ -51,3 +52,17 @@ def verify_token(token): @token_auth.error_handler def token_auth_error(status): return error_response(status) + + +def token_permission_required(permission): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + scodoc_dept = getattr(g, "scodoc_dept", None) + if not current_user.has_permission(permission, scodoc_dept): + abort(403) + return f(*args, **kwargs) + + return login_required(decorated_function) + + return decorator diff --git a/app/api/sco_api.py b/app/api/sco_api.py index e2619a0b..c3ee7424 100644 --- a/app/api/sco_api.py +++ b/app/api/sco_api.py @@ -48,9 +48,9 @@ from app.api.errors import bad_request from app import models -@bp.route("/ScoDoc/api/list_depts", methods=["GET"]) +@bp.route("list_depts", methods=["GET"]) @token_auth.login_required def list_depts(): depts = models.Departement.query.filter_by(visible=True).all() - data = {"items": [d.to_dict() for d in depts]} + data = [d.to_dict() for d in depts] return jsonify(data) diff --git a/app/auth/models.py b/app/auth/models.py index f243f0e7..86ebdb83 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -213,6 +213,9 @@ class User(UserMixin, db.Model): @staticmethod def check_token(token): + """Retreive user for given token, chek token's validity + and returns the user object. + """ user = User.query.filter_by(token=token).first() if user is None or user.token_expiration < datetime.utcnow(): return None diff --git a/app/decorators.py b/app/decorators.py index a688cb17..ce94743e 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -50,6 +50,7 @@ def scodoc(func): @wraps(func) def scodoc_function(*args, **kwargs): + # current_app.logger.info("@scodoc") # interdit les POST si pas loggué if request.method == "POST" and not current_user.is_authenticated: current_app.logger.info( @@ -71,6 +72,7 @@ def scodoc(func): # current_app.logger.info("setting dept to None") g.scodoc_dept = None g.scodoc_dept_id = -1 # invalide + return func(*args, **kwargs) return scodoc_function @@ -100,8 +102,8 @@ def permission_required_compat_scodoc7(permission): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): - # current_app.logger.warning("PERMISSION; kwargs=%s" % str(kwargs)) # cherche les paramètre d'auth: + # current_app.logger.info("@permission_required_compat_scodoc7") auth_ok = False if request.method == "GET": user_name = request.args.get("__ac_name") @@ -116,7 +118,6 @@ def permission_required_compat_scodoc7(permission): if u and u.check_password(user_password): auth_ok = True flask_login.login_user(u) - # reprend le chemin classique: scodoc_dept = getattr(g, "scodoc_dept", None) @@ -153,6 +154,7 @@ def scodoc7func(func): 2. or be called directly from Python. """ + # current_app.logger.info("@scodoc7func") # Détermine si on est appelé via une route ("toplevel") # ou par un appel de fonction python normal. top_level = not hasattr(g, "scodoc7_decorated") diff --git a/app/views/absences.py b/app/views/absences.py index c6fe5a1c..f4cb1596 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -1047,8 +1047,8 @@ def EtatAbsencesDate(group_ids=[], date=None): # list of groups to display # ----- Gestion des "billets d'absence": signalement par les etudiants eux mêmes (à travers le portail) @bp.route("/AddBilletAbsence", methods=["GET", "POST"]) # API ScoDoc 7 compat -@permission_required_compat_scodoc7(Permission.ScoAbsAddBillet) @scodoc +@permission_required_compat_scodoc7(Permission.ScoAbsAddBillet) @scodoc7func def AddBilletAbsence( begin, @@ -1238,8 +1238,8 @@ def listeBilletsEtud(etudid=False, format="html"): @bp.route( "/XMLgetBilletsEtud", methods=["GET", "POST"] ) # pour compat anciens clients PHP -@permission_required_compat_scodoc7(Permission.ScoView) @scodoc +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def XMLgetBilletsEtud(etudid=False): """Liste billets pour un etudiant""" @@ -1464,8 +1464,8 @@ def ProcessBilletAbsenceForm(billet_id): # @bp.route("/essai_api7") -# @permission_required_compat_scodoc7(Permission.ScoView) # @scodoc +# @permission_required_compat_scodoc7(Permission.ScoView) # @scodoc7func # def essai_api7(x="xxx"): # "un essai" @@ -1474,8 +1474,8 @@ def ProcessBilletAbsenceForm(billet_id): @bp.route("/XMLgetAbsEtud", methods=["GET", "POST"]) # pour compat anciens clients PHP -@permission_required_compat_scodoc7(Permission.ScoView) @scodoc +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def XMLgetAbsEtud(beg_date="", end_date=""): """returns list of absences in date interval""" diff --git a/app/views/notes.py b/app/views/notes.py index c549377b..b3b2535d 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -264,10 +264,10 @@ sco_publish( @bp.route( - "formsemestre_bulletinetud", methods=["GET", "POST"] + "/formsemestre_bulletinetud", methods=["GET", "POST"] ) # POST pour compat anciens clients PHP (deprecated) -@permission_required_compat_scodoc7(Permission.ScoView) @scodoc +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def formsemestre_bulletinetud( etudid=None, @@ -642,8 +642,8 @@ sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.ScoChangeFormatio @bp.route( "/formsemestre_list", methods=["GET", "POST"] ) # pour compat anciens clients PHP -@permission_required_compat_scodoc7(Permission.ScoView) @scodoc +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def formsemestre_list( format="json", @@ -669,8 +669,8 @@ def formsemestre_list( @bp.route( "/XMLgetFormsemestres", methods=["GET", "POST"] ) # pour compat anciens clients PHP -@permission_required_compat_scodoc7(Permission.ScoView) @scodoc +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def XMLgetFormsemestres(etape_apo=None, formsemestre_id=None): """List all formsemestres matching etape, XML format diff --git a/app/views/scolar.py b/app/views/scolar.py index 40c45a19..38f63f06 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -358,8 +358,8 @@ def search_etud_by_name(): @bp.route( "/Notes/XMLgetEtudInfos", methods=["GET", "POST"] ) # pour compat anciens clients PHP -@permission_required_compat_scodoc7(Permission.ScoView) @scodoc +@permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def etud_info(etudid=None, format="xml"): "Donne les informations sur un etudiant" diff --git a/misc/example-api-1.py b/misc/example-api-1.py deleted file mode 100644 index 37a06c56..00000000 --- a/misc/example-api-1.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -"""Exemple connexion sur ScoDoc et utilisation de l'API - -- Ouverture session -- Liste semestres -- Liste modules -- Creation d'une évaluation -- Saisie d'une note - -Attention: cet exemple est en Python 3 (>= 3.6) -""" - -import requests -import urllib3 -import pdb -from pprint import pprint as pp -from flask import g, url_for - -# A modifier pour votre serveur: -CHECK_CERTIFICATE = False # set to True in production -BASEURL = "https://scodoc.xxx.net/ScoDoc/RT/Scolarite" -USER = "XXX" -PASSWORD = "XXX" - -# --- -if not CHECK_CERTIFICATE: - urllib3.disable_warnings() - - -class ScoError(Exception): - pass - - -def GET(s, path, errmsg=None): - """Get and returns as JSON""" - r = s.get(BASEURL + "/" + path, verify=CHECK_CERTIFICATE) - if r.status_code != 200: - raise ScoError(errmsg or "erreur !") - return r.json() # decode la reponse JSON - - -def POST(s, path, data, errmsg=None): - """Post""" - r = s.post(BASEURL + "/" + path, data=data, verify=CHECK_CERTIFICATE) - if r.status_code != 200: - raise ScoError(errmsg or "erreur !") - return r.text - - -# --- Ouverture session (login) -s = requests.Session() -s.post( - "https://deb11.viennet.net/api/auth/login", - data={"user_name": USER, "password": PASSWORD}, -) -r = s.get(BASEURL, auth=(USER, PASSWORD), verify=CHECK_CERTIFICATE) -if r.status_code != 200: - raise ScoError("erreur de connection: vérifier adresse et identifiants") - -# --- Recupere la liste de tous les semestres: -sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !") - -# sems est une liste de semestres (dictionnaires) -for sem in sems: - if sem["etat"]: - break - -if sem["etat"] == "0": - raise ScoError("Aucun semestre non verrouillé !") - -# Affiche le semestre trouvé: -pp(sem) - -# ---- Récupère la description de ce semestre: -# semdescr = GET(s, f"Notes/formsemestre_description?formsemestre_id={sem['formsemestre_id']}&with_evals=0&format=json" ) - -# ---- Liste les modules et prend le premier -mods = GET(s, f"/Notes/moduleimpl_list?formsemestre_id={sem['formsemestre_id']}") -print(f"{len(mods)} modules dans le semestre {sem['titre']}") - -mod = mods[0] - -# ---- Etudiants inscrits dans ce module -inscrits = GET( - s, f"Notes/do_moduleimpl_inscription_list?moduleimpl_id={mod['moduleimpl_id']}" -) -print(f"{len(inscrits)} inscrits dans ce module") -# prend le premier inscrit, au hasard: -etudid = inscrits[0]["etudid"] - -# ---- Création d'une evaluation le dernier jour du semestre -jour = sem["date_fin"] -evaluation_id = POST( - s, - "/Notes/do_evaluation_create", - data={ - "moduleimpl_id": mod["moduleimpl_id"], - "coefficient": 1, - "jour": jour, # "5/9/2019", - "heure_debut": "9h00", - "heure_fin": "10h00", - "note_max": 20, # notes sur 20 - "description": "essai", - }, - errmsg="échec création évaluation", -) - -print( - f"Evaluation créée dans le module {mod['moduleimpl_id']}, evaluation_id={evaluation_id}" -) -print( - "Pour vérifier, aller sur: ", - url_for( - "notes.moduleimpl_status", - scodoc_dept="DEPT", - moduleimpl_id=mod["moduleimpl_id"], - ), -) - -# ---- Saisie d'une note -junk = POST( - s, - "/Notes/save_note", - data={ - "etudid": etudid, - "evaluation_id": evaluation_id, - "value": 16.66, # la note ! - "comment": "test API", - }, -) diff --git a/tests/api/exemple-api-basic.py b/tests/api/exemple-api-basic.py new file mode 100644 index 00000000..529c379e --- /dev/null +++ b/tests/api/exemple-api-basic.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +"""Exemple utilisation API ScoDoc 9 avec jeton obtenu par basic athentication + + +Utilisation: créer les variables d'environnement: (indiquer les valeurs +pour le serveur ScoDoc que vous voulez interroger) + +export SCODOC_URL="https://scodoc.xxx.net/" +export SCODOC_USER="xxx" +export SCODOC_PASSWD="xxx" +export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valide + +(on peut aussi placer ces valeurs dans un fichier .env du répertoire tests/api). + + +Travail en cours, un seul point d'API (list_depts). +""" + +from dotenv import load_dotenv +import os +import pdb +import requests +import urllib3 +from pprint import pprint as pp + +# --- Lecture configuration (variables d'env ou .env) +BASEDIR = os.path.abspath(os.path.dirname(__file__)) +load_dotenv(os.path.join(BASEDIR, ".env")) +CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) +SCODOC_URL = os.environ["SCODOC_URL"] +SCODOC_DEPT = os.environ["SCODOC_DEPT"] +DEPT_URL = SCODOC_URL + "/ScoDoc/" + SCODOC_DEPT + "/Scolarite/" +SCODOC_USER = os.environ["SCODOC_USER"] +SCODOC_PASSWORD = os.environ["SCODOC_PASSWD"] +print(f"SCODOC_URL={SCODOC_URL}") + +# --- +if not CHECK_CERTIFICATE: + urllib3.disable_warnings() + + +class ScoError(Exception): + pass + + +def GET(path: str, headers={}, errmsg=None): + """Get and returns as JSON""" + r = requests.get( + DEPT_URL + "/" + path, headers=headers or HEADERS, verify=CHECK_CERTIFICATE + ) + if r.status_code != 200: + raise ScoError(errmsg or "erreur !") + return r.json() # decode la reponse JSON + + +def POST(s, path: str, data: dict, errmsg=None): + """Post""" + r = s.post(DEPT_URL + "/" + path, data=data, verify=CHECK_CERTIFICATE) + if r.status_code != 200: + raise ScoError(errmsg or "erreur !") + return r.text + + +# --- Obtention du jeton (token) +r = requests.post( + SCODOC_URL + "/ScoDoc/api/tokens", auth=(SCODOC_USER, SCODOC_PASSWORD) +) +assert r.status_code == 200 +token = r.json()["token"] +HEADERS = {"Authorization": f"Bearer {token}"} + +r = requests.get( + SCODOC_URL + "/ScoDoc/api/list_depts", headers=HEADERS, verify=CHECK_CERTIFICATE +) +if r.status_code != 200: + raise ScoError("erreur de connexion: vérifier adresse et identifiants") + +pp(r.json()) + + +# # --- Recupere la liste de tous les semestres: +# sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !") + +# # sems est une liste de semestres (dictionnaires) +# for sem in sems: +# if sem["etat"]: +# break + +# if sem["etat"] == "0": +# raise ScoError("Aucun semestre non verrouillé !") + +# # Affiche le semestre trouvé: +# pp(sem) + +# # ---- Récupère la description de ce semestre: +# # semdescr = GET(s, f"Notes/formsemestre_description?formsemestre_id={sem['formsemestre_id']}&with_evals=0&format=json" ) + +# # ---- Liste les modules et prend le premier +# mods = GET(s, f"/Notes/moduleimpl_list?formsemestre_id={sem['formsemestre_id']}") +# print(f"{len(mods)} modules dans le semestre {sem['titre']}") + +# mod = mods[0] + +# # ---- Etudiants inscrits dans ce module +# inscrits = GET( +# s, f"Notes/do_moduleimpl_inscription_list?moduleimpl_id={mod['moduleimpl_id']}" +# ) +# print(f"{len(inscrits)} inscrits dans ce module") +# # prend le premier inscrit, au hasard: +# etudid = inscrits[0]["etudid"] + +# # ---- Création d'une evaluation le dernier jour du semestre +# jour = sem["date_fin"] +# evaluation_id = POST( +# s, +# "/Notes/do_evaluation_create", +# data={ +# "moduleimpl_id": mod["moduleimpl_id"], +# "coefficient": 1, +# "jour": jour, # "5/9/2019", +# "heure_debut": "9h00", +# "heure_fin": "10h00", +# "note_max": 20, # notes sur 20 +# "description": "essai", +# }, +# errmsg="échec création évaluation", +# ) + +# print( +# f"Evaluation créée dans le module {mod['moduleimpl_id']}, evaluation_id={evaluation_id}" +# ) +# print( +# f"Pour vérifier, aller sur: {DEPT_URL}/Notes/moduleimpl_status?moduleimpl_id={mod['moduleimpl_id']}", +# ) + +# # ---- Saisie d'une note +# junk = POST( +# s, +# "/Notes/save_note", +# data={ +# "etudid": etudid, +# "evaluation_id": evaluation_id, +# "value": 16.66, # la note ! +# "comment": "test API", +# }, +# ) diff --git a/tests/api/exemple-api-scodoc7.py b/tests/api/exemple-api-scodoc7.py new file mode 100644 index 00000000..c3190717 --- /dev/null +++ b/tests/api/exemple-api-scodoc7.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +"""Exemple connexion sur ScoDoc 9 et utilisation de l'ancienne API ScoDoc 7 +à la mode "PHP": les gens passaient directement __ac_name et __ac_password +dans chaque requête, en POST ou en GET. + +Cela n'a jamais été documenté mais était implitement supporté. C'est "deprecated" +et ne sera plus supporté à partir de juillet 2022. + +Ce script va tester: +- Liste semestres +- Liste modules +- Creation d'une évaluation +- Saisie d'une note + +Utilisation: créer les variables d'environnement: (indiquer les valeurs +pour le serveur ScoDoc que vous voulez interroger) + +export SCODOC_URL="https://scodoc.xxx.net/" +export SCODOC_USER="xxx" +export SCODOC_PASSWD="xxx" +export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valide + +(on peut aussi placer ces valeurs dans un fichier .env du répertoire tests/api). +""" + +from dotenv import load_dotenv +import os +import pdb +import requests +import urllib3 +from pprint import pprint as pp + +# --- Lecture configuration (variables d'env ou .env) +BASEDIR = os.path.abspath(os.path.dirname(__file__)) +load_dotenv(os.path.join(BASEDIR, ".env")) +CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) +SCODOC_URL = os.environ["SCODOC_URL"] +SCODOC_DEPT = os.environ["SCODOC_DEPT"] +DEPT_URL = SCODOC_URL + "/ScoDoc/" + SCODOC_DEPT + "/Scolarite" +SCODOC_USER = os.environ["SCODOC_USER"] +SCODOC_PASSWORD = os.environ["SCODOC_PASSWD"] +print(f"SCODOC_URL={SCODOC_URL}") + +# --- +if not CHECK_CERTIFICATE: + urllib3.disable_warnings() + + +class ScoError(Exception): + pass + + +def GET(path: str, params=None, errmsg=None): + """Get and returns as JSON""" + # ajoute auth + params["__ac_name"] = SCODOC_USER + params["__ac_password"] = SCODOC_PASSWORD + r = requests.get(DEPT_URL + "/" + path, params=params, verify=CHECK_CERTIFICATE) + if r.status_code != 200: + raise ScoError(errmsg or "erreur !") + return r.json() # decode la reponse JSON + + +def POST(path: str, data: dict, errmsg=None): + """Post""" + data["__ac_name"] = SCODOC_USER + data["__ac_password"] = SCODOC_PASSWORD + r = requests.post(DEPT_URL + "/" + path, data=data, verify=CHECK_CERTIFICATE) + if r.status_code != 200: + raise ScoError(errmsg or "erreur !") + return r.text + + +# --- +# pas besoin d'ouvrir une session, on y va directement: + +# --- Recupere la liste de tous les semestres: +sems = GET("Notes/formsemestre_list", params={"format": "json"}) + +# sems est une liste de semestres (dictionnaires) +for sem in sems: + if sem["etat"]: + break + +if sem["etat"] == "0": + raise ScoError("Aucun semestre non verrouillé !") + +# Affiche le semestre trouvé: +pp(sem) + +# Les fonctions ci-dessous ne fonctionne plus en ScoDoc 9 +# Voir https://scodoc.org/git/viennet/ScoDoc/issues/149 + +# # ---- Liste les modules et prend le premier +# mods = GET("/Notes/moduleimpl_list", params={"formsemestre_id": sem["formsemestre_id"]}) +# print(f"{len(mods)} modules dans le semestre {sem['titre']}") + +# mod = mods[0] + +# # ---- Etudiants inscrits dans ce module +# inscrits = GET( +# "Notes/do_moduleimpl_inscription_list", +# params={"moduleimpl_id": mod["moduleimpl_id"]}, +# ) +# print(f"{len(inscrits)} inscrits dans ce module") +# # prend le premier inscrit, au hasard: +# etudid = inscrits[0]["etudid"] + +# # ---- Création d'une evaluation le dernier jour du semestre +# jour = sem["date_fin"] +# evaluation_id = POST( +# "/Notes/do_evaluation_create", +# data={ +# "moduleimpl_id": mod["moduleimpl_id"], +# "coefficient": 1, +# "jour": jour, # "5/9/2019", +# "heure_debut": "9h00", +# "heure_fin": "10h00", +# "note_max": 20, # notes sur 20 +# "description": "essai", +# }, +# errmsg="échec création évaluation", +# ) + +# print( +# f"Evaluation créée dans le module {mod['moduleimpl_id']}, evaluation_id={evaluation_id}" +# ) +# print( +# f"Pour vérifier, aller sur: {DEPT_URL}/Notes/moduleimpl_status?moduleimpl_id={mod['moduleimpl_id']}", +# ) + +# # ---- Saisie d'une note +# junk = POST( +# "/Notes/save_note", +# data={ +# "etudid": etudid, +# "evaluation_id": evaluation_id, +# "value": 16.66, # la note ! +# "comment": "test API", +# }, +# ) From b1aa36b136d45559272bc7d629c2b4a976b1e2b0 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 28 Oct 2021 00:54:26 +0200 Subject: [PATCH 07/16] version 9.0.57 --- sco_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sco_version.py b/sco_version.py index 120cecdf..a99ffec7 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.0.56" +SCOVERSION = "9.0.57" SCONAME = "ScoDoc" From 2b91fd78df49b957381cdc25cb4202824b304fef Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 30 Oct 2021 12:01:17 +0200 Subject: [PATCH 08/16] ajout mention \'message automatique\' --- app/templates/email/welcome.html | 4 +++- app/templates/email/welcome.txt | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/templates/email/welcome.html b/app/templates/email/welcome.html index 41ee5f65..41fb1c61 100644 --- a/app/templates/email/welcome.html +++ b/app/templates/email/welcome.html @@ -15,4 +15,6 @@

{{ url_for('auth.reset_password', token=token, _external=True) }}

{% endif %} -

A bientôt !

\ No newline at end of file +

A bientôt !

+ +

Ce message a été généré automatiquement par le serveur ScoDoc.

\ No newline at end of file diff --git a/app/templates/email/welcome.txt b/app/templates/email/welcome.txt index b15bceae..2630a8bd 100644 --- a/app/templates/email/welcome.txt +++ b/app/templates/email/welcome.txt @@ -8,4 +8,6 @@ Votre identifiant de connexion est: {{ user.user_name }} {{ url_for('auth.reset_password', token=token, _external=True) }} {% endif %} -

A bientôt !

\ No newline at end of file +A bientôt ! + +Ce message a été généré automatiquement par le serveur ScoDoc. \ No newline at end of file From c248def7f27f9363d6a81b37660f4d8d8c644201 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 30 Oct 2021 12:03:21 +0200 Subject: [PATCH 09/16] refactoring Mode --- app/views/users.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/views/users.py b/app/views/users.py index 7a03a899..79b5e095 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -35,6 +35,7 @@ Emmanuel Viennet, 2021 """ import datetime import re +from enum import auto, IntEnum from xml.etree import ElementTree import flask @@ -116,6 +117,12 @@ class ChangePasswordForm(FlaskForm): raise ValidationError("Mot de passe actuel incorrect, ré-essayez") +class Mode(IntEnum): + WELCOME_AND_CHANGE_PASSWORD = auto() + WELCOME_ONLY = auto() + SILENT = auto() + + @bp.route("/") @bp.route("/index_html") @scodoc @@ -529,19 +536,20 @@ def create_user_form(user_name=None, edit=0, all_roles=1): ) return "\n".join(H) + msg + "\n" + tf[1] + F # Traitement initial (mode) : 3 cas + # cf énumération Mode # A: envoi de welcome + procedure de reset # B: envoi de welcome seulement (mot de passe saisie dans le formulaire) # C: Aucun envoi (mot de passe saisi dans le formulaire) if vals["welcome:list"] == "1": if vals["reset_password:list"] == "1": - mode = "A" + mode = Mode.WELCOME_AND_CHANGE_PASSWORD else: - mode = "B" + mode = Mode.WELCOME_ONLY else: - mode = "C" + mode = Mode.SILENT # check passwords - if mode == "A": + if mode == Mode.WELCOME_AND_CHANGE_PASSWORD: vals["password"] = generate_password() else: if vals["password"]: @@ -567,8 +575,8 @@ def create_user_form(user_name=None, edit=0, all_roles=1): db.session.add(u) db.session.commit() # envoi éventuel d'un message - if mode == "A" or mode == "B": - if mode == "A": + if mode == Mode.WELCOME_AND_CHANGE_PASSWORD or mode == Mode.WELCOME_ONLY: + if mode == Mode.WELCOME_AND_CHANGE_PASSWORD: token = u.get_reset_password_token() else: token = None From 8f1e465280f9b3bf43206856ae0ce744925ca35c Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 30 Oct 2021 12:05:51 +0200 Subject: [PATCH 10/16] show current user name while getting old_password --- app/templates/auth/change_password.html | 20 ++++++++++++++------ app/views/users.py | 6 +++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/app/templates/auth/change_password.html b/app/templates/auth/change_password.html index 975e8bc9..2bec7762 100644 --- a/app/templates/auth/change_password.html +++ b/app/templates/auth/change_password.html @@ -1,9 +1,13 @@ {% extends "base.html" %} {% import 'bootstrap/wtf.html' as wtf %} -{% macro render_field(field) %} +{% macro render_field(field, auth_name=None) %} - {{ field.label }} + {% if auth_name %} + {{ field.label }} ({{ auth_name }}): + {% else %} + {{ field.label }} + {% endif %} {{ field(**kwargs)|safe }} {% if field.errors %}
    @@ -20,16 +24,20 @@

    Modification du compte ScoDoc {{form.user_name.data}}

    Identifiez-vous avez votre mot de passe actuel

    -

    Vous pouvez changer le mot de passe et/ou l'adresse email.

    -

    Les champs vides ne seront pas changés.

    {{ form.user_name }} {{ form.csrf_token }} - {{ render_field(form.old_password, size=14, + {{ render_field(form.old_password, size=14, auth_name=auth_username, style="padding:1px; margin-left: 1em; margin-top: 4px;") }} - {{ render_field(form.new_password, size=14, + + + + {{ render_field(form.new_password, size=14, style="padding:1px; margin-left: 1em; margin-top: 12px;") }} {{ render_field(form.bis_password, size=14, style="padding:1px; margin-left: 1em; margin-top: 4px;") }} diff --git a/app/views/users.py b/app/views/users.py index 79b5e095..53f60030 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -152,6 +152,7 @@ def user_info(user_name, format="json"): def create_user_form(user_name=None, edit=0, all_roles=1): "form. création ou edition utilisateur" auth_dept = current_user.dept + auth_username = current_user.user_name initvalues = {} edit = int(edit) all_roles = int(all_roles) @@ -795,7 +796,10 @@ def form_change_password(user_name=None): return redirect(destination) return render_template( - "auth/change_password.html", form=form, title="Modification compte ScoDoc" + "auth/change_password.html", + form=form, + title="Modification compte ScoDoc", + auth_username=current_user.user_name, ) From 68ac7c293abd9aabb953c9dd6a9ed6e65c9630dc Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 30 Oct 2021 12:09:35 +0200 Subject: [PATCH 11/16] mail from current user rather than admin --- app/views/users.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/users.py b/app/views/users.py index 53f60030..fed26e52 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -153,6 +153,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1): "form. création ou edition utilisateur" auth_dept = current_user.dept auth_username = current_user.user_name + from_mail = current_user.email initvalues = {} edit = int(edit) all_roles = int(all_roles) @@ -583,7 +584,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1): token = None send_email( "[ScoDoc] Création de votre compte", - sender=current_app.config["ADMINS"][0], + sender=from_mail, # current_app.config["ADMINS"][0], recipients=[u.email], text_body=render_template("email/welcome.txt", user=u, token=token), html_body=render_template( From 4fc31d8b479eae505e6ff4b1d25acf4c2b42506b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 30 Oct 2021 23:27:27 +0200 Subject: [PATCH 12/16] Unit test: moyenne module --- sco_version.py | 2 +- tests/unit/test_notes_modules.py | 147 ++++++++++++++++++++++++++++ tests/unit/test_notes_rattrapage.py | 4 +- 3 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 tests/unit/test_notes_modules.py diff --git a/sco_version.py b/sco_version.py index a99ffec7..86a35aa7 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.0.57" +SCOVERSION = "9.0.58" SCONAME = "ScoDoc" diff --git a/tests/unit/test_notes_modules.py b/tests/unit/test_notes_modules.py new file mode 100644 index 00000000..7a2a7cfc --- /dev/null +++ b/tests/unit/test_notes_modules.py @@ -0,0 +1,147 @@ +"""Test calculs moyennes de modules +""" + +from config import TestConfig +from tests.unit import sco_fake_gen + +from flask import g + +import app +from app.scodoc import sco_bulletins +from app.scodoc import sco_cache +from app.scodoc import sco_moduleimpl +from app.scodoc import sco_utils as scu + +DEPT = TestConfig.DEPT_TEST + + +def test_notes_modules(test_client): + """Test quelques opérations élémentaires de ScoDoc + Création 1 étudiant, formation, semestre, inscription etudiant, + création 1 evaluation, saisie de notes. + Vérifie calcul moyenne avec absences (ABS), excuse (EXC), attente (ATT) + """ + app.set_sco_dept(DEPT) + + G = sco_fake_gen.ScoFake(verbose=False) + etuds = [G.create_etud(code_nip=None)] # un seul + + f = G.create_formation(acronyme="") + ue = G.create_ue(formation_id=f["formation_id"], acronyme="TST1", titre="ue test") + mat = G.create_matiere(ue_id=ue["ue_id"], titre="matière test") + mod = G.create_module( + matiere_id=mat["matiere_id"], + code="TSM1", + coefficient=1.0, + titre="module test", + ue_id=ue["ue_id"], + formation_id=f["formation_id"], + ) + + # --- Mise place d'un semestre + sem = G.create_formsemestre( + formation_id=f["formation_id"], + semestre_id=1, + date_debut="01/01/2020", + date_fin="30/06/2020", + ) + + mi = G.create_moduleimpl( + module_id=mod["module_id"], + formsemestre_id=sem["formsemestre_id"], + ) + + # --- Inscription des étudiants + for etud in etuds: + G.inscrit_etudiant(sem, etud) + etud = etuds[0] + # --- Creation évaluations: e1, e2 + coef_1 = 1.0 + coef_2 = 2.0 + e1 = G.create_evaluation( + moduleimpl_id=mi["moduleimpl_id"], + jour="01/01/2020", + description="evaluation 1", + coefficient=coef_1, + ) + e2 = G.create_evaluation( + moduleimpl_id=mi["moduleimpl_id"], + jour="01/01/2020", + description="evaluation 2", + coefficient=coef_2, + ) + # --- Notes ordinaires + note_1 = 12.0 + note_2 = 13.0 + _, _, _ = G.create_note(evaluation=e1, etud=etud, note=note_1) + _, _, _ = G.create_note(evaluation=e2, etud=etud, note=note_2) + b = sco_bulletins.formsemestre_bulletinetud_dict( + sem["formsemestre_id"], etud["etudid"] + ) + # Vérifie structure du bulletin: + assert b["etudid"] == etud["etudid"] + assert len(b["ues"][0]["modules"][0]["evaluations"]) == 2 + assert len(b["ues"][0]["modules"]) == 1 + # Note moyenne: + note_th = (coef_1 * note_1 + coef_2 * note_2) / (coef_1 + coef_2) + assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_th) + + # Absence à une évaluation + _, _, _ = G.create_note(evaluation=e1, etud=etud, note=None) # abs + _, _, _ = G.create_note(evaluation=e2, etud=etud, note=note_2) + b = sco_bulletins.formsemestre_bulletinetud_dict( + sem["formsemestre_id"], etud["etudid"] + ) + note_th = (coef_1 * 0.0 + coef_2 * note_2) / (coef_1 + coef_2) + assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_th) + # Absences aux deux évaluations + _, _, _ = G.create_note(evaluation=e1, etud=etud, note=None) # abs + _, _, _ = G.create_note(evaluation=e2, etud=etud, note=None) # abs + b = sco_bulletins.formsemestre_bulletinetud_dict( + sem["formsemestre_id"], etud["etudid"] + ) + assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(0.0) + # Note excusée EXC <-> scu.NOTES_NEUTRALISE + _, _, _ = G.create_note(evaluation=e1, etud=etud, note=note_1) + _, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC + b = sco_bulletins.formsemestre_bulletinetud_dict( + sem["formsemestre_id"], etud["etudid"] + ) + assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_1) + # Note en attente ATT <-> scu.NOTES_ATTENTE + _, _, _ = G.create_note(evaluation=e1, etud=etud, note=note_1) + _, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_ATTENTE) # ATT + b = sco_bulletins.formsemestre_bulletinetud_dict( + sem["formsemestre_id"], etud["etudid"] + ) + assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_1) + # Neutralisation (EXC) des 2 évals + _, _, _ = G.create_note(evaluation=e1, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC + _, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC + b = sco_bulletins.formsemestre_bulletinetud_dict( + sem["formsemestre_id"], etud["etudid"] + ) + assert b["ues"][0]["modules"][0]["mod_moy_txt"] == "-" + # Vérification bas niveau: (peut changer dans le futur !) + nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"]) + mod_moy = nt.get_etud_mod_moy(mi["moduleimpl_id"], etud["etudid"]) + assert mod_moy == "NA0" # peut changer dans le futur ! + # Non inscrit + # - désinscrit notre étudiant: + inscr = sco_moduleimpl.do_moduleimpl_inscription_list( + moduleimpl_id=mi["moduleimpl_id"], etudid=etud["etudid"] + ) + assert len(inscr) == 1 + oid = inscr[0]["moduleimpl_inscription_id"] + sco_moduleimpl.do_moduleimpl_inscription_delete( + oid, formsemestre_id=mi["formsemestre_id"] + ) + # - + b = sco_bulletins.formsemestre_bulletinetud_dict( + sem["formsemestre_id"], etud["etudid"] + ) + assert b["ues"] == [] # inscrit à aucune UE ! + # Vérification bas niveau: (peut changer dans le futur !) + nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"]) + mod_moy = nt.get_etud_mod_moy(mi["moduleimpl_id"], etud["etudid"]) + assert mod_moy == "NI" # peut changer dans le futur ! diff --git a/tests/unit/test_notes_rattrapage.py b/tests/unit/test_notes_rattrapage.py index 0ceb6767..bc2d7bbd 100644 --- a/tests/unit/test_notes_rattrapage.py +++ b/tests/unit/test_notes_rattrapage.py @@ -15,8 +15,8 @@ DEPT = TestConfig.DEPT_TEST def test_notes_rattrapage(test_client): """Test quelques opérations élémentaires de ScoDoc - Création 10 étudiants, formation, semestre, inscription etudiant, - creation 1 evaluation, saisie 10 notes. + Création 1 étudiant, formation, semestre, inscription etudiant, + creation 1 evaluation, saisie notes. """ app.set_sco_dept(DEPT) From d8e1c428b08d07701625ded4c9d4985843f5dbc4 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 1 Nov 2021 15:16:51 +0100 Subject: [PATCH 13/16] migrate_scodoc7_dept_archives (ajout du s au nom de la fonction) --- app/models/etudiants.py | 2 +- app/models/formsemestre.py | 2 +- scodoc.py | 4 ++-- tools/__init__.py | 2 +- tools/migrate_scodoc7_archives.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 2511f0ca..90733b85 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -41,7 +41,7 @@ class Identite(db.Model): code_nip = db.Column(db.Text()) code_ine = db.Column(db.Text()) # Ancien id ScoDoc7 pour les migrations de bases anciennes - # ne pas utiliser après migrate_scodoc7_dept_archive + # ne pas utiliser après migrate_scodoc7_dept_archives scodoc7_id = db.Column(db.Text(), nullable=True) # billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic") diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 73830b06..94f0188f 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -79,7 +79,7 @@ class FormSemestre(db.Model): ) # Ancien id ScoDoc7 pour les migrations de bases anciennes - # ne pas utiliser après migrate_scodoc7_dept_archive + # ne pas utiliser après migrate_scodoc7_dept_archives scodoc7_id = db.Column(db.Text(), nullable=True) def __init__(self, **kwargs): diff --git a/scodoc.py b/scodoc.py index 3785dc6e..d15c3f7d 100755 --- a/scodoc.py +++ b/scodoc.py @@ -319,9 +319,9 @@ def import_scodoc7_dept(dept: str, dept_db_name: str = ""): # import-scodoc7-de @app.cli.command() @click.argument("dept", default="") @with_appcontext -def migrate_scodoc7_dept_archive(dept: str): # migrate-scodoc7-dept-archive +def migrate_scodoc7_dept_archives(dept: str): # migrate-scodoc7-dept-archives """Post-migration: renomme les archives en fonction des id de ScoDoc 9""" - tools.migrate_scodoc7_dept_archive(dept) + tools.migrate_scodoc7_dept_archives(dept) @app.cli.command() diff --git a/tools/__init__.py b/tools/__init__.py index 599bdf0d..7ed85cae 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -6,4 +6,4 @@ from tools.import_scodoc7_user_db import import_scodoc7_user_db from tools.import_scodoc7_dept import import_scodoc7_dept -from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archive +from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archives diff --git a/tools/migrate_scodoc7_archives.py b/tools/migrate_scodoc7_archives.py index 6e7553db..66f52bcd 100644 --- a/tools/migrate_scodoc7_archives.py +++ b/tools/migrate_scodoc7_archives.py @@ -11,7 +11,7 @@ from app.models.formsemestre import FormSemestre from app.models.etudiants import Identite -def migrate_scodoc7_dept_archive(dept_name=""): +def migrate_scodoc7_dept_archives(dept_name=""): if dept_name: depts = Departement.query.filter_by(acronym=dept_name) else: From 9f9cb6cca22de1daa88397f34a9965361fff6a83 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 1 Nov 2021 15:21:38 +0100 Subject: [PATCH 14/16] migrate_scodoc7_dept_archives (ajout du s au nom de la fonction) --- tools/migrate_from_scodoc7.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/migrate_from_scodoc7.sh b/tools/migrate_from_scodoc7.sh index f8af51bd..b213abf2 100755 --- a/tools/migrate_from_scodoc7.sh +++ b/tools/migrate_from_scodoc7.sh @@ -233,7 +233,7 @@ do done # ----- Post-Migration: renomme archives en fonction des nouveaux ids -su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask migrate-scodoc7-dept-archive)" "$SCODOC_USER" || die "Erreur de la post-migration des archives" +su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask migrate-scodoc7-dept-archives)" "$SCODOC_USER" || die "Erreur de la post-migration des archives" # --- Si migration "en place", désactive ScoDoc 7 From 01a84f3b1217494cf30203e1a407f92f8629e3a6 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 1 Nov 2021 16:12:53 +0100 Subject: [PATCH 15/16] tests unitaires calculs moyennes modules et UE --- tests/unit/test_notes_modules.py | 202 +++++++++++++++++++++++++++---- 1 file changed, 181 insertions(+), 21 deletions(-) diff --git a/tests/unit/test_notes_modules.py b/tests/unit/test_notes_modules.py index 7a2a7cfc..c997cfd1 100644 --- a/tests/unit/test_notes_modules.py +++ b/tests/unit/test_notes_modules.py @@ -1,4 +1,6 @@ """Test calculs moyennes de modules + Vérif moyennes de modules des bulletins + et aussi moyennes modules et UE internes (via nt) """ from config import TestConfig @@ -15,24 +17,51 @@ from app.scodoc import sco_utils as scu DEPT = TestConfig.DEPT_TEST +def check_nt( + etudid, + formsemestre_id, + ue_id, + moduleimpl_id, + expected_moy_ue=False, + expected_mod_moy=False, + expected_sum_coefs_ue=False, +): + """Vérification bas niveau: vérif resultat avec l'API internet "nt" + (peut changer dans le futur, ne pas utiliser hors ScoDoc !) + ne vérifie que les valeurs expected non False + """ + nt = sco_cache.NotesTableCache.get(formsemestre_id) + mod_moy = nt.get_etud_mod_moy(moduleimpl_id, etudid) + if expected_moy_ue is not False: + ue_status = nt.get_etud_ue_status(etudid, ue_id) + assert expected_moy_ue == ue_status["moy"] + if expected_mod_moy is not False: + assert expected_mod_moy == mod_moy + if expected_sum_coefs_ue is not False: + ue_status = nt.get_etud_ue_status(etudid, ue_id) + assert expected_sum_coefs_ue == ue_status["sum_coefs"] + + def test_notes_modules(test_client): - """Test quelques opérations élémentaires de ScoDoc - Création 1 étudiant, formation, semestre, inscription etudiant, - création 1 evaluation, saisie de notes. + """Test calcul des moyennes de modules et d'UE + Création étudiant, formation, semestre, inscription etudiant, + création evaluation, saisie de notes. Vérifie calcul moyenne avec absences (ABS), excuse (EXC), attente (ATT) """ app.set_sco_dept(DEPT) G = sco_fake_gen.ScoFake(verbose=False) - etuds = [G.create_etud(code_nip=None)] # un seul + etuds = [G.create_etud(code_nip=None) for i in range(2)] # 2 étudiants f = G.create_formation(acronyme="") ue = G.create_ue(formation_id=f["formation_id"], acronyme="TST1", titre="ue test") - mat = G.create_matiere(ue_id=ue["ue_id"], titre="matière test") + ue_id = ue["ue_id"] + mat = G.create_matiere(ue_id=ue_id, titre="matière test") + coef_mod_1 = 1.5 mod = G.create_module( matiere_id=mat["matiere_id"], code="TSM1", - coefficient=1.0, + coefficient=coef_mod_1, titre="module test", ue_id=ue["ue_id"], formation_id=f["formation_id"], @@ -45,27 +74,28 @@ def test_notes_modules(test_client): date_debut="01/01/2020", date_fin="30/06/2020", ) - + formsemestre_id = sem["formsemestre_id"] mi = G.create_moduleimpl( module_id=mod["module_id"], - formsemestre_id=sem["formsemestre_id"], + formsemestre_id=formsemestre_id, ) - + moduleimpl_id = mi["moduleimpl_id"] # --- Inscription des étudiants for etud in etuds: G.inscrit_etudiant(sem, etud) etud = etuds[0] + etudid = etud["etudid"] # --- Creation évaluations: e1, e2 coef_1 = 1.0 coef_2 = 2.0 e1 = G.create_evaluation( - moduleimpl_id=mi["moduleimpl_id"], + moduleimpl_id=moduleimpl_id, jour="01/01/2020", description="evaluation 1", coefficient=coef_1, ) e2 = G.create_evaluation( - moduleimpl_id=mi["moduleimpl_id"], + moduleimpl_id=moduleimpl_id, jour="01/01/2020", description="evaluation 2", coefficient=coef_2, @@ -73,8 +103,10 @@ def test_notes_modules(test_client): # --- Notes ordinaires note_1 = 12.0 note_2 = 13.0 - _, _, _ = G.create_note(evaluation=e1, etud=etud, note=note_1) - _, _, _ = G.create_note(evaluation=e2, etud=etud, note=note_2) + _, _, _ = G.create_note(evaluation=e1, etud=etuds[0], note=note_1) + _, _, _ = G.create_note(evaluation=e2, etud=etuds[0], note=note_2) + _, _, _ = G.create_note(evaluation=e1, etud=etuds[1], note=note_1 / 2) + _, _, _ = G.create_note(evaluation=e2, etud=etuds[1], note=note_2 / 3) b = sco_bulletins.formsemestre_bulletinetud_dict( sem["formsemestre_id"], etud["etudid"] ) @@ -85,6 +117,15 @@ def test_notes_modules(test_client): # Note moyenne: note_th = (coef_1 * note_1 + coef_2 * note_2) / (coef_1 + coef_2) assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_th) + check_nt( + etudid, + formsemestre_id, + ue_id, + moduleimpl_id, + expected_mod_moy=note_th, + expected_moy_ue=note_th, + expected_sum_coefs_ue=coef_mod_1, + ) # Absence à une évaluation _, _, _ = G.create_note(evaluation=e1, etud=etud, note=None) # abs @@ -101,6 +142,16 @@ def test_notes_modules(test_client): sem["formsemestre_id"], etud["etudid"] ) assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(0.0) + check_nt( + etudid, + formsemestre_id, + ue_id, + moduleimpl_id, + expected_mod_moy=0.0, + expected_moy_ue=0.0, + expected_sum_coefs_ue=0.0, + ) + # Note excusée EXC <-> scu.NOTES_NEUTRALISE _, _, _ = G.create_note(evaluation=e1, etud=etud, note=note_1) _, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC @@ -108,6 +159,15 @@ def test_notes_modules(test_client): sem["formsemestre_id"], etud["etudid"] ) assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_1) + check_nt( + etudid, + formsemestre_id, + ue_id, + moduleimpl_id, + expected_mod_moy=note_1, + expected_moy_ue=note_1, + expected_sum_coefs_ue=coef_mod_1, + ) # Note en attente ATT <-> scu.NOTES_ATTENTE _, _, _ = G.create_note(evaluation=e1, etud=etud, note=note_1) _, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_ATTENTE) # ATT @@ -115,6 +175,15 @@ def test_notes_modules(test_client): sem["formsemestre_id"], etud["etudid"] ) assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_1) + check_nt( + etudid, + formsemestre_id, + ue_id, + moduleimpl_id, + expected_mod_moy=note_1, + expected_moy_ue=note_1, + expected_sum_coefs_ue=coef_mod_1, + ) # Neutralisation (EXC) des 2 évals _, _, _ = G.create_note(evaluation=e1, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC _, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC @@ -122,10 +191,31 @@ def test_notes_modules(test_client): sem["formsemestre_id"], etud["etudid"] ) assert b["ues"][0]["modules"][0]["mod_moy_txt"] == "-" - # Vérification bas niveau: (peut changer dans le futur !) - nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"]) - mod_moy = nt.get_etud_mod_moy(mi["moduleimpl_id"], etud["etudid"]) - assert mod_moy == "NA0" # peut changer dans le futur ! + check_nt( + etudid, + sem["formsemestre_id"], + ue["ue_id"], + mi["moduleimpl_id"], + expected_mod_moy="NA0", + expected_moy_ue=0.0, + expected_sum_coefs_ue=0.0, + ) + # Attente (ATT) sur les 2 evals + _, _, _ = G.create_note(evaluation=e1, etud=etud, note=scu.NOTES_ATTENTE) # ATT + _, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_ATTENTE) # ATT + b = sco_bulletins.formsemestre_bulletinetud_dict( + sem["formsemestre_id"], etud["etudid"] + ) + assert b["ues"][0]["modules"][0]["mod_moy_txt"] == "-" + check_nt( + etudid, + sem["formsemestre_id"], + ue["ue_id"], + mi["moduleimpl_id"], + expected_mod_moy="NA0", + expected_moy_ue=0.0, + expected_sum_coefs_ue=0.0, + ) # Non inscrit # - désinscrit notre étudiant: inscr = sco_moduleimpl.do_moduleimpl_inscription_list( @@ -141,7 +231,77 @@ def test_notes_modules(test_client): sem["formsemestre_id"], etud["etudid"] ) assert b["ues"] == [] # inscrit à aucune UE ! - # Vérification bas niveau: (peut changer dans le futur !) - nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"]) - mod_moy = nt.get_etud_mod_moy(mi["moduleimpl_id"], etud["etudid"]) - assert mod_moy == "NI" # peut changer dans le futur ! + check_nt( + etudid, + formsemestre_id, + ue_id, + moduleimpl_id, + expected_mod_moy="NI", + expected_moy_ue=0.0, + expected_sum_coefs_ue=0.0, + ) + # --- Maintenant avec 2 modules dans l'UE + mod2 = G.create_module( + matiere_id=mat["matiere_id"], + code="TSM2", + coefficient=coef_mod_2, + titre="module test 2", + ue_id=ue_id, + formation_id=f["formation_id"], + ) + mi2 = G.create_moduleimpl( + module_id=mod2["module_id"], + formsemestre_id=formsemestre_id, + ) + # Re-inscription au premier module de l'UE + sco_moduleimpl.do_moduleimpl_inscription_create( + {"etudid": etudid, "moduleimpl_id": mi["moduleimpl_id"]}, + formsemestre_id=formsemestre_id, + ) + _, _, _ = G.create_note(evaluation=e1, etud=etud, note=12.5) + nt = sco_cache.NotesTableCache.get(formsemestre_id) + ue_status = nt.get_etud_ue_status(etudid, ue_id) + assert ue_status["nb_missing"] == 1 # 1 même si etud non inscrit à l'autre module + assert ue_status["nb_notes"] == 1 + assert not ue_status["was_capitalized"] + # Inscription au deuxième module de l'UE + sco_moduleimpl.do_moduleimpl_inscription_create( + {"etudid": etudid, "moduleimpl_id": mi2["moduleimpl_id"]}, + formsemestre_id=formsemestre_id, + ) + nt = sco_cache.NotesTableCache.get(formsemestre_id) + ue_status = nt.get_etud_ue_status(etudid, ue_id) + assert ue_status["nb_missing"] == 1 # mi2 n'a pas encore de note + assert ue_status["nb_notes"] == 1 + # Note dans module 2: + e_m2 = G.create_evaluation( + moduleimpl_id=mi2["moduleimpl_id"], + jour="01/01/2020", + description="evaluation mod 2", + coefficient=1.0, + ) + _, _, _ = G.create_note(evaluation=e_m2, etud=etud, note=19.5) + nt = sco_cache.NotesTableCache.get(formsemestre_id) + ue_status = nt.get_etud_ue_status(etudid, ue_id) + assert ue_status["nb_missing"] == 0 + assert ue_status["nb_notes"] == 2 + + # Moyenne d'UE si l'un des modules est EXC ("NA0") + # 2 modules, notes EXC dans le premier, note valide n dans le second + # la moyenne de l'UE doit être n + _, _, _ = G.create_note(evaluation=e1, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC + _, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC + _, _, _ = G.create_note(evaluation=e_m2, etud=etud, note=12.5) + _, _, _ = G.create_note(evaluation=e1, etud=etuds[1], note=11.0) + _, _, _ = G.create_note(evaluation=e2, etud=etuds[1], note=11.0) + _, _, _ = G.create_note(evaluation=e_m2, etud=etuds[1], note=11.0) + b = sco_bulletins.formsemestre_bulletinetud_dict( + sem["formsemestre_id"], etud["etudid"] + ) + assert b["ues"][0]["ue_status"]["cur_moy_ue"] == 12.5 + assert b["ues"][0]["ue_status"]["moy"] == 12.5 + b2 = sco_bulletins.formsemestre_bulletinetud_dict( + sem["formsemestre_id"], etuds[1]["etudid"] + ) + assert b2["ues"][0]["ue_status"]["cur_moy_ue"] == 11.0 + assert b2["ues"][0]["ue_status"]["moy"] == 11 From 7589d4cc343ec98330493da1d2107671cabb5be1 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 1 Nov 2021 16:59:56 +0100 Subject: [PATCH 16/16] API ScoDoc 7: autorise POSTs, ajoute groups_view, script exemple/test --- app/decorators.py | 14 +++++-- app/views/absences.py | 20 +++++----- app/views/scolar.py | 29 +++++++++++++- tests/api/exemple-api-scodoc7.py | 67 ++++++++++++++++++++++++++++---- 4 files changed, 109 insertions(+), 21 deletions(-) diff --git a/app/decorators.py b/app/decorators.py index ce94743e..8ebf5dea 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -50,9 +50,15 @@ def scodoc(func): @wraps(func) def scodoc_function(*args, **kwargs): - # current_app.logger.info("@scodoc") + # print("@scodoc") # interdit les POST si pas loggué - if request.method == "POST" and not current_user.is_authenticated: + if ( + request.method == "POST" + and not current_user.is_authenticated + and not request.form.get( + "__ac_password" + ) # exception pour compat API ScoDoc7 + ): current_app.logger.info( "POST by non authenticated user (request.form=%s)", str(request.form)[:2048], @@ -103,7 +109,7 @@ def permission_required_compat_scodoc7(permission): @wraps(f) def decorated_function(*args, **kwargs): # cherche les paramètre d'auth: - # current_app.logger.info("@permission_required_compat_scodoc7") + # print("@permission_required_compat_scodoc7") auth_ok = False if request.method == "GET": user_name = request.args.get("__ac_name") @@ -154,7 +160,7 @@ def scodoc7func(func): 2. or be called directly from Python. """ - # current_app.logger.info("@scodoc7func") + # print("@scodoc7func") # Détermine si on est appelé via une route ("toplevel") # ou par un appel de fonction python normal. top_level = not hasattr(g, "scodoc7_decorated") diff --git a/app/views/absences.py b/app/views/absences.py index f4cb1596..9fb8f5c8 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -1058,7 +1058,8 @@ def AddBilletAbsence( code_nip=None, code_ine=None, justified=True, - xml_reply=True, + format="json", + xml_reply=True, # deprecated ): """Mémorise un "billet" begin et end sont au format ISO (eg "1999-01-08 04:05:06") @@ -1082,6 +1083,7 @@ def AddBilletAbsence( raise ValueError("invalid dates") # justified = bool(justified) + xml_reply = bool(xml_reply) # cnx = ndb.GetDBConnexion() billet_id = sco_abs.billet_absence_create( @@ -1095,14 +1097,14 @@ def AddBilletAbsence( "justified": justified, }, ) - if xml_reply: - # Renvoie le nouveau billet en XML - billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id}) - tab = _tableBillets(billets, etud=etud) - log("AddBilletAbsence: new billet_id=%s (%gs)" % (billet_id, time.time() - t0)) - return tab.make_page(format="xml") - else: - return billet_id + if xml_reply: # backward compat + format = "xml" + + # Renvoie le nouveau billet au format demandé + billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id}) + tab = _tableBillets(billets, etud=etud) + log("AddBilletAbsence: new billet_id=%s (%gs)" % (billet_id, time.time() - t0)) + return tab.make_page(format=format) @bp.route("/AddBilletAbsenceForm", methods=["GET", "POST"]) diff --git a/app/views/scolar.py b/app/views/scolar.py index 38f63f06..a3a91303 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -302,7 +302,34 @@ sco_publish( methods=["GET", "POST"], ) -sco_publish("/groups_view", sco_groups_view.groups_view, Permission.ScoView) + +@bp.route("/groups_view") +@scodoc +@permission_required_compat_scodoc7(Permission.ScoView) +@scodoc7func +def groups_view( + group_ids=(), + format="html", + # Options pour listes: + with_codes=0, + etat=None, + with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) + with_archives=0, # ajoute colonne avec noms fichiers archivés + with_annotations=0, + formsemestre_id=None, +): + return sco_groups_view.groups_view( + group_ids=(), + format=format, + # Options pour listes: + with_codes=with_codes, + etat=etat, + with_paiement=with_paiement, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) + with_archives=with_archives, # ajoute colonne avec noms fichiers archivés + with_annotations=with_annotations, + formsemestre_id=formsemestre_id, + ) + sco_publish( "/export_groups_as_moodle_csv", diff --git a/tests/api/exemple-api-scodoc7.py b/tests/api/exemple-api-scodoc7.py index c3190717..5dcbdf05 100644 --- a/tests/api/exemple-api-scodoc7.py +++ b/tests/api/exemple-api-scodoc7.py @@ -6,7 +6,7 @@ à la mode "PHP": les gens passaient directement __ac_name et __ac_password dans chaque requête, en POST ou en GET. -Cela n'a jamais été documenté mais était implitement supporté. C'est "deprecated" +Cela n'a jamais été documenté mais était implicitement supporté. C'est "deprecated" et ne sera plus supporté à partir de juillet 2022. Ce script va tester: @@ -27,6 +27,7 @@ export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valid """ from dotenv import load_dotenv +import json import os import pdb import requests @@ -66,12 +67,10 @@ def GET(path: str, params=None, errmsg=None): def POST(path: str, data: dict, errmsg=None): """Post""" - data["__ac_name"] = SCODOC_USER - data["__ac_password"] = SCODOC_PASSWORD + data["__ac_name"] = data.get("__ac_name", SCODOC_USER) + data["__ac_password"] = data.get("__ac_password", SCODOC_PASSWORD) r = requests.post(DEPT_URL + "/" + path, data=data, verify=CHECK_CERTIFICATE) - if r.status_code != 200: - raise ScoError(errmsg or "erreur !") - return r.text + return r # --- @@ -91,7 +90,61 @@ if sem["etat"] == "0": # Affiche le semestre trouvé: pp(sem) -# Les fonctions ci-dessous ne fonctionne plus en ScoDoc 9 +# Liste des étudiants dans le 1er semestre non verrouillé: +group_list = GET( + "groups_view", + params={ + "formsemestre_id": sem["formsemestre_id"], + "with_codes": 1, + "format": "json", + }, +) +if not group_list: + # config inadaptée pour les tests... + raise ScoError("aucun étudiant inscrit dans le semestre") + +etud = group_list[0] # le premier étudiant inscrit ici +# test un POST +r = POST( + "Absences/AddBilletAbsence", + { + "begin": "2021-10-25", + "end": "2021-10-26", + "description": "test API scodoc7", + "etudid": etud["etudid"], + }, +) +assert r.status_code == 200 +assert r.text.startswith('') +assert "billet_id" in r.text +# Essai avec un compte invalide +r_invalid = POST( + "Absences/AddBilletAbsence", + { + "__ac_name": "xxx", + "begin": "2021-10-25", + "end": "2021-10-26", + "description": "test API scodoc7", + "etudid": etud["etudid"], + }, +) +assert r_invalid.status_code == 403 # compte invalide => not authorized + +# AddBilletAbsence en json +r = POST( + "Absences/AddBilletAbsence", + { + "begin": "2021-10-25", + "end": "2021-10-26", + "description": "test API scodoc7", + "etudid": etud["etudid"], + "xml_reply": 0, + }, +) +assert r.status_code == 200 +assert isinstance(json.loads(r.text)[0]["billet_id"], int) + +# Les fonctions ci-dessous ne fonctionnent plus en ScoDoc 9 # Voir https://scodoc.org/git/viennet/ScoDoc/issues/149 # # ---- Liste les modules et prend le premier
    +

    Vous pouvez changer le mot de passe et/ou l'adresse email.

    +

    Les champs laissés vides ne seront pas modifiés.

    +