diff --git a/app/api/__init__.py b/app/api/__init__.py index 36da5c56..9be4d3b6 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -22,8 +22,6 @@ def requested_format(default_format="json", allowed_formats=None): from app.api import tokens -from app.api import sco_api -from app.api import test_api from app.api import departements from app.api import etudiants from app.api import formations diff --git a/app/api/absences.py b/app/api/absences.py index 35056a05..51c4673e 100644 --- a/app/api/absences.py +++ b/app/api/absences.py @@ -4,73 +4,63 @@ from flask import jsonify from app.api import bp from app.api.errors import error_response -from app.api.auth import token_permission_required -from app.api.tools import get_etu_from_etudid_or_nip_or_ine -from app.scodoc import notesdb as ndb +from app.api.auth import token_auth, token_permission_required +from app.models import Identite +from app.scodoc import notesdb as ndb from app.scodoc import sco_abs -from app.scodoc.sco_groups import get_group_members from app.scodoc.sco_permissions import Permission @bp.route("/absences/etudid/", methods=["GET"]) -@bp.route("/absences/nip/", methods=["GET"]) -@bp.route("/absences/ine/", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) -def absences(etudid: int = None, nip: int = None, ine: int = None): +def absences(etudid: int = None): """ Retourne la liste des absences d'un étudiant donné etudid : l'etudid d'un étudiant - nip: le code nip d'un étudiant - ine : le code ine d'un étudiant Exemple de résultat: - [ - { - "jour": "2022-04-15", - "matin": true, - "estabs": true, - "estjust": true, - "description": "", - "begin": "2022-04-15 08:00:00", - "end": "2022-04-15 11:59:59" - }, - { - "jour": "2022-04-15", - "matin": false, - "estabs": true, - "estjust": false, - "description": "", - "begin": "2022-04-15 12:00:00", - "end": "2022-04-15 17:59:59" - } - ] + [ + { + "jour": "2022-04-15", + "matin": true, + "estabs": true, + "estjust": true, + "description": "", + "begin": "2022-04-15 08:00:00", + "end": "2022-04-15 11:59:59" + }, + { + "jour": "2022-04-15", + "matin": false, + "estabs": true, + "estjust": false, + "description": "", + "begin": "2022-04-15 12:00:00", + "end": "2022-04-15 17:59:59" + } + ] """ - if etudid is None: - # Récupération de l'étudiant - etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) - if etud is None: - return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel.\n " - "Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", - ) - etudid = etud.etudid - - # Récupération des absences de l'étudiant + etud = Identite.query.get(etudid) + if etud is None: + return error_response( + 404, + message="id de l'étudiant (etudid, nip, ine) inconnu", + ) + # Absences de l'étudiant ndb.open_db_connection() - absences = sco_abs.list_abs_date(etudid) + absences = sco_abs.list_abs_date(etud.id) for absence in absences: absence["jour"] = absence["jour"].isoformat() return jsonify(absences) @bp.route("/absences/etudid//just", methods=["GET"]) -@bp.route("/absences/nip//just", methods=["GET"]) -@bp.route("/absences/ine//just", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) -def absences_just(etudid: int = None, nip: int = None, ine: int = None): +def absences_just(etudid: int = None): """ Retourne la liste des absences justifiées d'un étudiant donné @@ -79,76 +69,77 @@ def absences_just(etudid: int = None, nip: int = None, ine: int = None): ine : le code ine d'un étudiant Exemple de résultat : - [ - { - "jour": "2022-04-15", - "matin": true, - "estabs": true, - "estjust": true, - "description": "", - "begin": "2022-04-15 08:00:00", - "end": "2022-04-15 11:59:59" - }, - { - "jour": "Fri, 15 Apr 2022 00:00:00 GMT", - "matin": false, - "estabs": true, - "estjust": true, - "description": "", - "begin": "2022-04-15 12:00:00", - "end": "2022-04-15 17:59:59" - } - ] + [ + { + "jour": "2022-04-15", + "matin": true, + "estabs": true, + "estjust": true, + "description": "", + "begin": "2022-04-15 08:00:00", + "end": "2022-04-15 11:59:59" + }, + { + "jour": "Fri, 15 Apr 2022 00:00:00 GMT", + "matin": false, + "estabs": true, + "estjust": true, + "description": "", + "begin": "2022-04-15 12:00:00", + "end": "2022-04-15 17:59:59" + } + ] """ - if etudid is None: - # Récupération de l'étudiant - try: - etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) - etudid = etu.etudid - except AttributeError: - return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel.\n " - "Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", - ) - - # Récupération des absences justifiées de l'étudiant - absences = sco_abs.list_abs_date(etudid) - for absence in [absence for absence in absences if absence["estjust"]]: - absence["jour"] = absence["jour"].isoformat() - return jsonify(absences) - - -@bp.route( - "/absences/abs_group_etat/", - methods=["GET"], -) -@bp.route( - "/absences/abs_group_etat/group_id//date_debut//date_fin/", - methods=["GET"], -) -@token_permission_required(Permission.APIView) -def abs_groupe_etat( # XXX A REVOIR XXX - group_id: int, date_debut, date_fin, with_boursier=True, format="html" -): - """ - Retoune la liste des absences d'un ou plusieurs groupes entre deux dates - """ - # Fonction utilisée : app.scodoc.sco_groups.get_group_members() et app.scodoc.sco_abs.list_abs_date() - - try: - # Utilisation de la fonction get_group_members - members = get_group_members(group_id) - except ValueError: + etud = Identite.query.get(etudid) + if etud is None: return error_response( - 409, message="La requête ne peut être traitée en l’état actuel" + 404, + message="id de l'étudiant (etudid, nip, ine) inconnu", ) - data = [] - # Filtre entre les deux dates renseignées - for member in members: - abs = sco_abs.list_abs_date(member.id, date_debut, date_fin) - data.append(abs) + # Absences justifiées de l'étudiant + abs_just = [ + absence for absence in sco_abs.list_abs_date(etud.id) if absence["estjust"] + ] + for absence in abs_just: + absence["jour"] = absence["jour"].isoformat() + return jsonify(abs_just) - # return jsonify(data) # XXX TODO faire en sorte de pouvoir renvoyer sa (ex to_dict() dans absences) - return error_response(501, message="Not implemented") + +# XXX TODO INACHEVEE +# @bp.route( +# "/absences/abs_group_etat/", +# methods=["GET"], +# ) +# @bp.route( +# "/absences/abs_group_etat/group_id//date_debut//date_fin/", +# methods=["GET"], +# ) +# @token_auth.login_required +# @token_permission_required(Permission.APIView) +# def abs_groupe_etat( # XXX A REVOIR XXX +# group_id: int, date_debut, date_fin, with_boursier=True, format="html" +# ): +# """ +# Liste des absences d'un ou plusieurs groupes entre deux dates +# """ +# return error_response(501, message="Not implemented") + +# # Fonction utilisée : app.scodoc.sco_groups.get_group_members() et app.scodoc.sco_abs.list_abs_date() + +# try: +# # Utilisation de la fonction get_group_members +# members = get_group_members(group_id) +# except ValueError: +# return error_response( +# 404, message="La requête ne peut être traitée en l’état actuel" +# ) + +# data = [] +# # Filtre entre les deux dates renseignées +# for member in members: +# abs = sco_abs.list_abs_date(member.id, date_debut, date_fin) +# data.append(abs) + +# # return jsonify(data) # XXX TODO faire en sorte de pouvoir renvoyer sa (ex to_dict() dans absences) +# return error_response(501, message="Not implemented") diff --git a/app/api/auth.py b/app/api/auth.py index 20dd7ded..67d6fba1 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -30,6 +30,8 @@ from functools import wraps from flask import abort from flask import g from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth + +from app import log from app.auth.models import User from app.api.errors import error_response @@ -39,19 +41,23 @@ token_auth = HTTPTokenAuth() @basic_auth.verify_password def verify_password(username, password): + "Verify password for this user" user = User.query.filter_by(user_name=username).first() if user and user.check_password(password): g.current_user = user + # note: est aussi basic_auth.current_user() return user @basic_auth.error_handler def basic_auth_error(status): + "error response (401 for invalid auth.)" return error_response(status) @token_auth.verify_token -def verify_token(token): +def verify_token(token) -> User: + "Retrouve l'utilisateur à partir du jeton" user = User.check_token(token) if token else None g.current_user = user return user @@ -59,6 +65,7 @@ def verify_token(token): @token_auth.error_handler def token_auth_error(status): + "rréponse en cas d'erreur d'auth." return error_response(status) @@ -68,16 +75,22 @@ def get_user_roles(user): def token_permission_required(permission): + "Décorateur pour les fontions de l'API ScoDoc" + def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): - scodoc_dept = getattr(g, "scodoc_dept", None) - if hasattr(g, "current_user") and not g.current_user.has_permission( - permission, scodoc_dept - ): + # token_auth.login_required() + current_user = basic_auth.current_user() + if not current_user or not current_user.has_permission(permission, None): + if current_user: + log(f"API permission denied (user {current_user})") + else: + log("API permission denied (no user supplied)") abort(403) return f(*args, **kwargs) - return decorated_function # login_required(decorated_function) + # return decorated_function(token_auth.login_required()) + return decorated_function return decorator diff --git a/app/api/departements.py b/app/api/departements.py index 482bceca..fca5d6a8 100644 --- a/app/api/departements.py +++ b/app/api/departements.py @@ -1,55 +1,66 @@ ############################################### Departements ########################################################## -import app - -from app import models -from app.api import bp -from app.api.auth import token_permission_required -from app.api.errors import error_response -from app.scodoc.sco_permissions import Permission from flask import jsonify +import app +from app import models +from app.api import bp +from app.api.auth import token_auth, token_permission_required +from app.models import Departement, FormSemestre +from app.scodoc.sco_permissions import Permission -@bp.route("/departements", methods=["GET"]) + +def get_departement(dept_ident: str) -> Departement: + "Le departement, par id ou acronyme. Erreur 404 si pas trouvé." + try: + dept_id = int(dept_ident) + except ValueError: + dept_id = None + if dept_id is None: + return Departement.query.filter_by(acronym=dept_ident).first_or_404() + return Departement.query.get_or_404(dept_id) + + +@bp.route("/departements_ids", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) -def departements(): +def departements_ids(): + """Liste des ids de départements""" + return jsonify([dept.id for dept in Departement.query]) + + +@bp.route("/departement/", methods=["GET"]) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def departement(dept_ident: str): """ - Retourne la liste des ids de départements visibles + Info sur un département. Accès par id ou acronyme. Exemple de résultat : - [ { "id": 1, "acronym": "TAPI", "description": null, "visible": true, "date_creation": "Fri, 15 Apr 2022 12:19:28 GMT" - }, - { - "id": 2, - "acronym": "MMI", - "description": null, - "visible": false, - "date_creation": "Fri, 18 Apr 2022 11:20:8 GMT" - }, - ... - ] + } """ - # Récupération de tous les départements - depts = models.Departement.query.all() - - # Mise en place de la liste avec tous les départements - data = [d.to_dict() for d in depts] - - return jsonify(data) + dept = get_departement(dept_ident) + return jsonify(dept.to_dict()) -@bp.route("/departements//etudiants/liste", methods=["GET"]) -@bp.route( - "/departements//etudiants/liste/", methods=["GET"] -) +@bp.route("/departements", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) -def liste_etudiants(dept: str, formsemestre_id=None): +def departements(): + """Liste les départements""" + return jsonify([dept.to_dict() for dept in Departement.query]) + + +@bp.route("/departement//etudiants", methods=["GET"]) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def list_etudiants(dept_ident: str): """ Retourne la liste des étudiants d'un département @@ -59,56 +70,39 @@ def liste_etudiants(dept: str, formsemestre_id=None): Exemple de résultat : [ { - "civilite": "X", - "code_ine": null, - "code_nip": null, + "civilite": "M", + "ine": "7899X61616", + "nip": "F6777H88", "date_naissance": null, - "email": null, + "email": "toto@toto.fr", "emailperso": null, "etudid": 18, "nom": "MOREL", "prenom": "JACQUES" }, - { - "civilite": "X", - "code_ine": null, - "code_nip": null, - "date_naissance": null, - "email": null, - "emailperso": null, - "etudid": 19, - "nom": "FOURNIER", - "prenom": "ANNE" - }, ... ] """ - # Si le formsemestre_id a été renseigné - if formsemestre_id is not None: - # Récupération du formsemestre - formsemestre = models.FormSemestre.query.filter_by( - id=formsemestre_id - ).first_or_404() - # Récupération du département - departement = formsemestre.departement + # Le département, spécifié par un id ou un acronyme + dept = get_departement(dept_ident) - # Si le formsemestre_id n'a pas été renseigné - else: - # Récupération du formsemestre - departement = models.Departement.query.filter_by(acronym=dept).first_or_404() - - # Récupération des étudiants - etudiants = departement.etudiants.all() - - # Mise en forme des données - list_etu = [etu.to_dict_bul(include_urls=False) for etu in etudiants] - - return jsonify(list_etu) + return jsonify([etud.to_dict_short() for etud in dept.etudiants]) -@bp.route("/departements//semestres_courants", methods=["GET"]) +@bp.route("/departement//formsemestres_ids", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) -def liste_semestres_courant(dept: str): +def formsemestres_ids(dept_ident: str): + """liste des ids formsemestre du département""" + # Le département, spécifié par un id ou un acronyme + dept = get_departement(dept_ident) + return jsonify([formsemestre.id for formsemestre in dept.formsemestres]) + + +@bp.route("/departement//formsemestres_courants", methods=["GET"]) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def liste_semestres_courant(dept_ident: str): """ Liste des semestres actifs d'un départements donné @@ -149,41 +143,14 @@ def liste_semestres_courant(dept: str): ... ] """ - # Récupération des départements comportant l'acronym mit en paramètre - dept = models.Departement.query.filter_by(acronym=dept).first_or_404() + # Le département, spécifié par un id ou un acronyme + dept = get_departement(dept_ident) - # Récupération des semestres suivant id_dept - semestres = models.FormSemestre.query.filter_by(dept_id=dept.id, etat=True) + # Les semestres en cours de ce département + formsemestres = models.FormSemestre.query.filter( + FormSemestre.dept_id == dept.id, + FormSemestre.date_debut <= app.db.func.now(), + FormSemestre.date_fin >= app.db.func.now(), + ) - # Mise en forme des données - data = [d.to_dict() for d in semestres] - - return jsonify(data) - - -@bp.route( - "/departements//formations//referentiel_competences", - methods=["GET"], -) -@token_permission_required(Permission.APIView) -def referenciel_competences(dept: str, formation_id: int): - """ - Retourne le référentiel de compétences - - dept : l'acronym d'un département - formation_id : l'id d'une formation - """ - dept = models.Departement.query.filter_by(acronym=dept).first_or_404() - - formation = models.Formation.query.filter_by( - id=formation_id, dept_id=dept.id - ).first_or_404() - - ref_comp = formation.referentiel_competence_id - - if ref_comp is None: - return error_response( - 204, message="Pas de référenciel de compétences pour cette formation" - ) - else: - return jsonify(ref_comp) + return jsonify([d.to_dict() for d in formsemestres]) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index c04495ab..a193553f 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -1,14 +1,20 @@ -#################################################### Etudiants ######################################################## +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +""" + API : accès aux étudiants +""" from flask import jsonify import app -from app import models from app.api import bp from app.api.errors import error_response -from app.api.auth import token_permission_required -from app.api.tools import get_etu_from_etudid_or_nip_or_ine -from app.models import FormSemestreInscription, FormSemestre, Identite +from app.api.auth import token_auth, token_permission_required +from app.models import Departement, FormSemestreInscription, FormSemestre, Identite from app.scodoc import sco_bulletins from app.scodoc import sco_groups from app.scodoc.sco_permissions import Permission @@ -16,10 +22,11 @@ from app.scodoc.sco_permissions import Permission @bp.route("/etudiants/courant", defaults={"long": False}) @bp.route("/etudiants/courant/long", defaults={"long": True}) +@token_auth.login_required @token_permission_required(Permission.APIView) def etudiants_courant(long=False): """ - Retourne la liste des étudiants courant + Liste des étudiants inscrits dans un formsemestre actuellement en cours. Exemple de résultat : [ @@ -40,7 +47,6 @@ def etudiants_courant(long=False): ... ] """ - # Récupération de tous les étudiants etuds = Identite.query.filter( Identite.id == FormSemestreInscription.etudid, FormSemestreInscription.formsemestre_id == FormSemestre.id, @@ -51,110 +57,178 @@ def etudiants_courant(long=False): data = [etud.to_dict_bul(include_urls=False) for etud in etuds] else: data = [etud.to_dict_short() for etud in etuds] - print(jsonify(data)) return jsonify(data) @bp.route("/etudiant/etudid/", methods=["GET"]) -@bp.route("/etudiant/nip/", methods=["GET"]) -@bp.route("/etudiant/ine/", methods=["GET"]) +@bp.route("/etudiant/nip/", methods=["GET"]) +@bp.route("/etudiant/ine/", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) -def etudiant(etudid: int = None, nip: int = None, ine: int = None): +def etudiant(etudid: int = None, nip: str = None, ine: str = None): """ - Retourne les informations de l'étudiant correspondant à l'id passé en paramètres. + Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé. - etudid : l'etudid d'un étudiant - nip : le code nip d'un étudiant - ine : le code ine d'un étudiant + etudid : l'etudid de l'étudiant + nip : le code nip de l'étudiant + ine : le code ine de l'étudiant + + Les codes INE et NIP sont uniques au sein d'un département. + Si plusieurs objets ont le même code, on ramène le plus récemment inscrit. Exemple de résultat : - { - "civilite": "X", - "code_ine": "1", - "code_nip": "1", - "date_naissance": "", - "email": "SACHA.COSTA@example.com", - "emailperso": "", - "etudid": 1, - "nom": "COSTA", - "prenom": "SACHA", - "nomprenom": "Sacha COSTA", - "lieu_naissance": "", - "dept_naissance": "", - "nationalite": "", - "boursier": "", - "id": 1, - "codepostaldomicile": "", - "paysdomicile": "", - "telephonemobile": "", - "typeadresse": "domicile", - "domicile": "", - "villedomicile": "", - "telephone": "", - "fax": "", - "description": "" - } + { + "civilite": "X", + "code_ine": "1", + "code_nip": "1", + "date_naissance": "", + "email": "SACHA.COSTA@example.com", + "emailperso": "", + "etudid": 1, + "nom": "COSTA", + "prenom": "SACHA", + "nomprenom": "Sacha COSTA", + "lieu_naissance": "", + "dept_naissance": "", + "nationalite": "", + "boursier": "", + "id": 1, + "codepostaldomicile": "", + "paysdomicile": "", + "telephonemobile": "", + "typeadresse": "domicile", + "domicile": "", + "villedomicile": "", + "telephone": "", + "fax": "", + "description": "" + } """ - # Récupération de l'étudiant - etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) + if etudid is not None: + etud = Identite.query.get(etudid) + else: + if nip is not None: + query = Identite.query.filter_by(code_nip=nip) + elif ine is not None: + query = Identite.query.filter_by(code_ine=ine) + else: + return error_response( + 404, + message="parametre manquant", + ) + if query.count() > 1: # cas rare d'un étudiant présent dans plusieurs depts + etuds = [] + for e in query: + admission = e.admission.first() + etuds.append((((admission.annee or 0) if admission else 0), e)) + etuds.sort() + etud = etuds[-1][1] + else: + etud = query.first() - # Mise en forme des données - data = etud.to_dict_bul(include_urls=False) + if etud is None: + return error_response( + 404, + message="étudiant inconnu", + ) - return jsonify(data) + return jsonify(etud.to_dict_bul(include_urls=False)) + + +@bp.route("/etudiants/etudid/", methods=["GET"]) +@bp.route("/etudiants/nip/", methods=["GET"]) +@bp.route("/etudiants/ine/", methods=["GET"]) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def etudiants(etudid: int = None, nip: str = None, ine: str = None): + """ + Info sur le ou les étudiants correspondant. Comme /etudiant mais renvoie + toujours une liste. + Si non trouvé, liste vide, pas d'erreur. + Dans 99% des cas, la liste contient un seul étudiant, mais si l'étudiant a + été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.). + """ + if etudid is not None: + query = Identite.query.filter_by(id=etudid) + elif nip is not None: + query = Identite.query.filter_by(code_nip=nip) + elif ine is not None: + query = Identite.query.filter_by(code_ine=ine) + else: + return error_response( + 404, + message="parametre manquant", + ) + + return jsonify([etud.to_dict_bul(include_urls=False) for etud in query]) @bp.route("/etudiant/etudid//formsemestres") -@bp.route("/etudiant/nip//formsemestres") -@bp.route("/etudiant/ine//formsemestres") +@bp.route("/etudiant/nip//formsemestres") +@bp.route("/etudiant/ine//formsemestres") +@token_auth.login_required @token_permission_required(Permission.APIView) def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None): """ - Retourne la liste des semestres qu'un étudiant a suivis, triés par ordre chronologique. - - etudid : l'etudid d'un étudiant - nip : le code nip d'un étudiant - ine : le code ine d'un étudiant + Liste des semestres qu'un étudiant a suivi, triés par ordre chronologique. + Accès par etudid, nip ou ine Exemple de résultat : - [ - { - "date_fin": "31/08/2022", - "resp_can_edit": false, - "dept_id": 1, - "etat": true, - "resp_can_change_ens": true, - "id": 1, - "modalite": "FI", - "ens_can_edit_eval": false, - "formation_id": 1, - "gestion_compensation": false, - "elt_sem_apo": null, - "semestre_id": 1, - "bul_hide_xml": false, - "elt_annee_apo": null, - "titre": "Semestre test", - "block_moyennes": false, - "scodoc7_id": null, - "date_debut": "01/09/2021", - "gestion_semestrielle": false, - "bul_bgcolor": "white", - "formsemestre_id": 1, - "titre_num": "Semestre test semestre 1", - "date_debut_iso": "2021-09-01", - "date_fin_iso": "2022-08-31", - "responsables": [] - }, - ... - ] + [ + { + "date_fin": "31/08/2022", + "resp_can_edit": false, + "dept_id": 1, + "etat": true, + "resp_can_change_ens": true, + "id": 1, + "modalite": "FI", + "ens_can_edit_eval": false, + "formation_id": 1, + "gestion_compensation": false, + "elt_sem_apo": null, + "semestre_id": 1, + "bul_hide_xml": false, + "elt_annee_apo": null, + "titre": "Semestre test", + "block_moyennes": false, + "scodoc7_id": null, + "date_debut": "01/09/2021", + "gestion_semestrielle": false, + "bul_bgcolor": "white", + "formsemestre_id": 1, + "titre_num": "Semestre test semestre 1", + "date_debut_iso": "2021-09-01", + "date_fin_iso": "2022-08-31", + "responsables": [] + }, + ... + ] """ - # Récupération de l'étudiant - etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) + if etudid is not None: + query = FormSemestre.query.filter( + FormSemestreInscription.etudid == etudid, + FormSemestreInscription.formsemestre_id == FormSemestre.id, + ) + elif nip is not None: + query = FormSemestre.query.filter( + Identite.code_nip == nip, + FormSemestreInscription.etudid == Identite.id, + FormSemestreInscription.formsemestre_id == FormSemestre.id, + ) + elif ine is not None: + query = FormSemestre.query.filter( + Identite.code_ine == ine, + FormSemestreInscription.etudid == Identite.id, + FormSemestreInscription.formsemestre_id == FormSemestre.id, + ) + else: + return error_response( + 404, + message="parametre manquant", + ) - formsemestres = models.FormSemestre.query.filter( - models.FormSemestreInscription.etudid == etud.id, - models.FormSemestreInscription.formsemestre_id == models.FormSemestre.id, - ).order_by(models.FormSemestre.date_debut) + formsemestres = query.order_by(FormSemestre.date_debut) return jsonify([formsemestre.to_dict() for formsemestre in formsemestres]) @@ -162,18 +236,41 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) @bp.route( "/etudiant/etudid//formsemestre//bulletin", methods=["GET"], + defaults={"version": "long"}, ) @bp.route( - "/etudiant/nip//formsemestre//bulletin", + "/etudiant/nip//formsemestre//bulletin", methods=["GET"], + defaults={"version": "long"}, ) @bp.route( - "/etudiant/ine//formsemestre//bulletin", + "/etudiant/ine//formsemestre//bulletin", methods=["GET"], + defaults={"version": "long"}, ) +@bp.route( + "/etudiant/etudid//formsemestre//bulletin/short", + methods=["GET"], + defaults={"version": "short"}, +) +@bp.route( + "/etudiant/nip//formsemestre//bulletin/short", + methods=["GET"], + defaults={"version": "short"}, +) +@bp.route( + "/etudiant/ine//formsemestre//bulletin/short", + methods=["GET"], + defaults={"version": "short"}, +) +@token_auth.login_required @token_permission_required(Permission.APIView) def etudiant_bulletin_semestre( - formsemestre_id, etudid: int = None, nip: int = None, ine: int = None + formsemestre_id, + etudid: int = None, + nip: str = None, + ine: str = None, + version="long", ): """ Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné @@ -183,12 +280,12 @@ def etudiant_bulletin_semestre( nip : le code nip d'un étudiant ine : le code ine d'un étudiant Exemple de résultat : - { - "version": "0", - "type": "BUT", - "date": "2022-04-27T07:18:16.450634Z", - "publie": true, - "etudiant": { + { + "version": "0", + "type": "BUT", + "date": "2022-04-27T07:18:16.450634Z", + "publie": true, + "etudiant": { "civilite": "X", "code_ine": "1", "code_nip": "1", @@ -214,17 +311,17 @@ def etudiant_bulletin_semestre( "villedomicile": "", "telephone": "", "fax": "", - "description": "" - }, - "formation": { + "description": "", + }, + "formation": { "id": 1, "acronyme": "BUT R&T", "titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications", - "titre": "BUT R&T" - }, - "formsemestre_id": 1, - "etat_inscription": "I", - "options": { + "titre": "BUT R&T", + }, + "formsemestre_id": 1, + "etat_inscription": "I", + "options": { "show_abs": true, "show_abs_modules": false, "show_ects": true, @@ -243,151 +340,137 @@ def etudiant_bulletin_semestre( "show_temporary": true, "temporary_txt": "Provisoire", "show_uevalid": true, - "show_date_inscr": true - }, - "ressources": { + "show_date_inscr": true, + }, + "ressources": { "R101": { - "id": 1, - "titre": "Initiation aux r\u00e9seaux informatiques", - "code_apogee": null, - "url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=1", - "moyenne": {}, - "evaluations": [ - { - "id": 1, - "description": "eval1", - "date": "2022-04-20", - "heure_debut": "08:00", - "heure_fin": "09:00", - "coef": "01.00", - "poids": { - "RT1.1": 1.0, - }, - "note": { - "value": "12.00", - "min": "00.00", - "max": "18.00", - "moy": "10.88" - }, - "url": "/ScoDoc/TAPI/Scolarite/Notes/evaluation_listenotes?evaluation_id=1" - } - ] + "id": 1, + "titre": "Initiation aux r\u00e9seaux informatiques", + "code_apogee": null, + "url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=1", + "moyenne": {}, + "evaluations": [ + { + "id": 1, + "description": "eval1", + "date": "2022-04-20", + "heure_debut": "08:00", + "heure_fin": "09:00", + "coef": "01.00", + "poids": { + "RT1.1": 1.0, + }, + "note": { + "value": "12.00", + "min": "00.00", + "max": "18.00", + "moy": "10.88", + }, + "url": "/ScoDoc/TAPI/Scolarite/Notes/evaluation_listenotes?evaluation_id=1", + } + ], }, - }, - "saes": { + }, + "saes": { "SAE11": { - "id": 2, - "titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique et \u00e0 la cybers\u00e9curit\u00e9", - "code_apogee": null, - "url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=2", - "moyenne": {}, - "evaluations": [] + "id": 2, + "titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique et \u00e0 la cybers\u00e9curit\u00e9", + "code_apogee": null, + "url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=2", + "moyenne": {}, + "evaluations": [], }, - }, - "ues": { + }, + "ues": { "RT1.1": { - "id": 1, - "titre": "Administrer les r\u00e9seaux et l\u2019Internet", - "numero": 1, - "type": 0, - "color": "#B80004", - "competence": null, - "moyenne": { - "value": "08.50", - "min": "06.00", - "max": "16.50", - "moy": "11.31", - "rang": "12", - "total": 16 - }, - "bonus": "00.00", - "malus": "00.00", - "capitalise": null, - "ressources": { - "R101": { - "id": 1, - "coef": 12.0, - "moyenne": "12.00" + "id": 1, + "titre": "Administrer les r\u00e9seaux et l\u2019Internet", + "numero": 1, + "type": 0, + "color": "#B80004", + "competence": null, + "moyenne": { + "value": "08.50", + "min": "06.00", + "max": "16.50", + "moy": "11.31", + "rang": "12", + "total": 16, }, - }, - "saes": { - "SAE11": { - "id": 2, - "coef": 16.0, - "moyenne": "~" + "bonus": "00.00", + "malus": "00.00", + "capitalise": null, + "ressources": { + "R101": {"id": 1, "coef": 12.0, "moyenne": "12.00"}, }, - }, - "ECTS": { - "acquis": 0.0, - "total": 12.0 - } + "saes": { + "SAE11": {"id": 2, "coef": 16.0, "moyenne": "~"}, + }, + "ECTS": {"acquis": 0.0, "total": 12.0}, }, - "semestre": { - "etapes": [], - "date_debut": "2021-09-01", - "date_fin": "2022-08-31", - "annee_universitaire": "2021 - 2022", - "numero": 1, - "inscription": "", - "groupes": [], - "absences": { - "injustifie": 1, - "total": 2 + "semestre": { + "etapes": [], + "date_debut": "2021-09-01", + "date_fin": "2022-08-31", + "annee_universitaire": "2021 - 2022", + "numero": 1, + "inscription": "", + "groupes": [], + "absences": {"injustifie": 1, "total": 2}, + "ECTS": {"acquis": 0, "total": 30.0}, + "notes": {"value": "10.60", "min": "02.40", "moy": "11.05", "max": "17.40"}, + "rang": {"value": "10", "total": 16}, }, - "ECTS": { - "acquis": 0, - "total": 30.0 - }, - "notes": { - "value": "10.60", - "min": "02.40", - "moy": "11.05", - "max": "17.40" - }, - "rang": { - "value": "10", - "total": 16 - } - } - } + }, + } """ - formsemestre = models.FormSemestre.query.filter_by( - id=formsemestre_id - ).first_or_404() + formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() + dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() - dept = models.Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() - - app.set_sco_dept(dept.acronym) - - # Récupération de l'étudiant - try: - etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) - except AttributeError: + if etudid is not None: + query = Identite.query.filter_by(id=etudid) + elif nip is not None: + query = Identite.query.filter_by(code_nip=nip, dept_id=dept.id) + elif ine is not None: + query = Identite.query.filter_by(code_ine=ine, dept_id=dept.id) + else: return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel.\n " - "Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", + 404, + message="parametre manquant", ) - return sco_bulletins.get_formsemestre_bulletin_etud_json(formsemestre, etu) + etud = query.first() + if etud is None: + return error_response( + 404, + message="id de l'étudiant (etudid, nip, ine) inconnu", + ) + + app.set_sco_dept(dept.acronym) + return sco_bulletins.get_formsemestre_bulletin_etud_json( + formsemestre, etud, version + ) @bp.route( - "/etudiant/etudid//semestre//groups", + "/etudiant/etudid//formsemestre//groups", methods=["GET"], ) @bp.route( - "/etudiant/nip//semestre//groups", methods=["GET"] + "/etudiant/nip//formsemestre//groups", + methods=["GET"], ) @bp.route( - "/etudiant/ine//semestre//groups", methods=["GET"] + "/etudiant/ine//formsemestre//groups", + methods=["GET"], ) +@token_auth.login_required @token_permission_required(Permission.APIView) def etudiant_groups( formsemestre_id: int, etudid: int = None, nip: int = None, ine: int = None ): """ - Retourne la liste des groupes auxquels appartient l'étudiant dans le semestre indiqué + Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué formsemestre_id : l'id d'un formsemestre etudid : l'etudid d'un étudiant @@ -395,45 +478,57 @@ def etudiant_groups( ine : le code ine d'un étudiant Exemple de résultat : - [ - { - "partition_id": 1, - "id": 1, - "formsemestre_id": 1, - "partition_name": null, - "numero": 0, - "bul_show_rank": false, - "show_in_lists": true, - "group_id": 1, - "group_name": null - }, - { - "partition_id": 2, - "id": 2, - "formsemestre_id": 1, - "partition_name": "TD", - "numero": 1, - "bul_show_rank": false, - "show_in_lists": true, - "group_id": 2, - "group_name": "A" - } - ] + [ + { + "partition_id": 1, + "id": 1, + "formsemestre_id": 1, + "partition_name": null, + "numero": 0, + "bul_show_rank": false, + "show_in_lists": true, + "group_id": 1, + "group_name": null + }, + { + "partition_id": 2, + "id": 2, + "formsemestre_id": 1, + "partition_name": "TD", + "numero": 1, + "bul_show_rank": false, + "show_in_lists": true, + "group_id": 2, + "group_name": "A" + } + ] """ - if etudid is None: - etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) - if etud is None: - return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel.\n " - "Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", - ) - etudid = etud.etudid - # Récupération du formsemestre - sem = models.FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() - dept = models.Departement.query.get(sem.dept_id) + formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() + if formsemestre is None: + return error_response( + 404, + message="formsemestre inconnu", + ) + dept = Departement.query.get(formsemestre.dept_id) + if etudid is not None: + query = Identite.query.filter_by(id=etudid) + elif nip is not None: + query = Identite.query.filter_by(code_nip=nip, dept_id=dept.id) + elif ine is not None: + query = Identite.query.filter_by(code_ine=ine, dept_id=dept.id) + else: + return error_response( + 404, + message="parametre manquant", + ) + etud = query.first() + if etud is None: + return error_response( + 404, + message="etudiant inconnu", + ) app.set_sco_dept(dept.acronym) - data = sco_groups.get_etud_groups(etudid, sem.id) + data = sco_groups.get_etud_groups(etud.id, formsemestre.id) return jsonify(data) diff --git a/app/api/evaluations.py b/app/api/evaluations.py index 4cbfa9f3..7d2776d2 100644 --- a/app/api/evaluations.py +++ b/app/api/evaluations.py @@ -4,14 +4,16 @@ from flask import jsonify import app from app import models +from app.models import Evaluation from app.api import bp -from app.api.auth import token_permission_required +from app.api.auth import token_auth, token_permission_required from app.api.errors import error_response from app.scodoc.sco_evaluation_db import do_evaluation_get_all_notes from app.scodoc.sco_permissions import Permission @bp.route("/evaluations/", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) def evaluations(moduleimpl_id: int): """ @@ -45,7 +47,7 @@ def evaluations(moduleimpl_id: int): ] """ # Récupération de toutes les évaluations - evals = models.Evaluation.query.filter_by(id=moduleimpl_id) + evals = Evaluation.query.filter_by(id=moduleimpl_id) # Mise en forme des données data = [d.to_dict() for d in evals] @@ -53,7 +55,8 @@ def evaluations(moduleimpl_id: int): return jsonify(data) -@bp.route("/evaluations/eval_notes/", methods=["GET"]) +@bp.route("/evaluation/eval_notes/", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) def evaluation_notes(evaluation_id: int): """ @@ -86,26 +89,19 @@ def evaluation_notes(evaluation_id: int): """ # Fonction utilisée : app.scodoc.sco_evaluation_db.do_evaluation_get_all_notes() - eval = models.Evaluation.query.filter_by(id=evaluation_id).first_or_404() - - moduleimpl = models.ModuleImpl.query.filter_by(id=eval.moduleimpl_id).first_or_404() - - formsemestre = models.FormSemestre.query.filter_by( - id=moduleimpl.formsemestre_id - ).first_or_404() - - dept = models.Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() - + evaluation = models.Evaluation.query.filter_by(id=evaluation_id).first_or_404() + dept = models.Departement.query.filter_by( + id=evaluation.moduleimpl.formsemestre.dept_id + ).first() app.set_sco_dept(dept.acronym) try: # Utilisation de la fonction do_evaluation_get_all_notes data = do_evaluation_get_all_notes(evaluation_id) - except AttributeError: + except AttributeError: # ??? return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel. \n" - "Veillez vérifier la conformité du 'evaluation_id'", + 404, + message="La requête ne peut être traitée en l’état actuel.", ) return jsonify(data) diff --git a/app/api/formations.py b/app/api/formations.py index 920d9af4..6ca6fde1 100644 --- a/app/api/formations.py +++ b/app/api/formations.py @@ -1,50 +1,38 @@ ##############################################" Formations ############################################################ from flask import jsonify +import app from app import models from app.api import bp from app.api.errors import error_response -from app.api.auth import token_permission_required -from app.scodoc.sco_formations import formation_export +from app.api.auth import token_auth, token_permission_required +from app.models.formations import Formation +from app.scodoc import sco_formations from app.scodoc.sco_permissions import Permission -@bp.route("/formations", methods=["GET"]) +@bp.route("/formations_ids", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) -def formations(): +def formations_ids(): """ - Retourne la liste des formations + Retourne la liste de toutes les formations (tous départements) - Exemple de résultat : - [ - { - "id": 1, - "acronyme": "BUT R&T", - "titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications", - "formation_code": "V1RET", - "code_specialite": null, - "dept_id": 1, - "titre": "BUT R&T", - "version": 1, - "type_parcours": 700, - "referentiel_competence_id": null, - "formation_id": 1 - }, - ... - ] + Exemple de résultat : [ 17, 99, 32 ] """ # Récupération de toutes les formations list_formations = models.Formation.query.all() # Mise en forme des données - data = [d.to_dict() for d in list_formations] + data = [d.id for d in list_formations] return jsonify(data) -@bp.route("/formations/", methods=["GET"]) +@bp.route("/formation/", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) -def formations_by_id(formation_id: int): +def formation_by_id(formation_id: int): """ Retourne une formation en fonction d'un id donné @@ -66,15 +54,25 @@ def formations_by_id(formation_id: int): } """ # Récupération de la formation - forma = models.Formation.query.filter_by(id=formation_id).first_or_404() + formation = models.Formation.query.filter_by(id=formation_id).first_or_404() # Mise en forme des données - data = forma.to_dict() + data = formation.to_dict() return jsonify(data) -@bp.route("/formations/formation_export/", methods=["GET"]) +@bp.route( + "/formation/formation_export/", + methods=["GET"], + defaults={"export_ids": False}, +) +@bp.route( + "/formation/formation_export//with_ids", + methods=["GET"], + defaults={"export_ids": True}, +) +@token_auth.login_required @token_permission_required(Permission.APIView) def formation_export_by_formation_id(formation_id: int, export_ids=False): """ @@ -171,22 +169,20 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False): ] } """ - # Fonction utilité : app.scodoc.sco_formations.formation_export() - + formation = Formation.query.get_or_404(formation_id) + dept = models.Departement.query.filter_by(id=formation.dept_id).first() + app.set_sco_dept(dept.acronym) try: # Utilisation de la fonction formation_export - data = formation_export(formation_id, export_ids) + data = sco_formations.formation_export(formation_id, export_ids) except ValueError: - return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel. \n" - "Veillez vérifier la conformité du 'formation_id'", - ) + return error_response(500, message="Erreur inconnue") return jsonify(data) -@bp.route("/formations/moduleimpl/", methods=["GET"]) +@bp.route("/formation/moduleimpl/", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) def moduleimpl(moduleimpl_id: int): """ @@ -198,7 +194,6 @@ def moduleimpl(moduleimpl_id: int): { "id": 1, "formsemestre_id": 1, - "computation_expr": null, "module_id": 1, "responsable_id": 2, "moduleimpl_id": 1, @@ -224,65 +219,26 @@ def moduleimpl(moduleimpl_id: int): } } """ - # Récupération des tous les moduleimpl - moduleimpl = models.ModuleImpl.query.filter_by(id=moduleimpl_id).first_or_404() - - # Mise en forme des données - data = moduleimpl.to_dict() - + modimpl = models.ModuleImpl.query.filter_by(id=moduleimpl_id).first_or_404() + data = modimpl.to_dict() return jsonify(data) @bp.route( - "/formations/moduleimpl/formsemestre//liste", + "/formation//referentiel_competences", methods=["GET"], ) +@token_auth.login_required @token_permission_required(Permission.APIView) -def moduleimpls_sem(formsemestre_id: int): +def referentiel_competences(formation_id: int): """ - Retourne la liste des moduleimpl d'un semestre + Retourne le référentiel de compétences + formation_id : l'id d'une formation - formsemestre_id : l'id d'un formsemestre - - Exemple d'utilisation : - [ - { - "id": 1, - "formsemestre_id": 1, - "computation_expr": null, - "module_id": 1, - "responsable_id": 2, - "module": { - "heures_tp": 0.0, - "code_apogee": "", - "titre": "Initiation aux r\u00e9seaux informatiques", - "coefficient": 1.0, - "module_type": 2, - "id": 1, - "ects": null, - "abbrev": "Init aux r\u00e9seaux informatiques", - "ue_id": 1, - "code": "R101", - "formation_id": 1, - "heures_cours": 0.0, - "matiere_id": 1, - "heures_td": 0.0, - "semestre_id": 1, - "numero": 10, - "module_id": 1 - }, - "moduleimpl_id": 1, - "ens": [] - }, - ... - ] + return json, ou null si pas de référentiel associé. """ - formsemestre = models.FormSemestre.query.filter_by( - id=formsemestre_id - ).first_or_404() + formation = models.Formation.query.filter_by(id=formation_id).first_or_404() - moduleimpls = formsemestre.modimpls_sorted - - data = [moduleimpl.to_dict() for moduleimpl in moduleimpls] - - return jsonify(data) + if formation.referentiel_competence is None: + return jsonify(None) + return jsonify(formation.referentiel_competence.to_dict()) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 122abf14..725454b1 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -4,99 +4,80 @@ from flask import jsonify import app from app import models from app.api import bp -from app.api.errors import error_response -from app.api.auth import token_permission_required -from app.api.tools import get_etu_from_etudid_or_nip_or_ine -from app.models import FormSemestre, FormSemestreEtape +from app.api.auth import token_auth, token_permission_required +from app.models import Departement, FormSemestre, FormSemestreEtape from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json -from app.scodoc.sco_bulletins_json import make_json_formsemestre_bulletinetud from app.scodoc.sco_permissions import Permission -from app.scodoc.sco_pvjury import formsemestre_pvjury +from app.scodoc.sco_utils import ModuleType @bp.route("/formsemestre/", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) def formsemestre(formsemestre_id: int): """ - Retourne l'information sur le formsemestre correspondant au formsemestre_id + Information sur le formsemestre indiqué. - formsemestre_id : l'id d'un formsemestre + formsemestre_id : l'id du formsemestre Exemple de résultat : { + "block_moyennes": false, + "bul_bgcolor": "white", + "bul_hide_xml": false, + "date_debut_iso": "2021-09-01", + "date_debut": "01/09/2021", + "date_fin_iso": "2022-08-31", "date_fin": "31/08/2022", - "resp_can_edit": false, "dept_id": 1, + "elt_annee_apo": null, + "elt_sem_apo": null, + "ens_can_edit_eval": false, "etat": true, - "resp_can_change_ens": true, + "formation_id": 1, + "formsemestre_id": 1, + "gestion_compensation": false, + "gestion_semestrielle": false, "id": 1, "modalite": "FI", - "ens_can_edit_eval": false, - "formation_id": 1, - "gestion_compensation": false, - "elt_sem_apo": null, - "semestre_id": 1, - "bul_hide_xml": false, - "elt_annee_apo": null, - "titre": "Semestre test", - "block_moyennes": false, + "resp_can_change_ens": true, + "resp_can_edit": false, + "responsables": [1, 99], // uids "scodoc7_id": null, - "date_debut": "01/09/2021", - "gestion_semestrielle": false, - "bul_bgcolor": "white", - "formsemestre_id": 1, - "titre_num": "Semestre test semestre 1", - "date_debut_iso": "2021-09-01", - "date_fin_iso": "2022-08-31", - "responsables": [] + "semestre_id": 1, + "titre_formation" : "BUT GEA", + "titre_num": "BUT GEA semestre 1", + "titre": "BUT GEA", } """ - # Récupération de tous les formsemestres - formsemetre = models.FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() - - # Mise en forme des données - data = formsemetre.to_dict() - + formsemestre: FormSemestre = models.FormSemestre.query.filter_by( + id=formsemestre_id + ).first_or_404() + data = formsemestre.to_dict() + # Pour le moment on a besoin de fixer le departement + # pour accéder aux préferences + dept = Departement.query.get(formsemestre.dept_id) + app.set_sco_dept(dept.acronym) + data["annee_scolaire"] = formsemestre.annee_scolaire_str() + data["session_id"] = formsemestre.session_id() return jsonify(data) @bp.route("/formsemestre/apo/", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) def formsemestre_apo(etape_apo: str): """ - Retourne les informations sur les formsemestres + Retourne les informations sur les formsemestres ayant cette étape Apogée - etape_apo : l'id d'une étape apogée + etape_apo : un code étape apogée Exemple de résultat : - { - "date_fin": "31/08/2022", - "resp_can_edit": false, - "dept_id": 1, - "etat": true, - "resp_can_change_ens": true, - "id": 1, - "modalite": "FI", - "ens_can_edit_eval": false, - "formation_id": 1, - "gestion_compensation": false, - "elt_sem_apo": null, - "semestre_id": 1, - "bul_hide_xml": false, - "elt_annee_apo": null, - "titre": "Semestre test", - "block_moyennes": false, - "scodoc7_id": null, - "date_debut": "01/09/2021", - "gestion_semestrielle": false, - "bul_bgcolor": "white", - "formsemestre_id": 1, - "titre_num": "Semestre test semestre 1", - "date_debut_iso": "2021-09-01", - "date_fin_iso": "2022-08-31", - "responsables": [] - } + [ + { ...formsemestre... + }, ... + ] """ formsemestres = FormSemestre.query.filter( FormSemestreEtape.etape_apo == etape_apo, @@ -106,173 +87,8 @@ def formsemestre_apo(etape_apo: str): return jsonify([formsemestre.to_dict() for formsemestre in formsemestres]) -@bp.route( - "/formsemestre//etudiant/etudid//bulletin", - methods=["GET"], -) -@bp.route( - "/formsemestre//etudiant/nip//bulletin", - methods=["GET"], -) -@bp.route( - "/formsemestre//etudiant/ine//bulletin", - methods=["GET"], -) -@token_permission_required(Permission.APIView) -def etudiant_bulletin( - formsemestre_id, - etudid: int = None, - nip: int = None, - ine: int = None, -): - """ - Retourne le bulletin de note d'un étudiant - - formsemestre_id : l'id d'un formsemestre - etudid : l'etudid d'un étudiant - nip : le code nip d'un étudiant - ine : le code ine d'un étudiant - - Exemple de résultat : - { - "etudid":1, - "formsemestre_id":1, - "date":"2022-04-27T10:44:47.448094", - "publie":true, - "etapes":[ - - ], - "etudiant":{ - "etudid":1, - "code_nip":"1", - "code_ine":"1", - "nom":"COSTA", - "prenom":"Sacha", - "civilite":"", - "photo_url":"/ScoDoc/TAPI/Scolarite/get_photo_image?etudid=1&size=small", - "email":"SACHA.COSTA@example.com", - "emailperso":"", - "sexe":"" - }, - "note":{ - "value":"10.60", - "min":"-", - "max":"-", - "moy":"-" - }, - "rang":{ - "value":"10", - "ninscrits":16 - }, - "rang_group":[ - { - "group_type":"TD", - "group_name":"", - "value":"", - "ninscrits":"" - } - ], - "note_max":{ - "value":20 - }, - "bonus_sport_culture":{ - "value":0.0 - }, - "ue":[ - { - "id":1, - "numero":"1", - "acronyme":"RT1.1", - "titre":"Administrer les r\u00e9seaux et l\u2019Internet", - "note":{ - "value":"08.50", - "min":"06.00", - "max":"16.50", - "moy":"11.31" - }, - "rang":"12", - "effectif":16, - "ects":"12", - "code_apogee":"", - "module":[ - { - "id":1, - "code":"R101", - "coefficient":1.0, - "numero":10, - "titre":"Initiation aux r\u00e9seaux informatiques", - "abbrev":"Init aux r\u00e9seaux informatiques", - "note":{ - "value":"12.00", - "moy":"-", - "max":"-", - "min":"-", - "nb_notes":"-", - "nb_missing":"-", - "nb_valid_evals":"-" - }, - "code_apogee":"", - "evaluation":[ - { - "jour":"2022-04-20", - "heure_debut":"08:00:00", - "heure_fin":"09:00:00", - "coefficient":1.0, - "evaluation_type":0, - "evaluation_id":1, - "description":"eval1", - "note":"12.00" - } - ] - }, - ... - ] - } - ], - "ue_capitalisee":[], - "absences":{ - "nbabs":2, - "nbabsjust":1 - }, - "appreciation":[] - } - """ - # Fonction utilisée : app.scodoc.sco_bulletins_json.make_json_formsemestre_bulletinetud() - - try: - formsemestre = models.FormSemestre.query.filter_by( - id=formsemestre_id - ).first_or_404() - - dept = models.Departement.query.filter_by( - id=formsemestre.dept_id - ).first_or_404() - - app.set_sco_dept(dept.acronym) - except: - return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel.\n " - "Veilliez vérifier que le nom de département est valide", - ) - if etudid is None: - # Récupération de l'étudiant - try: - etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) - etudid = etu.etudid - except AttributeError: - return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel.\n " - "Veilliez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", - ) - - data = make_json_formsemestre_bulletinetud(formsemestre_id, etudid) - - return data - - @bp.route("/formsemestre//bulletins", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) def bulletins(formsemestre_id: int): """ @@ -452,73 +268,69 @@ def bulletins(formsemestre_id: int): ... ] """ - # Fonction utilisée : app.scodoc.sco_bulletins.get_formsemestre_bulletin_etud_json() - formsemestre = models.FormSemestre.query.filter_by( id=formsemestre_id ).first_or_404() - dept = models.Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() - app.set_sco_dept(dept.acronym) - etuds = formsemestre.etuds - data = [] - for etu in etuds: + for etu in formsemestre.etuds: bul_etu = get_formsemestre_bulletin_etud_json(formsemestre, etu) data.append(bul_etu.json) return jsonify(data) -@bp.route("/formsemestre//jury", methods=["GET"]) -@token_permission_required(Permission.APIView) -def jury(formsemestre_id: int): - """ - Retourne le récapitulatif des décisions jury +# XXX Attendre ScoDoc 9.3 +# @bp.route("/formsemestre//jury", methods=["GET"]) +# @token_auth.login_required +# @token_permission_required(Permission.APIView) +# def jury(formsemestre_id: int): +# """ +# Retourne le récapitulatif des décisions jury - formsemestre_id : l'id d'un formsemestre +# formsemestre_id : l'id d'un formsemestre - Exemple de résultat : +# Exemple de résultat : - """ - # Fonction utilisée : app.scodoc.sco_pvjury.formsemestre_pvjury() +# """ +# # Fonction utilisée : app.scodoc.sco_pvjury.formsemestre_pvjury() - formsemestre = models.FormSemestre.query.filter_by( - id=formsemestre_id - ).first_or_404() +# formsemestre = models.FormSemestre.query.filter_by( +# id=formsemestre_id +# ).first_or_404() - dept = models.Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() +# dept = models.Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() - app.set_sco_dept(dept.acronym) +# app.set_sco_dept(dept.acronym) - data = formsemestre_pvjury(formsemestre_id) +# data = formsemestre_pvjury(formsemestre_id) - # try: - # # Utilisation de la fonction formsemestre_pvjury - # data = formsemestre_pvjury(formsemestre_id) - # except AttributeError: - # return error_response( - # 409, - # message="La requête ne peut être traitée en l’état actuel. \n" - # "Veillez vérifier la conformité du 'formation_id'", - # ) +# # try: +# # # Utilisation de la fonction formsemestre_pvjury +# # data = formsemestre_pvjury(formsemestre_id) +# # except AttributeError: +# # return error_response( +# # 409, +# # message="La requête ne peut être traitée en l’état actuel. \n" +# # "Veillez vérifier la conformité du 'formation_id'", +# # ) - return jsonify(data) +# return jsonify(data) @bp.route( "/formsemestre//programme", methods=["GET"], ) +@token_auth.login_required @token_permission_required(Permission.APIView) -def semestre_index(formsemestre_id: int): +def formsemestre_programme(formsemestre_id: int): """ Retourne la liste des Ues, ressources et SAE d'un semestre - dept : l'acronym d'un département - formsemestre_id : l'id d'un formesemestre + formsemestre_id : l'id d'un formsemestre Exemple de résultat : { @@ -543,78 +355,61 @@ def semestre_index(formsemestre_id: int): ], "ressources": [ { - "titre": "Fondamentaux de la programmation", - "coefficient": 1.0, - "module_type": 2, - "id": 17, - "ects": null, - "abbrev": null, - "ue_id": 3, - "code": "R107", - "formation_id": 1, - "heures_cours": 0.0, - "matiere_id": 3, - "heures_td": 0.0, - "semestre_id": 1, - "heures_tp": 0.0, - "numero": 70, - "code_apogee": "", - "module_id": 17 + "ens": [ 10, 18 ], + "formsemestre_id": 1, + "id": 15, + "module": { + "abbrev": "Programmer", + "code": "SAE15", + "code_apogee": "V7GOP", + "coefficient": 1.0, + "formation_id": 1, + "heures_cours": 0.0, + "heures_td": 0.0, + "heures_tp": 0.0, + "id": 15, + "matiere_id": 3, + "module_id": 15, + "module_type": 3, + "numero": 50, + "semestre_id": 1, + "titre": "Programmer en Python", + "ue_id": 3 }, + "module_id": 15, + "moduleimpl_id": 15, + "responsable_id": 2 + }, ... ], "saes": [ { - "titre": "Se pr\u00e9senter sur Internet", - "coefficient": 1.0, - "module_type": 3, - "id": 14, - "ects": null, - "abbrev": null, - "ue_id": 3, - "code": "SAE14", - "formation_id": 1, - "heures_cours": 0.0, - "matiere_id": 3, - "heures_td": 0.0, - "semestre_id": 1, - "heures_tp": 0.0, - "numero": 40, - "code_apogee": "", - "module_id": 14 + ... }, ... - ] + ], + "modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ] } """ - - formsemestre = models.FormSemestre.query.filter_by( + formsemestre: FormSemestre = models.FormSemestre.query.filter_by( id=formsemestre_id ).first_or_404() ues = formsemestre.query_ues() - - ues_dict = [] - ressources = [] - saes = [] - - for ue in ues: - ues_dict.append(ue.to_dict()) - ressources = ue.get_ressources() - saes = ue.get_saes() - - data_ressources = [] - for ressource in ressources: - data_ressources.append(ressource.to_dict()) - - data_saes = [] - for sae in saes: - data_saes.append(sae.to_dict()) - - data = { - "ues": ues_dict, - "ressources": data_ressources, - "saes": data_saes, + m_list = { + ModuleType.RESSOURCE: [], + ModuleType.SAE: [], + ModuleType.STANDARD: [], } + for modimpl in formsemestre.modimpls_sorted: + d = modimpl.to_dict() + m_list[modimpl.module.module_type].append(d) - return data + return jsonify( + { + "ues": [ue.to_dict() for ue in ues], + "ressources": m_list[ModuleType.RESSOURCE], + "saes": m_list[ModuleType.SAE], + "modules": m_list[ModuleType.STANDARD], + } + ) diff --git a/app/api/jury.py b/app/api/jury.py index 98228077..1131a284 100644 --- a/app/api/jury.py +++ b/app/api/jury.py @@ -1,41 +1,41 @@ #################################################### Jury ############################################################# -from flask import jsonify +# from flask import jsonify -from app import models -from app.api import bp -from app.api.errors import error_response -from app.api.auth import token_permission_required -from app.scodoc.sco_prepajury import feuille_preparation_jury -from app.scodoc.sco_pvjury import formsemestre_pvjury +# from app import models +# from app.api import bp +# from app.api.errors import error_response +# from app.api.auth import token_auth, token_permission_required +# from app.scodoc.sco_prepajury import feuille_preparation_jury +# from app.scodoc.sco_pvjury import formsemestre_pvjury -@bp.route("/jury/formsemestre//preparation_jury", methods=["GET"]) -# @token_permission_required(Permission.?) -def jury_preparation(formsemestre_id: int): - """ - Retourne la feuille de préparation du jury +# # @bp.route("/jury/formsemestre//preparation_jury", methods=["GET"]) +# # @token_permission_required(Permission.?) +# def jury_preparation(formsemestre_id: int): +# """ +# Retourne la feuille de préparation du jury - formsemestre_id : l'id d'un formsemestre - """ - # Fonction utilisée : app.scodoc.sco_prepajury.feuille_preparation_jury() +# formsemestre_id : l'id d'un formsemestre +# """ +# # Fonction utilisée : app.scodoc.sco_prepajury.feuille_preparation_jury() - # Utilisation de la fonction feuille_preparation_jury - prepa_jury = feuille_preparation_jury(formsemestre_id) +# # Utilisation de la fonction feuille_preparation_jury +# prepa_jury = feuille_preparation_jury(formsemestre_id) - return error_response(501, message="Not implemented") +# return error_response(501, message="Not implemented") -@bp.route("/jury/formsemestre//decisions_jury", methods=["GET"]) -# @token_permission_required(Permission.?) -def jury_decisions(formsemestre_id: int): - """ - Retourne les décisions du jury suivant un formsemestre donné +# # @bp.route("/jury/formsemestre//decisions_jury", methods=["GET"]) +# # @token_permission_required(Permission.?) +# def jury_decisions(formsemestre_id: int): +# """ +# Retourne les décisions du jury suivant un formsemestre donné - formsemestre_id : l'id d'un formsemestre - """ - # Fonction utilisée : app.scodoc.sco_pvjury.formsemestre_pvjury() +# formsemestre_id : l'id d'un formsemestre +# """ +# # Fonction utilisée : app.scodoc.sco_pvjury.formsemestre_pvjury() - # Utilisation de la fonction formsemestre_pvjury - decision_jury = formsemestre_pvjury(formsemestre_id) +# # Utilisation de la fonction formsemestre_pvjury +# decision_jury = formsemestre_pvjury(formsemestre_id) - return error_response(501, message="Not implemented") +# return error_response(501, message="Not implemented") diff --git a/app/api/logos.py b/app/api/logos.py index 663ef602..a2c1fbde 100644 --- a/app/api/logos.py +++ b/app/api/logos.py @@ -36,7 +36,6 @@ from app.api import bp from app.api import requested_format from app.api.auth import token_auth from app.api.errors import error_response -from app.decorators import permission_required from app.models import Departement from app.scodoc.sco_logos import list_logos, find_logo from app.api.auth import token_auth, token_permission_required @@ -44,6 +43,7 @@ from app.scodoc.sco_permissions import Permission @bp.route("/logos", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) def api_get_glob_logos(): if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): @@ -56,6 +56,7 @@ def api_get_glob_logos(): @bp.route("/logos/", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) def api_get_glob_logo(logoname): if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): @@ -72,6 +73,7 @@ def api_get_glob_logo(logoname): @bp.route("/departements//logos", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) def api_get_local_logos(departement): dept_id = Departement.from_acronym(departement).id @@ -82,6 +84,7 @@ def api_get_local_logos(departement): @bp.route("/departements//logos/", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) def api_get_local_logo(departement, logoname): # format = requested_format("jpg", ['png', 'jpg']) XXX ? diff --git a/app/api/partitions.py b/app/api/partitions.py index d925d174..1ee74c46 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -5,12 +5,13 @@ from app import models from app.api import bp from app.api.errors import error_response -from app.api.auth import token_permission_required +from app.api.auth import token_auth, token_permission_required from app.scodoc.sco_groups import get_group_members, setGroups, get_partitions_list from app.scodoc.sco_permissions import Permission @bp.route("/partitions/", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) def partition(formsemestre_id: int): """ @@ -53,6 +54,7 @@ def partition(formsemestre_id: int): @bp.route("/partitions/groups/", methods=["GET"]) @bp.route("/partitions/groups//etat/", methods=["GET"]) +@token_auth.login_required @token_permission_required(Permission.APIView) def etud_in_group(group_id: int, etat=None): """ @@ -111,11 +113,7 @@ def etud_in_group(group_id: int, etat=None): data = get_group_members(group_id, etat) if len(data) == 0: - return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel. \n" - "Aucun groupe ne correspond au 'group_id' renseigné", - ) + return error_response(404, message="group_id inconnu") return jsonify(data) @@ -125,6 +123,7 @@ def etud_in_group(group_id: int, etat=None): "/create/", methods=["POST"], ) +@token_auth.login_required @token_permission_required(Permission.APIEtudChangeGroups) def set_groups( partition_id: int, groups_lists: str, groups_to_delete: str, groups_to_create: str @@ -143,8 +142,4 @@ def set_groups( setGroups(partition_id, groups_lists, groups_to_create, groups_to_delete) return error_response(200, message="Groups set") except ValueError: - return error_response( - 409, - message="La requête ne peut être traitée en l’état actuel. \n" - "Veillez vérifier la conformité des éléments passé en paramètres", - ) + return error_response(404, message="Erreur") diff --git a/app/api/remiser.py b/app/api/remiser.py deleted file mode 100644 index 156a4c5f..00000000 --- a/app/api/remiser.py +++ /dev/null @@ -1,253 +0,0 @@ -# @bp.route("/etudiants", methods=["GET"]) -# @token_permission_required(Permission.APIView) -# def etudiants(): -# """ -# Retourne la liste de tous les étudiants -# -# Exemple de résultat : -# { -# "civilite": "X", -# "code_ine": null, -# "code_nip": null, -# "date_naissance": null, -# "email": null, -# "emailperso": null, -# "etudid": 18, -# "nom": "MOREL", -# "prenom": "JACQUES" -# }, -# { -# "civilite": "X", -# "code_ine": null, -# "code_nip": null, -# "date_naissance": null, -# "email": null, -# "emailperso": null, -# "etudid": 19, -# "nom": "FOURNIER", -# "prenom": "ANNE" -# }, -# ... -# """ -# # Récupération de tous les étudiants -# etu = models.Identite.query.all() -# -# # Mise en forme des données -# data = [d.to_dict_bul(include_urls=False) for d in etu] -# -# return jsonify(data) - - -# @bp.route( -# "/evaluations/eval_set_notes?eval_id=&etudid=¬e=", -# methods=["POST"], -# ) -# @bp.route( -# "/evaluations/eval_set_notes?eval_id=&nip=¬e=", -# methods=["POST"], -# ) -# @bp.route( -# "/evaluations/eval_set_notes?eval_id=&ine=¬e=", -# methods=["POST"], -# ) -# @token_permission_required(Permission.APIEditAllNotes) -# def evaluation_set_notes( -# eval_id: int, note: float, etudid: int = None, nip: int = None, ine: int = None -# ): -# """ -# Set les notes d'une évaluation pour un étudiant donnée -# -# eval_id : l'id d'une évaluation -# note : la note à attribuer -# etudid : l'etudid d'un étudiant -# nip : le code nip d'un étudiant -# ine : le code ine d'un étudiant -# """ -# # Fonction utilisée : app.scodoc.sco_saisie_notes.notes_add() -# -# # Qu'est ce qu'un user ??? -# # notes_add() -# return error_response(501, message="Not implemented") - - -# ### Inutil en définitif ### -# @bp.route( -# "/absences/abs_signale?etudid=&date=&matin=&justif=" -# "&description=", -# methods=["POST"], -# ) -# @bp.route( -# "/absences/abs_signale?nip=&date=&matin=&justif=" -# "&description=", -# methods=["POST"], -# ) -# @bp.route( -# "/absences/abs_signale?ine=&date=&matin=&justif=" -# "&description=", -# methods=["POST"], -# ) -# @bp.route( -# "/absences/abs_signale?ine=&date=&matin=&justif=" -# "&description=&moduleimpl_id=", -# methods=["POST"], -# ) -# @token_permission_required(Permission.APIAbsChange) -# def abs_signale( -# date: datetime, -# matin: bool, -# justif: bool, -# etudid: int = None, -# nip: int = None, -# ine: int = None, ### Inutil en définitif -# description: str = None, -# moduleimpl_id: int = None, -# ): -# """ -# Permet d'ajouter une absence en base -# -# date : la date de l'absence -# matin : True ou False -# justif : True ou False -# etudid : l'etudid d'un étudiant -# nip: le code nip d'un étudiant -# ine : le code ine d'un étudiant -# description : description possible à ajouter sur l'absence -# moduleimpl_id : l'id d'un moduleimpl -# """ -# # Fonctions utilisées : app.scodoc.sco_abs.add_absence() et app.scodoc.sco_abs.add_justif() -# -# if etudid is None: -# # Récupération de l'étudiant -# try: -# etu = get_etu_from_request(etudid, nip, ine) -# etudid = etu.etudid -# except AttributeError: -# return error_response( -# 409, -# message="La requête ne peut être traitée en l’état actuel.\n " -# "Veilliez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", -# ) -# try: -# # Utilisation de la fonction add_absence -# add_absence(etudid, date, matin, justif, description, moduleimpl_id) -# if justif == True: -# # Utilisation de la fonction add_justif -# add_justif(etudid, date, matin, description) -# except ValueError: -# return error_response( -# 409, message="La requête ne peut être traitée en l’état actuel" -# ) -# @bp.route( -# "/absences/abs_annule_justif?etudid=&jour=&matin=", -# methods=["POST"], -# ) -# @bp.route( -# "/absences/abs_annule_justif?nip=&jour=&matin=", -# methods=["POST"], -# ) -# @bp.route( -# "/absences/abs_annule_justif?ine=&jour=&matin=", -# methods=["POST"], -# ) -# @token_permission_required(Permission.APIAbsChange) -# def abs_annule_justif( -# jour: datetime, matin: str, etudid: int = None, nip: int = None, ine: int = None -# ): -# """ -# Retourne un html - -# jour : la date de l'absence a annulé -# matin : True ou False -# etudid : l'etudid d'un étudiant -# nip: le code nip d'un étudiant -# ine : le code ine d'un étudiant -# """ -# # Fonction utilisée : app.scodoc.sco_abs.annule_justif() - -# if etudid is None: -# # Récupération de l'étudiant -# try: -# etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) -# etudid = etu.etudid -# except AttributeError: -# return error_response( -# 409, -# message="La requête ne peut être traitée en l’état actuel.\n " -# "Veilliez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide", -# ) -# try: -# # Utilisation de la fonction annule_justif -# annule_justif(etudid, jour, matin) -# except ValueError: -# return error_response( -# 409, -# message="La requête ne peut être traitée en l’état actuel.\n " -# "Veilliez vérifier que le 'jour' et le 'matin' sont valides", -# ) - -# return error_response(200, message="OK") - -# @bp.route( -# "/jury/set_decision/etudid?etudid=&formsemestre_id=" -# "&jury=&devenir=&assiduite=", -# methods=["POST"], -# ) -# @bp.route( -# "/jury/set_decision/nip?etudid=&formsemestre_id=" -# "&jury=&devenir=&assiduite=", -# methods=["POST"], -# ) -# @bp.route( -# "/jury/set_decision/ine?etudid=&formsemestre_id=" -# "&jury=&devenir=&assiduite=", -# methods=["POST"], -# ) -# # @token_permission_required(Permission.) -# def set_decision_jury( -# formsemestre_id: int, -# decision_jury: str, -# devenir_jury: str, -# assiduite: bool, -# etudid: int = None, -# nip: int = None, -# ine: int = None, -# ): -# """ -# Attribuer la décision du jury et le devenir à un etudiant -# -# formsemestre_id : l'id d'un formsemestre -# decision_jury : la décision du jury -# devenir_jury : le devenir du jury -# assiduite : True ou False -# etudid : l'etudid d'un étudiant -# nip: le code nip d'un étudiant -# ine : le code ine d'un étudiant -# """ -# return error_response(501, message="Not implemented") -# -# -# @bp.route( -# "/jury/etudid//formsemestre//annule_decision", -# methods=["DELETE"], -# ) -# @bp.route( -# "/jury/nip//formsemestre//annule_decision", -# methods=["DELETE"], -# ) -# @bp.route( -# "/jury/ine//formsemestre//annule_decision", -# methods=["DELETE"], -# ) -# # @token_permission_required(Permission.) -# def annule_decision_jury( -# formsemestre_id: int, etudid: int = None, nip: int = None, ine: int = None -# ): -# """ -# Supprime la déciosion du jury pour un étudiant donné -# -# formsemestre_id : l'id d'un formsemestre -# etudid : l'etudid d'un étudiant -# nip: le code nip d'un étudiant -# ine : le code ine d'un étudiant -# """ -# return error_response(501, message="Not implemented") diff --git a/app/api/sco_api.py b/app/api/sco_api.py deleted file mode 100644 index 8440aa55..00000000 --- a/app/api/sco_api.py +++ /dev/null @@ -1,152 +0,0 @@ -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -############################################################################## -# -# Gestion scolarite IUT -# -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Emmanuel Viennet emmanuel.viennet@viennet.net -# -############################################################################## - -"""API ScoDoc 9 -""" -# PAS ENCORE IMPLEMENTEE, juste un essai -# Pour P. Bouron, il faudrait en priorité l'équivalent de -# Scolarite/Notes/moduleimpl_withmodule_list (alias scodoc7 do_moduleimpl_withmodule_list) -# Scolarite/Notes/evaluation_create -# Scolarite/Notes/evaluation_delete -# Scolarite/Notes/formation_list -# Scolarite/Notes/formsemestre_list -# Scolarite/Notes/formsemestre_partition_list -# Scolarite/Notes/groups_view -# Scolarite/Notes/moduleimpl_status -# Scolarite/setGroups -from datetime import datetime - -from flask import jsonify, request, g, send_file -from sqlalchemy.sql import func - -from app import db, log -from app.api import bp, requested_format -from app.api.auth import token_auth -from app.api.errors import error_response -from app import models -from app.models import FormSemestre, FormSemestreInscription, Identite -from app.models import ApcReferentielCompetences -from app.scodoc.sco_abs import ( - annule_absence, - annule_justif, - add_absence, - add_justif, - list_abs_date, -) -from app.scodoc.sco_bulletins import formsemestre_bulletinetud_dict -from app.scodoc.sco_bulletins_json import make_json_formsemestre_bulletinetud -from app.scodoc.sco_evaluation_db import do_evaluation_get_all_notes -from app.scodoc.sco_formations import formation_export -from app.scodoc.sco_formsemestre_inscriptions import ( - do_formsemestre_inscription_listinscrits, -) -from app.scodoc.sco_groups import setGroups, get_etud_groups, get_group_members -from app.scodoc.sco_logos import list_logos, find_logo, _list_dept_logos -from app.scodoc.sco_moduleimpl import moduleimpl_list -from app.scodoc.sco_permissions import Permission - - -# ###################################################### Logos ########################################################## -# -# # XXX TODO voir get_logo déjà existant dans app/views/scodoc.py -# -# @bp.route("/logos", methods=["GET"]) -# def liste_logos(format="json"): -# """ -# Liste des logos définis pour le site scodoc. -# """ -# # fonction to use : list_logos() -# # try: -# # res = list_logos() -# # except ValueError: -# # return error_response(409, message="La requête ne peut être traitée en l’état actuel") -# # -# # if res is None: -# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés") -# # -# # return res -# -# -# -# @bp.route("/logos/", methods=["GET"]) -# def recup_logo_global(logo_name: str): -# """ -# Retourne l'image au format png ou jpg -# -# logo_name : le nom du logo rechercher -# """ -# # fonction to use find_logo -# # try: -# # res = find_logo(logo_name) -# # except ValueError: -# # return error_response(409, message="La requête ne peut être traitée en l’état actuel") -# # -# # if res is None: -# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés") -# # -# # return res -# -# -# @bp.route("/departements//logos", methods=["GET"]) -# def logo_dept(dept: str): -# """ -# Liste des logos définis pour le département visé. -# -# dept : l'id d'un département -# """ -# # fonction to use: _list_dept_logos -# # dept_id = models.Departement.query.filter_by(acronym=dept).first() -# # try: -# # res = _list_dept_logos(dept_id.id) -# # except ValueError: -# # return error_response(409, message="La requête ne peut être traitée en l’état actuel") -# # -# # if res is None: -# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés") -# # -# # return res -# -# -# @bp.route("/departement//logos/", methods=["GET"]) -# def recup_logo_dept_global(dept: str, logo_name: str): -# """ -# L'image format png ou jpg -# -# dept : l'id d'un département -# logo_name : le nom du logo rechercher -# """ -# # fonction to use find_logo -# # dept_id = models.Departement.query.filter_by(acronym=dept).first() -# # try: -# # res = find_logo(logo_name, dept_id.id) -# # except ValueError: -# # return error_response(409, message="La requête ne peut être traitée en l’état actuel") -# # -# # if res is None: -# # return error_response(200, message="Aucun logo trouvé correspondant aux informations renseignés") -# # -# # return res diff --git a/app/api/test_api.py b/app/api/test_api.py deleted file mode 100644 index 821e3a01..00000000 --- a/app/api/test_api.py +++ /dev/null @@ -1,444 +0,0 @@ -################################################## Tests ############################################################## - - -# XXX OBSOLETE ??? XXX - -import requests -import os - -from app import models -from app.api import bp, requested_format -from app.api.auth import token_auth -from app.api.errors import error_response - -SCODOC_USER = "test" -SCODOC_PASSWORD = "test" -SCODOC_URL = "http://192.168.1.12:5000" -CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) - -HEADERS = None - - -def get_token(): - """ - Permet de set le token dans le header - """ - global HEADERS - global SCODOC_USER - global SCODOC_PASSWORD - - r0 = requests.post( - SCODOC_URL + "/ScoDoc/api/tokens", auth=(SCODOC_USER, SCODOC_PASSWORD) - ) - token = r0.json()["token"] - HEADERS = {"Authorization": f"Bearer {token}"} - - -DEPT = None -FORMSEMESTRE = None -ETU = None - - -@bp.route("/test_dept", methods=["GET"]) -def get_departement(): - """ - Permet de tester departements() mais également de set un département dans DEPT pour la suite des tests - """ - - get_token() - - global HEADERS - global CHECK_CERTIFICATE - global SCODOC_USER - global SCODOC_PASSWORD - - # print(HEADERS) - # departements - r = requests.get( - SCODOC_URL + "/ScoDoc/api/departements", - headers=HEADERS, - verify=CHECK_CERTIFICATE, - ) - - if r.status_code == 200: - dept_id = r.json()[0] - # print(dept_id) - - dept = models.Departement.query.filter_by(id=dept_id).first() - dept = dept.to_dict() - - fields = ["id", "acronym", "description", "visible", "date_creation"] - - for field in dept: - if field not in fields: - return error_response(501, field + " field missing") - - global DEPT - DEPT = dept - - return error_response(200, "OK") - - return error_response(409, "La requête ne peut être traitée en l’état actuel") - - -@bp.route("/test_formsemestre", methods=["GET"]) -def get_formsemestre(): - """ - Permet de tester liste_semestres_courant() mais également de set un formsemestre dans FORMSEMESTRE - pour la suite des tests - """ - get_departement() - - global DEPT - dept_acronym = DEPT["acronym"] - - # liste_semestres_courant - r = requests.get( - SCODOC_URL + "/ScoDoc/api/departements/" + dept_acronym + "/semestres_courants", - auth=(SCODOC_USER, SCODOC_PASSWORD), - ) - - if r.status_code == 200: - formsemestre = r.json()[0] - print(r.json()[0]) - - fields = [ - "gestion_semestrielle", - "titre", - "scodoc7_id", - "date_debut", - "bul_bgcolor", - "date_fin", - "resp_can_edit", - "dept_id", - "etat", - "resp_can_change_ens", - "id", - "modalite", - "ens_can_edit_eval", - "formation_id", - "gestion_compensation", - "elt_sem_apo", - "semestre_id", - "bul_hide_xml", - "elt_annee_apo", - "block_moyennes", - "formsemestre_id", - "titre_num", - "date_debut_iso", - "date_fin_iso", - "responsables", - ] - - for field in formsemestre: - if field not in fields: - return error_response(501, field + " field missing") - - global FORMSEMESTRE - FORMSEMESTRE = formsemestre - - return error_response(200, "OK") - - return error_response(409, "La requête ne peut être traitée en l’état actuel") - - -@bp.route("/test_etu", methods=["GET"]) -def get_etudiant(): - """ - Permet de tester etudiants() mais également de set un etudiant dans ETU pour la suite des tests - """ - - # etudiants - r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiants/courant", - auth=(SCODOC_USER, SCODOC_PASSWORD), - ) - - if r.status_code == 200: - etu = r.json()[0] - - fields = [ - "civilite", - "code_ine", - "code_nip", - "date_naissance", - "email", - "emailperso", - "etudid", - "nom", - "prenom", - ] - - for field in etu: - if field not in fields: - return error_response(501, field + " field missing") - - global ETU - ETU = etu - print(etu) - - return error_response(200, "OK") - - return error_response(409, "La requête ne peut être traitée en l’état actuel") - - -############################################### Departements ########################################################## - - -@bp.route("/test_liste_etudiants") -def test_departements_liste_etudiants(): - """ - Test la route liste_etudiants - """ - # Set un département et un formsemestre pour les tests - get_departement() - get_formsemestre() - - global DEPT - global FORMSEMESTRE - - # Set les fields à vérifier - fields = [ - "civilite", - "code_ine", - "code_nip", - "date_naissance", - "email", - "emailperso", - "etudid", - "nom", - "prenom", - ] - - # liste_etudiants (sans formsemestre) - r1 = requests.get( - SCODOC_URL + "/ScoDoc/api/departements/" + DEPT["acronym"] + "/etudiants/liste", - auth=(SCODOC_USER, SCODOC_PASSWORD), - ) - - if r1.status_code == 200: # Si la requête est "OK" - # On récupère la liste des étudiants - etudiants = r1.json() - - # Vérification que tous les étudiants ont bien tous les bons champs - for etu in etudiants: - for field in etu: - if field not in fields: - return error_response(501, field + " field missing") - - # liste_etudiants (avec formsemestre) - r2 = requests.get( - SCODOC_URL - + "/ScoDoc/api/departements/" - + DEPT["acronym"] - + "/etudiants/liste/" - + str(FORMSEMESTRE["formsemestre_id"]), - auth=(SCODOC_USER, SCODOC_PASSWORD), - ) - - if r2.status_code == 200: # Si la requête est "OK" - # On récupère la liste des étudiants - etudiants = r2.json() - - # Vérification que tous les étudiants ont bien tous les bons champs - for etu in etudiants: - for field in etu: - if field not in fields: - return error_response(501, field + " field missing") - - return error_response(200, "OK") - - return error_response(409, "La requête ne peut être traitée en l’état actuel") - - -@bp.route("/test_referenciel_competences") -def test_departements_referenciel_competences(): - """ - Test la route referenciel_competences - """ - get_departement() - get_formsemestre() - - global DEPT - global FORMSEMESTRE - - # referenciel_competences - r = requests.post( - SCODOC_URL - + "/ScoDoc/api/departements/" - + DEPT["acronym"] - + "/formations/" - + FORMSEMESTRE["formation_id"] - + "/referentiel_competences", - auth=(SCODOC_USER, SCODOC_PASSWORD), - ) - - -@bp.route("/test_liste_semestre_index") -def test_departements_semestre_index(): - """ - Test la route semestre_index - """ - # semestre_index - r5 = requests.post( - SCODOC_URL - + "/ScoDoc/api/departements/" - + DEPT["acronym"] - + "/formsemestre/" - + FORMSEMESTRE["formation_id"] - + "/programme", - auth=(SCODOC_USER, SCODOC_PASSWORD), - ) - - -#################################################### Etudiants ######################################################## - - -def test_routes_etudiants(): - """ - Test les routes de la partie Etudiants - """ - # etudiants - r1 = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiants", auth=(SCODOC_USER, SCODOC_PASSWORD) - ) - - # etudiants_courant - r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # etudiant - r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # etudiant_formsemestres - r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # etudiant_bulletin_semestre - r5 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # etudiant_groups - r6 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - -def test_routes_formation(): - """ - Test les routes de la partie Formation - """ - # formations - r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # formations_by_id - r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # formation_export_by_formation_id - r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # formsemestre_apo - r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # moduleimpls - r5 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # moduleimpls_sem - r6 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - -def test_routes_formsemestres(): - """ - Test les routes de la partie Formsemestres - """ - # formsemestre - r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # etudiant_bulletin - r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # bulletins - r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # jury - r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - -def test_routes_partitions(): - """ - Test les routes de la partie Partitions - """ - # partition - r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # etud_in_group - r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # set_groups - r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - -def test_routes_evaluations(): - """ - Test les routes de la partie Evaluations - """ - # evaluations - r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # evaluation_notes - r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # evaluation_set_notes - r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - -def test_routes_jury(): - """ - Test les routes de la partie Jury - """ - # jury_preparation - r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # jury_decisions - r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # set_decision_jury - r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # annule_decision_jury - r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - -def test_routes_absences(): - """ - Test les routes de la partie Absences - """ - # absences - r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # absences_justify - r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # abs_signale - r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # abs_annule - r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # abs_annule_justif - r5 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # abs_groupe_etat - r6 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - -def test_routes_logos(): - """ - Test les routes de la partie Logos - """ - # liste_logos - r1 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # recup_logo_global - r2 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # logo_dept - r3 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) - - # recup_logo_dept_global - r4 = requests.post(SCODOC_URL + "/ScoDoc/api", auth=(SCODOC_USER, SCODOC_PASSWORD)) diff --git a/app/api/tokens.py b/app/api/tokens.py index f36ec7b0..f5c11b09 100644 --- a/app/api/tokens.py +++ b/app/api/tokens.py @@ -1,5 +1,5 @@ from flask import jsonify -from app import db +from app import db, log from app.api import bp from app.api.auth import basic_auth, token_auth @@ -7,7 +7,9 @@ from app.api.auth import basic_auth, token_auth @bp.route("/tokens", methods=["POST"]) @basic_auth.login_required def get_token(): + "renvoie un jeton jwt pour l'utilisateur courant" token = basic_auth.current_user().get_token() + log(f"API: giving token to {basic_auth.current_user()}") db.session.commit() return jsonify({"token": token}) @@ -15,6 +17,7 @@ def get_token(): @bp.route("/tokens", methods=["DELETE"]) @token_auth.login_required def revoke_token(): + "révoque le jeton de l'utilisateur courant" token_auth.current_user().revoke_token() db.session.commit() return "", 204 diff --git a/app/api/tools.py b/app/api/tools.py index 35b07cf2..e03d8334 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -1,15 +1,17 @@ from app import models -def get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine): +def get_etud_from_etudid_or_nip_or_ine( + etudid=None, nip=None, ine=None +) -> models.Identite: """ - Fonction qui retourne un etudiant en fonction de l'etudid, code nip et code ine rentré en paramètres + etudiant en fonction de l'etudid, code nip et code ine rentré en paramètres etudid : None ou un int etudid nip : None ou un int code_nip ine : None ou un int code_ine - Exemple de résultat: + Return None si étudiant inexistant. """ if etudid is None: if nip is None: # si ine diff --git a/app/auth/models.py b/app/auth/models.py index cfab21a9..ad56c506 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -18,7 +18,7 @@ from werkzeug.security import generate_password_hash, check_password_hash import jwt -from app import db, login +from app import db, log, login from app.models import Departement from app.models import SHORT_STR_LEN from app.scodoc.sco_exceptions import ScoValueError @@ -150,11 +150,22 @@ class User(UserMixin, db.Model): def verify_reset_password_token(token): "Vérification du token de reéinitialisation du mot de passe" try: - user_id = jwt.decode( + token = jwt.decode( token, current_app.config["SECRET_KEY"], algorithms=["HS256"] - )["reset_password"] + ) + except jwt.exceptions.ExpiredSignatureError: + log(f"verify_reset_password_token: token expired") except: - return + return None + try: + user_id = token["reset_password"] + # double check en principe inutile car déjà fait dans decode() + expire = float(token["exp"]) + if time() > expire: + log(f"verify_reset_password_token: token expired for uid={user_id}") + return None + except (TypeError, KeyError): + return None return User.query.get(user_id) def to_dict(self, include_email=True): @@ -214,6 +225,7 @@ class User(UserMixin, db.Model): self.add_role(role, dept) def get_token(self, expires_in=3600): + "Un jeton pour cet user. Stocké en base, non commité." now = datetime.utcnow() if self.token and self.token_expiration > now + timedelta(seconds=60): return self.token @@ -223,6 +235,7 @@ class User(UserMixin, db.Model): return self.token def revoke_token(self): + "Révoque le jeton de cet utilisateur" self.token_expiration = datetime.utcnow() - timedelta(seconds=1) @staticmethod @@ -335,7 +348,7 @@ class User(UserMixin, db.Model): return None def get_nom_fmt(self): - """Nom formatté: "Martin" """ + """Nom formaté: "Martin" """ if self.nom: return sco_etud.format_nom(self.nom, uppercase=False) else: diff --git a/app/auth/routes.py b/app/auth/routes.py index 24daa8ca..1f0259ab 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -71,7 +71,7 @@ def create_user(): flash("User {} created".format(user.user_name)) return redirect(url_for("scodoc.index")) return render_template( - "auth/register.html", title=u"Création utilisateur", form=form + "auth/register.html", title="Création utilisateur", form=form ) @@ -112,7 +112,7 @@ def reset_password(token): if current_user.is_authenticated: return redirect(url_for("scodoc.index")) user = User.verify_reset_password_token(token) - if not user: + if user is None: return redirect(url_for("scodoc.index")) form = ResetPasswordForm() if form.validate_on_submit(): diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 689ea9ed..f9dbb870 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -14,10 +14,12 @@ from flask import url_for, g from app.comp.res_but import ResultatsSemestreBUT from app.models import FormSemestre, Identite +from app.models.groups import GroupDescr from app.models.ues import UniteEns from app.scodoc import sco_bulletins, sco_utils as scu from app.scodoc import sco_bulletins_json from app.scodoc import sco_bulletins_pdf +from app.scodoc import sco_groups from app.scodoc import sco_preferences from app.scodoc.sco_codes_parcours import UE_SPORT, DEF from app.scodoc.sco_utils import fmt_note @@ -64,8 +66,16 @@ class BulletinBUT: # } return d - def etud_ue_results(self, etud: Identite, ue: UniteEns, decision_ue: dict) -> dict: - "dict synthèse résultats UE" + def etud_ue_results( + self, + etud: Identite, + ue: UniteEns, + decision_ue: dict, + etud_groups: list[GroupDescr] = None, + ) -> dict: + """dict synthèse résultats UE + etud_groups : liste des groupes, pour affichage du rang. + """ res = self.res d = { @@ -81,7 +91,7 @@ class BulletinBUT: if res.bonus_ues is not None and ue.id in res.bonus_ues else fmt_note(0.0), "malus": fmt_note(res.malus[ue.id][etud.id]), - "capitalise": None, # "AAAA-MM-JJ" TODO #sco92 + "capitalise": None, # "AAAA-MM-JJ" TODO #sco93 "ressources": self.etud_ue_mod_results(etud, ue, res.ressources), "saes": self.etud_ue_mod_results(etud, ue, res.saes), } @@ -103,7 +113,18 @@ class BulletinBUT: "moy": fmt_note(res.etud_moy_ue[ue.id].mean()), "rang": rang, "total": effectif, # nb etud avec note dans cette UE + "groupes": {}, } + if self.prefs["bul_show_ue_rangs"]: + for group in etud_groups: + if group.partition.bul_show_rank: + rang, effectif = self.res.get_etud_ue_rang( + ue.id, etud.id, group.id + ) + d["moyenne"]["groupes"][group.id] = { + "value": rang, + "total": effectif, + } else: # ceci suppose que l'on a une seule UE bonus, # en tous cas elles auront la même description @@ -275,6 +296,9 @@ class BulletinBUT: return d nbabs, nbabsjust = formsemestre.get_abs_count(etud.id) + etud_groups = sco_groups.get_etud_formsemestre_groups( + etud, formsemestre, only_to_show=True + ) semestre_infos = { "etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo], "date_debut": formsemestre.date_debut.isoformat(), @@ -282,7 +306,7 @@ class BulletinBUT: "annee_universitaire": formsemestre.annee_scolaire_str(), "numero": formsemestre.semestre_id, "inscription": "", # inutilisé mais nécessaire pour le js de Seb. - "groupes": [], # XXX TODO + "groupes": [group.to_dict() for group in etud_groups], } if self.prefs["bul_show_abs"]: semestre_infos["absences"] = { @@ -306,15 +330,25 @@ class BulletinBUT: "max": fmt_note(res.etud_moy_gen.max()), } if self.prefs["bul_show_rangs"] and not np.isnan(res.etud_moy_gen[etud.id]): - # classement wrt moyenne général, indicatif + # classement wrt moyenne générale, indicatif semestre_infos["rang"] = { "value": res.etud_moy_gen_ranks[etud.id], "total": nb_inscrits, + "groupes": {}, } + # Rangs par groupes + for group in etud_groups: + if group.partition.bul_show_rank: + rang, effectif = self.res.get_etud_rang_group(etud.id, group.id) + semestre_infos["rang"]["groupes"][group.id] = { + "value": rang, + "total": effectif, + } else: semestre_infos["rang"] = { "value": "-", "total": nb_inscrits, + "groupes": {}, } d.update( { @@ -324,7 +358,10 @@ class BulletinBUT: "saes": self.etud_mods_results(etud, res.saes, version=version), "ues": { ue.acronyme: self.etud_ue_results( - etud, ue, decision_ue=decisions_ues.get(ue.id, {}) + etud, + ue, + decision_ue=decisions_ues.get(ue.id, {}), + etud_groups=etud_groups, ) for ue in res.ues # si l'UE comporte des modules auxquels on est inscrit: diff --git a/app/but/bulletin_but_pdf.py b/app/but/bulletin_but_pdf.py index 36d11d1e..e3e40c51 100644 --- a/app/but/bulletin_but_pdf.py +++ b/app/but/bulletin_but_pdf.py @@ -127,6 +127,9 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): def ue_rows(self, rows: list, ue_acronym: str, ue: dict, title_bg: tuple): "Décrit une UE dans la table synthèse: titre, sous-titre et liste modules" + if (ue["type"] == UE_SPORT) and len(ue.get("modules", [])) == 0: + # ne mentionne l'UE que s'il y a des modules + return # 1er ligne titre UE moy_ue = ue.get("moyenne") t = { @@ -206,7 +209,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): for mod_code, mod in ue["modules"].items(): rows.append( { - "titre": f"{mod_code} {mod['titre']}", + "titre": f"{mod_code or ''} {mod['titre'] or ''}", } ) self.evaluations_rows(rows, mod["evaluations"]) @@ -313,7 +316,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): "lignes des évaluations" for e in evaluations: t = { - "titre": f"{e['description']}", + "titre": f"{e['description'] or ''}", "moyenne": e["note"]["value"], "_moyenne_pdf": Paragraph( f"""{e["note"]["value"]}""" diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 86d444c3..6cf0767f 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -266,6 +266,8 @@ class BonusSportMultiplicatif(BonusSport): amplitude = 0.005 # multiplie les points au dessus du seuil # En classique, les bonus multiplicatifs agissent par défaut sur les UE: classic_use_bonus_ues = True + # Facteur multiplicatif max: (bonus = moy_ue*factor) + factor_max = 1000.0 # infini # C'est un bonus "multiplicatif": on l'exprime en additif, # sur chaque moyenne d'UE m_0 @@ -285,6 +287,8 @@ class BonusSportMultiplicatif(BonusSport): notes = np.nan_to_num(notes, copy=False) factor = (notes - self.seuil_moy_gen) * self.amplitude # 5% si note=20 factor[factor <= 0] = 0.0 # note < seuil_moy_gen, pas de bonus + # note < seuil_moy_gen, pas de bonus: pas de facteur négatif, ni + factor.clip(0.0, self.factor_max, out=factor) # Ne s'applique qu'aux moyennes d'UE if len(factor.shape) == 1: # classic @@ -481,6 +485,19 @@ class BonusBezier(BonusSportAdditif): proportion_point = 0.03 +class BonusBlagnac(BonusSportAdditif): + """Calcul bonus modules optionnels (sport, culture), règle IUT de Blagnac. + + Le bonus est égal à 5% des points au dessus de 10 à appliquer sur toutes + les UE du semestre, applicable dans toutes les formations (DUT, BUT, ...). + """ + + name = "bonus_iutblagnac" + displayed_name = "IUT de Blagnac" + proportion_point = 0.05 + classic_use_bonus_ues = True # toujours sur les UE + + class BonusBordeaux1(BonusSportMultiplicatif): """Calcul bonus modules optionnels (sport, culture), règle IUT Bordeaux 1, sur moyenne générale et UEs. @@ -690,22 +707,123 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif): super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan) +class BonusIUTRennes1(BonusSportAdditif): + """Calcul bonus optionnels (sport, langue vivante, engagement étudiant), + règle IUT de l'Université de Rennes 1 (Lannion, Rennes, St Brieuc, St Malo). + +
    +
  • Les étudiants peuvent suivre un ou plusieurs activités optionnelles notées + dans les semestres pairs.
    + La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20. +
  • +
  • Le vingtième des points au dessus de 10 est ajouté à la moyenne de chaque UE + en BUT, ou à la moyenne générale pour les autres formations. +
  • +
  • Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/20 = 0,3 points + sur chaque UE. +
  • +
