diff --git a/app/api/__init__.py b/app/api/__init__.py
index 36da5c561e..9be4d3b69c 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 35056a05b3..51c4673e97 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 20dd7ded8f..67d6fba145 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 482bceca63..fca5d6a82c 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 c04495abe9..a193553fae 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 4cbfa9f33c..7d2776d299 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 920d9af4c4..6ca6fde13f 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 122abf148c..725454b189 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 982280776b..1131a2847d 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 663ef602ad..a2c1fbde7c 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 d925d17496..1ee74c46b8 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 156a4c5fd6..0000000000
--- 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 8440aa556c..0000000000
--- 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 821e3a01d3..0000000000
--- 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 f36ec7b0ee..f5c11b0969 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 35b07cf283..e03d833445 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 cfab21a9ca..ad56c5063f 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 24daa8ca03..1f0259ab56 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 689ea9ed97..f9dbb87062 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 36d11d1e4a..e3e40c51ff 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 86d444c314..6cf0767f0a 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:
-
- """
-
- # 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 f5efbeb254..8f8bd1a89f 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 7bc199ead4..645f067e60 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 8bbed09041..5ac18ff4ec 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 2c2aa3d5ea..d08719375a 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 917b01363a..0bce6d47e5 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 46244a9a8f..5b0960ba02 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 fda7238394..e243c39836 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 9cf5f23647..4c64ad543b 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 518bd72192..48d81a14f7 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 f4ada213b2..be5028f3c5 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 b49b6159bd..0000000000
--- 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 a4e1d3a720..c01588df6b 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 efc0b53415..df790649a6 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 f34923a5f8..78425028dc 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 d6925d8c62..f173b56ba2 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 2e9792e419..8fcc6f7efe 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 95b9dcc987..b338f9a7db 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 14c4765134..42f3db762b 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 aabfaddce2..606fc74219 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 ece30a3455..f99bb8a186 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 fd25e5da3f..33ffc69cf0 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 6152e88105..09bf210243 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%sModule : %s
'
+ 'Évaluation%sModule : %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 30750e3e5b..6c00530692 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 178138b220..ae6ecc0b6b 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 9dfcbbb672..0da85f2c3f 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 8e834b2312..79ed91cad1 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 d6def36973..b9e6a3ee05 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 3a99d2232a..2631ef45c4 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 512caa8a56..fd04e5105d 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 9ccb636280..49445bc95b 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 179014cfa1..1c5c19eb4d 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 9cda23725d..e75977359d 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 67ac5a6c71..9a13a8f5c9 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 b90afd0dd2..8259822868 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 ff91c14950..c1e7dca27a 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 24cd2157c3..fb57807f45 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 4dcd0c23a5..fd5068e609 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 8a287c01d9..f344fb1153 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 adde8b6a12..f924acb92f 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 fca2fc5148..d78c69a910 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 f4ef3e98c7..3de1255fd0 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 1d3b172627..01881bf7fa 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 0000000000..a365074d5f
--- /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 0000000000..e1857bd810
--- /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 88ba892f6d..a581a87838 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 a413f6fdcb..9791a975e0 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 0918af5367..687af7d620 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 b9f762b90e..fe83da7830 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 c0314a1e66..001d275250 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 1fb6ffbdfb..976eef61c7 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 346e333e0c..b61037da0b 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 ac84cbafd6..bdcf7e93ee 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 e9be6a017a..23454d00be 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 0e2250ffd0..9e3ba5a31f 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 f7af82b0a8..0ceb4e3396 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 0000000000..eeebb8b4d5
--- /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 4501cd3ccc..d1d14402b9 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 1fd4599f0b..e1d7ffb956 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 aba8cec76a..bfe4800e21 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
|