+ """ + + name = "bonus_iut_rennes1" + displayed_name = "IUTs de Rennes 1 (Lannion, Rennes, St Brieuc, St Malo)" + seuil_moy_gen = 10.0 + proportion_point = 1 / 20.0 + classic_use_bonus_ues = False + # S'applique aussi en classic, sur la moy. gen. + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + """calcul du bonus""" + # Prend la note de chaque modimpl, sans considération d'UE + if len(sem_modimpl_moys_inscrits.shape) > 2: # apc + sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0] + # ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic + note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds + nb_ues = self.formsemestre.query_ues(with_sport=False).count() + + bonus_moy_arr = np.where( + note_bonus_max > self.seuil_moy_gen, + (note_bonus_max - self.seuil_moy_gen) * self.proportion_point, + 0.0, + ) + # Seuil: bonus dans [min, max] (défaut [0,20]) + bonus_max = self.bonus_max or 20.0 + np.clip(bonus_moy_arr, self.bonus_min, bonus_max, out=bonus_moy_arr) + if self.formsemestre.formation.is_apc(): + bonus_moy_arr = np.stack([bonus_moy_arr] * nb_ues).T + + self.bonus_additif(bonus_moy_arr) + + +# juste pour compatibilité (nom bonus en base): +class BonusStBrieuc(BonusIUTRennes1): + name = "bonus_iut_stbrieuc" + displayed_name = "IUTs de Rennes 1/St-Brieuc" + __doc__ = BonusIUTRennes1.__doc__ + + +class BonusStMalo(BonusIUTRennes1): + name = "bonus_iut_stmalo" + displayed_name = "IUTs de Rennes 1/St-Malo" + __doc__ = BonusIUTRennes1.__doc__ + + class BonusLaRochelle(BonusSportAdditif): """Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle.
    -
  • Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point.
  • -
  • Si la note de sport est comprise entre 10 et 20 : ajout de 1% de cette - note sur la moyenne générale du semestre (ou sur les UE en BUT).
  • +
  • Si la note de sport est comprise entre 0 et 10 : pas d’ajout de point.
  • +
  • Si la note de sport est comprise entre 10 et 20 : +
      +
    • Pour le BUT, application pour chaque UE du semestre : +
        +
      • pour une note entre 18 et 20 => + 0,10 points
      • +
      • pour une note entre 16 et 17,99 => + 0,08 points
      • +
      • pour une note entre 14 et 15,99 => + 0,06 points
      • +
      • pour une note entre 12 et 13,99 => + 0,04 points
      • +
      • pour une note entre 10 et 11,99 => + 0,02 points
      • +
      +
    • +
    • Pour les DUT/LP : + ajout de 1% de la note sur la moyenne générale du semestre +
    • +
    +
""" name = "bonus_iutlr" displayed_name = "IUT de La Rochelle" + seuil_moy_gen = 10.0 # si bonus > 10, seuil_comptage = 0.0 # tous les points sont comptés proportion_point = 0.01 # 1% + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + """calcul du bonus""" + # La date du semestre ? + if self.formsemestre.formation.is_apc(): + if 0 in sem_modimpl_moys_inscrits.shape: + # pas d'étudiants ou pas d'UE ou pas de module... + return + # Calcule moyenne pondérée des notes de sport: + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + bonus_moy_arr = np.sum( + sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 + ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False) + bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0 + bonus_moy_arr[bonus_moy_arr >= 18.0] = 0.10 + bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.08 + bonus_moy_arr[bonus_moy_arr >= 14.0] = 0.06 + bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.04 + bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.02 + self.bonus_additif(bonus_moy_arr) + else: + # DUT et LP: + return super().compute_bonus( + sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan + ) + class BonusLeHavre(BonusSportAdditif): """Bonus sport IUT du Havre sur les moyennes d'UE @@ -908,7 +1026,7 @@ class BonusNantes(BonusSportAdditif): class BonusPoitiers(BonusSportAdditif): """Calcul bonus optionnels (sport, culture), règle IUT de Poitiers. - Les deux notes d'option supérieure à 10, bonifies les moyennes de chaque UE. + Les deux notes d'option supérieure à 10, bonifient les moyennes de chaque UE. bonus = (option1 - 10)*5% + (option2 - 10)*5% """ @@ -933,27 +1051,6 @@ class BonusRoanne(BonusSportAdditif): proportion_point = 1 -class BonusStBrieuc(BonusSportAdditif): - """IUT de Saint Brieuc - - Ne s'applique qu'aux semestres pairs (S2, S4, S6), et bonifie les moyennes d'UE: -
    -
  • Bonus = (S - 10)/20
  • -
- """ - - # Utilisé aussi par St Malo, voir plus bas - name = "bonus_iut_stbrieuc" - displayed_name = "IUT de Saint-Brieuc" - proportion_point = 1 / 20.0 - classic_use_bonus_ues = False - - def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): - """calcul du bonus""" - if self.formsemestre.semestre_id % 2 == 0: - super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan) - - class BonusStEtienne(BonusSportAdditif): """IUT de Saint-Etienne. @@ -984,27 +1081,42 @@ class BonusStDenis(BonusSportAdditif): bonus_max = 0.5 -class BonusStMalo(BonusStBrieuc): - # identique à St Brieux, sauf la doc - """IUT de Saint Malo +class BonusStNazaire(BonusSportMultiplicatif): + """IUT de Saint-Nazaire - Ne s'applique qu'aux semestres pairs (S2, S4, S6), et bonifie les moyennes d'UE: + Trois bonifications sont possibles : sport, culture et engagement citoyen + (qui seront déclarées comme des modules séparés de l'UE bonus).
    -
  • Bonus = (S - 10)/20
  • +
  • Chaque bonus est compris entre 0 et 20 points -> 4pt = 1%
    + (note 4/20: 1%, 8/20: 2%, 12/20: 3%, 16/20: 4%, 20/20: 5%) +
  • +
  • Le total des 3 bonus ne peut excéder 10%
  • +
  • La somme des bonus s'applique à la moyenne de chaque UE
+

Exemple: une moyenne d'UE de 10/20 avec un total des bonus de 6% donne + une moyenne de 10,6.

+

Les bonifications s'appliquent aussi au classement général du semestre + et de l'année. +

""" - name = "bonus_iut_stmalo" - displayed_name = "IUT de Saint-Malo" + + name = "bonus_iutSN" + displayed_name = "IUT de Saint-Nazaire" + classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP + seuil_moy_gen = 0.0 # tous les points comptent + amplitude = 0.01 / 4 # 4pt => 1% + factor_max = 0.1 # 10% max -class BonusTarbes(BonusSportAdditif): +class BonusTarbes(BonusIUTRennes1): """Calcul bonus optionnels (sport, culture), règle IUT de Tarbes.
  • Les étudiants opeuvent suivre un ou plusieurs activités optionnelles notées. La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20.
  • -
  • Le trentième des points au dessus de 10 est ajouté à la moyenne des UE. +
  • Le trentième des points au dessus de 10 est ajouté à la moyenne des UE en BUT, + ou à la moyenne générale en DUT et LP.
  • Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/30 = 0,2 points sur chaque UE. @@ -1018,29 +1130,6 @@ class BonusTarbes(BonusSportAdditif): proportion_point = 1 / 30.0 classic_use_bonus_ues = True - def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): - """calcul du bonus""" - # Prend la note de chaque modimpl, sans considération d'UE - if len(sem_modimpl_moys_inscrits.shape) > 2: # apc - sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0] - # ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic - note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds - ues = self.formsemestre.query_ues(with_sport=False).all() - ues_idx = [ue.id for ue in ues] - - if self.formsemestre.formation.is_apc(): # --- BUT - bonus_moy_arr = np.where( - note_bonus_max > self.seuil_moy_gen, - (note_bonus_max - self.seuil_moy_gen) * self.proportion_point, - 0.0, - ) - self.bonus_ues = pd.DataFrame( - np.stack([bonus_moy_arr] * len(ues)).T, - index=self.etuds_idx, - columns=ues_idx, - dtype=float, - ) - class BonusTours(BonusDirect): """Calcul bonus sport & culture IUT Tours. diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index f5efbeb2..8f8bd1a8 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -41,7 +41,8 @@ from app import db from app.models import ModuleImpl, Evaluation, EvaluationUEPoids from app.scodoc import sco_utils as scu from app.scodoc.sco_codes_parcours import UE_SPORT - +from app.scodoc import sco_cache +from app.scodoc.sco_exceptions import ScoBugCatcher from app.scodoc.sco_utils import ModuleType @@ -423,7 +424,9 @@ def moduleimpl_is_conforme( if nb_ues == 0: return False # situation absurde (pas d'UE) if len(modules_coefficients) != nb_ues: - raise ValueError("moduleimpl_is_conforme: nb ue incoherent") + # il arrive (#bug) que le cache ne soit pas à jour... + sco_cache.invalidate_formsemestre() + raise ScoBugCatcher("moduleimpl_is_conforme: nb ue incoherent") module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0 check = all( (modules_coefficients[moduleimpl.module_id].to_numpy() != 0) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 7bc199ea..645f067e 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -18,7 +18,7 @@ from app.auth.models import User from app.comp.res_cache import ResultatsCache from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults -from app.models import FormSemestre, FormSemestreUECoef, formsemestre +from app.models import FormSemestre, FormSemestreUECoef from app.models import Identite from app.models import ModuleImpl, ModuleImplInscription from app.models.ues import UniteEns @@ -70,6 +70,7 @@ class ResultatsSemestre(ResultatsCache): self.etud_moy_gen: pd.Series = None self.etud_moy_gen_ranks = {} self.etud_moy_gen_ranks_int = {} + self.moy_gen_rangs_by_group = None # virtual self.modimpl_inscr_df: pd.DataFrame = None "Inscriptions: row etudid, col modimlpl_id" self.modimpls_results: ModuleImplResults = None @@ -151,6 +152,7 @@ class ResultatsSemestre(ResultatsCache): if m.module.module_type == scu.ModuleType.SAE ] + # --- JURY... def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]: """Liste des UEs du semestre qui doivent être validées @@ -396,7 +398,7 @@ class ResultatsSemestre(ResultatsCache): - titles: { column_id : title } - columns_ids: (liste des id de colonnes) - . Si convert_values, transforme les notes en chaines ("12.34"). + Si convert_values, transforme les notes en chaines ("12.34"). Les colonnes générées sont: etudid rang : rang indicatif (basé sur moy gen) @@ -588,7 +590,9 @@ class ResultatsSemestre(ResultatsCache): f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" ) val_fmt = val_fmt_html = fmt_note(val) - if modimpl.module.module_type == scu.ModuleType.MALUS: + if convert_values and ( + modimpl.module.module_type == scu.ModuleType.MALUS + ): val_fmt_html = ( (scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else "" ) @@ -823,17 +827,25 @@ class ResultatsSemestre(ResultatsCache): self.formsemestre.id ) first_partition = True + col_order = 10 for partition in partitions: cid = f"part_{partition['partition_id']}" + rg_cid = cid + "_rg" # rang dans la partition titles[cid] = partition["partition_name"] if first_partition: klass = "partition" else: klass = "partition partition_aux" titles[f"_{cid}_class"] = klass - titles[f"_{cid}_col_order"] = 10 + titles[f"_{cid}_col_order"] = col_order + titles[f"_{rg_cid}_col_order"] = col_order + 1 + col_order += 2 + if partition["bul_show_rank"]: + titles[rg_cid] = f"Rg {partition['partition_name']}" + titles[f"_{rg_cid}_class"] = "partition_rangs" partition_etud_groups = partitions_etud_groups[partition["partition_id"]] for row in rows: + group = None # group (dict) de l'étudiant dans cette partition # dans NotesTableCompat, à revoir etud_etat = self.get_etud_etat(row["etudid"]) if etud_etat == "D": @@ -846,8 +858,17 @@ class ResultatsSemestre(ResultatsCache): group = partition_etud_groups.get(row["etudid"]) gr_name = group["group_name"] if group else "" if gr_name: - row[f"{cid}"] = gr_name + row[cid] = gr_name row[f"_{cid}_class"] = klass + # Rangs dans groupe + if ( + partition["bul_show_rank"] + and (group is not None) + and (group["id"] in self.moy_gen_rangs_by_group) + ): + rang = self.moy_gen_rangs_by_group[group["id"]][0] + row[rg_cid] = rang.get(row["etudid"], "") + first_partition = False def _recap_add_evaluations( diff --git a/app/comp/res_compat.py b/app/comp/res_compat.py index 8bbed090..5ac18ff4 100644 --- a/app/comp/res_compat.py +++ b/app/comp/res_compat.py @@ -35,7 +35,9 @@ class NotesTableCompat(ResultatsSemestre): "malus", "etud_moy_gen_ranks", "etud_moy_gen_ranks_int", + "moy_gen_rangs_by_group", "ue_rangs", + "ue_rangs_by_group", ) def __init__(self, formsemestre: FormSemestre): @@ -48,6 +50,8 @@ class NotesTableCompat(ResultatsSemestre): self.moy_min = "NA" self.moy_max = "NA" self.moy_moy = "NA" + self.moy_gen_rangs_by_group = {} # { group_id : (Series, Series) } + self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}} self.expr_diagnostics = "" self.parcours = self.formsemestre.formation.get_parcours() @@ -153,31 +157,83 @@ class NotesTableCompat(ResultatsSemestre): def compute_rangs(self): """Calcule les classements Moyenne générale: etud_moy_gen_ranks - Par UE (sauf ue bonus) + Par UE (sauf ue bonus): ue_rangs[ue.id] + Par groupe: classements selon moy_gen et UE: + moy_gen_rangs_by_group[group_id] + ue_rangs_by_group[group_id] """ ( self.etud_moy_gen_ranks, self.etud_moy_gen_ranks_int, ) = moy_sem.comp_ranks_series(self.etud_moy_gen) - for ue in self.formsemestre.query_ues(): + ues = self.formsemestre.query_ues() + for ue in ues: moy_ue = self.etud_moy_ue[ue.id] self.ue_rangs[ue.id] = ( moy_sem.comp_ranks_series(moy_ue)[0], # juste en chaine int(moy_ue.count()), ) # .count() -> nb of non NaN values + # Rangs dans les groupes (moy. gen et par UE) + self.moy_gen_rangs_by_group = {} # { group_id : (Series, Series) } + self.ue_rangs_by_group = {} + partitions_avec_rang = self.formsemestre.partitions.filter_by( + bul_show_rank=True + ) + for partition in partitions_avec_rang: + for group in partition.groups: + # on prend l'intersection car les groupes peuvent inclure des étudiants désinscrits + group_members = list( + {etud.id for etud in group.etuds}.intersection( + self.etud_moy_gen.index + ) + ) + # list() car pandas veut une sequence pour take() + # Rangs / moyenne générale: + group_moys_gen = self.etud_moy_gen[group_members] + self.moy_gen_rangs_by_group[group.id] = moy_sem.comp_ranks_series( + group_moys_gen + ) + # Rangs / UEs: + for ue in ues: + group_moys_ue = self.etud_moy_ue[ue.id][group_members] + self.ue_rangs_by_group.setdefault(ue.id, {})[ + group.id + ] = moy_sem.comp_ranks_series(group_moys_ue) - def get_etud_ue_rang(self, ue_id, etudid) -> tuple[str, int]: + def get_etud_rang(self, etudid: int) -> str: + """Le rang (classement) de l'étudiant dans le semestre. + Result: "13" ou "12 ex" + """ + return self.etud_moy_gen_ranks.get(etudid, 99999) + + def get_etud_ue_rang(self, ue_id, etudid, group_id=None) -> tuple[str, int]: """Le rang de l'étudiant dans cette ue + Si le group_id est spécifié, rang au sein de ce groupe, sinon global. Result: rang:str, effectif:str """ - rangs, effectif = self.ue_rangs[ue_id] - if rangs is not None: - rang = rangs[etudid] + if group_id is None: + rangs, effectif = self.ue_rangs[ue_id] + if rangs is not None: + rang = rangs[etudid] + else: + return "", "" else: - return "", "" + rangs = self.ue_rangs_by_group[ue_id][group_id][0] + rang = rangs[etudid] + effectif = len(rangs) return rang, effectif + def get_etud_rang_group(self, etudid: int, group_id: int) -> tuple[str, int]: + """Rang de l'étudiant (selon moy gen) et effectif dans ce groupe. + Si le groupe n'a pas de rang (partition avec bul_show_rank faux), ramène "", 0 + """ + if group_id in self.moy_gen_rangs_by_group: + r = self.moy_gen_rangs_by_group[group_id][0] # version en str + return (r[etudid], len(r)) + else: + return "", 0 + def etud_check_conditions_ues(self, etudid): """Vrai si les conditions sur les UE sont remplies. Ne considère que les UE ayant des notes (moyenne calculée). @@ -298,16 +354,6 @@ class NotesTableCompat(ResultatsSemestre): "ects_pot_fond": 0.0, # not implemented (anciennemment pour école ingé) } - def get_etud_rang(self, etudid: int) -> str: - """Le rang (classement) de l'étudiant dans le semestre. - Result: "13" ou "12 ex" - """ - return self.etud_moy_gen_ranks.get(etudid, 99999) - - def get_etud_rang_group(self, etudid: int, group_id: int): - "Le rang de l'étudiant dans ce groupe (NON IMPLEMENTE)" - return (None, 0) # XXX unimplemented TODO - def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]: """Liste d'informations (compat NotesTable) sur évaluations completes de ce module. diff --git a/app/forms/main/config_main.py b/app/forms/main/config_main.py index 2c2aa3d5..d0871937 100644 --- a/app/forms/main/config_main.py +++ b/app/forms/main/config_main.py @@ -82,7 +82,9 @@ def configuration(): form_bonus.data["bonus_sport_func_name"] ) app.clear_scodoc_cache() - flash(f"Fonction bonus sport&culture configurée.") + flash("""Fonction bonus sport&culture configurée.""") + else: + flash("Fonction bonus inchangée.") return redirect(url_for("scodoc.index")) elif form_scodoc.submit_scodoc.data and form_scodoc.validate(): if ScoDocSiteConfig.enable_entreprises( diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 917b0136..0bce6d47 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -56,11 +56,11 @@ class Identite(db.Model): # adresses = db.relationship("Adresse", lazy="dynamic", backref="etud") billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic") - # one-to-one relation: + # admission = db.relationship("Admission", backref="identite", lazy="dynamic") def __repr__(self): - return f"" + return f"" @classmethod def from_request(cls, etudid=None, code_nip=None): @@ -146,6 +146,7 @@ class Identite(db.Model): return { "id": self.id, "nip": self.code_nip, + "ine": self.code_ine, "nom": self.nom, "nom_usuel": self.nom_usuel, "prenom": self.prenom, @@ -177,6 +178,8 @@ class Identite(db.Model): "date_naissance": self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else "", + "dept_id": self.dept_id, + "dept_acronym": self.departement.acronym, "email": self.get_first_email() or "", "emailperso": self.get_first_email("emailperso"), "etudid": self.id, diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 46244a9a..5b0960ba 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -5,8 +5,6 @@ import datetime from app import db -from app.models import formsemestre -from app.models.formsemestre import FormSemestre from app.models.moduleimpls import ModuleImpl from app.models.ues import UniteEns @@ -48,13 +46,25 @@ class Evaluation(db.Model): def __repr__(self): return f"""""" - def to_dict(self): + def to_dict(self) -> dict: + "Représentation dict, pour json" e = dict(self.__dict__) e.pop("_sa_instance_state", None) # ScoDoc7 output_formators e["evaluation_id"] = self.id - e["jour"] = ndb.DateISOtoDMY(e["jour"]) + e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else "" + if self.jour is None: + e["date_debut"] = None + e["date_fin"] = None + else: + e["date_debut"] = datetime.datetime.combine( + self.jour, self.heure_debut or datetime.time(0, 0) + ).isoformat() + e["date_fin"] = datetime.datetime.combine( + self.jour, self.heure_fin or datetime.time(0, 0) + ).isoformat() e["numero"] = ndb.int_null_is_zero(e["numero"]) + e["poids"] = self.get_ue_poids_dict() # { ue_id : poids } return evaluation_enrich_dict(e) def from_dict(self, data): @@ -153,7 +163,7 @@ class EvaluationUEPoids(db.Model): # Fonction héritée de ScoDoc7 à refactorer def evaluation_enrich_dict(e): - """add or convert some fileds in an evaluation dict""" + """add or convert some fields in an evaluation dict""" # For ScoDoc7 compat heure_debut_dt = e["heure_debut"] or datetime.time( 8, 00 @@ -178,11 +188,12 @@ def evaluation_enrich_dict(e): else: e["descrheure"] = "" # matin, apresmidi: utile pour se referer aux absences: - if heure_debut_dt < datetime.time(12, 00): + + if e["jour"] and heure_debut_dt < datetime.time(12, 00): e["matin"] = 1 else: e["matin"] = 0 - if heure_fin_dt > datetime.time(12, 00): + if e["jour"] and heure_fin_dt > datetime.time(12, 00): e["apresmidi"] = 1 else: e["apresmidi"] = 0 diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index fda72383..e243c398 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -139,6 +139,7 @@ class FormSemestre(db.Model): else: d["date_fin"] = d["date_fin_iso"] = "" d["responsables"] = [u.id for u in self.responsables] + d["titre_formation"] = self.titre_formation() return d def get_infos_dict(self) -> dict: @@ -286,7 +287,7 @@ class FormSemestre(db.Model): """ if not self.etapes: return "" - return ", ".join(sorted([str(x.etape_apo) for x in self.etapes])) + return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape])) def responsables_str(self, abbrev_prenom=True) -> str: """chaîne "J. Dupond, X. Martin" @@ -329,9 +330,10 @@ class FormSemestre(db.Model): ANNEE=annee universitaire de debut (exemple: un S2 de 2013-2014 sera S2-2013) """ - imputation_dept = sco_preferences.get_preference("ImputationDept", self.id) + prefs = sco_preferences.SemPreferences(dept_id=self.dept_id) + imputation_dept = prefs["ImputationDept"] if not imputation_dept: - imputation_dept = sco_preferences.get_preference("DeptName") + imputation_dept = prefs["DeptName"] imputation_dept = imputation_dept.upper() parcours_name = self.formation.get_parcours().NAME modalite = self.modalite @@ -346,7 +348,7 @@ class FormSemestre(db.Model): scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month) ) return scu.sanitize_string( - "-".join((imputation_dept, parcours_name, modalite, semestre_id, annee_sco)) + f"{imputation_dept}-{parcours_name}-{modalite}-{semestre_id}-{annee_sco}" ) def titre_annee(self) -> str: @@ -358,6 +360,12 @@ class FormSemestre(db.Model): titre_annee += "-" + str(self.date_fin.year) return titre_annee + def titre_formation(self): + """Titre avec formation, court, pour passerelle: "BUT R&T" + (méthode de formsemestre car on pourrait ajouter le semestre, ou d'autres infos, à voir) + """ + return self.formation.acronyme + def titre_mois(self) -> str: """Le titre et les dates du semestre, pour affichage dans des listes Ex: "BUT QLIO (PN 2022) semestre 1 FI (Sept 2022 - Jan 2023)" @@ -441,10 +449,15 @@ class FormSemestreEtape(db.Model): db.Integer, db.ForeignKey("notes_formsemestre.id"), ) + # etape_apo aurait du etre not null, mais oublié etape_apo = db.Column(db.String(APO_CODE_STR_LEN), index=True) + def __bool__(self): + "Etape False if code empty" + return self.etape_apo is not None and (len(self.etape_apo) > 0) + def __repr__(self): - return f"" + return f"" def as_apovdi(self): return ApoEtapeVDI(self.etape_apo) diff --git a/app/models/groups.py b/app/models/groups.py index 9cf5f236..4c64ad54 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -25,9 +25,11 @@ class Partition(db.Model): partition_name = db.Column(db.String(SHORT_STR_LEN)) # numero = ordre de presentation) numero = db.Column(db.Integer) + # Calculer le rang ? bul_show_rank = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) + # Montrer quand on indique les groupes de l'étudiant ? show_in_lists = db.Column( db.Boolean(), nullable=False, default=True, server_default="true" ) @@ -50,6 +52,18 @@ class Partition(db.Model): def __repr__(self): return f"""<{self.__class__.__name__} {self.id} "{self.partition_name or '(default)'}">""" + def to_dict(self, with_groups=False) -> dict: + """as a dict, with or without groups""" + d = { + "id": self.id, + "formsemestre_id": self.partition_id, + "name": self.partition_name, + "numero": self.numero, + } + if with_groups: + d["groups"] = [group.to_dict(with_partition=False) for group in self.groups] + return d + class GroupDescr(db.Model): """Description d'un groupe d'une partition""" @@ -78,6 +92,17 @@ class GroupDescr(db.Model): "Nom avec partition: 'TD A'" return f"{self.partition.partition_name or ''} {self.group_name or '-'}" + def to_dict(self, with_partition=True) -> dict: + """as a dict, with or without partition""" + d = { + "id": self.id, + "partition_id": self.partition_id, + "name": self.group_name, + } + if with_partition: + d["partition"] = self.partition.to_dict(with_groups=False) + return d + group_membership = db.Table( "group_membership", @@ -85,3 +110,11 @@ group_membership = db.Table( db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")), db.UniqueConstraint("etudid", "group_id"), ) +# class GroupMembership(db.Model): +# """Association groupe / étudiant""" + +# __tablename__ = "group_membership" +# __table_args__ = (db.UniqueConstraint("etudid", "group_id"),) +# id = db.Column(db.Integer, primary_key=True) +# etudid = db.Column(db.Integer, db.ForeignKey("identite.id")) +# group_id = db.Column(db.Integer, db.ForeignKey("group_descr.id")) diff --git a/app/models/ues.py b/app/models/ues.py index 518bd721..48d81a14 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -75,6 +75,15 @@ class UniteEns(db.Model): return sco_edit_ue.ue_is_locked(self.id) + def can_be_deleted(self) -> bool: + """True si l'UE n'est pas utilisée dans des formsemestre + et n'a pas de module rattachés + """ + # "pas un seul module de cette UE n'a de modimpl..."" + return (self.modules.count() == 0) or not any( + m.modimpls.all() for m in self.modules + ) + def guess_semestre_idx(self) -> None: """Lorsqu'on prend une ancienne formation non APC, les UE n'ont pas d'indication de semestre. diff --git a/app/pe/pe_settag.py b/app/pe/pe_settag.py index f4ada213..be5028f3 100644 --- a/app/pe/pe_settag.py +++ b/app/pe/pe_settag.py @@ -97,7 +97,7 @@ class SetTag(pe_tagtable.TableTag): """Mémorise les semtag nécessaires au jury.""" self.SemTagDict = {fid: SemTagDict[fid] for fid in self.get_Fids_in_settag()} if PE_DEBUG >= 1: - pe_print(u" => %d semestres fusionnés" % len(self.SemTagDict)) + pe_print(" => %d semestres fusionnés" % len(self.SemTagDict)) # ------------------------------------------------------------------------------------------------------------------- def comp_data_settag(self): @@ -210,7 +210,7 @@ class SetTagInterClasse(pe_tagtable.TableTag): # ------------------------------------------------------------------------------------------------------------------- def __init__(self, nom_combinaison, diplome): - pe_tagtable.TableTag.__init__(self, nom=nom_combinaison + "_%d" % diplome) + pe_tagtable.TableTag.__init__(self, nom=f"{nom_combinaison}_{diplome or ''}") self.combinaison = nom_combinaison self.parcoursDict = {} @@ -243,7 +243,7 @@ class SetTagInterClasse(pe_tagtable.TableTag): fid: SetTagDict[fid] for fid in self.get_Fids_in_settag() if fid != None } if PE_DEBUG >= 1: - pe_print(u" => %d semestres utilisés" % len(self.SetTagDict)) + pe_print(" => %d semestres utilisés" % len(self.SetTagDict)) # ------------------------------------------------------------------------------------------------------------------- def comp_data_settag(self): diff --git a/app/scodoc/bonus_sport.py b/app/scodoc/bonus_sport.py deleted file mode 100644 index b49b6159..00000000 --- a/app/scodoc/bonus_sport.py +++ /dev/null @@ -1,492 +0,0 @@ -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -############################################################################## -# -# Gestion scolarite IUT -# -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Emmanuel Viennet emmanuel.viennet@viennet.net -# -############################################################################## - -from operator import mul -import pprint - -""" ANCIENS BONUS SPORT pour ScoDoc < 9.2 NON UTILISES A PARTIR DE 9.2 (voir comp/bonus_spo.py) - -La fonction bonus_sport reçoit: - - - notes_sport: la liste des notes des modules de sport et culture (une note par module - de l'UE de type sport/culture, toujours dans remise sur 20); - - coefs: un coef (float) pondérant chaque note (la plupart des bonus les ignorent); - - infos: dictionnaire avec des données pouvant être utilisées pour les calculs. - Ces données dépendent du type de formation. - infos = { - "moy" : la moyenne générale (float). 0. en BUT. - "sem" : { - "date_debut_iso" : "2010-08-01", # date de début de semestre - } - "moy_ues": { - ue_id : { # ue_status - "is_capitalized" : True|False, - "moy" : float, # moyenne d'UE prise en compte (peut-être capitalisée) - "sum_coefs": float, # > 0 si UE avec la moyenne calculée - "cur_moy_ue": float, # moyenne de l'UE (sans capitalisation)) - } - } - } - -Les notes passées sont: - - pour les formations classiques, la moyenne dans le module, calculée comme d'habitude - (moyenne pondérée des notes d'évaluations); - - pour le BUT: pareil, *en ignorant* les éventuels poids des évaluations. Le coefficient - de l'évaluation est pris en compte, mais pas les poids vers les UE. - -Pour modifier les moyennes d'UE: - - modifier infos["moy_ues"][ue_id][["cur_moy_ue"] - et, seulement si l'UE n'est pas capitalisée, infos["moy_ues"][ue_id][["moy"]/ - -La valeur retournée est: - - formations classiques: ajoutée à la moyenne générale - - BUT: valeur multipliée par la somme des coefs modules sport ajoutée à chaque UE. - -""" - - -def bonus_iutv(notes_sport, coefs, infos=None): - """Calcul bonus modules optionels (sport, culture), règle IUT Villetaneuse - - Les étudiants de l'IUT peuvent suivre des enseignements optionnels - de l'Université Paris 13 (sports, musique, deuxième langue, - culture, etc) non rattachés à une unité d'enseignement. Les points - au-dessus de 10 sur 20 obtenus dans chacune des matières - optionnelles sont cumulés et 5% de ces points cumulés s'ajoutent à - la moyenne générale du semestre déjà obtenue par l'étudiant. - """ - bonus = sum([(x - 10) / 20.0 for x in notes_sport if x > 10]) - return bonus - - -def bonus_direct(notes_sport, coefs, infos=None): - """Un bonus direct et sans chichis: les points sont directement ajoutés à la moyenne générale. - Les coefficients sont ignorés: tous les points de bonus sont sommés. - (rappel: la note est ramenée sur 20 avant application). - """ - return sum(notes_sport) - - -def bonus_iut_stdenis(notes_sport, coefs, infos=None): - """Semblable à bonus_iutv mais total limité à 0.5 points.""" - points = sum([x - 10 for x in notes_sport if x > 10]) # points au dessus de 10 - bonus = points * 0.05 # ou / 20 - return min(bonus, 0.5) # bonus limité à 1/2 point - - -def bonus_colmar(notes_sport, coefs, infos=None): - """Calcul bonus modules optionels (sport, culture), règle IUT Colmar. - - Les étudiants de l'IUT peuvent suivre des enseignements optionnels - de l'U.H.A. (sports, musique, deuxième langue, culture, etc) non - rattachés à une unité d'enseignement. Les points au-dessus de 10 - sur 20 obtenus dans chacune des matières optionnelles sont cumulés - dans la limite de 10 points. 5% de ces points cumulés s'ajoutent à - la moyenne générale du semestre déjà obtenue par l'étudiant. - - """ - # les coefs sont ignorés - points = sum([x - 10 for x in notes_sport if x > 10]) - points = min(10, points) # limite total à 10 - bonus = points / 20.0 # 5% - return bonus - - -def bonus_iutva(notes_sport, coefs, infos=None): - """Calcul bonus modules optionels (sport, culture), règle IUT Ville d'Avray - - Les étudiants de l'IUT peuvent suivre des enseignements optionnels - de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement. - Si la note est >= 10 et < 12, bonus de 0.1 point - Si la note est >= 12 et < 16, bonus de 0.2 point - Si la note est >= 16, bonus de 0.3 point - Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par - l'étudiant. - """ - sumc = sum(coefs) # assumes sum. coefs > 0 - note_sport = sum(map(mul, notes_sport, coefs)) / sumc # moyenne pondérée - if note_sport >= 16.0: - return 0.3 - if note_sport >= 12.0: - return 0.2 - if note_sport >= 10.0: - return 0.1 - return 0 - - -def bonus_iut1grenoble_2017(notes_sport, coefs, infos=None): - """Calcul bonus sport IUT Grenoble sur la moyenne générale (version 2017) - - La note de sport de nos étudiants va de 0 à 5 points. - Chaque point correspond à un % qui augmente la moyenne de chaque UE et la moyenne générale. - Par exemple : note de sport 2/5 : la moyenne générale sera augmentée de 2%. - - Calcul ici du bonus sur moyenne générale - """ - # les coefs sont ignorés - # notes de 0 à 5 - points = sum([x for x in notes_sport]) - factor = (points / 4.0) / 100.0 - bonus = infos["moy"] * factor - - return bonus - - -def bonus_lille(notes_sport, coefs, infos=None): - """calcul bonus modules optionels (sport, culture), règle IUT Villeneuve d'Ascq - - Les étudiants de l'IUT peuvent suivre des enseignements optionnels - de l'Université Lille 1 (sports,etc) non rattachés à une unité d'enseignement. Les points - au-dessus de 10 sur 20 obtenus dans chacune des matières - optionnelles sont cumulés et 4% (2% avant aout 2010) de ces points cumulés s'ajoutent à - la moyenne générale du semestre déjà obtenue par l'étudiant. - """ - if ( - infos["sem"]["date_debut_iso"] > "2010-08-01" - ): # changement de regle en aout 2010. - return sum([(x - 10) / 25.0 for x in notes_sport if x > 10]) - return sum([(x - 10) / 50.0 for x in notes_sport if x > 10]) - - -# Fonction Le Havre, par Dom. Soud. -def bonus_iutlh(notes_sport, coefs, infos=None): - """Calcul bonus sport IUT du Havre sur moyenne générale et UE - - La note de sport de nos étudiants va de 0 à 20 points. - m2=m1*(1+0.005*((10-N1)+(10-N2)) - m2 : Nouvelle moyenne de l'unité d'enseignement si note de sport et/ou de langue supérieure à 10 - m1 : moyenne de l'unité d'enseignement avant bonification - N1 : note de sport si supérieure à 10 - N2 : note de seconde langue si supérieure à 10 - Par exemple : sport 15/20 et langue 12/20 : chaque UE sera multipliée par 1+0.005*7, ainsi que la moyenne générale. - Calcul ici de la moyenne générale et moyennes d'UE non capitalisées. - """ - # les coefs sont ignorés - points = sum([x - 10 for x in notes_sport if x > 10]) - points = min(10, points) # limite total à 10 - factor = 1.0 + (0.005 * points) - # bonus nul puisque les moyennes sont directement modifiées par factor - bonus = 0 - # Modifie la moyenne générale - infos["moy"] = infos["moy"] * factor - # Modifie les moyennes de toutes les UE: - for ue_id in infos["moy_ues"]: - ue_status = infos["moy_ues"][ue_id] - if ue_status["sum_coefs"] > 0: - # modifie moyenne UE ds semestre courant - ue_status["cur_moy_ue"] = ue_status["cur_moy_ue"] * factor - if not ue_status["is_capitalized"]: - # si non capitalisee, modifie moyenne prise en compte - ue_status["moy"] = ue_status["cur_moy_ue"] - - # open('/tmp/log','a').write( pprint.pformat(ue_status) + '\n\n' ) - return bonus - - -def bonus_nantes(notes_sport, coefs, infos=None): - """IUT de Nantes (Septembre 2018) - Nous avons différents types de bonification - bonfication Sport / Culture / engagement citoyen - Nous ajoutons sur le bulletin une bonification de 0,2 pour chaque item - la bonification totale ne doit pas excéder les 0,5 point. - Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications. - - Dans ScoDoc: on a déclaré une UE "sport&culture" dans laquelle on aura des modules - pour chaque activité (Sport, Associations, ...) - avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20, mais en fait ce sera la - valeur de la bonification: entrer 0,1/20 signifiera un bonus de 0,1 point la moyenne générale) - """ - bonus = min(0.5, sum([x for x in notes_sport])) # plafonnement à 0.5 points - return bonus - - -# Bonus sport IUT Tours -def bonus_tours(notes_sport, coefs, infos=None): - """Calcul bonus sport & culture IUT Tours sur moyenne generale - - La note de sport & culture de nos etudiants est applique sur la moyenne generale. - """ - return min(1.0, sum(notes_sport)) # bonus maximum de 1 point - - -def bonus_iutr(notes_sport, coefs, infos=None): - """Calcul du bonus , règle de l'IUT de Roanne - (contribuée par Raphael C., nov 2012) - - Le bonus est compris entre 0 et 0.35 point. - cette procédure modifie la moyenne de chaque UE capitalisable. - - """ - # modifie les moyennes de toutes les UE: - # le bonus est le minimum entre 0.35 et la somme de toutes les bonifs - bonus = min(0.35, sum([x for x in notes_sport])) - for ue_id in infos["moy_ues"]: - # open('/tmp/log','a').write( str(ue_id) + infos['moy_ues'] + '\n\n' ) - ue_status = infos["moy_ues"][ue_id] - if ue_status["sum_coefs"] > 0: - # modifie moyenne UE dans semestre courant - ue_status["cur_moy_ue"] = ue_status["cur_moy_ue"] + bonus - if not ue_status["is_capitalized"]: - ue_status["moy"] = ue_status["cur_moy_ue"] - return bonus - - -def bonus_iutam(notes_sport, coefs, infos=None): - """Calcul bonus modules optionels (sport), regle IUT d'Amiens. - Les etudiants de l'IUT peuvent suivre des enseignements optionnels. - Si la note est de 10.00 a 10.49 -> 0.50% de la moyenne - Si la note est de 10.50 a 10.99 -> 0.75% - Si la note est de 11.00 a 11.49 -> 1.00% - Si la note est de 11.50 a 11.99 -> 1.25% - Si la note est de 12.00 a 12.49 -> 1.50% - Si la note est de 12.50 a 12.99 -> 1.75% - Si la note est de 13.00 a 13.49 -> 2.00% - Si la note est de 13.50 a 13.99 -> 2.25% - Si la note est de 14.00 a 14.49 -> 2.50% - Si la note est de 14.50 a 14.99 -> 2.75% - Si la note est de 15.00 a 15.49 -> 3.00% - Si la note est de 15.50 a 15.99 -> 3.25% - Si la note est de 16.00 a 16.49 -> 3.50% - Si la note est de 16.50 a 16.99 -> 3.75% - Si la note est de 17.00 a 17.49 -> 4.00% - Si la note est de 17.50 a 17.99 -> 4.25% - Si la note est de 18.00 a 18.49 -> 4.50% - Si la note est de 18.50 a 18.99 -> 4.75% - Si la note est de 19.00 a 20.00 -> 5.00% - Ce bonus s'ajoute a la moyenne generale du semestre de l'etudiant. - """ - # une seule note - note_sport = notes_sport[0] - if note_sport < 10.0: - return 0.0 - prc = min((int(2 * note_sport - 20.0) + 2) * 0.25, 5) - bonus = infos["moy"] * prc / 100.0 - return bonus - - -def bonus_saint_etienne(notes_sport, coefs, infos=None): - """IUT de Saint-Etienne (jan 2014) - Nous avons différents types de bonification - bonfication Sport / Associations - coopératives de département / Bureau Des Étudiants - / engagement citoyen / Langues optionnelles - Nous ajoutons sur le bulletin une bonification qui varie entre 0,1 et 0,3 ou 0,35 pour chaque item - la bonification totale ne doit pas excéder les 0,6 point. - Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications. - - - Dans ScoDoc: on a déclarer une UE "sport&culture" dans laquelle on aura des modules - pour chaque activité (Sport, Associations, ...) - avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20, mais en fait ce sera la - valeur de la bonification: entrer 0,1/20 signifiera un bonus de 0,1 point la moyenne générale) - """ - bonus = min(0.6, sum([x for x in notes_sport])) # plafonnement à 0.6 points - - return bonus - - -def bonus_iutTarbes(notes_sport, coefs, infos=None): - """Calcul bonus modules optionnels - (sport, Langues, action sociale, Théâtre), règle IUT Tarbes - Les coefficients ne sont pas pris en compte, - seule la meilleure note est prise en compte - le 1/30ème des points au-dessus de 10 sur 20 est retenu et s'ajoute à - la moyenne générale du semestre déjà obtenue par l'étudiant. - """ - bonus = max([(x - 10) / 30.0 for x in notes_sport if x > 10] or [0.0]) - return bonus - - -def bonus_iutSN(notes_sport, coefs, infos=None): - """Calcul bonus sport IUT Saint-Nazaire sur moyenne générale - - La note de sport de nos étudiants va de 0 à 5 points. - La note de culture idem, - Elles sont cumulables, - Chaque point correspond à un % qui augmente la moyenne générale. - Par exemple : note de sport 2/5 : la moyenne générale sera augmentée de 2%. - - Calcul ici du bonus sur moyenne générale et moyennes d'UE non capitalisées. - """ - # les coefs sont ignorés - # notes de 0 à 5 - points = sum([x for x in notes_sport]) - factor = points / 100.0 - bonus = infos["moy"] * factor - return bonus - - -def bonus_iutBordeaux1(notes_sport, coefs, infos=None): - """Calcul bonus modules optionels (sport, culture), règle IUT Bordeaux 1, sur moyenne générale et UE - - Les étudiants de l'IUT peuvent suivre des enseignements optionnels - de l'Université Bordeaux 1 (sport, théâtre) non rattachés à une unité d'enseignement. - En cas de double activité, c'est la meilleure des 2 notes qui compte. - Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un % - qui augmente la moyenne de chaque UE et la moyenne générale. - Formule : le % = points>moyenne / 2 - Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale. - - Calcul ici du bonus sur moyenne générale et moyennes d'UE non capitalisées. - """ - # open('/tmp/log','a').write( '\n---------------\n' + pprint.pformat(infos) + '\n' ) - # les coefs sont ignorés - # on récupère la note maximum et les points au-dessus de la moyenne - sport = max(notes_sport) - points = max(0, sport - 10) - # on calcule le bonus - factor = (points / 2.0) / 100.0 - bonus = infos["moy"] * factor - # Modifie les moyennes de toutes les UE: - for ue_id in infos["moy_ues"]: - ue_status = infos["moy_ues"][ue_id] - if ue_status["sum_coefs"] > 0: - # modifie moyenne UE ds semestre courant - ue_status["cur_moy_ue"] = ue_status["cur_moy_ue"] * (1.0 + factor) - if not ue_status["is_capitalized"]: - # si non capitalisee, modifie moyenne prise en compte - ue_status["moy"] = ue_status["cur_moy_ue"] - - # open('/tmp/log','a').write( pprint.pformat(ue_status) + '\n\n' ) - return bonus - - -def bonus_iuto(notes_sport, coefs, infos=None): # OBSOLETE => EN ATTENTE (27/01/2022) - """Calcul bonus modules optionels (sport, culture), règle IUT Orleans - * Avant aout 2013 - Un bonus de 2,5% de la note de sport est accordé à chaque UE sauf - les UE de Projet et Stages - * Après aout 2013 - Un bonus de 2,5% de la note de sport est accordé à la moyenne générale - """ - sumc = sum(coefs) # assumes sum. coefs > 0 - note_sport = sum(map(mul, notes_sport, coefs)) / sumc # moyenne pondérée - bonus = note_sport * 2.5 / 100 - if ( - infos["sem"]["date_debut_iso"] > "2013-08-01" - ): # changement de regle en aout 2013. - return bonus - coefs = 0.0 - coefs_total = 0.0 - for ue_id in infos["moy_ues"]: - ue_status = infos["moy_ues"][ue_id] - coefs_total = coefs_total + ue_status["sum_coefs"] - # Extremement spécifique (et n'est plus utilisé) - if ue_status["ue"]["ue_code"] not in { - "ORA14", - "ORA24", - "ORA34", - "ORA44", - "ORB34", - "ORB44", - "ORD42", - "ORE14", - "ORE25", - "ORN44", - "ORO44", - "ORP44", - "ORV34", - "ORV42", - "ORV43", - }: - if ue_status["sum_coefs"] > 0: - coefs = coefs + ue_status["sum_coefs"] - # modifie moyenne UE ds semestre courant - ue_status["cur_moy_ue"] = ue_status["cur_moy_ue"] + bonus - if not ue_status["is_capitalized"]: - # si non capitalisee, modifie moyenne prise en compte - ue_status["moy"] = ue_status["cur_moy_ue"] - return bonus * coefs / coefs_total - - -def bonus_iutbethune(notes_sport, coefs, infos=None): - """Calcul bonus modules optionels (sport), règle IUT Bethune - - Les points au dessus de la moyenne de 10 apportent un bonus pour le semestre. - Ce bonus est égal au nombre de points divisé par 200 et multiplié par la - moyenne générale du semestre de l'étudiant. - """ - # les coefs sont ignorés - points = sum([x - 10 for x in notes_sport if x > 10]) - points = min(10, points) # limite total à 10 - bonus = int(infos["moy"] * points / 2) / 100.0 # moyenne-semestre x points x 0,5% - return bonus - - -def bonus_iutbeziers(notes_sport, coefs, infos=None): - """Calcul bonus modules optionels (sport, culture), regle IUT BEZIERS - - Les étudiants de l'IUT peuvent suivre des enseignements optionnels - sport , etc) non rattaches à une unité d'enseignement. Les points - au-dessus de 10 sur 20 obtenus dans chacune des matières - optionnelles sont cumulés et 3% de ces points cumulés s'ajoutent à - la moyenne générale du semestre déjà obtenue par l'étudiant. - """ - sumc = sum(coefs) # assumes sum. coefs > 0 - # note_sport = sum(map(mul, notes_sport, coefs)) / sumc # moyenne pondérée - bonus = sum([(x - 10) * 0.03 for x in notes_sport if x > 10]) - # le total du bonus ne doit pas dépasser 0.3 - Fred, 28/01/2020 - - if bonus > 0.3: - bonus = 0.3 - return bonus - - -def bonus_iutlemans(notes_sport, coefs, infos=None): - "fake: formule inutilisée en ScoDoc 9.2 mais doiut être présente" - return 0.0 - - -def bonus_iutlr(notes_sport, coefs, infos=None): - """Calcul bonus modules optionels (sport, culture), règle IUT La Rochelle - Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point - Si la note de sport est comprise entre 10.1 et 20 : ajout de 1% de cette note sur la moyenne générale du semestre - """ - # les coefs sont ignorés - # une seule note - note_sport = notes_sport[0] - if note_sport <= 10: - return 0 - bonus = note_sport * 0.01 # 1% - return bonus - - -def bonus_demo(notes_sport, coefs, infos=None): - """Fausse fonction "bonus" pour afficher les informations disponibles - et aider les développeurs. - Les informations sont placées dans le fichier /tmp/scodoc_bonus.log - qui est ECRASE à chaque appel. - *** Ne pas utiliser en production !!! *** - """ - with open("/tmp/scodoc_bonus.log", "w") as f: # mettre 'a' pour ajouter en fin - f.write("\n---------------\n" + pprint.pformat(infos) + "\n") - # Statut de chaque UE - # for ue_id in infos['moy_ues']: - # ue_status = infos['moy_ues'][ue_id] - # #open('/tmp/log','a').write( pprint.pformat(ue_status) + '\n\n' ) - - return 0.0 diff --git a/app/scodoc/sco_abs_views.py b/app/scodoc/sco_abs_views.py index a4e1d3a7..c01588df 100644 --- a/app/scodoc/sco_abs_views.py +++ b/app/scodoc/sco_abs_views.py @@ -965,7 +965,7 @@ def _tables_abs_etud( )[0] if format == "html": ex.append( - f"""{mod["module"]["code"] or "(module sans code)"}""" ) @@ -983,7 +983,8 @@ def _tables_abs_etud( )[0] if format == "html": ex.append( - f"""{mod["module"]["code"] or '(module sans code)'}""" ) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index efc0b534..df790649 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -251,7 +251,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): rang = "" rang_gr, ninscrits_gr, gr_name = get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt + etudid, partitions, partitions_etud_groups, nt ) if nt.get_moduleimpls_attente(): @@ -651,7 +651,7 @@ def _ue_mod_bulletin( def get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt + etudid: int, partitions, partitions_etud_groups, nt: NotesTableCompat ): """Ramene rang et nb inscrits dans chaque partition""" rang_gr, ninscrits_gr, gr_name = {}, {}, {} diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index f34923a5..78425028 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -31,7 +31,6 @@ import datetime import json -from app.but import bulletin_but from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models.formsemestre import FormSemestre @@ -92,7 +91,7 @@ def formsemestre_bulletinetud_published_dict( sem = sco_formsemestre.get_formsemestre(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - d = {} + d = {"type": "classic", "version": "0"} if (not sem["bul_hide_xml"]) or force_publishing: published = True @@ -166,7 +165,7 @@ def formsemestre_bulletinetud_published_dict( else: rang = str(nt.get_etud_rang(etudid)) rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt + etudid, partitions, partitions_etud_groups, nt ) d["note"] = dict( diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py index d6925d8c..f173b56b 100644 --- a/app/scodoc/sco_bulletins_xml.py +++ b/app/scodoc/sco_bulletins_xml.py @@ -172,7 +172,7 @@ def make_xml_formsemestre_bulletinetud( else: rang = str(nt.get_etud_rang(etudid)) rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt + etudid, partitions, partitions_etud_groups, nt ) doc.append( diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index 2e9792e4..8fcc6f7e 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -228,7 +228,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa if getattr(g, "defer_cache_invalidation", False): g.sem_to_invalidate.add(formsemestre_id) return - log("inval_cache, formsemestre_id={formsemestre_id} pdfonly={pdfonly}") + log(f"inval_cache, formsemestre_id={formsemestre_id} pdfonly={pdfonly}") if formsemestre_id is None: # clear all caches log("----- invalidate_formsemestre: clearing all caches -----") @@ -272,7 +272,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa SemBulletinsPDFCache.invalidate_sems(formsemestre_ids) -class DefferedSemCacheManager: +class DeferredSemCacheManager: """Contexte pour effectuer des opérations indépendantes dans la même requete qui invalident le cache. Par exemple, quand on inscrit des étudiants un par un à un semestre, chaque inscription va invalider diff --git a/app/scodoc/sco_config.py b/app/scodoc/sco_config.py index 95b9dcc9..b338f9a7 100644 --- a/app/scodoc/sco_config.py +++ b/app/scodoc/sco_config.py @@ -1,13 +1,11 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -"""Configuration de ScoDoc (version ScoDOc 9) +"""Configuration de ScoDoc (version ScoDoc 9) NE PAS MODIFIER localement ce fichier ! mais éditer /opt/scodoc-data/config/scodoc_local.py """ -from app.scodoc import bonus_sport - class AttrDict(dict): def __init__(self, *args, **kwargs): diff --git a/app/scodoc/sco_dept.py b/app/scodoc/sco_dept.py index 14c47651..42f3db76 100644 --- a/app/scodoc/sco_dept.py +++ b/app/scodoc/sco_dept.py @@ -131,8 +131,10 @@ def index_html(showcodes=0, showsemtable=0): if not showsemtable: H.append( f"""
    -

    Voir tous les semestres ({len(othersems)} verrouillés) +

    Voir table des semestres (dont {len(othersems)} + verrouillé{'s' if len(othersems) else ''})

    """ ) diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py index aabfaddc..606fc742 100644 --- a/app/scodoc/sco_edit_formation.py +++ b/app/scodoc/sco_edit_formation.py @@ -66,8 +66,9 @@ def formation_delete(formation_id=None, dialog_confirmed=False): sems = sco_formsemestre.do_formsemestre_list({"formation_id": formation_id}) if sems: H.append( - """

    Impossible de supprimer cette formation, car les sessions suivantes l'utilisent:

    -
      """ + """

      Impossible de supprimer cette formation, + car les sessions suivantes l'utilisent:

      +
        """ ) for sem in sems: H.append( diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index ece30a34..f99bb8a1 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -33,7 +33,7 @@ from flask import url_for, render_template from flask import g, request from flask_login import current_user -from app import log +from app import db, log from app import models from app.models import APO_CODE_STR_LEN from app.models import Formation, Matiere, Module, UniteEns @@ -359,7 +359,6 @@ def can_delete_module(module): def do_module_delete(oid): "delete module" - from app.scodoc import sco_formations module = Module.query.get_or_404(oid) mod = module_list({"module_id": oid})[0] # sco7 @@ -422,13 +421,14 @@ def module_delete(module_id=None): H = [ html_sco_header.sco_header(page_title="Suppression d'un module"), - """

        Suppression du module %(titre)s (%(code)s)

        """ % mod, + f"""

        Suppression du module {module.titre or "sans titre"} ({module.code})

        """, ] dest_url = url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=str(mod["formation_id"]), + formation_id=module.formation_id, + semestre_idx=module.ue.semestre_idx, ) tf = TrivialFormulator( request.base_url, @@ -848,21 +848,13 @@ def module_count_moduleimpls(module_id): def formation_add_malus_modules(formation_id, titre=None, redirect=True): """Création d'un module de "malus" dans chaque UE d'une formation""" - from app.scodoc import sco_edit_ue - ues = sco_edit_ue.ue_list(args={"formation_id": formation_id}) + formation = Formation.query.get_or_404(formation_id) - for ue in ues: - # Un seul module de malus par UE: - nb_mod_malus = len( - [ - mod - for mod in module_list(args={"ue_id": ue["ue_id"]}) - if mod["module_type"] == scu.ModuleType.MALUS - ] - ) - if nb_mod_malus == 0: - ue_add_malus_module(ue["ue_id"], titre=titre) + for ue in formation.ues: + ue_add_malus_module(ue, titre=titre) + + formation.invalidate_cached_sems() if redirect: return flask.redirect( @@ -872,46 +864,58 @@ def formation_add_malus_modules(formation_id, titre=None, redirect=True): ) -def ue_add_malus_module(ue_id, titre=None, code=None): - """Add a malus module in this ue""" - from app.scodoc import sco_edit_ue +def ue_add_malus_module(ue: UniteEns, titre=None, code=None) -> int: + """Add a malus module in this ue. + If already exists, do nothing. + Returns id of malus module. + """ + modules_malus = [m for m in ue.modules if m.module_type == scu.ModuleType.MALUS] + if len(modules_malus) > 0: + return modules_malus[0].id # déjà existant - ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0] - - if titre is None: - titre = "" - if code is None: - code = "MALUS%d" % ue["numero"] + titre = titre or "" + code = code or f"MALUS{ue.numero}" # Tout module doit avoir un semestre_id (indice 1, 2, ...) - semestre_ids = sco_edit_ue.ue_list_semestre_ids(ue) - if semestre_ids: - semestre_id = semestre_ids[0] + if ue.semestre_idx is None: + semestre_ids = sorted(list(set([m.semestre_id for m in ue.modules]))) + if len(semestre_ids) > 0: + semestre_id = semestre_ids[0] + else: + # c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement + # le semestre ? ou affecter le malus au semestre 1 ??? + raise ScoValueError( + "Impossible d'ajouter un malus s'il n'y a pas d'autres modules" + ) else: - # c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement - # le semestre ? ou affecter le malus au semestre 1 ??? - raise ScoValueError( - "Impossible d'ajouter un malus s'il n'y a pas d'autres modules" - ) + semestre_id = ue.semestre_idx # Matiere pour placer le module malus - Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue_id}) - numero = max([mat["numero"] for mat in Matlist]) + 10 - matiere_id = sco_edit_matiere.do_matiere_create( - {"ue_id": ue_id, "titre": "Malus", "numero": numero} - ) + titre_matiere_malus = "Malus" - module_id = do_module_create( - { - "titre": titre, - "code": code, - "coefficient": 0.0, # unused - "ue_id": ue_id, - "matiere_id": matiere_id, - "formation_id": ue["formation_id"], - "semestre_id": semestre_id, - "module_type": scu.ModuleType.MALUS, - }, - ) + matieres_malus = [mat for mat in ue.matieres if mat.titre == titre_matiere_malus] + if len(matieres_malus) > 0: + # matière Malus déjà existante, l'utilise + matiere = matieres_malus[0] + else: + if ue.matieres.count() > 0: + numero = max([mat.numero for mat in ue.matieres]) + 10 + else: + numero = 0 + matiere = Matiere(ue_id=ue.id, titre=titre_matiere_malus, numero=numero) + db.session.add(matiere) - return module_id + module = Module( + titre=titre, + code=code, + coefficient=0.0, + ue=ue, + matiere=matiere, + formation=ue.formation, + semestre_id=semestre_id, + module_type=scu.ModuleType.MALUS, + ) + db.session.add(module) + db.session.commit() + + return module.id diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index fd25e5da..33ffc69c 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -142,29 +142,23 @@ def do_ue_create(args): return ue_id -def can_delete_ue(ue: UniteEns) -> bool: - """True si l'UE n'est pas utilisée dans des formsemestre - et n'a pas de module rattachés - """ - # "pas un seul module de cette UE n'a de modimpl..."" - return (not len(ue.modules.all())) and not any(m.modimpls.all() for m in ue.modules) - - def do_ue_delete(ue_id, delete_validations=False, force=False): "delete UE and attached matieres (but not modules)" from app.scodoc import sco_formations from app.scodoc import sco_parcours_dut ue = UniteEns.query.get_or_404(ue_id) - if not can_delete_ue(ue): + formation_id = ue.formation_id + semestre_idx = ue.semestre_idx + if not ue.can_be_deleted(): raise ScoNonEmptyFormationObject( - "UE", + f"UE (id={ue.id}, dud)", msg=ue.titre, dest_url=url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=ue.formation_id, - semestre_idx=ue.semestre_idx, + formation_id=formation_id, + semestre_idx=semestre_idx, ), ) @@ -187,13 +181,13 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): cancel_url=url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=ue.formation_id, - semestre_idx=ue.semestre_idx, + formation_id=formation_id, + semestre_idx=semestre_idx, ), parameters={"ue_id": ue.id, "dialog_confirmed": 1}, ) if delete_validations: - log("deleting all validations of UE %s" % ue.id) + log(f"deleting all validations of UE {ue.id}") ndb.SimpleQuery( "DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s", {"ue_id": ue.id}, @@ -215,10 +209,10 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): # utilisé: acceptable de tout invalider): sco_cache.invalidate_formsemestre() # news - F = sco_formations.formation_list(args={"formation_id": ue.formation_id})[0] + F = sco_formations.formation_list(args={"formation_id": formation_id})[0] ScolarNews.add( typ=ScolarNews.NEWS_FORM, - obj=ue.formation_id, + obj=formation_id, text=f"Modification de la formation {F['acronyme']}", max_frequency=10 * 60, ) @@ -228,8 +222,8 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=ue.formation_id, - semestre_idx=ue.semestre_idx, + formation_id=formation_id, + semestre_idx=semestre_idx, ) ) return None @@ -538,9 +532,9 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False): semestre_idx=ue.semestre_idx, ), ) - if not can_delete_ue(ue): + if not ue.can_be_deleted(): raise ScoNonEmptyFormationObject( - "UE", + f"UE", msg=ue.titre, dest_url=url_for( "notes.ue_table", @@ -1352,16 +1346,6 @@ def ue_is_locked(ue_id): return len(r) > 0 -def ue_list_semestre_ids(ue: dict): - """Liste triée des numeros de semestres des modules dans cette UE - Il est recommandable que tous les modules d'une UE aient le même indice de semestre. - Mais cela n'a pas toujours été le cas dans les programmes pédagogiques officiels, - aussi ScoDoc laisse le choix. - """ - modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]}) - return sorted(list(set([mod["semestre_id"] for mod in modules]))) - - UE_PALETTE = [ "#B80004", # rouge "#F97B3D", # Orange Crayola diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index 6152e881..09bf2102 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -606,12 +606,10 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"): # -------------- VIEWS -def evaluation_describe(evaluation_id="", edit_in_place=True): +def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True): """HTML description of evaluation, for page headers edit_in_place: allow in-place editing when permitted (not implemented) """ - from app.scodoc import sco_saisie_notes - E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] moduleimpl_id = E["moduleimpl_id"] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] @@ -646,7 +644,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True): if Mod["module_type"] == ModuleType.MALUS: etit += ' (points de malus)' H = [ - 'Evaluation%s

        Module : %s

        ' + 'Évaluation%s

        Module : %s

        ' % (etit, mod_descr) ] if Mod["module_type"] == ModuleType.MALUS: @@ -689,12 +687,16 @@ def evaluation_describe(evaluation_id="", edit_in_place=True): modifier l'évaluation - + """ + ) + if link_saisie: + H.append( + f""" saisie des notes """ - ) + ) H.append("

        ") return '
        ' + "\n".join(H) + "
        " diff --git a/app/scodoc/sco_export_results.py b/app/scodoc/sco_export_results.py index 30750e3e..6c005306 100644 --- a/app/scodoc/sco_export_results.py +++ b/app/scodoc/sco_export_results.py @@ -208,25 +208,29 @@ def _build_results_list(dpv_by_sem, etuds_infos): return rows, titles, columns_ids -def get_set_formsemestre_id_dates(start_date, end_date): +def get_set_formsemestre_id_dates(start_date, end_date) -> set: """Ensemble des formsemestre_id entre ces dates""" s = ndb.SimpleDictFetch( """SELECT id FROM notes_formsemestre - WHERE date_debut >= %(start_date)s AND date_fin <= %(end_date)s + WHERE date_debut >= %(start_date)s + AND date_fin <= %(end_date)s + AND dept_id = %(dept_id)s """, - {"start_date": start_date, "end_date": end_date}, + {"start_date": start_date, "end_date": end_date, "dept_id": g.scodoc_dept_id}, ) return {x["id"] for x in s} -def scodoc_table_results(start_date="", end_date="", types_parcours=[], format="html"): +def scodoc_table_results( + start_date="", end_date="", types_parcours: list = None, format="html" +): """Page affichant la table des résultats Les dates sont en dd/mm/yyyy (datepicker javascript) types_parcours est la liste des types de parcours à afficher (liste de chaines, eg ['100', '210'] ) """ - log("scodoc_table_results: start_date=%s" % (start_date,)) # XXX + log(f"scodoc_table_results: start_date={start_date!r}") if not types_parcours: types_parcours = [] if not isinstance(types_parcours, list): diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index 178138b2..ae6ecc0b 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -256,6 +256,8 @@ def formation_import_xml(doc: str, import_tags=True): mod_info[1]["formation_id"] = formation_id mod_info[1]["matiere_id"] = mat_id mod_info[1]["ue_id"] = ue_id + if not "module_type" in mod_info[1]: + mod_info[1]["module_type"] = scu.ModuleType.STANDARD mod_id = sco_edit_module.do_module_create(mod_info[1]) if xml_module_id: modules_old2new[int(xml_module_id)] = mod_id diff --git a/app/scodoc/sco_formsemestre_exterieurs.py b/app/scodoc/sco_formsemestre_exterieurs.py index 9dfcbbb6..0da85f2c 100644 --- a/app/scodoc/sco_formsemestre_exterieurs.py +++ b/app/scodoc/sco_formsemestre_exterieurs.py @@ -262,7 +262,7 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid): ) -def _make_page(etud, sem, tf, message=""): +def _make_page(etud: dict, sem, tf, message="") -> list: formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) moy_gen = nt.get_etud_moy_gen(etud["etudid"]) @@ -277,21 +277,20 @@ def _make_page(etud, sem, tf, message=""):

        """ % etud, - """

        La moyenne de ce semestre serait: - %s / 20 + f"""

        La moyenne de ce semestre serait: + {moy_gen} / 20

        - """ - % moy_gen, + """, '
        ', tf[1], "
        ", - """ - """ - % (sem["formsemestre_id"], etud["etudid"]), + f""" + """, html_sco_header.sco_footer(), ] return H diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 8e834b23..79ed91ca 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -43,13 +43,14 @@ from xml.etree.ElementTree import Element import flask from flask import g, request from flask import url_for, make_response +from sqlalchemy.sql import text from app import db from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre, formsemestre +from app.models import FormSemestre, Identite from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN -from app.models.groups import Partition +from app.models.groups import GroupDescr, Partition import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log, cache @@ -61,7 +62,6 @@ from app.scodoc import sco_etud from app.scodoc import sco_permissions_check from app.scodoc import sco_xml from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError -from app.scodoc.sco_permissions import Permission from app.scodoc.TrivialFormulator import TrivialFormulator @@ -413,8 +413,43 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"): return R -def etud_add_group_infos(etud, formsemestre_id, sep=" "): - """Add informations on partitions and group memberships to etud (a dict with an etudid)""" +def get_etud_formsemestre_groups( + etud: Identite, formsemestre: FormSemestre, only_to_show=True +) -> list[GroupDescr]: + """Liste les groupes auxquels est inscrit""" + # Note: je n'ai pas réussi à cosntruire une requete SQLAlechemy avec + # la Table d'association group_membership + cursor = db.session.execute( + text( + """ + SELECT g.id + FROM group_descr g, group_membership gm, partition p + WHERE gm.etudid = :etudid + AND gm.group_id = g.id + AND g.partition_id = p.id + AND p.formsemestre_id = :formsemestre_id + AND p.partition_name is not NULL + """ + + (" and (p.show_in_lists is True) " if only_to_show else "") + + """ + ORDER BY p.numero + """ + ), + {"etudid": etud.id, "formsemestre_id": formsemestre.id}, + ) + return [GroupDescr.query.get(group_id) for group_id in cursor] + + +# Ancienne fonction: +def etud_add_group_infos(etud, formsemestre_id, sep=" ", only_to_show=False): + """Add informations on partitions and group memberships to etud + (a dict with an etudid) + If only_to_show, restrict to partions such that show_in_lists is True. + + etud['partitions'] = { partition_id : group + partition_name } + etud['groupes'] = "TDB, Gr2, TPB1" + etud['partitionsgroupes'] = "Groupes TD:TDB, Groupes TP:Gr2 (...)" + """ etud[ "partitions" ] = collections.OrderedDict() # partition_id : group + partition_name @@ -423,11 +458,14 @@ def etud_add_group_infos(etud, formsemestre_id, sep=" "): return etud infos = ndb.SimpleDictFetch( - """SELECT p.partition_name, g.*, g.id AS group_id + """SELECT p.partition_name, p.show_in_lists, g.*, g.id AS group_id FROM group_descr g, partition p, group_membership gm WHERE gm.etudid=%(etudid)s and gm.group_id = g.id and g.partition_id = p.id and p.formsemestre_id = %(formsemestre_id)s + """ + + (" and (p.show_in_lists is True) " if only_to_show else "") + + """ ORDER BY p.numero """, {"etudid": etud["etudid"], "formsemestre_id": formsemestre_id}, @@ -443,7 +481,7 @@ def etud_add_group_infos(etud, formsemestre_id, sep=" "): ) etud["partitionsgroupes"] = sep.join( [ - gr["partition_name"] + ":" + gr["group_name"] + (gr["partition_name"] or "") + ":" + gr["group_name"] for gr in infos if gr["group_name"] is not None ] @@ -1008,9 +1046,7 @@ def partition_set_attr(partition_id, attr, value): partition[attr] = value partitionEditor.edit(cnx, partition) # invalid bulletin cache - sco_cache.invalidate_formsemestre( - pdfonly=True, formsemestre_id=partition["formsemestre_id"] - ) + sco_cache.invalidate_formsemestre(formsemestre_id=partition["formsemestre_id"]) return "enregistré" diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index d6def369..b9e6a3ee 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -277,7 +277,11 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): if modimpl.module.is_apc(): H.append(_ue_coefs_html(modimpl.module.ue_coefs_list())) else: - H.append(f"Coef. dans le semestre: {modimpl.module.coefficient}") + H.append( + f"""Coef. dans le semestre: { + "non défini" if modimpl.module.coefficient is None else modimpl.module.coefficient + }""" + ) H.append("""""") # 3ieme ligne: Formation H.append( diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index 3a99d223..2631ef45 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -153,14 +153,14 @@ def ficheEtud(etudid=None): try: # pour les bookmarks avec d'anciens ids... etudid = int(etudid) except ValueError: - raise ScoValueError("id invalide !") + raise ScoValueError("id invalide !") from ValueError # la sidebar est differente s'il y a ou pas un etudid # voir html_sidebar.sidebar() g.etudid = etudid args = make_etud_args(etudid=etudid) etuds = sco_etud.etudident_list(cnx, args) if not etuds: - log("ficheEtud: etudid=%s request.args=%s" % (etudid, request.args)) + log(f"ficheEtud: etudid={etudid!r} request.args={request.args!r}") raise ScoValueError("Etudiant inexistant !") etud = etuds[0] etudid = etud["etudid"] @@ -173,7 +173,7 @@ def ficheEtud(etudid=None): if info["lieu_naissance"]: info["info_naissance"] += " à " + info["lieu_naissance"] if info["dept_naissance"]: - info["info_naissance"] += " (%s)" % info["dept_naissance"] + info["info_naissance"] += f" ({info['dept_naissance']})" info["etudfoto"] = sco_photos.etud_photo_html(etud) if ( (not info["domicile"]) @@ -205,7 +205,7 @@ def ficheEtud(etudid=None): ) else: info["emaillink"] = "(pas d'adresse e-mail)" - # champs dependant des permissions + # Champ dépendant des permissions: if authuser.has_permission(Permission.ScoEtudChangeAdr): info["modifadresse"] = ( 'modifier adresse' @@ -216,9 +216,10 @@ def ficheEtud(etudid=None): # Groupes: sco_groups.etud_add_group_infos( - info, info["cursem"]["formsemestre_id"] if info["cursem"] else None + info, + info["cursem"]["formsemestre_id"] if info["cursem"] else None, + only_to_show=True, ) - # Parcours de l'étudiant if info["sems"]: info["last_formsemestre_id"] = info["sems"][0]["formsemestre_id"] @@ -235,15 +236,28 @@ def ficheEtud(etudid=None): ) grlink = '%s' % descr["situation"] else: - group = sco_groups.get_etud_main_group(etudid, sem["formsemestre_id"]) - if group["partition_name"]: - gr_name = group["group_name"] - else: - gr_name = "tous" - grlink = ( - 'groupe %s' - % (group["group_id"], gr_name) + e = {"etudid": etudid} + sco_groups.etud_add_group_infos( + e, + sem["formsemestre_id"], + only_to_show=True, ) + + grlinks = [] + for partition in e["partitions"].values(): + if partition["partition_name"]: + gr_name = partition["group_name"] + else: + gr_name = "tous" + + grlinks.append( + f"""{gr_name} + """ + ) + grlink = ", ".join(grlinks) # infos ajoutées au semestre dans le parcours (groupe, menu) menu = _menuScolarite(authuser, sem, etudid) if menu: @@ -296,17 +310,18 @@ def ficheEtud(etudid=None): if not sco_permissions_check.can_suppress_annotation(a["id"]): a["dellink"] = "" else: - a[ - "dellink" - ] = '%s' % ( - etudid, - a["id"], - scu.icontag( - "delete_img", - border="0", - alt="suppress", - title="Supprimer cette annotation", - ), + a["dellink"] = ( + '%s' + % ( + etudid, + a["id"], + scu.icontag( + "delete_img", + border="0", + alt="suppress", + title="Supprimer cette annotation", + ), + ) ) author = sco_users.user_info(a["author"]) alist.append( @@ -422,9 +437,11 @@ def ficheEtud(etudid=None): # if info["groupes"].strip(): - info["groupes_row"] = ( - 'Groupe :%(groupes)s' % info - ) + info[ + "groupes_row" + ] = f""" + Groupes :{info['groupes']} + """ else: info["groupes_row"] = "" info["menus_etud"] = menus_etud(etudid) diff --git a/app/scodoc/sco_permissions.py b/app/scodoc/sco_permissions.py index 512caa8a..fd04e510 100644 --- a/app/scodoc/sco_permissions.py +++ b/app/scodoc/sco_permissions.py @@ -50,10 +50,10 @@ _SCO_PERMISSIONS = ( (1 << 27, "RelationsEntreprisesCorrespondants", "Voir les correspondants"), # 27 à 39 ... réservé pour "entreprises" # Api scodoc9 - (1 << 40, "APIView", "Voir"), - (1 << 41, "APIEtudChangeGroups", "Modifier les groupes"), - (1 << 42, "APIEditAllNotes", "Modifier toutes les notes"), - (1 << 43, "APIAbsChange", "Saisir des absences"), + (1 << 40, "APIView", "API: Lecture"), + (1 << 41, "APIEtudChangeGroups", "API: Modifier les groupes"), + (1 << 42, "APIEditAllNotes", "API: Modifier toutes les notes"), + (1 << 43, "APIAbsChange", "API: Saisir des absences"), ) @@ -70,7 +70,8 @@ class Permission(object): setattr(Permission, symbol, perm) Permission.description[symbol] = description Permission.permission_by_name[symbol] = perm - Permission.NBITS = len(_SCO_PERMISSIONS) + max_perm = max(p[0] for p in _SCO_PERMISSIONS) + Permission.NBITS = max_perm.bit_length() @staticmethod def get_by_name(permission_name: str) -> int: diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index 9ccb6362..49445bc9 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -132,9 +132,12 @@ def clear_base_preferences(): g._SCO_BASE_PREFERENCES = {} # { dept_id: BasePreferences instance } -def get_base_preferences(): - """Return global preferences for the current department""" - dept_id = g.scodoc_dept_id +def get_base_preferences(dept_id: int = None): + """Return global preferences for the specified department + or the current departement + """ + if dept_id is None: + dept_id = g.scodoc_dept_id if not hasattr(g, "_SCO_BASE_PREFERENCES"): g._SCO_BASE_PREFERENCES = {} if not dept_id in g._SCO_BASE_PREFERENCES: @@ -142,12 +145,12 @@ def get_base_preferences(): return g._SCO_BASE_PREFERENCES[dept_id] -def get_preference(name, formsemestre_id=None): +def get_preference(name, formsemestre_id=None, dept_id=None): """Returns value of named preference. All preferences have a sensible default value, so this function always returns a usable value for all defined preferences names. """ - return get_base_preferences().get(formsemestre_id, name) + return get_base_preferences(dept_id=dept_id).get(formsemestre_id, name) def _convert_pref_type(p, pref_spec): @@ -2145,9 +2148,9 @@ class BasePreferences(object): class SemPreferences: """Preferences for a formsemestre""" - def __init__(self, formsemestre_id=None): + def __init__(self, formsemestre_id=None, dept_id=None): self.formsemestre_id = formsemestre_id - self.base_prefs = get_base_preferences() + self.base_prefs = get_base_preferences(dept_id=dept_id) def __getitem__(self, name): return self.base_prefs.get(self.formsemestre_id, name) diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 179014cf..1c5c19eb 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -32,7 +32,7 @@ import time from xml.etree import ElementTree from flask import g, request -from flask import url_for +from flask import abort, url_for from app import log from app.but import bulletin_but @@ -83,13 +83,18 @@ def formsemestre_recapcomplet( force_publishing: publie les xml et json même si bulletins non publiés selected_etudid: etudid sélectionné (pour scroller au bon endroit) """ + if not isinstance(formsemestre_id, int): + abort(404) formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - + file_formats = {"csv", "json", "xls", "xlsx", "xlsall", "xml"} + supported_formats = file_formats | {"html", "evals"} + if tabformat not in supported_formats: + raise ScoValueError(f"Format non supporté: {tabformat}") + is_file = tabformat in file_formats modejury = int(modejury) - xml_with_decisions = int(xml_with_decisions) force_publishing = int(force_publishing) - is_file = tabformat in {"csv", "json", "xls", "xlsx", "xlsall", "xml"} + data = _do_formsemestre_recapcomplet( formsemestre_id, format=tabformat, @@ -128,6 +133,8 @@ def formsemestre_recapcomplet( for (format, label) in ( ("html", "Tableau"), ("evals", "Avec toutes les évaluations"), + ("xlsx", "Excel (non formaté)"), + ("xlsall", "Excel avec évaluations"), ("xml", "Bulletins XML (obsolète)"), ("json", "Bulletins JSON"), ): diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 9cda2372..e7597735 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -799,22 +799,22 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]): evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id}) if not evals: raise ScoValueError("invalid evaluation_id") - E = evals[0] - M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] + eval_dict = evals[0] + M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=eval_dict["moduleimpl_id"])[0] formsemestre_id = M["formsemestre_id"] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) mod_responsable = sco_users.user_info(M["responsable_id"]) - if E["jour"]: - indication_date = ndb.DateDMYtoISO(E["jour"]) + if eval_dict["jour"]: + indication_date = ndb.DateDMYtoISO(eval_dict["jour"]) else: - indication_date = scu.sanitize_filename(E["description"])[:12] - evalname = "%s-%s" % (Mod["code"], indication_date) + indication_date = scu.sanitize_filename(eval_dict["description"])[:12] + eval_name = "%s-%s" % (Mod["code"], indication_date) - if E["description"]: - evaltitre = "%s du %s" % (E["description"], E["jour"]) + if eval_dict["description"]: + evaltitre = "%s du %s" % (eval_dict["description"], eval_dict["jour"]) else: - evaltitre = "évaluation du %s" % E["jour"] + evaltitre = "évaluation du %s" % eval_dict["jour"] description = "%s en %s (%s) resp. %s" % ( evaltitre, Mod["abbrev"] or "", @@ -847,7 +847,7 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]): # une liste de liste de chaines: lignes de la feuille de calcul L = [] - etuds = _get_sorted_etuds(E, etudids, formsemestre_id) + etuds = _get_sorted_etuds(eval_dict, etudids, formsemestre_id) for e in etuds: etudid = e["etudid"] groups = sco_groups.get_etud_groups(etudid, formsemestre_id) @@ -865,8 +865,10 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]): ] ) - filename = "notes_%s_%s" % (evalname, gr_title_filename) - xls = sco_excel.excel_feuille_saisie(E, sem["titreannee"], description, lines=L) + filename = "notes_%s_%s" % (eval_name, gr_title_filename) + xls = sco_excel.excel_feuille_saisie( + eval_dict, sem["titreannee"], description, lines=L + ) return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE) # return sco_excel.send_excel_file(xls, filename) @@ -941,7 +943,9 @@ def saisie_notes(evaluation_id, group_ids=[]): cssstyles=sco_groups_view.CSSSTYLES, init_qtip=True, ), - sco_evaluations.evaluation_describe(evaluation_id=evaluation_id), + sco_evaluations.evaluation_describe( + evaluation_id=evaluation_id, link_saisie=False + ), '
        Saisie des notes', ] H.append("""
        """) @@ -1008,10 +1012,9 @@ def saisie_notes(evaluation_id, group_ids=[]): return "\n".join(H) -def _get_sorted_etuds(E, etudids, formsemestre_id): - sem = sco_formsemestre.get_formsemestre(formsemestre_id) +def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int): notes_db = sco_evaluation_db.do_evaluation_get_all_notes( - E["evaluation_id"] + eval_dict["evaluation_id"] ) # Notes existantes cnx = ndb.GetDBConnexion() etuds = [] @@ -1028,9 +1031,9 @@ def _get_sorted_etuds(E, etudids, formsemestre_id): e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id) # Information sur absence (tenant compte de la demi-journée) - jour_iso = ndb.DateDMYtoISO(E["jour"]) + jour_iso = ndb.DateDMYtoISO(eval_dict["jour"]) warn_abs_lst = [] - if E["matin"]: + if eval_dict["matin"]: nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=1) nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=1) if nbabs: @@ -1038,7 +1041,7 @@ def _get_sorted_etuds(E, etudids, formsemestre_id): warn_abs_lst.append("absent justifié le matin !") else: warn_abs_lst.append("absent le matin !") - if E["apresmidi"]: + if eval_dict["apresmidi"]: nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=0) nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=0) if nbabs: diff --git a/app/scodoc/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py index 67ac5a6c..9a13a8f5 100644 --- a/app/scodoc/sco_synchro_etuds.py +++ b/app/scodoc/sco_synchro_etuds.py @@ -252,7 +252,7 @@ def formsemestre_synchro_etuds( etudids_a_desinscrire = [nip2etudid(x) for x in a_desinscrire] etudids_a_desinscrire += a_desinscrire_without_key # - with sco_cache.DefferedSemCacheManager(): + with sco_cache.DeferredSemCacheManager(): do_import_etuds_from_portal(sem, a_importer, etudsapo_ident) sco_inscr_passage.do_inscrit(sem, etudids_a_inscrire) sco_inscr_passage.do_desinscrit(sem, etudids_a_desinscrire) diff --git a/app/scodoc/sco_ue_external.py b/app/scodoc/sco_ue_external.py index b90afd0d..82598228 100644 --- a/app/scodoc/sco_ue_external.py +++ b/app/scodoc/sco_ue_external.py @@ -56,6 +56,7 @@ Solution proposée (nov 2014): import flask from flask import request from flask_login import current_user +from app.models.formsemestre import FormSemestre import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -65,7 +66,6 @@ from app.scodoc import sco_codes_parcours from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_module from app.scodoc import sco_edit_ue -from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db from app.scodoc import sco_formations from app.scodoc import sco_formsemestre @@ -85,17 +85,21 @@ def external_ue_create( ects=0.0, ): """Crée UE/matiere/module/evaluation puis saisie les notes""" - log("external_ue_create( formsemestre_id=%s, titre=%s )" % (formsemestre_id, titre)) - sem = sco_formsemestre.get_formsemestre(formsemestre_id) + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + log(f"creating external UE in {formsemestre}: {acronyme}") + # Contrôle d'accès: if not current_user.has_permission(Permission.ScoImplement): - if not sem["resp_can_edit"] or (current_user.id not in sem["responsables"]): + if (not formsemestre.resp_can_edit) or ( + current_user.id not in [u.id for u in formsemestre.responsables] + ): raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération") # - formation_id = sem["formation_id"] - log("creating external UE in %s: %s" % (formsemestre_id, acronyme)) + formation_id = formsemestre.formation.id - numero = sco_edit_ue.next_ue_numero(formation_id, semestre_id=sem["semestre_id"]) + numero = sco_edit_ue.next_ue_numero( + formation_id, semestre_id=formsemestre.semestre_id + ) ue_id = sco_edit_ue.do_ue_create( { "formation_id": formation_id, @@ -120,7 +124,8 @@ def external_ue_create( "ue_id": ue_id, "matiere_id": matiere_id, "formation_id": formation_id, - "semestre_id": sem["semestre_id"], + "semestre_id": formsemestre.semestre_id, + "module_type": scu.ModuleType.STANDARD, }, ) @@ -129,17 +134,23 @@ def external_ue_create( "module_id": module_id, "formsemestre_id": formsemestre_id, # affecte le 1er responsable du semestre comme resp. du module - "responsable_id": sem["responsables"][0], + "responsable_id": formsemestre.responsables[0].id + if len(formsemestre.responsables) + else None, }, ) return moduleimpl_id -def external_ue_inscrit_et_note(moduleimpl_id, formsemestre_id, notes_etuds): +def external_ue_inscrit_et_note( + moduleimpl_id: int, formsemestre_id: int, notes_etuds: dict +): + """Inscrit les étudiants au moduleimpl, crée au besoin une évaluation + et enregistre les notes. + """ log( - "external_ue_inscrit_et_note(moduleimpl_id=%s, notes_etuds=%s)" - % (moduleimpl_id, notes_etuds) + f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl_id}, notes_etuds={notes_etuds})" ) # Inscription des étudiants sco_moduleimpl.do_moduleimpl_inscrit_etuds( @@ -175,17 +186,17 @@ def external_ue_inscrit_et_note(moduleimpl_id, formsemestre_id, notes_etuds): ) -def get_existing_external_ue(formation_id): - "la liste de toutes les UE externes définies dans cette formation" +def get_existing_external_ue(formation_id: int) -> list[dict]: + "Liste de toutes les UE externes définies dans cette formation" return sco_edit_ue.ue_list(args={"formation_id": formation_id, "is_external": True}) -def get_external_moduleimpl_id(formsemestre_id, ue_id): +def get_external_moduleimpl_id(formsemestre_id: int, ue_id: int) -> int: "moduleimpl correspondant à l'UE externe indiquée de ce formsemestre" r = ndb.SimpleDictFetch( """ SELECT mi.id AS moduleimpl_id FROM notes_moduleimpl mi, notes_modules mo - WHERE mi.id = %(formsemestre_id)s + WHERE mi.formsemestre_id = %(formsemestre_id)s AND mi.module_id = mo.id AND mo.ue_id = %(ue_id)s """, @@ -194,11 +205,14 @@ def get_external_moduleimpl_id(formsemestre_id, ue_id): if r: return r[0]["moduleimpl_id"] else: - raise ScoValueError("aucun module externe ne correspond") + raise ScoValueError( + f"""Aucun module externe ne correspond + (formsemestre_id={formsemestre_id}, ue_id={ue_id})""" + ) # Web function -def external_ue_create_form(formsemestre_id, etudid): +def external_ue_create_form(formsemestre_id: int, etudid: int): """Formulaire création UE externe + inscription étudiant et saisie note - Demande UE: peut-être existante (liste les UE externes de cette formation), ou sinon spécifier titre, acronyme, type, ECTS @@ -233,7 +247,9 @@ def external_ue_create_form(formsemestre_id, etudid): html_footer = html_sco_header.sco_footer() Fo = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"]) - ue_types = parcours.ALLOWED_UE_TYPES + ue_types = [ + typ for typ in parcours.ALLOWED_UE_TYPES if typ != sco_codes_parcours.UE_SPORT + ] ue_types.sort() ue_types_names = [sco_codes_parcours.UE_TYPE_NAME[k] for k in ue_types] ue_types = [str(x) for x in ue_types] @@ -255,7 +271,7 @@ def external_ue_create_form(formsemestre_id, etudid): "input_type": "menu", "title": "UE externe existante:", "allowed_values": [""] - + [ue["ue_id"] for ue in existing_external_ue], + + [str(ue["ue_id"]) for ue in existing_external_ue], "labels": [default_label] + [ "%s (%s)" % (ue["titre"], ue["acronyme"]) @@ -337,7 +353,7 @@ def external_ue_create_form(formsemestre_id, etudid): + html_footer ) if tf[2]["existing_ue"]: - ue_id = tf[2]["existing_ue"] + ue_id = int(tf[2]["existing_ue"]) moduleimpl_id = get_external_moduleimpl_id(formsemestre_id, ue_id) else: acronyme = tf[2]["acronyme"].strip() diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index ff91c149..c1e7dca2 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -191,7 +191,7 @@ def fmt_note(val, note_max=None, keep_numeric=False): return "EXC" # excuse, note neutralise if val == NOTES_ATTENTE: return "ATT" # attente, note neutralisee - if isinstance(val, float) or isinstance(val, int): + if not isinstance(val, str): if np.isnan(val): return "~" if (note_max is not None) and note_max > 0: diff --git a/app/static/js/sco_ue_external.js b/app/static/js/sco_ue_external.js index 24cd2157..fb57807f 100644 --- a/app/static/js/sco_ue_external.js +++ b/app/static/js/sco_ue_external.js @@ -9,19 +9,22 @@ function toggle_new_ue_form(state) { text_color = 'rgb(0,0,0)'; } - $("#tf_extue_titre td:eq(1) input").prop( "disabled", state ); - $("#tf_extue_titre td:eq(1) input").css('color', text_color) + $("#tf_extue_titre td:eq(1) input").prop("disabled", state); + $("#tf_extue_titre").css('color', text_color) - $("#tf_extue_acronyme td:eq(1) input").prop( "disabled", state ); - $("#tf_extue_acronyme td:eq(1) input").css('color', text_color) + $("#tf_extue_acronyme td:eq(1) input").prop("disabled", state); + $("#tf_extue_acronyme").css('color', text_color) - $("#tf_extue_ects td:eq(1) input").prop( "disabled", state ); - $("#tf_extue_ects td:eq(1) input").css('color', text_color) + $("#tf_extue_type td:eq(1) select").prop("disabled", state); + $("#tf_extue_type").css('color', text_color) + + $("#tf_extue_ects td:eq(1) input").prop("disabled", state); + $("#tf_extue_ects").css('color', text_color) } function update_external_ue_form() { - var state = (tf.existing_ue.value != "") + var state = (tf.existing_ue.value != ""); toggle_new_ue_form(state); } diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js index 4dcd0c23..fd5068e6 100644 --- a/app/static/js/table_recap.js +++ b/app/static/js/table_recap.js @@ -15,13 +15,23 @@ $(function () { }, { name: "toggle_partitions", - text: "Toutes les partitions", + text: "Montrer groupes", action: function (e, dt, node, config) { let visible = dt.columns(".partition_aux").visible()[0]; dt.columns(".partition_aux").visible(!visible); - dt.buttons('toggle_partitions:name').text(visible ? "Toutes les partitions" : "Cacher les partitions"); + dt.buttons('toggle_partitions:name').text(visible ? "Montrer groupes" : "Cacher les groupes"); } - }]; + }, + { + name: "toggle_partitions_rangs", + text: "Rangs groupes", + action: function (e, dt, node, config) { + let rangs_visible = dt.columns(".partition_rangs").visible()[0]; + dt.columns(".partition_rangs").visible(!rangs_visible); + dt.buttons('toggle_partitions_rangs:name').text(rangs_visible ? "Rangs groupes" : "Cacher rangs groupes"); + } + }, + ]; if (!$('table.table_recap').hasClass("jury")) { buttons.push( $('table.table_recap').hasClass("apc") ? @@ -95,12 +105,12 @@ $(function () { "columnDefs": [ { // cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides - targets: ["codes", "identite_detail", "partition_aux", "admission", "col_empty"], + targets: ["codes", "identite_detail", "partition_aux", "partition_rangs", "admission", "col_empty"], visible: false, }, { // Elimine les 0 à gauche pour les exports excel et les "copy" - targets: ["col_mod", "col_moy_gen", "col_ue", "col_res", "col_sae"], + targets: ["col_mod", "col_moy_gen", "col_ue", "col_res", "col_sae", "evaluation"], render: function (data, type, row) { return type === 'export' ? data.replace(/0(\d\..*)/, '$1') : data; } diff --git a/app/templates/auth/user_info_page.html b/app/templates/auth/user_info_page.html index 8a287c01..f344fb11 100644 --- a/app/templates/auth/user_info_page.html +++ b/app/templates/auth/user_info_page.html @@ -45,7 +45,7 @@ {# Liste des permissions #}
        -

        Permissions de cet utilisateur dans le département {dept}:

        +

        Permissions de cet utilisateur dans le département {{dept}}:

          {% for p in Permission.description %}
        • {{Permission.description[p]}} : diff --git a/app/views/notes.py b/app/views/notes.py index adde8b6a..f924acb9 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -292,7 +292,7 @@ def formsemestre_bulletinetud( format = format or "html" if not isinstance(formsemestre_id, int): - raise ValueError("formsemestre_id must be an integer !") + abort(404, description="formsemestre_id must be an integer !") formsemestre = FormSemestre.query.get_or_404(formsemestre_id) if etudid: etud = models.Identite.query.get_or_404(etudid) @@ -314,7 +314,7 @@ def formsemestre_bulletinetud( ) if format == "json": return sco_bulletins.get_formsemestre_bulletin_etud_json( - formsemestre, etud, version=version + formsemestre, etud, version=version, force_publishing=force_publishing ) if formsemestre.formation.is_apc() and format == "html": return render_template( @@ -648,17 +648,6 @@ def formation_export(formation_id, export_ids=False, format=None): ) -@bp.route("/formation_import_xml") -@scodoc -@permission_required(Permission.ScoChangeFormation) -@scodoc7func -def formation_import_xml(file): - "import d'une formation en XML" - log("formation_import_xml") - doc = file.read() - return sco_formations.formation_import_xml(doc) - - @bp.route("/formation_import_xml_form", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoChangeFormation) diff --git a/config.py b/config.py index fca2fc51..d78c69a9 100755 --- a/config.py +++ b/config.py @@ -13,7 +13,7 @@ class Config: SQLALCHEMY_DATABASE_URI = None # set in subclass FLASK_ENV = None # # set in subclass - SECRET_KEY = os.environ.get("SECRET_KEY") or "90e01e75831e4176a3c70d29564b425f" + SECRET_KEY = os.environ.get("SECRET_KEY") or "90e01e75831e4276a4c70d29564b425f" SQLALCHEMY_TRACK_MODIFICATIONS = False LOG_TO_STDOUT = os.environ.get("LOG_TO_STDOUT") MAIL_SERVER = os.environ.get("MAIL_SERVER", "localhost") @@ -46,6 +46,7 @@ class Config: class ProdConfig(Config): + "mode production, normalement derrière nginx/gunicorn" FLASK_ENV = "production" DEBUG = False TESTING = False @@ -56,6 +57,7 @@ class ProdConfig(Config): class DevConfig(Config): + "mode développement" FLASK_ENV = "development" DEBUG = True TESTING = False @@ -66,6 +68,7 @@ class DevConfig(Config): class TestConfig(DevConfig): + "Pour les tests unitaires" TESTING = True DEBUG = True SQLALCHEMY_DATABASE_URI = ( @@ -76,6 +79,19 @@ class TestConfig(DevConfig): SECRET_KEY = os.environ.get("TEST_SECRET_KEY") or "c7ecff5db1594c208f573ff30e0f6bca" +class TestAPIConfig(Config): + "Pour les tests de l'API" + FLASK_ENV = "test_api" + TESTING = False + DEBUG = True + SQLALCHEMY_DATABASE_URI = ( + os.environ.get("SCODOC_TEST_API_DATABASE_URI") + or "postgresql:///SCODOC_TEST_API" + ) + DEPT_TEST = "TAPI_" # nom du département, ne pas l'utiliser pour un "vrai" + SECRET_KEY = os.environ.get("TEST_SECRET_KEY") or "c7ecff5db15946789Hhahbh88aja175" + + mode = os.environ.get("FLASK_ENV", "production") if mode == "production": RunningConfig = ProdConfig @@ -83,3 +99,5 @@ elif mode == "development": RunningConfig = DevConfig elif mode == "test": RunningConfig = TestConfig +elif mode == "test_api": + RunningConfig = TestAPIConfig diff --git a/sco_version.py b/sco_version.py index f4ef3e98..3de1255f 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.2.15" +SCOVERSION = "9.2.24" SCONAME = "ScoDoc" diff --git a/scodoc.py b/scodoc.py index 1d3b1726..01881bf7 100755 --- a/scodoc.py +++ b/scodoc.py @@ -496,12 +496,11 @@ def clear_cache(sanitize): # clear-cache @app.cli.command() -def init_test_database(): +def init_test_database(): # init-test-database """Initialise les objets en base pour les tests API (à appliquer sur SCODOC_TEST ou SCODOC_DEV) """ click.echo("Initialisation base de test API...") - # import app as mapp # le package app ctx = app.test_request_context() ctx.push() diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 00000000..a365074d --- /dev/null +++ b/tests/api/__init__.py @@ -0,0 +1 @@ +# API tests diff --git a/tests/api/dotenv_exemple b/tests/api/dotenv_exemple new file mode 100644 index 00000000..e1857bd8 --- /dev/null +++ b/tests/api/dotenv_exemple @@ -0,0 +1,11 @@ +# Configuration du _client_ test API +# A renommer .env +# and /opt/scodoc/tests/api/ +# et à remplir. + +# URL du serveur ScoDoc à interroger +SCODOC_URL = "http://localhost:5000/" + +# Le client (python) doit-il vérifier le certificat SSL du serveur ? +# ou True si serveur de production avec certif SSL valide +CHECK_CERTIFICATE = False diff --git a/tests/api/exemple-api-basic.py b/tests/api/exemple-api-basic.py index 88ba892f..a581a878 100644 --- a/tests/api/exemple-api-basic.py +++ b/tests/api/exemple-api-basic.py @@ -16,29 +16,32 @@ export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valid (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). +Travail en cours. """ 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__)) +try: + BASEDIR = os.path.abspath(os.path.dirname(__file__)) +except NameError: + BASEDIR = "." + 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/" +CHK_CERT = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) +SCODOC_URL = os.environ["SCODOC_URL"] or "http://localhost:5000" +API_URL = SCODOC_URL + "/ScoDoc/api" SCODOC_USER = os.environ["SCODOC_USER"] -SCODOC_PASSWORD = os.environ["SCODOC_PASSWD"] +SCODOC_PASSWORD = os.environ["SCODOC_PASSWORD"] print(f"SCODOC_URL={SCODOC_URL}") +print(f"API URL={API_URL}") # --- -if not CHECK_CERTIFICATE: +if not CHK_CERT: urllib3.disable_warnings() @@ -48,9 +51,7 @@ class ScoError(Exception): 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 - ) + r = requests.get(API_URL + "/" + path, headers=headers or HEADERS, verify=CHK_CERT) if r.status_code != 200: raise ScoError(errmsg or "erreur !") return r.json() # decode la reponse JSON @@ -58,39 +59,59 @@ def GET(path: str, headers={}, errmsg=None): def POST(s, path: str, data: dict, errmsg=None): """Post""" - r = s.post(DEPT_URL + "/" + path, data=data, verify=CHECK_CERTIFICATE) + r = s.post(API_URL + "/" + path, data=data, verify=CHK_CERT) 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) -) +r = requests.post(API_URL + "/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, -) +r = requests.get(API_URL + "/departements", headers=HEADERS, verify=CHK_CERT) if r.status_code != 200: raise ScoError("erreur de connexion: vérifier adresse et identifiants") pp(r.json()) -# Liste des tous les étudiants en cours (de tous les depts) -r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiants/courant", - headers=HEADERS, - verify=CHECK_CERTIFICATE, -) +# Liste de tous les étudiants en cours (de tous les depts) +r = requests.get(API_URL + "/etudiants/courant", headers=HEADERS, verify=CHK_CERT) if r.status_code != 200: raise ScoError("erreur de connexion: vérifier adresse et identifiants") +print(f"{len(r.json())} étudiants courants") + +# Bulletin d'un BUT +formsemestre_id = 1052 # A adapter +etudid = 16400 +bul = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin") + +# d'un DUT +formsemestre_id = 1028 # A adapter +etudid = 14721 +bul_dut = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin") + + +# Infos sur un étudiant +etudid = 3561 +code_nip = "11303314" +etud = GET(f"/etudiant/etudid/{etudid}") +print(etud) + +etud = GET(f"/etudiant/nip/{code_nip}") +print(etud) + +sems = GET(f"/etudiant/etudid/{etudid}/formsemestres") +print("\n".join([s["titre_num"] for s in sems])) + +sems = GET(f"/etudiant/nip/{code_nip}/formsemestres") +print("\n".join([s["titre_num"] for s in sems])) + +# Evaluation +evals = GET("/evaluations/1") # # --- Recupere la liste de tous les semestres: # sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !") @@ -146,15 +167,3 @@ if r.status_code != 200: # 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/setup_test_api.py b/tests/api/setup_test_api.py index a413f6fd..9791a975 100644 --- a/tests/api/setup_test_api.py +++ b/tests/api/setup_test_api.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- -"""Test Logos +"""Test API 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 API_USER="xxx" export SCODOC_PASSWD="xxx" export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valide @@ -15,23 +15,26 @@ Utilisation : """ import os import requests +from dotenv import load_dotenv +import pytest + +BASEDIR = "/opt/scodoc/tests/api" +load_dotenv(os.path.join(BASEDIR, ".env")) +CHECK_CERTIFICATE = bool(os.environ.get("CHECK_CERTIFICATE", False)) +SCODOC_URL = os.environ["SCODOC_URL"] +API_URL = SCODOC_URL + "/ScoDoc/api" +API_USER = os.environ.get("API_USER", "test") +API_PASSWORD = os.environ.get("API_PASSWD", "test") +DEPT_ACRONYM = "TAPI" +print(f"SCODOC_URL={SCODOC_URL}") +print(f"API URL={API_URL}") -SCODOC_USER = "test" -SCODOC_PASSWORD = "test" -SCODOC_URL = "http://192.168.1.12:5000" -CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) - - -def get_token(): +@pytest.fixture +def api_headers() -> dict: """ - Permet de set le token dans le header + Demande un jeton et renvoie un dict à utiliser dans les en-têtes de requêtes http """ - r0 = requests.post( - SCODOC_URL + "/ScoDoc/api/tokens", auth=(SCODOC_USER, SCODOC_PASSWORD) - ) + r0 = requests.post(API_URL + "/tokens", auth=(API_USER, API_PASSWORD)) token = r0.json()["token"] return {"Authorization": f"Bearer {token}"} - - -HEADERS = get_token() diff --git a/tests/api/test_api_absences.py b/tests/api/test_api_absences.py index 0918af53..687af7d6 100644 --- a/tests/api/test_api_absences.py +++ b/tests/api/test_api_absences.py @@ -18,63 +18,47 @@ Utilisation : """ import requests -from tests.api.setup_test_api import SCODOC_URL, CHECK_CERTIFICATE, HEADERS +from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers + +# Etudiant pour les tests +ETUDID = 1 # absences -def test_absences(): +def test_absences(api_headers): + """ + Route: /absences/etudid/ + """ r = requests.get( - SCODOC_URL + "/ScoDoc/api/absences/etudid/", - headers=HEADERS, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - - r = requests.get( - SCODOC_URL + "/ScoDoc/api/absences/nip/", - headers=HEADERS, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - - r = requests.get( - SCODOC_URL + "/ScoDoc/api/absences/ine/", - headers=HEADERS, + f"{API_URL}/absences/etudid/{ETUDID}", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 # absences_justify -def test_absences_justify(): +def test_absences_justify(api_headers): + """ + Route: /absences/etudid//just + """ r = requests.get( - SCODOC_URL + "/ScoDoc/api/absences/etudid/1/just", - headers=HEADERS, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - - r = requests.get( - SCODOC_URL + "/ScoDoc/api/absences/nip/1/just", - headers=HEADERS, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - - r = requests.get( - SCODOC_URL + "/ScoDoc/api/absences/ine/1/just", - headers=HEADERS, + API_URL + f"/absences/etudid/{ETUDID}/just", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 + # TODO vérifier résultat -# abs_groupe_etat -def test_abs_groupe_etat(): - r = requests.get( - SCODOC_URL - + "/ScoDoc/api/absences/abs_group_etat/?group_id=&date_debut=date_debut&date_fin=date_fin", - headers=HEADERS, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 +# XXX TODO +# def test_abs_groupe_etat(api_headers): +# """ +# Route: +# """ +# r = requests.get( +# API_URL + "/absences/abs_group_etat/?group_id=&date_debut=date_debut&date_fin=date_fin", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 diff --git a/tests/api/test_api_departements.py b/tests/api/test_api_departements.py index b9f762b9..fe83da78 100644 --- a/tests/api/test_api_departements.py +++ b/tests/api/test_api_departements.py @@ -19,94 +19,89 @@ Utilisation : import requests -from tests.api.setup_test_api import SCODOC_URL, CHECK_CERTIFICATE, HEADERS +from tests.api.setup_test_api import ( + API_URL, + CHECK_CERTIFICATE, + DEPT_ACRONYM, + api_headers, +) from tests.api.tools_test_api import verify_fields -# departements -def test_departements(): - fields = [ - "id", - "acronym", - "description", - "visible", - "date_creation", - ] +DEPARTEMENT_FIELDS = [ + "id", + "acronym", + "description", + "visible", + "date_creation", +] + +def test_departements(api_headers): + """ " + Routes: /departements_ids, /departement, /departement//formsemestres_ids + + """ + # --- Liste des ids r = requests.get( - SCODOC_URL + "/ScoDoc/api/departements", - headers=HEADERS, + API_URL + "/departements_ids", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 1 - - dept = r.json()[0] - - fields_OK = verify_fields(dept, fields) - - assert fields_OK is True - - -# liste_etudiants -def test_liste_etudiants(): - fields = [ - "civilite", - "code_ine", - "code_nip", - "date_naissance", - "email", - "emailperso", - "etudid", - "nom", - "prenom", - "nomprenom", - "lieu_naissance", - "dept_naissance", - "nationalite", - "boursier", - "id", - "codepostaldomicile", - "paysdomicile", - "telephonemobile", - "typeadresse", - "domicile", - "villedomicile", - "telephone", - "fax", - "description", - ] + departements_ids = r.json() + assert isinstance(departements_ids, list) + assert len(departements_ids) > 0 + assert all(isinstance(x, int) for x in departements_ids) + dept_id = departements_ids[0] + # --- Infos sur un département, accès par id r = requests.get( - SCODOC_URL + "/ScoDoc/api/departements/TAPI/etudiants/liste", - headers=HEADERS, + f"{API_URL}/departement/{dept_id}", + headers=api_headers, verify=CHECK_CERTIFICATE, ) - - etu = r.json()[0] - - fields_OK = verify_fields(etu, fields) - assert r.status_code == 200 - assert len(r.json()) == 16 - assert fields_OK is True - + dept_a = r.json() + assert verify_fields(dept_a, DEPARTEMENT_FIELDS) is True + # --- Infos sur un département, accès par acronyme4 r = requests.get( - SCODOC_URL + "/ScoDoc/api/departements/TAPI/etudiants/liste/1", - headers=HEADERS, + f"{API_URL}/departement/{dept_a['acronym']}", + headers=api_headers, verify=CHECK_CERTIFICATE, ) - - etu = r.json()[0] - - fields_OK = verify_fields(etu, fields) - assert r.status_code == 200 - assert len(r.json()) == 16 - assert fields_OK is True + dept_b = r.json() + assert dept_a == dept_b + + # Liste des formsemestres + r = requests.get( + f"{API_URL}/departement/{dept_a['acronym']}/formsemestres_ids", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + dept_ids = r.json() + assert isinstance(dept_ids, list) + assert all(isinstance(x, int) for x in dept_ids) + assert len(dept_ids) > 0 + assert dept_id in dept_ids + + +def test_list_etudiants(api_headers): + fields = {"id", "nip", "ine", "nom", "nom_usuel", "prenom", "civilite"} + + r = requests.get( + f"{API_URL}/departement/{DEPT_ACRONYM}/etudiants", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + etud = r.json()[0] + assert verify_fields(etud, fields) is True # liste_semestres_courant -def test_semestres_courant(): +def test_semestres_courant(api_headers): fields = [ "titre", "gestion_semestrielle", @@ -130,32 +125,39 @@ def test_semestres_courant(): "block_moyennes", "formsemestre_id", "titre_num", + "titre_formation", "date_debut_iso", "date_fin_iso", "responsables", ] - + dept_id = 1 r = requests.get( - SCODOC_URL + "/ScoDoc/api/departements/TAPI/semestres_courants", - headers=HEADERS, + f"{API_URL}/departement/{dept_id}", + headers=api_headers, verify=CHECK_CERTIFICATE, ) - - sem = r.json()[0] - - fields_OK = verify_fields(sem, fields) - assert r.status_code == 200 - assert len(r.json()) == 1 - assert fields_OK is True - - -# referenciel_competences -def test_referenciel_competences(): + dept = r.json() + assert dept["id"] == dept_id + # Accès via acronyme r = requests.get( - SCODOC_URL - + "/ScoDoc/api/departements/TAPI/formations/1/referentiel_competences", - headers=HEADERS, + f"{API_URL}/departement/{dept['acronym']}/formsemestres_courants", + headers=api_headers, verify=CHECK_CERTIFICATE, ) - assert r.status_code == 200 or 204 + assert r.status_code == 200 + result_a = r.json() + assert isinstance(result_a, list) # liste de formsemestres + assert len(result_a) > 0 + sem = result_a[0] + assert verify_fields(sem, fields) is True + + # accès via dept_id + r = requests.get( + f"{API_URL}/departement/{dept['id']}/formsemestres_courants", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + result_b = r.json() + assert result_a == result_b diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py index c0314a1e..001d2752 100644 --- a/tests/api/test_api_etudiants.py +++ b/tests/api/test_api_etudiants.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -"""Test Logos +"""Test API: accès aux étudiants Utilisation : créer les variables d'environnement: (indiquer les valeurs @@ -16,281 +16,222 @@ Utilisation : Lancer : pytest tests/api/test_api_etudiants.py """ -from random import randint import requests -from tests.api.setup_test_api import SCODOC_URL, CHECK_CERTIFICATE, HEADERS +from tests.api.setup_test_api import ( + API_URL, + CHECK_CERTIFICATE, + DEPT_ACRONYM, + api_headers, +) from tests.api.tools_test_api import verify_fields +from tests.api.tools_test_api import ETUD_FIELDS, FSEM_FIELDS -# etudiants_courant -def test_etudiants_courant(): - fields = [ - "id", - "nip", - "nom", - "nom_usuel", - "prenom", - "civilite", - ] +def test_etudiants_courant(api_headers): + """ + Route: /etudiants/courant + """ + fields = {"id", "nip", "ine", "nom", "nom_usuel", "prenom", "civilite"} r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiants/courant", - headers=HEADERS, + API_URL + "/etudiants/courant", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 16 + etudiants = r.json() + assert len(etudiants) > 0 - # Choisis aléatoirement un étudiant dans la liste des étudiants - etu = r.json()[randint(0, len(r.json())) - 1] - - fields_OK = verify_fields(etu, fields) - - assert fields_OK is True - - ########## Version long################ - - fields_long = [ - "civilite", - "code_ine", - "code_nip", - "date_naissance", - "email", - "emailperso", - "etudid", - "nom", - "prenom", - "nomprenom", - "lieu_naissance", - "dept_naissance", - "nationalite", - "boursier", - "id", - "codepostaldomicile", - "paysdomicile", - "telephonemobile", - "typeadresse", - "domicile", - "villedomicile", - "telephone", - "fax", - "description", - ] + etud = etudiants[-1] + assert verify_fields(etud, fields) is True + ########## Version long ################ r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiants/courant/long", - headers=HEADERS, + API_URL + "/etudiants/courant/long", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 16 + etudiants = r.json() + assert len(etudiants) == 16 # HARDCODED - # Choisis aléatoirement un étudiant dans la liste des étudiants - etu = r.json()[randint(0, len(r.json())) - 1] - - fields_OK = verify_fields(etu, fields_long) - - assert fields_OK is True + etud = etudiants[-1] + assert verify_fields(etud, ETUD_FIELDS) is True -# etudiant -def test_etudiant(): - - fields = [ - "civilite", - "code_ine", - "code_nip", - "date_naissance", - "email", - "emailperso", - "etudid", - "nom", - "prenom", - "nomprenom", - "lieu_naissance", - "dept_naissance", - "nationalite", - "boursier", - "id", - "domicile", - "villedomicile", - "telephone", - "fax", - "description", - "codepostaldomicile", - "paysdomicile", - "telephonemobile", - "typeadresse", - ] +def test_etudiant(api_headers): + """ + Routes: /etudiant/etudid, /etudiant/nip, /etudiant/ine + """ ######### Test etudid ######### - r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiant/etudid/1", - headers=HEADERS, + API_URL + "/etudiant/etudid/1", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 24 + etud = r.json() - etu = r.json() - - fields_OK = verify_fields(etu, fields) - - assert fields_OK is True + assert verify_fields(etud, ETUD_FIELDS) is True ######### Test code nip ######### r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiant/nip/1", - headers=HEADERS, + API_URL + "/etudiant/nip/1", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 24 - - etu = r.json() - - fields_OK = verify_fields(etu, fields) - - assert fields_OK is True - + etud = r.json() + fields_ok = verify_fields(etud, ETUD_FIELDS) + assert fields_ok is True + assert etud["dept_acronym"] == DEPT_ACRONYM ######### Test code ine ######### r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiant/ine/1", - headers=HEADERS, + API_URL + "/etudiant/ine/INE1", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 24 + etud = r.json() + assert len(etud) == 25 + fields_ok = verify_fields(etud, ETUD_FIELDS) + assert fields_ok is True - etu = r.json() - - fields_OK = verify_fields(etu, fields) - - assert fields_OK is True + # Vérifie le requetage des 3 1er étudiants + for etudid in (1, 2, 3): + r = requests.get( + f"{API_URL }/etudiant/etudid/{etudid}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + etud = r.json() + nip = etud["code_nip"] + ine = etud["code_ine"] + assert isinstance(etud["id"], int) + assert isinstance(nip, str) + assert isinstance(ine, str) + r = requests.get( + f"{API_URL }/etudiant/nip/{nip}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + etud_nip = r.json() + # On doit avoir obtenue le même étudiant + assert etud_nip == etud + r = requests.get( + f"{API_URL }/etudiant/ine/{ine}", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + etud_ine = r.json() + # On doit avoir obtenue le même étudiant + assert etud_ine == etud -# etudiant_formsemestres -def test_etudiant_formsemestres(): - - fields = [ - "date_fin", - "resp_can_edit", - "dept_id", - "etat", - "resp_can_change_ens", - "id", - "modalite", - "ens_can_edit_eval", - "formation_id", - "gestion_compensation", - "elt_sem_apo", - "semestre_id", - "bul_hide_xml", - "elt_annee_apo", - "titre", - "block_moyennes", - "scodoc7_id", - "date_debut", - "gestion_semestrielle", - "bul_bgcolor", - "formsemestre_id", - "titre_num", - "date_debut_iso", - "date_fin_iso", - "responsables", - ] +def test_etudiant_formsemestres(api_headers): + """ + Route: /etudiant/etudid//formsemestres + """ ######### Test etudid ######### r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiant/etudid/1/formsemestres", - headers=HEADERS, + API_URL + "/etudiant/etudid/1/formsemestres", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 1 + formsemestres = r.json() + assert len(formsemestres) == 1 - formsemestre = r.json()[0] - - fields_OK = verify_fields(formsemestre, fields) - - assert fields_OK is True + formsemestre = formsemestres[0] + assert verify_fields(formsemestre, FSEM_FIELDS) is True ######### Test code nip ######### - r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiant/nip/1/formsemestres", - headers=HEADERS, + API_URL + "/etudiant/nip/1/formsemestres", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 1 + formsemestres = r.json() + assert len(formsemestres) == 1 - formsemestre = r.json()[0] - - fields_OK = verify_fields(formsemestre, fields) - - assert fields_OK is True + formsemestre = formsemestres[0] + assert verify_fields(formsemestre, FSEM_FIELDS) is True ######### Test code ine ######### - r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiant/ine/1/formsemestres", - headers=HEADERS, + API_URL + "/etudiant/ine/INE1/formsemestres", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 1 + formsemestres = r.json() + assert len(formsemestres) == 1 - formsemestre = r.json()[0] - - fields_OK = verify_fields(formsemestre, fields) - - assert fields_OK is True + formsemestre = formsemestres[0] + assert verify_fields(formsemestre, FSEM_FIELDS) is True -# etudiant_bulletin_semestre -def test_etudiant_bulletin_semestre(): - +def test_etudiant_bulletin_semestre(api_headers): + """ + Route: /etudiant/etudid//formsemestre//bulletin + """ ######### Test etudid ######### r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiant/etudid/1/formsemestre/1/bulletin", - headers=HEADERS, + API_URL + "/etudiant/etudid/1/formsemestre/1/bulletin", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 13 + bul = r.json() + assert len(bul) == 13 # HARDCODED ######### Test code nip ######### r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiant/nip/1/formsemestre/1/bulletin", - headers=HEADERS, + API_URL + "/etudiant/nip/1/formsemestre/1/bulletin", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 13 + bul = r.json() + assert len(bul) == 13 # HARDCODED ######### Test code ine ######### - r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiant/ine/1/formsemestre/1/bulletin", - headers=HEADERS, + API_URL + "/etudiant/ine/INE1/formsemestre/1/bulletin", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 13 + bul = r.json() + assert len(bul) == 13 # HARDCODED + + ### --- Test étudiant inexistant + r = requests.get( + API_URL + "/etudiant/ine/189919919119191/formsemestre/1/bulletin", + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 404 -# etudiant_groups -def test_etudiant_groups(): - +def test_etudiant_groups(api_headers): + """ + Route: + /etudiant/etudid//formsemestre//groups + """ fields = [ "partition_id", "id", @@ -306,47 +247,39 @@ def test_etudiant_groups(): ######### Test etudid ######### r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiant/etudid/1/semestre/1/groups", - headers=HEADERS, + API_URL + "/etudiant/etudid/1/formsemestre/1/groups", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 1 - - groups = r.json()[0] - - fields_OK = verify_fields(groups, fields) - - assert fields_OK is True + groups = r.json() + assert len(groups) == 1 # dans un seul groupe + group = groups[0] + fields_ok = verify_fields(group, fields) + assert fields_ok is True ######### Test code nip ######### - r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiant/nip/1/semestre/1/groups", - headers=HEADERS, + API_URL + "/etudiant/nip/1/formsemestre/1/groups", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 1 - - groups = r.json()[0] - - fields_OK = verify_fields(groups, fields) - - assert fields_OK is True + groups = r.json() + assert len(groups) == 1 # dans un seul groupe + group = groups[0] + fields_ok = verify_fields(group, fields) + assert fields_ok is True ######### Test code ine ######### - r = requests.get( - SCODOC_URL + "/ScoDoc/api/etudiant/ine/1/semestre/1/groups", - headers=HEADERS, + API_URL + "/etudiant/ine/INE1/formsemestre/1/groups", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 1 - - groups = r.json()[0] - - fields_OK = verify_fields(groups, fields) - - assert fields_OK is True + groups = r.json() + assert len(groups) == 1 # dans un seul groupe + group = groups[0] + fields_ok = verify_fields(group, fields) + assert fields_ok is True diff --git a/tests/api/test_api_evaluations.py b/tests/api/test_api_evaluations.py index 1fb6ffbd..976eef61 100644 --- a/tests/api/test_api_evaluations.py +++ b/tests/api/test_api_evaluations.py @@ -19,23 +19,31 @@ Utilisation : import requests -from tests.api.setup_test_api import SCODOC_URL, CHECK_CERTIFICATE, HEADERS +from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers -# evaluations -def test_evaluations(): + +def test_evaluations(api_headers): + """ + Route: /evaluation/ + """ r = requests.get( - SCODOC_URL + "/ScoDoc/api/evaluations/1", - headers=HEADERS, + API_URL + "/evaluations/1", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 + # TODO -# evaluation_notes -def test_evaluation_notes(): - r = requests.get( - SCODOC_URL + "/ScoDoc/api/evaluations/eval_notes/1", - headers=HEADERS, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 +# TODO car pas d'évaluations créées à ce stade +# def test_evaluation_notes(api_headers): +# """ +# Route: /evaluation/eval_notes/ +# """ +# r = requests.get( +# API_URL + "/evaluation/eval_notes/1", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 +# # TODO diff --git a/tests/api/test_api_formations.py b/tests/api/test_api_formations.py index 346e333e..b61037da 100644 --- a/tests/api/test_api_formations.py +++ b/tests/api/test_api_formations.py @@ -19,163 +19,91 @@ Utilisation : import requests -from tests.api.setup_test_api import SCODOC_URL, CHECK_CERTIFICATE, HEADERS +from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers from tests.api.tools_test_api import verify_fields +from tests.api.tools_test_api import FORMATION_FIELDS, MODIMPL_FIELDS -# formations -def test_formations(): - fields = [ - "id", - "acronyme", - "titre_officiel", - "formation_code", - "code_specialite", - "dept_id", - "titre", - "version", - "type_parcours", - "referentiel_competence_id", - "formation_id", - ] - +def test_formations_ids(api_headers): + """ + Route: /formations_ids + """ r = requests.get( - SCODOC_URL + "/ScoDoc/api/formations", - headers=HEADERS, + API_URL + "/formations_ids", + headers=api_headers, verify=CHECK_CERTIFICATE, ) - - formation = r.json()[0] - - fields_OK = verify_fields(formation, fields) - assert r.status_code == 200 - assert len(r.json()) == 1 - assert fields_OK is True + formations_ids = r.json() + # Une liste non vide d'entiers + assert isinstance(formations_ids, list) + assert len(formations_ids) > 0 + assert all(isinstance(x, int) for x in formations_ids) -# formations_by_id -def test_formations_by_id(): - fields = [ - "id", - "acronyme", - "titre_officiel", - "formation_code", - "code_specialite", - "dept_id", - "titre", - "version", - "type_parcours", - "referentiel_competence_id", - "formation_id", - ] - +def test_formations_by_id(api_headers): + """ + Route: /formation/ + """ r = requests.get( - SCODOC_URL + "/ScoDoc/api/formations/1", - headers=HEADERS, + API_URL + "/formation/1", + headers=api_headers, verify=CHECK_CERTIFICATE, ) - + assert r.status_code == 200 formation = r.json() - - fields_OK = verify_fields(formation, fields) - - assert r.status_code == 200 - assert fields_OK is True + assert verify_fields(formation, FORMATION_FIELDS) is True + # TODO tester le contenu de certains champs -# formation_export_by_formation_id -def test_formation_export_by_formation_id(): - fields = [ - "id", - "acronyme", - "titre_officiel", - "formation_code", - "code_specialite", - "dept_id", - "titre", - "version", - "type_parcours", - "referentiel_competence_id", - "formation_id", - "ue", - ] +def test_formation_export(api_headers): + """ + Route: /formation/formation_export/ + """ r = requests.get( - SCODOC_URL + "/ScoDoc/api/formations/formation_export/1", - headers=HEADERS, + API_URL + "/formation/formation_export/1", + headers=api_headers, verify=CHECK_CERTIFICATE, ) - - export_formation = r.json() - - fields_OK = verify_fields(export_formation, fields) - assert r.status_code == 200 - assert fields_OK is True + export_formation = r.json() + assert verify_fields(export_formation, FORMATION_FIELDS) is True + # TODO tester le contenu de certains champs -# formsemestre_apo -# def test_formsemestre_apo(): +# TODO +# def test_formsemestre_apo(api_headers): # r = requests.get( -# SCODOC_URL + "/ScoDoc/api/formations/apo/", -# headers=HEADERS, +# API_URL + "/formation/apo/", +# headers=api_headers, # verify=CHECK_CERTIFICATE, # ) # assert r.status_code == 200 -# moduleimpl -def test_moduleimpl(): - - fields = [ - "id", - "formsemestre_id", - "computation_expr", - "module_id", - "responsable_id", - "moduleimpl_id", - "ens", - "module", - ] - +def test_moduleimpl(api_headers): + """ + Route: /formation/moduleimpl/ + """ r = requests.get( - SCODOC_URL + "/ScoDoc/api/formations/moduleimpl/1", - headers=HEADERS, + API_URL + "/formation/moduleimpl/1", + headers=api_headers, verify=CHECK_CERTIFICATE, ) - + assert r.status_code == 200 moduleimpl = r.json() - - fields_OK = verify_fields(moduleimpl, fields) - - assert r.status_code == 200 - assert fields_OK is True + assert verify_fields(moduleimpl, MODIMPL_FIELDS) is True + # TODO tester le contenu de certains champs -# moduleimpls_sem -def test_moduleimpls_sem(): - - fields = [ - "id", - "formsemestre_id", - "computation_expr", - "module_id", - "responsable_id", - "moduleimpl_id", - "ens", - "module", - "moduleimpl_id", - "ens", - ] +def test_referentiel_competences(api_headers): + """ + Route: "/formation//referentiel_competences", + """ r = requests.get( - SCODOC_URL + "/ScoDoc/api/formations/moduleimpl/formsemestre/1/liste", - headers=HEADERS, + API_URL + "/formation/1/referentiel_competences", + headers=api_headers, verify=CHECK_CERTIFICATE, ) - moduleimpl = r.json()[0] - - fields_OK = verify_fields(moduleimpl, fields) - assert r.status_code == 200 - assert len(r.json()) == 21 - assert fields_OK is True + # XXX A compléter diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py index ac84cbaf..bdcf7e93 100644 --- a/tests/api/test_api_formsemestre.py +++ b/tests/api/test_api_formsemestre.py @@ -18,83 +18,74 @@ Utilisation : """ import requests +from app.api.formsemestres import formsemestre -from tests.api.setup_test_api import SCODOC_URL, CHECK_CERTIFICATE, HEADERS -from tests.api.tools_test_api import verify_fields +from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers +from tests.api.tools_test_api import MODIMPL_FIELDS, verify_fields +from tests.api.tools_test_api import FSEM_FIELDS, UE_FIELDS, MODULE_FIELDS -# formsemestre -def test_formsemestre(): +# Etudiant pour les tests +ETUDID = 1 +NIP = "1" +INE = "INE1" + + +def test_formsemestre(api_headers): + """ + Route: /formsemestre/ + """ r = requests.get( - SCODOC_URL + "/ScoDoc/api/formsemestre/1", - headers=HEADERS, + API_URL + "/formsemestre/1", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - formsemestre = r.json() - - fields = [ - "date_fin", - "resp_can_edit", - "dept_id", - "etat", - "resp_can_change_ens", - "id", - "modalite", - "ens_can_edit_eval", - "formation_id", - "gestion_compensation", - "elt_sem_apo", - "semestre_id", - "bul_hide_xml", - "elt_annee_apo", - "titre", - "block_moyennes", - "scodoc7_id", - "date_debut", - "gestion_semestrielle", - "bul_bgcolor", - "formsemestre_id", - "titre_num", - "date_debut_iso", - "date_fin_iso", - "responsables", - ] - - fields_OK = verify_fields(formsemestre, fields) - - assert fields_OK is True + assert verify_fields(formsemestre, FSEM_FIELDS) -# etudiant_bulletin -def test_etudiant_bulletin(): +def test_etudiant_bulletin(api_headers): + """ + Route: + """ + formsemestre_id = 1 r = requests.get( - SCODOC_URL + "/ScoDoc/api/formsemestre/1/etudiant/etudid/1/bulletin", - headers=HEADERS, + f"{API_URL}/etudiant/etudid/1/formsemestre/{formsemestre_id}/bulletin", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 + bull_a = r.json() r = requests.get( - SCODOC_URL + "/ScoDoc/api/formsemestre/1/etudiant/nip/1/bulletin", - headers=HEADERS, + f"{API_URL}/etudiant/nip/{NIP}/formsemestre/{formsemestre_id}/bulletin", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 + bull_b = r.json() r = requests.get( - SCODOC_URL + "/ScoDoc/api/formsemestre/1/etudiant/ine/1/bulletin", - headers=HEADERS, + f"{API_URL}/etudiant/ine/{INE}/formsemestre/{formsemestre_id}/bulletin", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 + bull_c = r.json() + # elimine les dates de publication pour comparer les autres champs + del bull_a["date"] + del bull_b["date"] + del bull_c["date"] + assert bull_a == bull_b == bull_c -# bulletins -def test_bulletins(): +def test_bulletins(api_headers): + """ + Route: + """ r = requests.get( - SCODOC_URL + "/ScoDoc/api/formsemestre/1/bulletins", - headers=HEADERS, + API_URL + "/formsemestre/1/bulletins", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 @@ -103,88 +94,40 @@ def test_bulletins(): # # jury # def test_jury(): # r = requests.get( -# SCODOC_URL + "/ScoDoc/api/formsemestre/1/jury", -# headers=HEADERS, +# API_URL + "/formsemestre/1/jury", +# headers=api_headers, # verify=CHECK_CERTIFICATE, # ) # assert r.status_code == 200 -# semestre_index -def test_semestre_index(): - ue_fields = [ - "semestre_idx", - "type", - "formation_id", - "ue_code", - "id", - "ects", - "acronyme", - "is_external", - "numero", - "code_apogee", - "titre", - "coefficient", - "color", - "ue_id", - ] - - ressource_fields = [ - "heures_tp", - "code_apogee", - "titre", - "coefficient", - "module_type", - "id", - "ects", - "abbrev", - "ue_id", - "code", - "formation_id", - "heures_cours", - "matiere_id", - "heures_td", - "semestre_id", - "numero", - "module_id", - ] - - sae_fields = [ - "heures_tp", - "code_apogee", - "titre", - "coefficient", - "module_type", - "id", - "ects", - "abbrev", - "ue_id", - "code", - "formation_id", - "heures_cours", - "matiere_id", - "heures_td", - "semestre_id", - "numero", - "module_id", - ] +def test_formsemestre_programme(api_headers): + """ + Route: /formsemestre/1/programme + """ r = requests.get( - SCODOC_URL + "/ScoDoc/api/formsemestre/1/programme", - headers=HEADERS, + API_URL + "/formsemestre/1/programme", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 - assert len(r.json()) == 3 + prog = r.json() + assert isinstance(prog, dict) + assert "ues" in prog + assert "modules" in prog + assert "ressources" in prog + assert "saes" in prog + assert isinstance(prog["ues"], list) + assert isinstance(prog["modules"], list) + ue = prog["ues"][0] + modules = prog["modules"] + # Il y a toujours au moins une SAE et une ressources dans notre base de test + ressource = prog["ressources"][0] + sae = prog["saes"][0] - ue = r.json()["ues"][0] - ressource = r.json()["ressources"][0] - sae = r.json()["saes"][0] - - fields_ue_OK = verify_fields(ue, ue_fields) - fields_ressource_OK = verify_fields(ressource, ressource_fields) - fields_sae_OK = verify_fields(sae, sae_fields) - - assert fields_ue_OK is True - assert fields_ressource_OK is True - assert fields_sae_OK is True + assert verify_fields(ue, UE_FIELDS) + if len(modules) > 1: + assert verify_fields(modules[0], MODIMPL_FIELDS) + assert verify_fields(ressource, MODIMPL_FIELDS) + assert verify_fields(sae, MODIMPL_FIELDS) diff --git a/tests/api/test_api_jury.py b/tests/api/test_api_jury.py index e9be6a01..23454d00 100644 --- a/tests/api/test_api_jury.py +++ b/tests/api/test_api_jury.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -"""Test Logos +"""Test API Jurys XXX TODO A ECRIRE Utilisation : créer les variables d'environnement: (indiquer les valeurs @@ -19,37 +19,41 @@ Utilisation : import requests -from tests.api.setup_test_api import SCODOC_URL, CHECK_CERTIFICATE, HEADERS +from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers -# jury_preparation -def test_jury_preparation(): + +def test_jury_preparation(api_headers): + """ + Route: + """ r = requests.get( SCODOC_URL + "/ScoDoc/api/jury/formsemestre//preparation_jury", - headers=HEADERS, + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 -# jury_decisions -def test_jury_decisions(): +def test_jury_decisions(api_headers): + """ + Route: + """ r = requests.get( - SCODOC_URL - + "/ScoDoc/api/jury/formsemestre//decisions_jury", - headers=HEADERS, + API_URL + "/jury/formsemestre//decisions_jury", + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 # set_decision_jury -def test_set_decision_jury(): +def test_set_decision_jury(api_headers): r = requests.get( SCODOC_URL + "/ScoDoc/api/jury/set_decision/etudid?etudid=&formsemestre_id=" "&jury=&devenir=&assiduite=", - headers=HEADERS, + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 @@ -58,7 +62,7 @@ def test_set_decision_jury(): SCODOC_URL + "/ScoDoc/api/jury/set_decision/nip?etudid=&formsemestre_id=" "&jury=&devenir=&assiduite=", - headers=HEADERS, + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 @@ -67,34 +71,36 @@ def test_set_decision_jury(): SCODOC_URL + "/ScoDoc/api/jury/set_decision/ine?etudid=&formsemestre_id=" "&jury=&devenir=&assiduite=", - headers=HEADERS, + headers=api_headers, verify=CHECK_CERTIFICATE, ) assert r.status_code == 200 -# annule_decision_jury -def test_annule_decision_jury(): - r = requests.get( - SCODOC_URL - + "/ScoDoc/api/jury/etudid//formsemestre//annule_decision", - headers=HEADERS, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 +# def test_annule_decision_jury(api_headers): +# """ +# Route: +# """ +# r = requests.get( +# SCODOC_URL +# + "/ScoDoc/api/jury/etudid//formsemestre//annule_decision", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 - r = requests.get( - SCODOC_URL - + "/ScoDoc/api/jury/nip//formsemestre//annule_decision", - headers=HEADERS, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 +# r = requests.get( +# SCODOC_URL +# + "/ScoDoc/api/jury/nip//formsemestre//annule_decision", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 - r = requests.get( - SCODOC_URL - + "/ScoDoc/api/jury/ine//formsemestre//annule_decision", - headers=HEADERS, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 +# r = requests.get( +# SCODOC_URL +# + "/ScoDoc/api/jury/ine//formsemestre//annule_decision", +# headers=api_headers, +# verify=CHECK_CERTIFICATE, +# ) +# assert r.status_code == 200 diff --git a/tests/api/test_api_logos.py b/tests/api/test_api_logos.py index 0e2250ff..9e3ba5a3 100644 --- a/tests/api/test_api_logos.py +++ b/tests/api/test_api_logos.py @@ -5,11 +5,16 @@ """Exemple utilisation API ScoDoc 9 avec jeton obtenu par basic authentication utilisation: - à faire fonctionner en environnment de test (FLASK_ENV=test dans le fichier .env) + à faire fonctionner en environnment de test (FLASK_ENV=test_api dans le fichier .env) pytest tests/api/test_api_logos.py """ -from tests.api.setup_test_api import SCODOC_URL, CHECK_CERTIFICATE, HEADERS + +# XXX TODO +# Ce test a une logique très différente des autres : A UNIFIER + + +from tests.api.setup_test_api import API_URL from scodoc import app from tests.unit.config_test_logos import ( @@ -22,35 +27,47 @@ from tests.unit.config_test_logos import ( def test_super_access(create_super_token): + """ + Route: + """ dept1, dept2, dept3, token = create_super_token - HEADERS = {"Authorization": f"Bearer {token}"} + headers = {"Authorization": f"Bearer {token}"} with app.test_client() as client: - response = client.get("/ScoDoc/api/logos", headers=HEADERS) + response = client.get(API_URL + "/logos", headers=headers) assert response.status_code == 200 assert response.json is not None def test_admin_access(create_admin_token): + """ + Route: + """ dept1, dept2, dept3, token = create_admin_token headers = {"Authorization": f"Bearer {token}"} with app.test_client() as client: - response = client.get("/ScoDoc/api/logos", headers=headers) + response = client.get(API_URL + "/logos", headers=headers) assert response.status_code == 401 def test_lambda_access(create_lambda_token): + """ + Route: + """ dept1, dept2, dept3, token = create_lambda_token headers = {"Authorization": f"Bearer {token}"} with app.test_client() as client: - response = client.get("/ScoDoc/api/logos", headers=headers) + response = client.get(API_URL + "/logos", headers=headers) assert response.status_code == 401 def test_initial_with_header_and_footer(create_super_token): + """ + Route: + """ dept1, dept2, dept3, token = create_super_token headers = {"Authorization": f"Bearer {token}"} with app.test_client() as client: - response = client.get("/ScoDoc/api/logos", headers=headers) + response = client.get(API_URL + "/logos", headers=headers) assert response.status_code == 200 assert response.json is not None assert len(response.json) == 7 diff --git a/tests/api/test_api_partitions.py b/tests/api/test_api_partitions.py index f7af82b0..0ceb4e33 100644 --- a/tests/api/test_api_partitions.py +++ b/tests/api/test_api_partitions.py @@ -19,12 +19,14 @@ Utilisation : import requests -from tests.api.setup_test_api import SCODOC_URL, CHECK_CERTIFICATE, HEADERS +from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers from tests.api.tools_test_api import verify_fields -# partition -def test_partition(): +def test_partition(api_headers): + """ + Route: + """ fields = [ "partition_id", "id", @@ -36,23 +38,22 @@ def test_partition(): ] r = requests.get( - SCODOC_URL + "/ScoDoc/api/partitions/1", - headers=HEADERS, + API_URL + "/partitions/1", + headers=api_headers, verify=CHECK_CERTIFICATE, ) - - partition = r.json()[0] - - fields_OK = verify_fields(partition, fields) - assert r.status_code == 200 - assert len(r.json()) == 2 - assert fields_OK is True + partitions = r.json() + assert len(partitions) == 1 + partition = partitions[0] + fields_ok = verify_fields(partition, fields) + assert fields_ok is True -# etud_in_group -def test_etud_in_group(): - +def test_etud_in_group(api_headers): + """ + Route: + """ fields = [ "etudid", "id", @@ -92,33 +93,36 @@ def test_etud_in_group(): ] r = requests.get( - SCODOC_URL + "/ScoDoc/api/partitions/groups/1", - headers=HEADERS, + API_URL + "/partitions/groups/1", + headers=api_headers, verify=CHECK_CERTIFICATE, ) etu = r.json()[0] - fields_OK = verify_fields(etu, fields) + fields_ok = verify_fields(etu, fields) assert r.status_code == 200 assert len(r.json()) == 16 - assert fields_OK is True + assert fields_ok is True # r = requests.get( - # SCODOC_URL + "/ScoDoc/api/partitions/groups/1/etat/", - # headers=HEADERS, + # API_URL + "/partitions/groups/1/etat/", + # headers=api_headers, # verify=CHECK_CERTIFICATE, # ) # assert r.status_code == 200 # # set_groups -# def test_set_groups(): +# def test_set_groups(api_headers): +# """ +# Route: +# """ # r = requests.get( # SCODOC_URL # + "/partitions/set_groups/partition//groups/" # "/delete//create/", -# headers=HEADERS, +# headers=api_headers, # verify=CHECK_CERTIFICATE, # ) # assert r.status_code == 200 diff --git a/tests/api/test_api_permissions.py b/tests/api/test_api_permissions.py new file mode 100644 index 00000000..eeebb8b4 --- /dev/null +++ b/tests/api/test_api_permissions.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +"""Test permissions + + On a deux utilisateurs dans la base test API: + - "test", avec le rôle LecteurAPI qui a APIView, + - et "other", qui n'a aucune permission. + + + Lancer : + pytest tests/api/test_api_permissions.py +""" + +import requests + +import flask +from tests.api.setup_test_api import API_URL, SCODOC_URL, CHECK_CERTIFICATE, api_headers +from tests.api.tools_test_api import verify_fields + +from app import create_app +from config import RunningConfig + + +def test_permissions(api_headers): + """ + vérification de la permissions APIView et du non accès sans role + de toutes les routes de l'API + """ + # Ce test va récupérer toutes les routes de l'API + app = create_app(RunningConfig) + assert app + # Les routes de l'API avec GET, excluant les logos pour le momeent XXX + api_rules = [ + r + for r in app.url_map.iter_rules() + if str(r).startswith("/ScoDoc/api") + and not "logo" in str(r) # ignore logos + and "GET" in r.methods + ] + assert len(api_rules) > 0 + args = { + "etudid": 1, + # "date_debut": + # "date_fin": + "dept": "TAPI", + "dept_ident": "TAPI", + "dept_id": 1, + "etape_apo": "???", + "etat": "I", + "evaluation_id": 1, + "formation_id": 1, + "formsemestre_id": 1, + "group_id": 1, + "ine": "1", + "module_id": 1, + "moduleimpl_id": 1, + "nip": 1, + "partition_id": 1, + } + for rule in api_rules: + path = rule.build(args)[1] + if not "GET" in rule.methods: + # skip all POST routes + continue + r = requests.get( + SCODOC_URL + path, + headers=api_headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 200 + + # Même chose sans le jeton: + for rule in api_rules: + path = rule.build(args)[1] + if not "GET" in rule.methods: + # skip all POST routes + continue + r = requests.get( + SCODOC_URL + path, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 401 + + # Demande un jeton pour "other" + r = requests.post(API_URL + "/tokens", auth=("other", "other")) + assert r.status_code == 200 + token = r.json()["token"] + headers = {"Authorization": f"Bearer {token}"} + # Vérifie que tout est interdit + for rule in api_rules: + path = rule.build(args)[1] + if not "GET" in rule.methods: + # skip all POST routes + continue + r = requests.get( + SCODOC_URL + path, + headers=headers, + verify=CHECK_CERTIFICATE, + ) + assert r.status_code == 403 diff --git a/tests/api/tools_test_api.py b/tests/api/tools_test_api.py index 4501cd3c..d1d14402 100644 --- a/tests/api/tools_test_api.py +++ b/tests/api/tools_test_api.py @@ -1,13 +1,133 @@ -def verify_fields(json_response, fields): +"""Utilitaires pour les tests de l'API +""" + + +def verify_fields(json_response: dict, expected_fields: set) -> bool: """ - Vérifie si les champs de la réponse json sont corrects + Vérifie si les champs attendu de la réponse json sont présents json_response : la réponse de la requête - fields : une liste avec l'ensemble des champs à vérifier + expected_fields : ensemble des champs à vérifier Retourne True ou False """ - for field in json_response: - if field not in fields: - return False - return True + return all(field in json_response for field in expected_fields) + + +ETUD_FIELDS = { + "boursier", + "civilite", + "code_ine", + "code_nip", + "codepostaldomicile", + "date_naissance", + "dept_acronym", + "dept_id", + "dept_naissance", + "description", + "domicile", + "email", + "emailperso", + "etudid", + "id", + "lieu_naissance", + "nationalite", + "nom", + "nomprenom", + "paysdomicile", + "prenom", + "telephone", + "telephonemobile", + "typeadresse", + "villedomicile", +} + +FORMATION_FIELDS = { + "id", + "acronyme", + "titre_officiel", + "formation_code", + "code_specialite", + "dept_id", + "titre", + "version", + "type_parcours", + "referentiel_competence_id", + "formation_id", +} + +FSEM_FIELDS = { + "block_moyennes", + "bul_bgcolor", + "bul_hide_xml", + "date_debut_iso", + "date_debut", + "date_fin_iso", + "date_fin", + "dept_id", + "elt_annee_apo", + "elt_sem_apo", + "ens_can_edit_eval", + "etat", + "formation_id", + "formsemestre_id", + "gestion_compensation", + "gestion_semestrielle", + "id", + "modalite", + "resp_can_change_ens", + "resp_can_edit", + "responsables", + "semestre_id", + "titre_formation", + "titre_num", + "titre", +} + +MODIMPL_FIELDS = { + "id", + "formsemestre_id", + "computation_expr", + "module_id", + "responsable_id", + "moduleimpl_id", + "ens", + "module", +} + +MODULE_FIELDS = { + "heures_tp", + "code_apogee", + "titre", + "coefficient", + "module_type", + "id", + "ects", + "abbrev", + "ue_id", + "code", + "formation_id", + "heures_cours", + "matiere_id", + "heures_td", + "semestre_id", + "numero", + "module_id", +} + +UE_FIELDS = { + "semestre_idx", + "type", + "formation_id", + "ue_code", + "id", + "ects", + "acronyme", + "is_external", + "numero", + "code_apogee", + "titre", + "coefficient", + "color", + "ue_id", +} diff --git a/tools/create_database.sh b/tools/create_database.sh index 1fd4599f..e1d7ffb9 100755 --- a/tools/create_database.sh +++ b/tools/create_database.sh @@ -9,8 +9,16 @@ die() { echo exit 1 } -[ $# = 1 ] || die "Usage $0 db_name" -db_name="$1" +[ $# = 1 ] || [ $# = 2 ] || die "Usage $0 [--drop] db_name" + +if [ "$1" = "--drop" ] +then + db_name="$2" + echo "Dropping database $db_name..." + dropdb "$db_name" +else + db_name="$1" +fi # Le répertoire de ce script: SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index aba8cec7..bfe4800e 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -5,60 +5,59 @@ Création des départements, formations, semestres, étudiants, groupes... utilisation: - 1) modifier le .env pour indiquer - SCODOC_DATABASE_URI="postgresql:///SCO_TEST_API" + 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 SCO_TEST_API + tools/create_database.sh SCODOC_TEST_API flask db upgrade flask sco-db-init --erase flask init-test-database - flask user-role -a Admin -d TAPI test - flask user-password test - flask create-role APIUserViewer - flask edit-role APIUserViewer -a APIView - flask user-role test -a APIUserViewer + 3) relancer ScoDoc: flask run --host 0.0.0.0 - 4) lancer client de test (ou vérifier dans le navigateur) + 4) lancer client de test """ import datetime import random +import sys -random.seed(12345678) # tests reproductibles - -from flask_login import login_user - -from app import auth +from app.auth.models import Role, User from app import models +from app.models import Departement, Formation, FormSemestre, Identite from app import db from app.scodoc import ( + sco_cache, + sco_evaluation_db, sco_formations, - sco_formsemestre, sco_formsemestre_inscriptions, sco_groups, ) +from app.scodoc.sco_permissions import Permission from tools.fakeportal.gen_nomprenoms import nomprenom +random.seed(12345678) # tests reproductibles + # La formation à utiliser: FORMATION_XML_FILENAME = "tests/ressources/formations/scodoc_formation_RT_BUT_RT_v1.xml" -def init_departement(acronym): +def init_departement(acronym: str) -> Departement: "Create dept, and switch context into it." import app as mapp - dept = models.Departement(acronym=acronym) + dept = Departement(acronym=acronym) db.session.add(dept) mapp.set_sco_dept(acronym) db.session.commit() return dept -def import_formation() -> models.Formation: +def import_formation() -> Formation: """Import formation from XML. Returns formation_id """ @@ -66,28 +65,48 @@ def import_formation() -> models.Formation: doc = f.read() # --- Création de la formation f = sco_formations.formation_import_xml(doc) - return models.Formation.query.get(f[0]) + return Formation.query.get(f[0]) -def create_user(dept): - """créé les utilisaterurs nécessaires aux tests""" - user = auth.models.User( - user_name="test", nom="Doe", prenom="John", dept=dept.acronym - ) +def create_users(dept: Departement) -> tuple: + """créé les utilisateurs nécessaires aux tests""" + # Un utilisateur "test" (passwd test) pouvant lire l'API + user = User(user_name="test", nom="Doe", prenom="John", dept=dept.acronym) + user.set_password("test") db.session.add(user) + + # Le rôle standard LecteurAPI existe déjà + role = Role.query.filter_by(name="LecteurAPI").first() + if role is None: + print("Erreur: rôle LecteurAPI non existant") + sys.exit(1) + perm_api_view = Permission.get_by_name("APIView") + role.add_permission(perm_api_view) + db.session.add(role) + + user.add_role(role, None) + + # Un utilisateur "other" n'ayant aucune permission sur l'API + other = User(user_name="other", nom="Sans", prenom="Permission", dept=dept.acronym) + other.set_password("other") + db.session.add(other) + db.session.commit() - return user + return user, other -def create_fake_etud(dept): - """Créé un faux étudiant et l'insère dans la base""" +def create_fake_etud(dept: Departement) -> Identite: + """Créé un faux étudiant et l'insère dans la base.""" civilite = random.choice(("M", "F", "X")) nom, prenom = nomprenom(civilite) - etud = models.Identite(civilite=civilite, nom=nom, prenom=prenom, dept_id=dept.id) + etud: Identite = Identite( + civilite=civilite, nom=nom, prenom=prenom, dept_id=dept.id + ) db.session.add(etud) db.session.commit() - etud.code_nip = etud.id - etud.code_ine = etud.id + # créé un étudiant sur deux avec un NIP et INE alphanumérique + etud.code_nip = f"{etud.id}" if (etud.id % 2) else f"NIP{etud.id}" + etud.code_ine = f"INE{etud.id}" if (etud.id % 2) else f"{etud.id}" db.session.add(etud) db.session.commit() adresse = models.Adresse( @@ -100,14 +119,18 @@ def create_fake_etud(dept): return etud -def create_etuds(dept, nb=16): +def create_etuds(dept: Departement, nb=16) -> list: "create nb etuds" return [create_fake_etud(dept) for _ in range(nb)] -def create_formsemestre(formation, user, semestre_idx=1): - """Create formsemestre and moduleimpls""" - formsemestre = models.FormSemestre( +def create_formsemestre( + formation: Formation, responsable: User, semestre_idx=1 +) -> FormSemestre: + """Create formsemestre and moduleimpls + responsable: resp. du formsemestre + """ + formsemestre = FormSemestre( dept_id=formation.dept_id, semestre_id=semestre_idx, titre="Semestre test", @@ -121,7 +144,9 @@ def create_formsemestre(formation, user, semestre_idx=1): # Crée un modulimpl par module de ce semestre: for module in formation.modules.filter_by(semestre_id=semestre_idx): modimpl = models.ModuleImpl( - module_id=module.id, formsemestre_id=formsemestre.id, responsable_id=user.id + module_id=module.id, + formsemestre_id=formsemestre.id, + responsable_id=responsable.id, ) db.session.add(modimpl) db.session.commit() @@ -132,7 +157,7 @@ def create_formsemestre(formation, user, semestre_idx=1): return formsemestre -def inscrit_etudiants(etuds, formsemestre): +def inscrit_etudiants(etuds: list, formsemestre: FormSemestre): """Inscrit les etudiants aux semestres et à tous ses modules""" for etud in etuds: sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( @@ -144,14 +169,38 @@ def inscrit_etudiants(etuds, formsemestre): ) -def init_test_database(): +def create_evaluations(formsemestre: FormSemestre): + "creation d'une evaluation dans cahque modimpl du semestre" + for modimpl in formsemestre.modimpls: + args = { + "moduleimpl_id": modimpl.id, + "jour": None, + "heure_debut": "8h00", + "heure_fin": "9h00", + "description": None, + "note_max": 20, + "coefficient": 1.0, + "visibulletin": True, + "publish_incomplete": True, + "evaluation_type": None, + "numero": None, + } + evaluation_id = sco_evaluation_db.do_evaluation_create(**args) + +def init_test_database(): + """Appelé par la commande `flask init-test-database` + + Création d'un département et de son contenu pour les tests + """ dept = init_departement("TAPI") - user = create_user(dept) - etuds = create_etuds(dept) - formation = import_formation() - formsemestre = create_formsemestre(formation, user) - inscrit_etudiants(etuds, formsemestre) + user_lecteur, user_autre = create_users(dept) + with sco_cache.DeferredSemCacheManager(): + etuds = create_etuds(dept) + formation = import_formation() + formsemestre = create_formsemestre(formation, user_lecteur) + create_evaluations(formsemestre) + inscrit_etudiants(etuds, formsemestre) # à compléter # - groupes # - absences