Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into entreprises

This commit is contained in:
Arthur ZHU 2022-06-10 17:58:07 +02:00
commit f0edcb28f7
80 changed files with 2432 additions and 3448 deletions

View File

@ -22,8 +22,6 @@ def requested_format(default_format="json", allowed_formats=None):
from app.api import tokens 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 departements
from app.api import etudiants from app.api import etudiants
from app.api import formations from app.api import formations

View File

@ -4,26 +4,22 @@ from flask import jsonify
from app.api import bp from app.api import bp
from app.api.errors import error_response 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.api.tools import get_etu_from_etudid_or_nip_or_ine from app.models import Identite
from app.scodoc import notesdb as ndb
from app.scodoc import notesdb as ndb
from app.scodoc import sco_abs from app.scodoc import sco_abs
from app.scodoc.sco_groups import get_group_members
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@bp.route("/absences/etudid/<int:etudid>", methods=["GET"]) @bp.route("/absences/etudid/<int:etudid>", methods=["GET"])
@bp.route("/absences/nip/<int:nip>", methods=["GET"]) @token_auth.login_required
@bp.route("/absences/ine/<int:ine>", methods=["GET"])
@token_permission_required(Permission.APIView) @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é Retourne la liste des absences d'un étudiant donné
etudid : l'etudid d'un étudiant etudid : l'etudid d'un étudiant
nip: le code nip d'un étudiant
ine : le code ine d'un étudiant
Exemple de résultat: Exemple de résultat:
[ [
@ -47,30 +43,24 @@ def absences(etudid: int = None, nip: int = None, ine: int = None):
} }
] ]
""" """
if etudid is None: etud = Identite.query.get(etudid)
# Récupération de l'étudiant
etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine)
if etud is None: if etud is None:
return error_response( return error_response(
409, 404,
message="La requête ne peut être traitée en létat actuel.\n " message="id de l'étudiant (etudid, nip, ine) inconnu",
"Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide",
) )
etudid = etud.etudid # Absences de l'étudiant
# Récupération des absences de l'étudiant
ndb.open_db_connection() ndb.open_db_connection()
absences = sco_abs.list_abs_date(etudid) absences = sco_abs.list_abs_date(etud.id)
for absence in absences: for absence in absences:
absence["jour"] = absence["jour"].isoformat() absence["jour"] = absence["jour"].isoformat()
return jsonify(absences) return jsonify(absences)
@bp.route("/absences/etudid/<int:etudid>/just", methods=["GET"]) @bp.route("/absences/etudid/<int:etudid>/just", methods=["GET"])
@bp.route("/absences/nip/<int:nip>/just", methods=["GET"]) @token_auth.login_required
@bp.route("/absences/ine/<int:ine>/just", methods=["GET"])
@token_permission_required(Permission.APIView) @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é Retourne la liste des absences justifiées d'un étudiant donné
@ -100,55 +90,56 @@ def absences_just(etudid: int = None, nip: int = None, ine: int = None):
} }
] ]
""" """
if etudid is None: etud = Identite.query.get(etudid)
# Récupération de l'étudiant if etud is None:
try:
etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine)
etudid = etu.etudid
except AttributeError:
return error_response( return error_response(
409, 404,
message="La requête ne peut être traitée en létat actuel.\n " message="id de l'étudiant (etudid, nip, ine) inconnu",
"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 justifiées de l'étudiant
absences = sco_abs.list_abs_date(etudid) abs_just = [
for absence in [absence for absence in absences if absence["estjust"]]: absence for absence in sco_abs.list_abs_date(etud.id) if absence["estjust"]
]
for absence in abs_just:
absence["jour"] = absence["jour"].isoformat() absence["jour"] = absence["jour"].isoformat()
return jsonify(absences) return jsonify(abs_just)
@bp.route( # XXX TODO INACHEVEE
"/absences/abs_group_etat/<int:group_id>", # @bp.route(
methods=["GET"], # "/absences/abs_group_etat/<int:group_id>",
) # methods=["GET"],
@bp.route( # )
"/absences/abs_group_etat/group_id/<int:group_id>/date_debut/<string:date_debut>/date_fin/<string:date_fin>", # @bp.route(
methods=["GET"], # "/absences/abs_group_etat/group_id/<int:group_id>/date_debut/<string:date_debut>/date_fin/<string:date_fin>",
) # methods=["GET"],
@token_permission_required(Permission.APIView) # )
def abs_groupe_etat( # XXX A REVOIR XXX # @token_auth.login_required
group_id: int, date_debut, date_fin, with_boursier=True, format="html" # @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() # Liste des absences d'un ou plusieurs groupes entre deux dates
# """
# return error_response(501, message="Not implemented")
try: # # Fonction utilisée : app.scodoc.sco_groups.get_group_members() et app.scodoc.sco_abs.list_abs_date()
# Utilisation de la fonction get_group_members
members = get_group_members(group_id)
except ValueError:
return error_response(
409, message="La requête ne peut être traitée en létat actuel"
)
data = [] # try:
# Filtre entre les deux dates renseignées # # Utilisation de la fonction get_group_members
for member in members: # members = get_group_members(group_id)
abs = sco_abs.list_abs_date(member.id, date_debut, date_fin) # except ValueError:
data.append(abs) # return error_response(
# 404, message="La requête ne peut être traitée en létat actuel"
# )
# return jsonify(data) # XXX TODO faire en sorte de pouvoir renvoyer sa (ex to_dict() dans absences) # data = []
return error_response(501, message="Not implemented") # # 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")

View File

@ -30,6 +30,8 @@ from functools import wraps
from flask import abort from flask import abort
from flask import g from flask import g
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
from app import log
from app.auth.models import User from app.auth.models import User
from app.api.errors import error_response from app.api.errors import error_response
@ -39,19 +41,23 @@ token_auth = HTTPTokenAuth()
@basic_auth.verify_password @basic_auth.verify_password
def verify_password(username, password): def verify_password(username, password):
"Verify password for this user"
user = User.query.filter_by(user_name=username).first() user = User.query.filter_by(user_name=username).first()
if user and user.check_password(password): if user and user.check_password(password):
g.current_user = user g.current_user = user
# note: est aussi basic_auth.current_user()
return user return user
@basic_auth.error_handler @basic_auth.error_handler
def basic_auth_error(status): def basic_auth_error(status):
"error response (401 for invalid auth.)"
return error_response(status) return error_response(status)
@token_auth.verify_token @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 user = User.check_token(token) if token else None
g.current_user = user g.current_user = user
return user return user
@ -59,6 +65,7 @@ def verify_token(token):
@token_auth.error_handler @token_auth.error_handler
def token_auth_error(status): def token_auth_error(status):
"rréponse en cas d'erreur d'auth."
return error_response(status) return error_response(status)
@ -68,16 +75,22 @@ def get_user_roles(user):
def token_permission_required(permission): def token_permission_required(permission):
"Décorateur pour les fontions de l'API ScoDoc"
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
scodoc_dept = getattr(g, "scodoc_dept", None) # token_auth.login_required()
if hasattr(g, "current_user") and not g.current_user.has_permission( current_user = basic_auth.current_user()
permission, scodoc_dept 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) abort(403)
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated_function # login_required(decorated_function) # return decorated_function(token_auth.login_required())
return decorated_function
return decorator return decorator

View File

@ -1,55 +1,66 @@
############################################### Departements ########################################################## ############################################### 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 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) @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/<string:dept_ident>", 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 : Exemple de résultat :
[
{ {
"id": 1, "id": 1,
"acronym": "TAPI", "acronym": "TAPI",
"description": null, "description": null,
"visible": true, "visible": true,
"date_creation": "Fri, 15 Apr 2022 12:19:28 GMT" "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 dept = get_departement(dept_ident)
depts = models.Departement.query.all() return jsonify(dept.to_dict())
# Mise en place de la liste avec tous les départements
data = [d.to_dict() for d in depts]
return jsonify(data)
@bp.route("/departements/<string:dept>/etudiants/liste", methods=["GET"]) @bp.route("/departements", methods=["GET"])
@bp.route( @token_auth.login_required
"/departements/<string:dept>/etudiants/liste/<int:formsemestre_id>", methods=["GET"]
)
@token_permission_required(Permission.APIView) @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/<string:dept_ident>/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 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 : Exemple de résultat :
[ [
{ {
"civilite": "X", "civilite": "M",
"code_ine": null, "ine": "7899X61616",
"code_nip": null, "nip": "F6777H88",
"date_naissance": null, "date_naissance": null,
"email": null, "email": "toto@toto.fr",
"emailperso": null, "emailperso": null,
"etudid": 18, "etudid": 18,
"nom": "MOREL", "nom": "MOREL",
"prenom": "JACQUES" "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é # Le département, spécifié par un id ou un acronyme
if formsemestre_id is not None: dept = get_departement(dept_ident)
# 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
# Si le formsemestre_id n'a pas été renseigné return jsonify([etud.to_dict_short() for etud in dept.etudiants])
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)
@bp.route("/departements/<string:dept>/semestres_courants", methods=["GET"]) @bp.route("/departement/<string:dept_ident>/formsemestres_ids", methods=["GET"])
@token_auth.login_required
@token_permission_required(Permission.APIView) @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/<string:dept_ident>/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é 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 # Le département, spécifié par un id ou un acronyme
dept = models.Departement.query.filter_by(acronym=dept).first_or_404() dept = get_departement(dept_ident)
# Récupération des semestres suivant id_dept # Les semestres en cours de ce département
semestres = models.FormSemestre.query.filter_by(dept_id=dept.id, etat=True) formsemestres = models.FormSemestre.query.filter(
FormSemestre.dept_id == dept.id,
# Mise en forme des données FormSemestre.date_debut <= app.db.func.now(),
data = [d.to_dict() for d in semestres] FormSemestre.date_fin >= app.db.func.now(),
return jsonify(data)
@bp.route(
"/departements/<string:dept>/formations/<int:formation_id>/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 return jsonify([d.to_dict() for d in formsemestres])
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)

View File

@ -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 from flask import jsonify
import app import app
from app import models
from app.api import bp from app.api import bp
from app.api.errors import error_response 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.api.tools import get_etu_from_etudid_or_nip_or_ine from app.models import Departement, FormSemestreInscription, FormSemestre, Identite
from app.models import FormSemestreInscription, FormSemestre, Identite
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc.sco_permissions import Permission 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", defaults={"long": False})
@bp.route("/etudiants/courant/long", defaults={"long": True}) @bp.route("/etudiants/courant/long", defaults={"long": True})
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def etudiants_courant(long=False): 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 : Exemple de résultat :
[ [
@ -40,7 +47,6 @@ def etudiants_courant(long=False):
... ...
] ]
""" """
# Récupération de tous les étudiants
etuds = Identite.query.filter( etuds = Identite.query.filter(
Identite.id == FormSemestreInscription.etudid, Identite.id == FormSemestreInscription.etudid,
FormSemestreInscription.formsemestre_id == FormSemestre.id, FormSemestreInscription.formsemestre_id == FormSemestre.id,
@ -51,21 +57,24 @@ def etudiants_courant(long=False):
data = [etud.to_dict_bul(include_urls=False) for etud in etuds] data = [etud.to_dict_bul(include_urls=False) for etud in etuds]
else: else:
data = [etud.to_dict_short() for etud in etuds] data = [etud.to_dict_short() for etud in etuds]
print(jsonify(data))
return jsonify(data) return jsonify(data)
@bp.route("/etudiant/etudid/<int:etudid>", methods=["GET"]) @bp.route("/etudiant/etudid/<int:etudid>", methods=["GET"])
@bp.route("/etudiant/nip/<int:nip>", methods=["GET"]) @bp.route("/etudiant/nip/<string:nip>", methods=["GET"])
@bp.route("/etudiant/ine/<int:ine>", methods=["GET"]) @bp.route("/etudiant/ine/<string:ine>", methods=["GET"])
@token_auth.login_required
@token_permission_required(Permission.APIView) @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 etudid : l'etudid de l'étudiant
nip : le code nip d'un étudiant nip : le code nip de l'étudiant
ine : le code ine d'un é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 : Exemple de résultat :
{ {
@ -95,26 +104,74 @@ def etudiant(etudid: int = None, nip: int = None, ine: int = None):
"description": "" "description": ""
} }
""" """
# Récupération de l'étudiant if etudid is not None:
etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) 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 if etud is None:
data = etud.to_dict_bul(include_urls=False) return error_response(
404,
message="étudiant inconnu",
)
return jsonify(data) return jsonify(etud.to_dict_bul(include_urls=False))
@bp.route("/etudiants/etudid/<int:etudid>", methods=["GET"])
@bp.route("/etudiants/nip/<string:nip>", methods=["GET"])
@bp.route("/etudiants/ine/<string: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/<int:etudid>/formsemestres") @bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
@bp.route("/etudiant/nip/<int:nip>/formsemestres") @bp.route("/etudiant/nip/<string:nip>/formsemestres")
@bp.route("/etudiant/ine/<int:ine>/formsemestres") @bp.route("/etudiant/ine/<string:ine>/formsemestres")
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None): 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. Liste des semestres qu'un étudiant a suivi, triés par ordre chronologique.
Accès par etudid, nip ou ine
etudid : l'etudid d'un étudiant
nip : le code nip d'un étudiant
ine : le code ine d'un étudiant
Exemple de résultat : Exemple de résultat :
[ [
@ -148,13 +205,30 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
... ...
] ]
""" """
# Récupération de l'étudiant if etudid is not None:
etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) 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( formsemestres = query.order_by(FormSemestre.date_debut)
models.FormSemestreInscription.etudid == etud.id,
models.FormSemestreInscription.formsemestre_id == models.FormSemestre.id,
).order_by(models.FormSemestre.date_debut)
return jsonify([formsemestre.to_dict() for formsemestre in formsemestres]) 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( @bp.route(
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin", "/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin",
methods=["GET"], methods=["GET"],
defaults={"version": "long"},
) )
@bp.route( @bp.route(
"/etudiant/nip/<int:nip>/formsemestre/<int:formsemestre_id>/bulletin", "/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin",
methods=["GET"], methods=["GET"],
defaults={"version": "long"},
) )
@bp.route( @bp.route(
"/etudiant/ine/<int:ine>/formsemestre/<int:formsemestre_id>/bulletin", "/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin",
methods=["GET"], methods=["GET"],
defaults={"version": "long"},
) )
@bp.route(
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short",
methods=["GET"],
defaults={"version": "short"},
)
@bp.route(
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/short",
methods=["GET"],
defaults={"version": "short"},
)
@bp.route(
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/short",
methods=["GET"],
defaults={"version": "short"},
)
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def etudiant_bulletin_semestre( 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é Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné
@ -214,13 +311,13 @@ def etudiant_bulletin_semestre(
"villedomicile": "", "villedomicile": "",
"telephone": "", "telephone": "",
"fax": "", "fax": "",
"description": "" "description": "",
}, },
"formation": { "formation": {
"id": 1, "id": 1,
"acronyme": "BUT R&amp;T", "acronyme": "BUT R&amp;T",
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications", "titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
"titre": "BUT R&amp;T" "titre": "BUT R&amp;T",
}, },
"formsemestre_id": 1, "formsemestre_id": 1,
"etat_inscription": "I", "etat_inscription": "I",
@ -243,7 +340,7 @@ def etudiant_bulletin_semestre(
"show_temporary": true, "show_temporary": true,
"temporary_txt": "Provisoire", "temporary_txt": "Provisoire",
"show_uevalid": true, "show_uevalid": true,
"show_date_inscr": true "show_date_inscr": true,
}, },
"ressources": { "ressources": {
"R101": { "R101": {
@ -267,11 +364,11 @@ def etudiant_bulletin_semestre(
"value": "12.00", "value": "12.00",
"min": "00.00", "min": "00.00",
"max": "18.00", "max": "18.00",
"moy": "10.88" "moy": "10.88",
}, },
"url": "/ScoDoc/TAPI/Scolarite/Notes/evaluation_listenotes?evaluation_id=1" "url": "/ScoDoc/TAPI/Scolarite/Notes/evaluation_listenotes?evaluation_id=1",
} }
] ],
}, },
}, },
"saes": { "saes": {
@ -281,7 +378,7 @@ def etudiant_bulletin_semestre(
"code_apogee": null, "code_apogee": null,
"url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=2", "url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=2",
"moyenne": {}, "moyenne": {},
"evaluations": [] "evaluations": [],
}, },
}, },
"ues": { "ues": {
@ -298,29 +395,18 @@ def etudiant_bulletin_semestre(
"max": "16.50", "max": "16.50",
"moy": "11.31", "moy": "11.31",
"rang": "12", "rang": "12",
"total": 16 "total": 16,
}, },
"bonus": "00.00", "bonus": "00.00",
"malus": "00.00", "malus": "00.00",
"capitalise": null, "capitalise": null,
"ressources": { "ressources": {
"R101": { "R101": {"id": 1, "coef": 12.0, "moyenne": "12.00"},
"id": 1,
"coef": 12.0,
"moyenne": "12.00"
},
}, },
"saes": { "saes": {
"SAE11": { "SAE11": {"id": 2, "coef": 16.0, "moyenne": "~"},
"id": 2,
"coef": 16.0,
"moyenne": "~"
}, },
}, "ECTS": {"acquis": 0.0, "total": 12.0},
"ECTS": {
"acquis": 0.0,
"total": 12.0
}
}, },
"semestre": { "semestre": {
"etapes": [], "etapes": [],
@ -330,64 +416,61 @@ def etudiant_bulletin_semestre(
"numero": 1, "numero": 1,
"inscription": "", "inscription": "",
"groupes": [], "groupes": [],
"absences": { "absences": {"injustifie": 1, "total": 2},
"injustifie": 1, "ECTS": {"acquis": 0, "total": 30.0},
"total": 2 "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( formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
id=formsemestre_id dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
).first_or_404()
dept = models.Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() if etudid is not None:
query = Identite.query.filter_by(id=etudid)
app.set_sco_dept(dept.acronym) elif nip is not None:
query = Identite.query.filter_by(code_nip=nip, dept_id=dept.id)
# Récupération de l'étudiant elif ine is not None:
try: query = Identite.query.filter_by(code_ine=ine, dept_id=dept.id)
etu = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) else:
except AttributeError:
return error_response( return error_response(
409, 404,
message="La requête ne peut être traitée en létat actuel.\n " message="parametre manquant",
"Veuillez vérifier que l'id de l'étudiant (etudid, nip, ine) est valide",
) )
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( @bp.route(
"/etudiant/etudid/<int:etudid>/semestre/<int:formsemestre_id>/groups", "/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups",
methods=["GET"], methods=["GET"],
) )
@bp.route( @bp.route(
"/etudiant/nip/<int:nip>/semestre/<int:formsemestre_id>/groups", methods=["GET"] "/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/groups",
methods=["GET"],
) )
@bp.route( @bp.route(
"/etudiant/ine/<int:ine>/semestre/<int:formsemestre_id>/groups", methods=["GET"] "/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/groups",
methods=["GET"],
) )
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def etudiant_groups( def etudiant_groups(
formsemestre_id: int, etudid: int = None, nip: int = None, ine: int = None 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 formsemestre_id : l'id d'un formsemestre
etudid : l'etudid d'un étudiant etudid : l'etudid d'un étudiant
@ -420,20 +503,32 @@ def etudiant_groups(
} }
] ]
""" """
if etudid is None:
etud = get_etu_from_etudid_or_nip_or_ine(etudid, nip, ine) 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: if etud is None:
return error_response( return error_response(
409, 404,
message="La requête ne peut être traitée en létat actuel.\n " message="etudiant inconnu",
"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)
app.set_sco_dept(dept.acronym) 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) return jsonify(data)

View File

@ -4,14 +4,16 @@ from flask import jsonify
import app import app
from app import models from app import models
from app.models import Evaluation
from app.api import bp 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.api.errors import error_response
from app.scodoc.sco_evaluation_db import do_evaluation_get_all_notes from app.scodoc.sco_evaluation_db import do_evaluation_get_all_notes
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@bp.route("/evaluations/<int:moduleimpl_id>", methods=["GET"]) @bp.route("/evaluations/<int:moduleimpl_id>", methods=["GET"])
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def evaluations(moduleimpl_id: int): def evaluations(moduleimpl_id: int):
""" """
@ -45,7 +47,7 @@ def evaluations(moduleimpl_id: int):
] ]
""" """
# Récupération de toutes les évaluations # 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 # Mise en forme des données
data = [d.to_dict() for d in evals] data = [d.to_dict() for d in evals]
@ -53,7 +55,8 @@ def evaluations(moduleimpl_id: int):
return jsonify(data) return jsonify(data)
@bp.route("/evaluations/eval_notes/<int:evaluation_id>", methods=["GET"]) @bp.route("/evaluation/eval_notes/<int:evaluation_id>", methods=["GET"])
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def evaluation_notes(evaluation_id: int): 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() # 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() evaluation = models.Evaluation.query.filter_by(id=evaluation_id).first_or_404()
dept = models.Departement.query.filter_by(
moduleimpl = models.ModuleImpl.query.filter_by(id=eval.moduleimpl_id).first_or_404() id=evaluation.moduleimpl.formsemestre.dept_id
).first()
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()
app.set_sco_dept(dept.acronym) app.set_sco_dept(dept.acronym)
try: try:
# Utilisation de la fonction do_evaluation_get_all_notes # Utilisation de la fonction do_evaluation_get_all_notes
data = do_evaluation_get_all_notes(evaluation_id) data = do_evaluation_get_all_notes(evaluation_id)
except AttributeError: except AttributeError: # ???
return error_response( return error_response(
409, 404,
message="La requête ne peut être traitée en létat actuel. \n" message="La requête ne peut être traitée en létat actuel.",
"Veillez vérifier la conformité du 'evaluation_id'",
) )
return jsonify(data) return jsonify(data)

View File

@ -1,50 +1,38 @@
##############################################" Formations ############################################################ ##############################################" Formations ############################################################
from flask import jsonify from flask import jsonify
import app
from app import models from app import models
from app.api import bp from app.api import bp
from app.api.errors import error_response 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_formations import formation_export from app.models.formations import Formation
from app.scodoc import sco_formations
from app.scodoc.sco_permissions import Permission 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) @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 : Exemple de résultat : [ 17, 99, 32 ]
[
{
"id": 1,
"acronyme": "BUT R&amp;T",
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
"formation_code": "V1RET",
"code_specialite": null,
"dept_id": 1,
"titre": "BUT R&amp;T",
"version": 1,
"type_parcours": 700,
"referentiel_competence_id": null,
"formation_id": 1
},
...
]
""" """
# Récupération de toutes les formations # Récupération de toutes les formations
list_formations = models.Formation.query.all() list_formations = models.Formation.query.all()
# Mise en forme des données # 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) return jsonify(data)
@bp.route("/formations/<int:formation_id>", methods=["GET"]) @bp.route("/formation/<int:formation_id>", methods=["GET"])
@token_auth.login_required
@token_permission_required(Permission.APIView) @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é 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 # 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 # Mise en forme des données
data = forma.to_dict() data = formation.to_dict()
return jsonify(data) return jsonify(data)
@bp.route("/formations/formation_export/<int:formation_id>", methods=["GET"]) @bp.route(
"/formation/formation_export/<int:formation_id>",
methods=["GET"],
defaults={"export_ids": False},
)
@bp.route(
"/formation/formation_export/<int:formation_id>/with_ids",
methods=["GET"],
defaults={"export_ids": True},
)
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def formation_export_by_formation_id(formation_id: int, export_ids=False): 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: try:
# Utilisation de la fonction formation_export # Utilisation de la fonction formation_export
data = formation_export(formation_id, export_ids) data = sco_formations.formation_export(formation_id, export_ids)
except ValueError: except ValueError:
return error_response( return error_response(500, message="Erreur inconnue")
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("/formations/moduleimpl/<int:moduleimpl_id>", methods=["GET"]) @bp.route("/formation/moduleimpl/<int:moduleimpl_id>", methods=["GET"])
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def moduleimpl(moduleimpl_id: int): def moduleimpl(moduleimpl_id: int):
""" """
@ -198,7 +194,6 @@ def moduleimpl(moduleimpl_id: int):
{ {
"id": 1, "id": 1,
"formsemestre_id": 1, "formsemestre_id": 1,
"computation_expr": null,
"module_id": 1, "module_id": 1,
"responsable_id": 2, "responsable_id": 2,
"moduleimpl_id": 1, "moduleimpl_id": 1,
@ -224,65 +219,26 @@ def moduleimpl(moduleimpl_id: int):
} }
} }
""" """
# Récupération des tous les moduleimpl modimpl = models.ModuleImpl.query.filter_by(id=moduleimpl_id).first_or_404()
moduleimpl = models.ModuleImpl.query.filter_by(id=moduleimpl_id).first_or_404() data = modimpl.to_dict()
# Mise en forme des données
data = moduleimpl.to_dict()
return jsonify(data) return jsonify(data)
@bp.route( @bp.route(
"/formations/moduleimpl/formsemestre/<int:formsemestre_id>/liste", "/formation/<int:formation_id>/referentiel_competences",
methods=["GET"], methods=["GET"],
) )
@token_auth.login_required
@token_permission_required(Permission.APIView) @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 return json, ou null si pas de référentiel associé.
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": []
},
...
]
""" """
formsemestre = models.FormSemestre.query.filter_by( formation = models.Formation.query.filter_by(id=formation_id).first_or_404()
id=formsemestre_id
).first_or_404()
moduleimpls = formsemestre.modimpls_sorted if formation.referentiel_competence is None:
return jsonify(None)
data = [moduleimpl.to_dict() for moduleimpl in moduleimpls] return jsonify(formation.referentiel_competence.to_dict())
return jsonify(data)

View File

@ -4,99 +4,80 @@ from flask import jsonify
import app import app
from app import models from app import models
from app.api import bp from app.api import bp
from app.api.errors import error_response from app.api.auth import token_auth, token_permission_required
from app.api.auth import token_permission_required from app.models import Departement, FormSemestre, FormSemestreEtape
from app.api.tools import get_etu_from_etudid_or_nip_or_ine
from app.models import FormSemestre, FormSemestreEtape
from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json 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_permissions import Permission
from app.scodoc.sco_pvjury import formsemestre_pvjury from app.scodoc.sco_utils import ModuleType
@bp.route("/formsemestre/<int:formsemestre_id>", methods=["GET"]) @bp.route("/formsemestre/<int:formsemestre_id>", methods=["GET"])
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def formsemestre(formsemestre_id: int): 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 : 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", "date_fin": "31/08/2022",
"resp_can_edit": false,
"dept_id": 1, "dept_id": 1,
"elt_annee_apo": null,
"elt_sem_apo": null,
"ens_can_edit_eval": false,
"etat": true, "etat": true,
"resp_can_change_ens": true, "formation_id": 1,
"formsemestre_id": 1,
"gestion_compensation": false,
"gestion_semestrielle": false,
"id": 1, "id": 1,
"modalite": "FI", "modalite": "FI",
"ens_can_edit_eval": false, "resp_can_change_ens": true,
"formation_id": 1, "resp_can_edit": false,
"gestion_compensation": false, "responsables": [1, 99], // uids
"elt_sem_apo": null,
"semestre_id": 1,
"bul_hide_xml": false,
"elt_annee_apo": null,
"titre": "Semestre test",
"block_moyennes": false,
"scodoc7_id": null, "scodoc7_id": null,
"date_debut": "01/09/2021", "semestre_id": 1,
"gestion_semestrielle": false, "titre_formation" : "BUT GEA",
"bul_bgcolor": "white", "titre_num": "BUT GEA semestre 1",
"formsemestre_id": 1, "titre": "BUT GEA",
"titre_num": "Semestre test semestre 1",
"date_debut_iso": "2021-09-01",
"date_fin_iso": "2022-08-31",
"responsables": []
} }
""" """
# Récupération de tous les formsemestres formsemestre: FormSemestre = models.FormSemestre.query.filter_by(
formsemetre = models.FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() id=formsemestre_id
).first_or_404()
# Mise en forme des données data = formsemestre.to_dict()
data = formsemetre.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) return jsonify(data)
@bp.route("/formsemestre/apo/<string:etape_apo>", methods=["GET"]) @bp.route("/formsemestre/apo/<string:etape_apo>", methods=["GET"])
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def formsemestre_apo(etape_apo: str): 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 : Exemple de résultat :
{ [
"date_fin": "31/08/2022", { ...formsemestre...
"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": []
}
""" """
formsemestres = FormSemestre.query.filter( formsemestres = FormSemestre.query.filter(
FormSemestreEtape.etape_apo == etape_apo, FormSemestreEtape.etape_apo == etape_apo,
@ -106,173 +87,8 @@ def formsemestre_apo(etape_apo: str):
return jsonify([formsemestre.to_dict() for formsemestre in formsemestres]) return jsonify([formsemestre.to_dict() for formsemestre in formsemestres])
@bp.route(
"/formsemestre/<int:formsemestre_id>/etudiant/etudid/<int:etudid>/bulletin",
methods=["GET"],
)
@bp.route(
"/formsemestre/<int:formsemestre_id>/etudiant/nip/<int:nip>/bulletin",
methods=["GET"],
)
@bp.route(
"/formsemestre/<int:formsemestre_id>/etudiant/ine/<int: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&amp;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/<int:formsemestre_id>/bulletins", methods=["GET"]) @bp.route("/formsemestre/<int:formsemestre_id>/bulletins", methods=["GET"])
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def bulletins(formsemestre_id: int): 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( formsemestre = models.FormSemestre.query.filter_by(
id=formsemestre_id id=formsemestre_id
).first_or_404() ).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)
etuds = formsemestre.etuds
data = [] data = []
for etu in etuds: for etu in formsemestre.etuds:
bul_etu = get_formsemestre_bulletin_etud_json(formsemestre, etu) bul_etu = get_formsemestre_bulletin_etud_json(formsemestre, etu)
data.append(bul_etu.json) data.append(bul_etu.json)
return jsonify(data) return jsonify(data)
@bp.route("/formsemestre/<int:formsemestre_id>/jury", methods=["GET"]) # XXX Attendre ScoDoc 9.3
@token_permission_required(Permission.APIView) # @bp.route("/formsemestre/<int:formsemestre_id>/jury", methods=["GET"])
def jury(formsemestre_id: int): # @token_auth.login_required
""" # @token_permission_required(Permission.APIView)
Retourne le récapitulatif des décisions jury # 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( # formsemestre = models.FormSemestre.query.filter_by(
id=formsemestre_id # id=formsemestre_id
).first_or_404() # ).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)
# try:
# # Utilisation de la fonction formsemestre_pvjury
# data = formsemestre_pvjury(formsemestre_id) # 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) # # 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)
@bp.route( @bp.route(
"/formsemestre/<int:formsemestre_id>/programme", "/formsemestre/<int:formsemestre_id>/programme",
methods=["GET"], methods=["GET"],
) )
@token_auth.login_required
@token_permission_required(Permission.APIView) @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 Retourne la liste des Ues, ressources et SAE d'un semestre
dept : l'acronym d'un département formsemestre_id : l'id d'un formsemestre
formsemestre_id : l'id d'un formesemestre
Exemple de résultat : Exemple de résultat :
{ {
@ -543,78 +355,61 @@ def semestre_index(formsemestre_id: int):
], ],
"ressources": [ "ressources": [
{ {
"titre": "Fondamentaux de la programmation", "ens": [ 10, 18 ],
"formsemestre_id": 1,
"id": 15,
"module": {
"abbrev": "Programmer",
"code": "SAE15",
"code_apogee": "V7GOP",
"coefficient": 1.0, "coefficient": 1.0,
"module_type": 2,
"id": 17,
"ects": null,
"abbrev": null,
"ue_id": 3,
"code": "R107",
"formation_id": 1, "formation_id": 1,
"heures_cours": 0.0, "heures_cours": 0.0,
"matiere_id": 3,
"heures_td": 0.0, "heures_td": 0.0,
"semestre_id": 1,
"heures_tp": 0.0, "heures_tp": 0.0,
"numero": 70, "id": 15,
"code_apogee": "", "matiere_id": 3,
"module_id": 17 "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": [ "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: FormSemestre = models.FormSemestre.query.filter_by(
formsemestre = models.FormSemestre.query.filter_by(
id=formsemestre_id id=formsemestre_id
).first_or_404() ).first_or_404()
ues = formsemestre.query_ues() ues = formsemestre.query_ues()
m_list = {
ues_dict = [] ModuleType.RESSOURCE: [],
ressources = [] ModuleType.SAE: [],
saes = [] ModuleType.STANDARD: [],
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,
} }
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],
}
)

View File

@ -1,41 +1,41 @@
#################################################### Jury ############################################################# #################################################### Jury #############################################################
from flask import jsonify # from flask import jsonify
from app import models # from app import models
from app.api import bp # from app.api import bp
from app.api.errors import error_response # 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_prepajury import feuille_preparation_jury # from app.scodoc.sco_prepajury import feuille_preparation_jury
from app.scodoc.sco_pvjury import formsemestre_pvjury # from app.scodoc.sco_pvjury import formsemestre_pvjury
@bp.route("/jury/formsemestre/<int:formsemestre_id>/preparation_jury", methods=["GET"]) # # @bp.route("/jury/formsemestre/<int:formsemestre_id>/preparation_jury", methods=["GET"])
# @token_permission_required(Permission.?) # # @token_permission_required(Permission.?)
def jury_preparation(formsemestre_id: int): # def jury_preparation(formsemestre_id: int):
""" # """
Retourne la feuille de préparation du jury # Retourne la feuille de préparation du jury
formsemestre_id : l'id d'un formsemestre # formsemestre_id : l'id d'un formsemestre
""" # """
# Fonction utilisée : app.scodoc.sco_prepajury.feuille_preparation_jury() # # Fonction utilisée : app.scodoc.sco_prepajury.feuille_preparation_jury()
# Utilisation de la fonction feuille_preparation_jury # # Utilisation de la fonction feuille_preparation_jury
prepa_jury = feuille_preparation_jury(formsemestre_id) # 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/<int:formsemestre_id>/decisions_jury", methods=["GET"]) # # @bp.route("/jury/formsemestre/<int:formsemestre_id>/decisions_jury", methods=["GET"])
# @token_permission_required(Permission.?) # # @token_permission_required(Permission.?)
def jury_decisions(formsemestre_id: int): # def jury_decisions(formsemestre_id: int):
""" # """
Retourne les décisions du jury suivant un formsemestre donné # Retourne les décisions du jury suivant un formsemestre donné
formsemestre_id : l'id d'un formsemestre # formsemestre_id : l'id d'un formsemestre
""" # """
# Fonction utilisée : app.scodoc.sco_pvjury.formsemestre_pvjury() # # Fonction utilisée : app.scodoc.sco_pvjury.formsemestre_pvjury()
# Utilisation de la fonction formsemestre_pvjury # # Utilisation de la fonction formsemestre_pvjury
decision_jury = formsemestre_pvjury(formsemestre_id) # decision_jury = formsemestre_pvjury(formsemestre_id)
return error_response(501, message="Not implemented") # return error_response(501, message="Not implemented")

View File

@ -36,7 +36,6 @@ from app.api import bp
from app.api import requested_format from app.api import requested_format
from app.api.auth import token_auth from app.api.auth import token_auth
from app.api.errors import error_response from app.api.errors import error_response
from app.decorators import permission_required
from app.models import Departement from app.models import Departement
from app.scodoc.sco_logos import list_logos, find_logo from app.scodoc.sco_logos import list_logos, find_logo
from app.api.auth import token_auth, token_permission_required 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"]) @bp.route("/logos", methods=["GET"])
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def api_get_glob_logos(): def api_get_glob_logos():
if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): if not g.current_user.has_permission(Permission.ScoSuperAdmin, None):
@ -56,6 +56,7 @@ def api_get_glob_logos():
@bp.route("/logos/<string:logoname>", methods=["GET"]) @bp.route("/logos/<string:logoname>", methods=["GET"])
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def api_get_glob_logo(logoname): def api_get_glob_logo(logoname):
if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): if not g.current_user.has_permission(Permission.ScoSuperAdmin, None):
@ -72,6 +73,7 @@ def api_get_glob_logo(logoname):
@bp.route("/departements/<string:departement>/logos", methods=["GET"]) @bp.route("/departements/<string:departement>/logos", methods=["GET"])
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def api_get_local_logos(departement): def api_get_local_logos(departement):
dept_id = Departement.from_acronym(departement).id dept_id = Departement.from_acronym(departement).id
@ -82,6 +84,7 @@ def api_get_local_logos(departement):
@bp.route("/departements/<string:departement>/logos/<string:logoname>", methods=["GET"]) @bp.route("/departements/<string:departement>/logos/<string:logoname>", methods=["GET"])
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def api_get_local_logo(departement, logoname): def api_get_local_logo(departement, logoname):
# format = requested_format("jpg", ['png', 'jpg']) XXX ? # format = requested_format("jpg", ['png', 'jpg']) XXX ?

View File

@ -5,12 +5,13 @@ from app import models
from app.api import bp from app.api import bp
from app.api.errors import error_response 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_groups import get_group_members, setGroups, get_partitions_list
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@bp.route("/partitions/<int:formsemestre_id>", methods=["GET"]) @bp.route("/partitions/<int:formsemestre_id>", methods=["GET"])
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def partition(formsemestre_id: int): def partition(formsemestre_id: int):
""" """
@ -53,6 +54,7 @@ def partition(formsemestre_id: int):
@bp.route("/partitions/groups/<int:group_id>", methods=["GET"]) @bp.route("/partitions/groups/<int:group_id>", methods=["GET"])
@bp.route("/partitions/groups/<int:group_id>/etat/<string:etat>", methods=["GET"]) @bp.route("/partitions/groups/<int:group_id>/etat/<string:etat>", methods=["GET"])
@token_auth.login_required
@token_permission_required(Permission.APIView) @token_permission_required(Permission.APIView)
def etud_in_group(group_id: int, etat=None): 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) data = get_group_members(group_id, etat)
if len(data) == 0: if len(data) == 0:
return error_response( return error_response(404, message="group_id inconnu")
409,
message="La requête ne peut être traitée en létat actuel. \n"
"Aucun groupe ne correspond au 'group_id' renseigné",
)
return jsonify(data) return jsonify(data)
@ -125,6 +123,7 @@ def etud_in_group(group_id: int, etat=None):
"/create/<string:groups_to_create>", "/create/<string:groups_to_create>",
methods=["POST"], methods=["POST"],
) )
@token_auth.login_required
@token_permission_required(Permission.APIEtudChangeGroups) @token_permission_required(Permission.APIEtudChangeGroups)
def set_groups( def set_groups(
partition_id: int, groups_lists: str, groups_to_delete: str, groups_to_create: str 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) setGroups(partition_id, groups_lists, groups_to_create, groups_to_delete)
return error_response(200, message="Groups set") return error_response(200, message="Groups set")
except ValueError: except ValueError:
return error_response( return error_response(404, message="Erreur")
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",
)

View File

@ -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=<int:eval_id>&etudid=<int:etudid>&note=<float:note>",
# methods=["POST"],
# )
# @bp.route(
# "/evaluations/eval_set_notes?eval_id=<int:eval_id>&nip=<int:nip>&note=<float:note>",
# methods=["POST"],
# )
# @bp.route(
# "/evaluations/eval_set_notes?eval_id=<int:eval_id>&ine=<int:ine>&note=<float:note>",
# 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=<int:etudid>&date=<string:date>&matin=<string:matin>&justif=<string:justif>"
# "&description=<string:description>",
# methods=["POST"],
# )
# @bp.route(
# "/absences/abs_signale?nip=<int:nip>&date=<string:date>&matin=<string:matin>&justif=<string:justif>"
# "&description=<string:description>",
# methods=["POST"],
# )
# @bp.route(
# "/absences/abs_signale?ine=<int:ine>&date=<string:date>&matin=<string:matin>&justif=<string:justif>"
# "&description=<string:description>",
# methods=["POST"],
# )
# @bp.route(
# "/absences/abs_signale?ine=<int:ine>&date=<string:date>&matin=<string:matin>&justif=<string:justif>"
# "&description=<string:description>&moduleimpl_id=<int: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=<int:etudid>&jour=<string:jour>&matin=<string:matin>",
# methods=["POST"],
# )
# @bp.route(
# "/absences/abs_annule_justif?nip=<int:nip>&jour=<string:jour>&matin=<string:matin>",
# methods=["POST"],
# )
# @bp.route(
# "/absences/abs_annule_justif?ine=<int:ine>&jour=<string:jour>&matin=<string: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=<int:etudid>&formsemestre_id=<int:formesemestre_id>"
# "&jury=<string:decision_jury>&devenir=<string:devenir_jury>&assiduite=<bool>",
# methods=["POST"],
# )
# @bp.route(
# "/jury/set_decision/nip?etudid=<int:etudid>&formsemestre_id=<int:formesemestre_id>"
# "&jury=<string:decision_jury>&devenir=<string:devenir_jury>&assiduite=<bool>",
# methods=["POST"],
# )
# @bp.route(
# "/jury/set_decision/ine?etudid=<int:etudid>&formsemestre_id=<int:formesemestre_id>"
# "&jury=<string:decision_jury>&devenir=<string:devenir_jury>&assiduite=<bool>",
# 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/<int:etudid>/formsemestre/<int:formsemestre_id>/annule_decision",
# methods=["DELETE"],
# )
# @bp.route(
# "/jury/nip/<int:nip>/formsemestre/<int:formsemestre_id>/annule_decision",
# methods=["DELETE"],
# )
# @bp.route(
# "/jury/ine/<int:ine>/formsemestre/<int:formsemestre_id>/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")

View File

@ -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/<string:logo_name>", 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/<string:dept>/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/<string:dept>/logos/<string:logo_name>", 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

View File

@ -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))

View File

@ -1,5 +1,5 @@
from flask import jsonify from flask import jsonify
from app import db from app import db, log
from app.api import bp from app.api import bp
from app.api.auth import basic_auth, token_auth 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"]) @bp.route("/tokens", methods=["POST"])
@basic_auth.login_required @basic_auth.login_required
def get_token(): def get_token():
"renvoie un jeton jwt pour l'utilisateur courant"
token = basic_auth.current_user().get_token() token = basic_auth.current_user().get_token()
log(f"API: giving token to {basic_auth.current_user()}")
db.session.commit() db.session.commit()
return jsonify({"token": token}) return jsonify({"token": token})
@ -15,6 +17,7 @@ def get_token():
@bp.route("/tokens", methods=["DELETE"]) @bp.route("/tokens", methods=["DELETE"])
@token_auth.login_required @token_auth.login_required
def revoke_token(): def revoke_token():
"révoque le jeton de l'utilisateur courant"
token_auth.current_user().revoke_token() token_auth.current_user().revoke_token()
db.session.commit() db.session.commit()
return "", 204 return "", 204

View File

@ -1,15 +1,17 @@
from app import models 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 etudid : None ou un int etudid
nip : None ou un int code_nip nip : None ou un int code_nip
ine : None ou un int code_ine ine : None ou un int code_ine
Exemple de résultat: <Itendite> Return None si étudiant inexistant.
""" """
if etudid is None: if etudid is None:
if nip is None: # si ine if nip is None: # si ine

View File

@ -18,7 +18,7 @@ from werkzeug.security import generate_password_hash, check_password_hash
import jwt import jwt
from app import db, login from app import db, log, login
from app.models import Departement from app.models import Departement
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
@ -150,11 +150,22 @@ class User(UserMixin, db.Model):
def verify_reset_password_token(token): def verify_reset_password_token(token):
"Vérification du token de reéinitialisation du mot de passe" "Vérification du token de reéinitialisation du mot de passe"
try: try:
user_id = jwt.decode( token = jwt.decode(
token, current_app.config["SECRET_KEY"], algorithms=["HS256"] token, current_app.config["SECRET_KEY"], algorithms=["HS256"]
)["reset_password"] )
except jwt.exceptions.ExpiredSignatureError:
log(f"verify_reset_password_token: token expired")
except: 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) return User.query.get(user_id)
def to_dict(self, include_email=True): def to_dict(self, include_email=True):
@ -214,6 +225,7 @@ class User(UserMixin, db.Model):
self.add_role(role, dept) self.add_role(role, dept)
def get_token(self, expires_in=3600): def get_token(self, expires_in=3600):
"Un jeton pour cet user. Stocké en base, non commité."
now = datetime.utcnow() now = datetime.utcnow()
if self.token and self.token_expiration > now + timedelta(seconds=60): if self.token and self.token_expiration > now + timedelta(seconds=60):
return self.token return self.token
@ -223,6 +235,7 @@ class User(UserMixin, db.Model):
return self.token return self.token
def revoke_token(self): def revoke_token(self):
"Révoque le jeton de cet utilisateur"
self.token_expiration = datetime.utcnow() - timedelta(seconds=1) self.token_expiration = datetime.utcnow() - timedelta(seconds=1)
@staticmethod @staticmethod
@ -335,7 +348,7 @@ class User(UserMixin, db.Model):
return None return None
def get_nom_fmt(self): def get_nom_fmt(self):
"""Nom formatté: "Martin" """ """Nom formaté: "Martin" """
if self.nom: if self.nom:
return sco_etud.format_nom(self.nom, uppercase=False) return sco_etud.format_nom(self.nom, uppercase=False)
else: else:

View File

@ -71,7 +71,7 @@ def create_user():
flash("User {} created".format(user.user_name)) flash("User {} created".format(user.user_name))
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
return render_template( 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: if current_user.is_authenticated:
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
user = User.verify_reset_password_token(token) user = User.verify_reset_password_token(token)
if not user: if user is None:
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
form = ResetPasswordForm() form = ResetPasswordForm()
if form.validate_on_submit(): if form.validate_on_submit():

View File

@ -14,10 +14,12 @@ from flask import url_for, g
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, Identite from app.models import FormSemestre, Identite
from app.models.groups import GroupDescr
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc import sco_bulletins, sco_utils as scu from app.scodoc import sco_bulletins, sco_utils as scu
from app.scodoc import sco_bulletins_json from app.scodoc import sco_bulletins_json
from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_groups
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
from app.scodoc.sco_utils import fmt_note from app.scodoc.sco_utils import fmt_note
@ -64,8 +66,16 @@ class BulletinBUT:
# } # }
return d return d
def etud_ue_results(self, etud: Identite, ue: UniteEns, decision_ue: dict) -> dict: def etud_ue_results(
"dict synthèse résultats UE" 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 res = self.res
d = { d = {
@ -81,7 +91,7 @@ class BulletinBUT:
if res.bonus_ues is not None and ue.id in res.bonus_ues if res.bonus_ues is not None and ue.id in res.bonus_ues
else fmt_note(0.0), else fmt_note(0.0),
"malus": fmt_note(res.malus[ue.id][etud.id]), "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), "ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
"saes": self.etud_ue_mod_results(etud, ue, res.saes), "saes": self.etud_ue_mod_results(etud, ue, res.saes),
} }
@ -103,6 +113,17 @@ class BulletinBUT:
"moy": fmt_note(res.etud_moy_ue[ue.id].mean()), "moy": fmt_note(res.etud_moy_ue[ue.id].mean()),
"rang": rang, "rang": rang,
"total": effectif, # nb etud avec note dans cette UE "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: else:
# ceci suppose que l'on a une seule UE bonus, # ceci suppose que l'on a une seule UE bonus,
@ -275,6 +296,9 @@ class BulletinBUT:
return d return d
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id) nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
etud_groups = sco_groups.get_etud_formsemestre_groups(
etud, formsemestre, only_to_show=True
)
semestre_infos = { semestre_infos = {
"etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo], "etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo],
"date_debut": formsemestre.date_debut.isoformat(), "date_debut": formsemestre.date_debut.isoformat(),
@ -282,7 +306,7 @@ class BulletinBUT:
"annee_universitaire": formsemestre.annee_scolaire_str(), "annee_universitaire": formsemestre.annee_scolaire_str(),
"numero": formsemestre.semestre_id, "numero": formsemestre.semestre_id,
"inscription": "", # inutilisé mais nécessaire pour le js de Seb. "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"]: if self.prefs["bul_show_abs"]:
semestre_infos["absences"] = { semestre_infos["absences"] = {
@ -306,15 +330,25 @@ class BulletinBUT:
"max": fmt_note(res.etud_moy_gen.max()), "max": fmt_note(res.etud_moy_gen.max()),
} }
if self.prefs["bul_show_rangs"] and not np.isnan(res.etud_moy_gen[etud.id]): 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"] = { semestre_infos["rang"] = {
"value": res.etud_moy_gen_ranks[etud.id], "value": res.etud_moy_gen_ranks[etud.id],
"total": nb_inscrits, "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: else:
semestre_infos["rang"] = { semestre_infos["rang"] = {
"value": "-", "value": "-",
"total": nb_inscrits, "total": nb_inscrits,
"groupes": {},
} }
d.update( d.update(
{ {
@ -324,7 +358,10 @@ class BulletinBUT:
"saes": self.etud_mods_results(etud, res.saes, version=version), "saes": self.etud_mods_results(etud, res.saes, version=version),
"ues": { "ues": {
ue.acronyme: self.etud_ue_results( 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 for ue in res.ues
# si l'UE comporte des modules auxquels on est inscrit: # si l'UE comporte des modules auxquels on est inscrit:

View File

@ -127,6 +127,9 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
def ue_rows(self, rows: list, ue_acronym: str, ue: dict, title_bg: tuple): 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" "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 # 1er ligne titre UE
moy_ue = ue.get("moyenne") moy_ue = ue.get("moyenne")
t = { t = {
@ -206,7 +209,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
for mod_code, mod in ue["modules"].items(): for mod_code, mod in ue["modules"].items():
rows.append( rows.append(
{ {
"titre": f"{mod_code} {mod['titre']}", "titre": f"{mod_code or ''} {mod['titre'] or ''}",
} }
) )
self.evaluations_rows(rows, mod["evaluations"]) self.evaluations_rows(rows, mod["evaluations"])
@ -313,7 +316,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
"lignes des évaluations" "lignes des évaluations"
for e in evaluations: for e in evaluations:
t = { t = {
"titre": f"{e['description']}", "titre": f"{e['description'] or ''}",
"moyenne": e["note"]["value"], "moyenne": e["note"]["value"],
"_moyenne_pdf": Paragraph( "_moyenne_pdf": Paragraph(
f"""<para align=right>{e["note"]["value"]}</para>""" f"""<para align=right>{e["note"]["value"]}</para>"""

View File

@ -266,6 +266,8 @@ class BonusSportMultiplicatif(BonusSport):
amplitude = 0.005 # multiplie les points au dessus du seuil amplitude = 0.005 # multiplie les points au dessus du seuil
# En classique, les bonus multiplicatifs agissent par défaut sur les UE: # En classique, les bonus multiplicatifs agissent par défaut sur les UE:
classic_use_bonus_ues = True 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, # C'est un bonus "multiplicatif": on l'exprime en additif,
# sur chaque moyenne d'UE m_0 # sur chaque moyenne d'UE m_0
@ -285,6 +287,8 @@ class BonusSportMultiplicatif(BonusSport):
notes = np.nan_to_num(notes, copy=False) notes = np.nan_to_num(notes, copy=False)
factor = (notes - self.seuil_moy_gen) * self.amplitude # 5% si note=20 factor = (notes - self.seuil_moy_gen) * self.amplitude # 5% si note=20
factor[factor <= 0] = 0.0 # note < seuil_moy_gen, pas de bonus 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 # Ne s'applique qu'aux moyennes d'UE
if len(factor.shape) == 1: # classic if len(factor.shape) == 1: # classic
@ -481,6 +485,19 @@ class BonusBezier(BonusSportAdditif):
proportion_point = 0.03 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): class BonusBordeaux1(BonusSportMultiplicatif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT Bordeaux 1, """Calcul bonus modules optionnels (sport, culture), règle IUT Bordeaux 1,
sur moyenne générale et UEs. 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) 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).
<ul>
<li>Les étudiants peuvent suivre un ou plusieurs activités optionnelles notées
dans les semestres pairs.<br>
La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20.
</li>
<li>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.
</li>
<li> Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/20 = 0,3 points
sur chaque UE.
</li>
</ul>
"""
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): class BonusLaRochelle(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle. """Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle.
<ul> <ul>
<li>Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point.</li> <li>Si la note de sport est comprise entre 0 et 10 : pas dajout de point.</li>
<li>Si la note de sport est comprise entre 10 et 20 : ajout de 1% de cette <li>Si la note de sport est comprise entre 10 et 20 :
note sur la moyenne générale du semestre (ou sur les UE en BUT).</li> <ul>
<li>Pour le BUT, application pour chaque UE du semestre :
<ul>
<li>pour une note entre 18 et 20 => + 0,10 points</li>
<li>pour une note entre 16 et 17,99 => + 0,08 points</li>
<li>pour une note entre 14 et 15,99 => + 0,06 points</li>
<li>pour une note entre 12 et 13,99 => + 0,04 points</li>
<li>pour une note entre 10 et 11,99 => + 0,02 points</li>
</ul>
</li>
<li>Pour les DUT/LP :
ajout de 1% de la note sur la moyenne générale du semestre
</li>
</ul>
</li>
</ul> </ul>
""" """
name = "bonus_iutlr" name = "bonus_iutlr"
displayed_name = "IUT de La Rochelle" displayed_name = "IUT de La Rochelle"
seuil_moy_gen = 10.0 # si bonus > 10, seuil_moy_gen = 10.0 # si bonus > 10,
seuil_comptage = 0.0 # tous les points sont comptés seuil_comptage = 0.0 # tous les points sont comptés
proportion_point = 0.01 # 1% 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): class BonusLeHavre(BonusSportAdditif):
"""Bonus sport IUT du Havre sur les moyennes d'UE """Bonus sport IUT du Havre sur les moyennes d'UE
@ -908,7 +1026,7 @@ class BonusNantes(BonusSportAdditif):
class BonusPoitiers(BonusSportAdditif): class BonusPoitiers(BonusSportAdditif):
"""Calcul bonus optionnels (sport, culture), règle IUT de Poitiers. """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% bonus = (option1 - 10)*5% + (option2 - 10)*5%
""" """
@ -933,27 +1051,6 @@ class BonusRoanne(BonusSportAdditif):
proportion_point = 1 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:
<ul>
<li>Bonus = (S - 10)/20</li>
</ul>
"""
# 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): class BonusStEtienne(BonusSportAdditif):
"""IUT de Saint-Etienne. """IUT de Saint-Etienne.
@ -984,27 +1081,42 @@ class BonusStDenis(BonusSportAdditif):
bonus_max = 0.5 bonus_max = 0.5
class BonusStMalo(BonusStBrieuc): class BonusStNazaire(BonusSportMultiplicatif):
# identique à St Brieux, sauf la doc """IUT de Saint-Nazaire
"""IUT de Saint Malo
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).
<ul> <ul>
<li>Bonus = (S - 10)/20</li> <li>Chaque bonus est compris entre 0 et 20 points -> 4pt = 1%<br>
(note 4/20: 1%, 8/20: 2%, 12/20: 3%, 16/20: 4%, 20/20: 5%)
</li>
<li>Le total des 3 bonus ne peut excéder 10%</li>
<li>La somme des bonus s'applique à la moyenne de chaque UE</li>
</ul> </ul>
<p>Exemple: une moyenne d'UE de 10/20 avec un total des bonus de 6% donne
une moyenne de 10,6.</p>
<p>Les bonifications s'appliquent aussi au classement général du semestre
et de l'année.
</p>
""" """
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. """Calcul bonus optionnels (sport, culture), règle IUT de Tarbes.
<ul> <ul>
<li>Les étudiants opeuvent suivre un ou plusieurs activités optionnelles notées. <li>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. La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20.
</li> </li>
<li>Le trentième des points au dessus de 10 est ajouté à la moyenne des UE. <li>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.
</li> </li>
<li> Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/30 = 0,2 points <li> Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/30 = 0,2 points
sur chaque UE. sur chaque UE.
@ -1018,29 +1130,6 @@ class BonusTarbes(BonusSportAdditif):
proportion_point = 1 / 30.0 proportion_point = 1 / 30.0
classic_use_bonus_ues = True 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): class BonusTours(BonusDirect):
"""Calcul bonus sport & culture IUT Tours. """Calcul bonus sport & culture IUT Tours.

View File

@ -41,7 +41,8 @@ from app import db
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_codes_parcours import UE_SPORT 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 from app.scodoc.sco_utils import ModuleType
@ -423,7 +424,9 @@ def moduleimpl_is_conforme(
if nb_ues == 0: if nb_ues == 0:
return False # situation absurde (pas d'UE) return False # situation absurde (pas d'UE)
if len(modules_coefficients) != nb_ues: 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 module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0
check = all( check = all(
(modules_coefficients[moduleimpl.module_id].to_numpy() != 0) (modules_coefficients[moduleimpl.module_id].to_numpy() != 0)

View File

@ -18,7 +18,7 @@ from app.auth.models import User
from app.comp.res_cache import ResultatsCache from app.comp.res_cache import ResultatsCache
from app.comp import res_sem from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults 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 Identite
from app.models import ModuleImpl, ModuleImplInscription from app.models import ModuleImpl, ModuleImplInscription
from app.models.ues import UniteEns from app.models.ues import UniteEns
@ -70,6 +70,7 @@ class ResultatsSemestre(ResultatsCache):
self.etud_moy_gen: pd.Series = None self.etud_moy_gen: pd.Series = None
self.etud_moy_gen_ranks = {} self.etud_moy_gen_ranks = {}
self.etud_moy_gen_ranks_int = {} self.etud_moy_gen_ranks_int = {}
self.moy_gen_rangs_by_group = None # virtual
self.modimpl_inscr_df: pd.DataFrame = None self.modimpl_inscr_df: pd.DataFrame = None
"Inscriptions: row etudid, col modimlpl_id" "Inscriptions: row etudid, col modimlpl_id"
self.modimpls_results: ModuleImplResults = None self.modimpls_results: ModuleImplResults = None
@ -151,6 +152,7 @@ class ResultatsSemestre(ResultatsCache):
if m.module.module_type == scu.ModuleType.SAE if m.module.module_type == scu.ModuleType.SAE
] ]
# --- JURY...
def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]: def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]:
"""Liste des UEs du semestre qui doivent être validées """Liste des UEs du semestre qui doivent être validées
@ -396,7 +398,7 @@ class ResultatsSemestre(ResultatsCache):
- titles: { column_id : title } - titles: { column_id : title }
- columns_ids: (liste des id de colonnes) - 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: Les colonnes générées sont:
etudid etudid
rang : rang indicatif (basé sur moy gen) rang : rang indicatif (basé sur moy gen)
@ -588,7 +590,9 @@ class ResultatsSemestre(ResultatsCache):
f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
) )
val_fmt = val_fmt_html = fmt_note(val) 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 = ( val_fmt_html = (
(scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else "" (scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else ""
) )
@ -823,17 +827,25 @@ class ResultatsSemestre(ResultatsCache):
self.formsemestre.id self.formsemestre.id
) )
first_partition = True first_partition = True
col_order = 10
for partition in partitions: for partition in partitions:
cid = f"part_{partition['partition_id']}" cid = f"part_{partition['partition_id']}"
rg_cid = cid + "_rg" # rang dans la partition
titles[cid] = partition["partition_name"] titles[cid] = partition["partition_name"]
if first_partition: if first_partition:
klass = "partition" klass = "partition"
else: else:
klass = "partition partition_aux" klass = "partition partition_aux"
titles[f"_{cid}_class"] = klass 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"]] partition_etud_groups = partitions_etud_groups[partition["partition_id"]]
for row in rows: for row in rows:
group = None # group (dict) de l'étudiant dans cette partition
# dans NotesTableCompat, à revoir # dans NotesTableCompat, à revoir
etud_etat = self.get_etud_etat(row["etudid"]) etud_etat = self.get_etud_etat(row["etudid"])
if etud_etat == "D": if etud_etat == "D":
@ -846,8 +858,17 @@ class ResultatsSemestre(ResultatsCache):
group = partition_etud_groups.get(row["etudid"]) group = partition_etud_groups.get(row["etudid"])
gr_name = group["group_name"] if group else "" gr_name = group["group_name"] if group else ""
if gr_name: if gr_name:
row[f"{cid}"] = gr_name row[cid] = gr_name
row[f"_{cid}_class"] = klass 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 first_partition = False
def _recap_add_evaluations( def _recap_add_evaluations(

View File

@ -35,7 +35,9 @@ class NotesTableCompat(ResultatsSemestre):
"malus", "malus",
"etud_moy_gen_ranks", "etud_moy_gen_ranks",
"etud_moy_gen_ranks_int", "etud_moy_gen_ranks_int",
"moy_gen_rangs_by_group",
"ue_rangs", "ue_rangs",
"ue_rangs_by_group",
) )
def __init__(self, formsemestre: FormSemestre): def __init__(self, formsemestre: FormSemestre):
@ -48,6 +50,8 @@ class NotesTableCompat(ResultatsSemestre):
self.moy_min = "NA" self.moy_min = "NA"
self.moy_max = "NA" self.moy_max = "NA"
self.moy_moy = "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.expr_diagnostics = ""
self.parcours = self.formsemestre.formation.get_parcours() self.parcours = self.formsemestre.formation.get_parcours()
@ -153,31 +157,83 @@ class NotesTableCompat(ResultatsSemestre):
def compute_rangs(self): def compute_rangs(self):
"""Calcule les classements """Calcule les classements
Moyenne générale: etud_moy_gen_ranks 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,
self.etud_moy_gen_ranks_int, self.etud_moy_gen_ranks_int,
) = moy_sem.comp_ranks_series(self.etud_moy_gen) ) = 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] moy_ue = self.etud_moy_ue[ue.id]
self.ue_rangs[ue.id] = ( self.ue_rangs[ue.id] = (
moy_sem.comp_ranks_series(moy_ue)[0], # juste en chaine moy_sem.comp_ranks_series(moy_ue)[0], # juste en chaine
int(moy_ue.count()), int(moy_ue.count()),
) )
# .count() -> nb of non NaN values # .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 """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 Result: rang:str, effectif:str
""" """
if group_id is None:
rangs, effectif = self.ue_rangs[ue_id] rangs, effectif = self.ue_rangs[ue_id]
if rangs is not None: if rangs is not None:
rang = rangs[etudid] rang = rangs[etudid]
else: else:
return "", "" return "", ""
else:
rangs = self.ue_rangs_by_group[ue_id][group_id][0]
rang = rangs[etudid]
effectif = len(rangs)
return rang, effectif 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): def etud_check_conditions_ues(self, etudid):
"""Vrai si les conditions sur les UE sont remplies. """Vrai si les conditions sur les UE sont remplies.
Ne considère que les UE ayant des notes (moyenne calculée). 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é) "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]: def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:
"""Liste d'informations (compat NotesTable) sur évaluations completes """Liste d'informations (compat NotesTable) sur évaluations completes
de ce module. de ce module.

View File

@ -82,7 +82,9 @@ def configuration():
form_bonus.data["bonus_sport_func_name"] form_bonus.data["bonus_sport_func_name"]
) )
app.clear_scodoc_cache() 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")) return redirect(url_for("scodoc.index"))
elif form_scodoc.submit_scodoc.data and form_scodoc.validate(): elif form_scodoc.submit_scodoc.data and form_scodoc.validate():
if ScoDocSiteConfig.enable_entreprises( if ScoDocSiteConfig.enable_entreprises(

View File

@ -56,11 +56,11 @@ class Identite(db.Model):
# #
adresses = db.relationship("Adresse", lazy="dynamic", backref="etud") adresses = db.relationship("Adresse", lazy="dynamic", backref="etud")
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic") billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
# one-to-one relation: #
admission = db.relationship("Admission", backref="identite", lazy="dynamic") admission = db.relationship("Admission", backref="identite", lazy="dynamic")
def __repr__(self): def __repr__(self):
return f"<Etud {self.id} {self.nom} {self.prenom}>" return f"<Etud {self.id}/{self.departement.acronym} {self.nom} {self.prenom}>"
@classmethod @classmethod
def from_request(cls, etudid=None, code_nip=None): def from_request(cls, etudid=None, code_nip=None):
@ -146,6 +146,7 @@ class Identite(db.Model):
return { return {
"id": self.id, "id": self.id,
"nip": self.code_nip, "nip": self.code_nip,
"ine": self.code_ine,
"nom": self.nom, "nom": self.nom,
"nom_usuel": self.nom_usuel, "nom_usuel": self.nom_usuel,
"prenom": self.prenom, "prenom": self.prenom,
@ -177,6 +178,8 @@ class Identite(db.Model):
"date_naissance": self.date_naissance.strftime("%d/%m/%Y") "date_naissance": self.date_naissance.strftime("%d/%m/%Y")
if self.date_naissance if self.date_naissance
else "", else "",
"dept_id": self.dept_id,
"dept_acronym": self.departement.acronym,
"email": self.get_first_email() or "", "email": self.get_first_email() or "",
"emailperso": self.get_first_email("emailperso"), "emailperso": self.get_first_email("emailperso"),
"etudid": self.id, "etudid": self.id,

View File

@ -5,8 +5,6 @@
import datetime import datetime
from app import db from app import db
from app.models import formsemestre
from app.models.formsemestre import FormSemestre
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
from app.models.ues import UniteEns from app.models.ues import UniteEns
@ -48,13 +46,25 @@ class Evaluation(db.Model):
def __repr__(self): def __repr__(self):
return f"""<Evaluation {self.id} {self.jour.isoformat() if self.jour else ''} "{self.description[:16] if self.description else ''}">""" return f"""<Evaluation {self.id} {self.jour.isoformat() if self.jour else ''} "{self.description[:16] if self.description else ''}">"""
def to_dict(self): def to_dict(self) -> dict:
"Représentation dict, pour json"
e = dict(self.__dict__) e = dict(self.__dict__)
e.pop("_sa_instance_state", None) e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators # ScoDoc7 output_formators
e["evaluation_id"] = self.id 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["numero"] = ndb.int_null_is_zero(e["numero"])
e["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
return evaluation_enrich_dict(e) return evaluation_enrich_dict(e)
def from_dict(self, data): def from_dict(self, data):
@ -153,7 +163,7 @@ class EvaluationUEPoids(db.Model):
# Fonction héritée de ScoDoc7 à refactorer # Fonction héritée de ScoDoc7 à refactorer
def evaluation_enrich_dict(e): 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 # For ScoDoc7 compat
heure_debut_dt = e["heure_debut"] or datetime.time( heure_debut_dt = e["heure_debut"] or datetime.time(
8, 00 8, 00
@ -178,11 +188,12 @@ def evaluation_enrich_dict(e):
else: else:
e["descrheure"] = "" e["descrheure"] = ""
# matin, apresmidi: utile pour se referer aux absences: # 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 e["matin"] = 1
else: else:
e["matin"] = 0 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 e["apresmidi"] = 1
else: else:
e["apresmidi"] = 0 e["apresmidi"] = 0

View File

@ -139,6 +139,7 @@ class FormSemestre(db.Model):
else: else:
d["date_fin"] = d["date_fin_iso"] = "" d["date_fin"] = d["date_fin_iso"] = ""
d["responsables"] = [u.id for u in self.responsables] d["responsables"] = [u.id for u in self.responsables]
d["titre_formation"] = self.titre_formation()
return d return d
def get_infos_dict(self) -> dict: def get_infos_dict(self) -> dict:
@ -286,7 +287,7 @@ class FormSemestre(db.Model):
""" """
if not self.etapes: if not self.etapes:
return "" 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: def responsables_str(self, abbrev_prenom=True) -> str:
"""chaîne "J. Dupond, X. Martin" """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) 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: if not imputation_dept:
imputation_dept = sco_preferences.get_preference("DeptName") imputation_dept = prefs["DeptName"]
imputation_dept = imputation_dept.upper() imputation_dept = imputation_dept.upper()
parcours_name = self.formation.get_parcours().NAME parcours_name = self.formation.get_parcours().NAME
modalite = self.modalite modalite = self.modalite
@ -346,7 +348,7 @@ class FormSemestre(db.Model):
scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month) scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month)
) )
return scu.sanitize_string( 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: def titre_annee(self) -> str:
@ -358,6 +360,12 @@ class FormSemestre(db.Model):
titre_annee += "-" + str(self.date_fin.year) titre_annee += "-" + str(self.date_fin.year)
return titre_annee 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: def titre_mois(self) -> str:
"""Le titre et les dates du semestre, pour affichage dans des listes """Le titre et les dates du semestre, pour affichage dans des listes
Ex: "BUT QLIO (PN 2022) semestre 1 FI (Sept 2022 - Jan 2023)" Ex: "BUT QLIO (PN 2022) semestre 1 FI (Sept 2022 - Jan 2023)"
@ -441,10 +449,15 @@ class FormSemestreEtape(db.Model):
db.Integer, db.Integer,
db.ForeignKey("notes_formsemestre.id"), 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) 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): def __repr__(self):
return f"<Etape {self.id} apo={self.etape_apo}>" return f"<Etape {self.id} apo={self.etape_apo!r}>"
def as_apovdi(self): def as_apovdi(self):
return ApoEtapeVDI(self.etape_apo) return ApoEtapeVDI(self.etape_apo)

View File

@ -25,9 +25,11 @@ class Partition(db.Model):
partition_name = db.Column(db.String(SHORT_STR_LEN)) partition_name = db.Column(db.String(SHORT_STR_LEN))
# numero = ordre de presentation) # numero = ordre de presentation)
numero = db.Column(db.Integer) numero = db.Column(db.Integer)
# Calculer le rang ?
bul_show_rank = db.Column( bul_show_rank = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" db.Boolean(), nullable=False, default=False, server_default="false"
) )
# Montrer quand on indique les groupes de l'étudiant ?
show_in_lists = db.Column( show_in_lists = db.Column(
db.Boolean(), nullable=False, default=True, server_default="true" db.Boolean(), nullable=False, default=True, server_default="true"
) )
@ -50,6 +52,18 @@ class Partition(db.Model):
def __repr__(self): def __repr__(self):
return f"""<{self.__class__.__name__} {self.id} "{self.partition_name or '(default)'}">""" 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): class GroupDescr(db.Model):
"""Description d'un groupe d'une partition""" """Description d'un groupe d'une partition"""
@ -78,6 +92,17 @@ class GroupDescr(db.Model):
"Nom avec partition: 'TD A'" "Nom avec partition: 'TD A'"
return f"{self.partition.partition_name or ''} {self.group_name or '-'}" 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 = db.Table(
"group_membership", "group_membership",
@ -85,3 +110,11 @@ group_membership = db.Table(
db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")), db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")),
db.UniqueConstraint("etudid", "group_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"))

View File

@ -75,6 +75,15 @@ class UniteEns(db.Model):
return sco_edit_ue.ue_is_locked(self.id) 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: def guess_semestre_idx(self) -> None:
"""Lorsqu'on prend une ancienne formation non APC, """Lorsqu'on prend une ancienne formation non APC,
les UE n'ont pas d'indication de semestre. les UE n'ont pas d'indication de semestre.

View File

@ -97,7 +97,7 @@ class SetTag(pe_tagtable.TableTag):
"""Mémorise les semtag nécessaires au jury.""" """Mémorise les semtag nécessaires au jury."""
self.SemTagDict = {fid: SemTagDict[fid] for fid in self.get_Fids_in_settag()} self.SemTagDict = {fid: SemTagDict[fid] for fid in self.get_Fids_in_settag()}
if PE_DEBUG >= 1: 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): def comp_data_settag(self):
@ -210,7 +210,7 @@ class SetTagInterClasse(pe_tagtable.TableTag):
# ------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------
def __init__(self, nom_combinaison, diplome): 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.combinaison = nom_combinaison
self.parcoursDict = {} 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 fid: SetTagDict[fid] for fid in self.get_Fids_in_settag() if fid != None
} }
if PE_DEBUG >= 1: 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): def comp_data_settag(self):

View File

@ -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

View File

@ -965,7 +965,7 @@ def _tables_abs_etud(
)[0] )[0]
if format == "html": if format == "html":
ex.append( ex.append(
f"""<a href="{url_for('notes.moduleimpl_status', f"""<a title="{mod['module']['titre']}" href="{url_for('notes.moduleimpl_status',
scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])} scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])}
">{mod["module"]["code"] or "(module sans code)"}</a>""" ">{mod["module"]["code"] or "(module sans code)"}</a>"""
) )
@ -983,7 +983,8 @@ def _tables_abs_etud(
)[0] )[0]
if format == "html": if format == "html":
ex.append( ex.append(
f"""<a href="{url_for('notes.moduleimpl_status', f"""<a title="{mod['module']['titre']}"
href="{url_for('notes.moduleimpl_status',
scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])} scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])}
">{mod["module"]["code"] or '(module sans code)'}</a>""" ">{mod["module"]["code"] or '(module sans code)'}</a>"""
) )

View File

@ -251,7 +251,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
rang = "" rang = ""
rang_gr, ninscrits_gr, gr_name = get_etud_rangs_groups( 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(): if nt.get_moduleimpls_attente():
@ -651,7 +651,7 @@ def _ue_mod_bulletin(
def get_etud_rangs_groups( 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""" """Ramene rang et nb inscrits dans chaque partition"""
rang_gr, ninscrits_gr, gr_name = {}, {}, {} rang_gr, ninscrits_gr, gr_name = {}, {}, {}

View File

@ -31,7 +31,6 @@
import datetime import datetime
import json import json
from app.but import bulletin_but
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
@ -92,7 +91,7 @@ def formsemestre_bulletinetud_published_dict(
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
d = {} d = {"type": "classic", "version": "0"}
if (not sem["bul_hide_xml"]) or force_publishing: if (not sem["bul_hide_xml"]) or force_publishing:
published = True published = True
@ -166,7 +165,7 @@ def formsemestre_bulletinetud_published_dict(
else: else:
rang = str(nt.get_etud_rang(etudid)) rang = str(nt.get_etud_rang(etudid))
rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups( 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( d["note"] = dict(

View File

@ -172,7 +172,7 @@ def make_xml_formsemestre_bulletinetud(
else: else:
rang = str(nt.get_etud_rang(etudid)) rang = str(nt.get_etud_rang(etudid))
rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups( 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( doc.append(

View File

@ -228,7 +228,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
if getattr(g, "defer_cache_invalidation", False): if getattr(g, "defer_cache_invalidation", False):
g.sem_to_invalidate.add(formsemestre_id) g.sem_to_invalidate.add(formsemestre_id)
return 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: if formsemestre_id is None:
# clear all caches # clear all caches
log("----- invalidate_formsemestre: clearing 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) SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
class DefferedSemCacheManager: class DeferredSemCacheManager:
"""Contexte pour effectuer des opérations indépendantes dans la """Contexte pour effectuer des opérations indépendantes dans la
même requete qui invalident le cache. Par exemple, quand on inscrit même requete qui invalident le cache. Par exemple, quand on inscrit
des étudiants un par un à un semestre, chaque inscription va invalider des étudiants un par un à un semestre, chaque inscription va invalider

View File

@ -1,13 +1,11 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Configuration de ScoDoc (version ScoDOc 9) """Configuration de ScoDoc (version ScoDoc 9)
NE PAS MODIFIER localement ce fichier ! NE PAS MODIFIER localement ce fichier !
mais éditer /opt/scodoc-data/config/scodoc_local.py mais éditer /opt/scodoc-data/config/scodoc_local.py
""" """
from app.scodoc import bonus_sport
class AttrDict(dict): class AttrDict(dict):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -131,8 +131,10 @@ def index_html(showcodes=0, showsemtable=0):
if not showsemtable: if not showsemtable:
H.append( H.append(
f"""<hr> f"""<hr>
<p><a class="stdlink" href="{url_for('scolar.index_html', scodoc_dept=g.scodoc_dept, showsemtable=1) <p><a class="stdlink" href="{url_for('scolar.index_html',
}">Voir tous les semestres ({len(othersems)} verrouillés)</a> scodoc_dept=g.scodoc_dept, showsemtable=1)
}">Voir table des semestres (dont {len(othersems)}
verrouillé{'s' if len(othersems) else ''})</a>
</p>""" </p>"""
) )

View File

@ -66,7 +66,8 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
sems = sco_formsemestre.do_formsemestre_list({"formation_id": formation_id}) sems = sco_formsemestre.do_formsemestre_list({"formation_id": formation_id})
if sems: if sems:
H.append( H.append(
"""<p class="warning">Impossible de supprimer cette formation, car les sessions suivantes l'utilisent:</p> """<p class="warning">Impossible de supprimer cette formation,
car les sessions suivantes l'utilisent:</p>
<ul>""" <ul>"""
) )
for sem in sems: for sem in sems:

View File

@ -33,7 +33,7 @@ from flask import url_for, render_template
from flask import g, request from flask import g, request
from flask_login import current_user from flask_login import current_user
from app import log from app import db, log
from app import models from app import models
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import Formation, Matiere, Module, UniteEns from app.models import Formation, Matiere, Module, UniteEns
@ -359,7 +359,6 @@ def can_delete_module(module):
def do_module_delete(oid): def do_module_delete(oid):
"delete module" "delete module"
from app.scodoc import sco_formations
module = Module.query.get_or_404(oid) module = Module.query.get_or_404(oid)
mod = module_list({"module_id": oid})[0] # sco7 mod = module_list({"module_id": oid})[0] # sco7
@ -422,13 +421,14 @@ def module_delete(module_id=None):
H = [ H = [
html_sco_header.sco_header(page_title="Suppression d'un module"), html_sco_header.sco_header(page_title="Suppression d'un module"),
"""<h2>Suppression du module %(titre)s (%(code)s)</h2>""" % mod, f"""<h2>Suppression du module {module.titre or "<em>sans titre</em>"} ({module.code})</h2>""",
] ]
dest_url = url_for( dest_url = url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=str(mod["formation_id"]), formation_id=module.formation_id,
semestre_idx=module.ue.semestre_idx,
) )
tf = TrivialFormulator( tf = TrivialFormulator(
request.base_url, request.base_url,
@ -848,21 +848,13 @@ def module_count_moduleimpls(module_id):
def formation_add_malus_modules(formation_id, titre=None, redirect=True): def formation_add_malus_modules(formation_id, titre=None, redirect=True):
"""Création d'un module de "malus" dans chaque UE d'une formation""" """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: for ue in formation.ues:
# Un seul module de malus par UE: ue_add_malus_module(ue, titre=titre)
nb_mod_malus = len(
[ formation.invalidate_cached_sems()
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)
if redirect: if redirect:
return flask.redirect( return flask.redirect(
@ -872,20 +864,22 @@ def formation_add_malus_modules(formation_id, titre=None, redirect=True):
) )
def ue_add_malus_module(ue_id, titre=None, code=None): def ue_add_malus_module(ue: UniteEns, titre=None, code=None) -> int:
"""Add a malus module in this ue""" """Add a malus module in this ue.
from app.scodoc import sco_edit_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] titre = titre or ""
code = code or f"MALUS{ue.numero}"
if titre is None:
titre = ""
if code is None:
code = "MALUS%d" % ue["numero"]
# Tout module doit avoir un semestre_id (indice 1, 2, ...) # Tout module doit avoir un semestre_id (indice 1, 2, ...)
semestre_ids = sco_edit_ue.ue_list_semestre_ids(ue) if ue.semestre_idx is None:
if semestre_ids: semestre_ids = sorted(list(set([m.semestre_id for m in ue.modules])))
if len(semestre_ids) > 0:
semestre_id = semestre_ids[0] semestre_id = semestre_ids[0]
else: else:
# c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement # c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement
@ -893,25 +887,35 @@ def ue_add_malus_module(ue_id, titre=None, code=None):
raise ScoValueError( raise ScoValueError(
"Impossible d'ajouter un malus s'il n'y a pas d'autres modules" "Impossible d'ajouter un malus s'il n'y a pas d'autres modules"
) )
else:
semestre_id = ue.semestre_idx
# Matiere pour placer le module malus # Matiere pour placer le module malus
Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue_id}) titre_matiere_malus = "Malus"
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}
)
module_id = do_module_create( matieres_malus = [mat for mat in ue.matieres if mat.titre == titre_matiere_malus]
{ if len(matieres_malus) > 0:
"titre": titre, # matière Malus déjà existante, l'utilise
"code": code, matiere = matieres_malus[0]
"coefficient": 0.0, # unused else:
"ue_id": ue_id, if ue.matieres.count() > 0:
"matiere_id": matiere_id, numero = max([mat.numero for mat in ue.matieres]) + 10
"formation_id": ue["formation_id"], else:
"semestre_id": semestre_id, numero = 0
"module_type": scu.ModuleType.MALUS, 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

View File

@ -142,29 +142,23 @@ def do_ue_create(args):
return ue_id 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): def do_ue_delete(ue_id, delete_validations=False, force=False):
"delete UE and attached matieres (but not modules)" "delete UE and attached matieres (but not modules)"
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_parcours_dut from app.scodoc import sco_parcours_dut
ue = UniteEns.query.get_or_404(ue_id) 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( raise ScoNonEmptyFormationObject(
"UE", f"UE (id={ue.id}, dud)",
msg=ue.titre, msg=ue.titre,
dest_url=url_for( dest_url=url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=ue.formation_id, formation_id=formation_id,
semestre_idx=ue.semestre_idx, semestre_idx=semestre_idx,
), ),
) )
@ -187,13 +181,13 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
cancel_url=url_for( cancel_url=url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=ue.formation_id, formation_id=formation_id,
semestre_idx=ue.semestre_idx, semestre_idx=semestre_idx,
), ),
parameters={"ue_id": ue.id, "dialog_confirmed": 1}, parameters={"ue_id": ue.id, "dialog_confirmed": 1},
) )
if delete_validations: if delete_validations:
log("deleting all validations of UE %s" % ue.id) log(f"deleting all validations of UE {ue.id}")
ndb.SimpleQuery( ndb.SimpleQuery(
"DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s", "DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s",
{"ue_id": ue.id}, {"ue_id": ue.id},
@ -215,10 +209,10 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
# utilisé: acceptable de tout invalider): # utilisé: acceptable de tout invalider):
sco_cache.invalidate_formsemestre() sco_cache.invalidate_formsemestre()
# news # 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( ScolarNews.add(
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
obj=ue.formation_id, obj=formation_id,
text=f"Modification de la formation {F['acronyme']}", text=f"Modification de la formation {F['acronyme']}",
max_frequency=10 * 60, max_frequency=10 * 60,
) )
@ -228,8 +222,8 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
url_for( url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=ue.formation_id, formation_id=formation_id,
semestre_idx=ue.semestre_idx, semestre_idx=semestre_idx,
) )
) )
return None return None
@ -538,9 +532,9 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
semestre_idx=ue.semestre_idx, semestre_idx=ue.semestre_idx,
), ),
) )
if not can_delete_ue(ue): if not ue.can_be_deleted():
raise ScoNonEmptyFormationObject( raise ScoNonEmptyFormationObject(
"UE", f"UE",
msg=ue.titre, msg=ue.titre,
dest_url=url_for( dest_url=url_for(
"notes.ue_table", "notes.ue_table",
@ -1352,16 +1346,6 @@ def ue_is_locked(ue_id):
return len(r) > 0 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 = [ UE_PALETTE = [
"#B80004", # rouge "#B80004", # rouge
"#F97B3D", # Orange Crayola "#F97B3D", # Orange Crayola

View File

@ -606,12 +606,10 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
# -------------- VIEWS # -------------- 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 """HTML description of evaluation, for page headers
edit_in_place: allow in-place editing when permitted (not implemented) 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] E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
moduleimpl_id = E["moduleimpl_id"] moduleimpl_id = E["moduleimpl_id"]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] 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: if Mod["module_type"] == ModuleType.MALUS:
etit += ' <span class="eval_malus">(points de malus)</span>' etit += ' <span class="eval_malus">(points de malus)</span>'
H = [ H = [
'<span class="eval_title">Evaluation%s</span><p><b>Module : %s</b></p>' '<span class="eval_title">Évaluation%s</span><p><b>Module : %s</b></p>'
% (etit, mod_descr) % (etit, mod_descr)
] ]
if Mod["module_type"] == ModuleType.MALUS: if Mod["module_type"] == ModuleType.MALUS:
@ -689,7 +687,11 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
<a class="stdlink" href="{url_for( <a class="stdlink" href="{url_for(
"notes.evaluation_edit", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id) "notes.evaluation_edit", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
}">modifier l'évaluation</a> }">modifier l'évaluation</a>
"""
)
if link_saisie:
H.append(
f"""
<a class="stdlink" href="{url_for( <a class="stdlink" href="{url_for(
"notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id) "notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
}">saisie des notes</a> }">saisie des notes</a>

View File

@ -208,25 +208,29 @@ def _build_results_list(dpv_by_sem, etuds_infos):
return rows, titles, columns_ids 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""" """Ensemble des formsemestre_id entre ces dates"""
s = ndb.SimpleDictFetch( s = ndb.SimpleDictFetch(
"""SELECT id """SELECT id
FROM notes_formsemestre 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} 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 """Page affichant la table des résultats
Les dates sont en dd/mm/yyyy (datepicker javascript) Les dates sont en dd/mm/yyyy (datepicker javascript)
types_parcours est la liste des types de parcours à afficher types_parcours est la liste des types de parcours à afficher
(liste de chaines, eg ['100', '210'] ) (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: if not types_parcours:
types_parcours = [] types_parcours = []
if not isinstance(types_parcours, list): if not isinstance(types_parcours, list):

View File

@ -256,6 +256,8 @@ def formation_import_xml(doc: str, import_tags=True):
mod_info[1]["formation_id"] = formation_id mod_info[1]["formation_id"] = formation_id
mod_info[1]["matiere_id"] = mat_id mod_info[1]["matiere_id"] = mat_id
mod_info[1]["ue_id"] = ue_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]) mod_id = sco_edit_module.do_module_create(mod_info[1])
if xml_module_id: if xml_module_id:
modules_old2new[int(xml_module_id)] = mod_id modules_old2new[int(xml_module_id)] = mod_id

View File

@ -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"]) formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
moy_gen = nt.get_etud_moy_gen(etud["etudid"]) moy_gen = nt.get_etud_moy_gen(etud["etudid"])
@ -277,21 +277,20 @@ def _make_page(etud, sem, tf, message=""):
</p> </p>
""" """
% etud, % etud,
"""<p>La moyenne de ce semestre serait: f"""<p>La moyenne de ce semestre serait:
<span class="ext_sem_moy"><span class="ext_sem_moy_val">%s</span> / 20</span> <span class="ext_sem_moy"><span class="ext_sem_moy_val">{moy_gen}</span> / 20</span>
</p> </p>
""" """,
% moy_gen,
'<div id="formsemestre_ext_edit_ue_validations">', '<div id="formsemestre_ext_edit_ue_validations">',
tf[1], tf[1],
"</div>", "</div>",
"""<div> f"""<div>
<a class="stdlink" <a class="stdlink"
href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s"> href="{url_for("notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept,
retour au bulletin de notes formsemestre_id=formsemestre.id, etudid=etud['etudid']
</a></div> )}">retour au bulletin de notes</a>
""" </div>
% (sem["formsemestre_id"], etud["etudid"]), """,
html_sco_header.sco_footer(), html_sco_header.sco_footer(),
] ]
return H return H

View File

@ -43,13 +43,14 @@ from xml.etree.ElementTree import Element
import flask import flask
from flask import g, request from flask import g, request
from flask import url_for, make_response from flask import url_for, make_response
from sqlalchemy.sql import text
from app import db from app import db
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat 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 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.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log, cache 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_permissions_check
from app.scodoc import sco_xml from app.scodoc import sco_xml
from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
@ -413,8 +413,43 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"):
return R return R
def etud_add_group_infos(etud, formsemestre_id, sep=" "): def get_etud_formsemestre_groups(
"""Add informations on partitions and group memberships to etud (a dict with an etudid)""" 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[ etud[
"partitions" "partitions"
] = collections.OrderedDict() # partition_id : group + partition_name ] = collections.OrderedDict() # partition_id : group + partition_name
@ -423,11 +458,14 @@ def etud_add_group_infos(etud, formsemestre_id, sep=" "):
return etud return etud
infos = ndb.SimpleDictFetch( 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 FROM group_descr g, partition p, group_membership gm WHERE gm.etudid=%(etudid)s
and gm.group_id = g.id and gm.group_id = g.id
and g.partition_id = p.id and g.partition_id = p.id
and p.formsemestre_id = %(formsemestre_id)s and p.formsemestre_id = %(formsemestre_id)s
"""
+ (" and (p.show_in_lists is True) " if only_to_show else "")
+ """
ORDER BY p.numero ORDER BY p.numero
""", """,
{"etudid": etud["etudid"], "formsemestre_id": formsemestre_id}, {"etudid": etud["etudid"], "formsemestre_id": formsemestre_id},
@ -443,7 +481,7 @@ def etud_add_group_infos(etud, formsemestre_id, sep=" "):
) )
etud["partitionsgroupes"] = sep.join( etud["partitionsgroupes"] = sep.join(
[ [
gr["partition_name"] + ":" + gr["group_name"] (gr["partition_name"] or "") + ":" + gr["group_name"]
for gr in infos for gr in infos
if gr["group_name"] is not None if gr["group_name"] is not None
] ]
@ -1008,9 +1046,7 @@ def partition_set_attr(partition_id, attr, value):
partition[attr] = value partition[attr] = value
partitionEditor.edit(cnx, partition) partitionEditor.edit(cnx, partition)
# invalid bulletin cache # invalid bulletin cache
sco_cache.invalidate_formsemestre( sco_cache.invalidate_formsemestre(formsemestre_id=partition["formsemestre_id"])
pdfonly=True, formsemestre_id=partition["formsemestre_id"]
)
return "enregistré" return "enregistré"

View File

@ -277,7 +277,11 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
if modimpl.module.is_apc(): if modimpl.module.is_apc():
H.append(_ue_coefs_html(modimpl.module.ue_coefs_list())) H.append(_ue_coefs_html(modimpl.module.ue_coefs_list()))
else: 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("""</td><td></td></tr>""") H.append("""</td><td></td></tr>""")
# 3ieme ligne: Formation # 3ieme ligne: Formation
H.append( H.append(

View File

@ -153,14 +153,14 @@ def ficheEtud(etudid=None):
try: # pour les bookmarks avec d'anciens ids... try: # pour les bookmarks avec d'anciens ids...
etudid = int(etudid) etudid = int(etudid)
except ValueError: except ValueError:
raise ScoValueError("id invalide !") raise ScoValueError("id invalide !") from ValueError
# la sidebar est differente s'il y a ou pas un etudid # la sidebar est differente s'il y a ou pas un etudid
# voir html_sidebar.sidebar() # voir html_sidebar.sidebar()
g.etudid = etudid g.etudid = etudid
args = make_etud_args(etudid=etudid) args = make_etud_args(etudid=etudid)
etuds = sco_etud.etudident_list(cnx, args) etuds = sco_etud.etudident_list(cnx, args)
if not etuds: 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 !") raise ScoValueError("Etudiant inexistant !")
etud = etuds[0] etud = etuds[0]
etudid = etud["etudid"] etudid = etud["etudid"]
@ -173,7 +173,7 @@ def ficheEtud(etudid=None):
if info["lieu_naissance"]: if info["lieu_naissance"]:
info["info_naissance"] += " à " + info["lieu_naissance"] info["info_naissance"] += " à " + info["lieu_naissance"]
if info["dept_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) info["etudfoto"] = sco_photos.etud_photo_html(etud)
if ( if (
(not info["domicile"]) (not info["domicile"])
@ -205,7 +205,7 @@ def ficheEtud(etudid=None):
) )
else: else:
info["emaillink"] = "<em>(pas d'adresse e-mail)</em>" info["emaillink"] = "<em>(pas d'adresse e-mail)</em>"
# champs dependant des permissions # Champ dépendant des permissions:
if authuser.has_permission(Permission.ScoEtudChangeAdr): if authuser.has_permission(Permission.ScoEtudChangeAdr):
info["modifadresse"] = ( info["modifadresse"] = (
'<a class="stdlink" href="formChangeCoordonnees?etudid=%s">modifier adresse</a>' '<a class="stdlink" href="formChangeCoordonnees?etudid=%s">modifier adresse</a>'
@ -216,9 +216,10 @@ def ficheEtud(etudid=None):
# Groupes: # Groupes:
sco_groups.etud_add_group_infos( 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 # Parcours de l'étudiant
if info["sems"]: if info["sems"]:
info["last_formsemestre_id"] = info["sems"][0]["formsemestre_id"] info["last_formsemestre_id"] = info["sems"][0]["formsemestre_id"]
@ -235,15 +236,28 @@ def ficheEtud(etudid=None):
) )
grlink = '<span class="fontred">%s</span>' % descr["situation"] grlink = '<span class="fontred">%s</span>' % descr["situation"]
else: else:
group = sco_groups.get_etud_main_group(etudid, sem["formsemestre_id"]) e = {"etudid": etudid}
if group["partition_name"]: sco_groups.etud_add_group_infos(
gr_name = group["group_name"] 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: else:
gr_name = "tous" gr_name = "tous"
grlink = (
'<a class="discretelink" href="groups_view?group_ids=%s" title="Liste du groupe">groupe %s</a>' grlinks.append(
% (group["group_id"], gr_name) f"""<a class="discretelink" href="{
url_for('scolar.groups_view',
scodoc_dept=g.scodoc_dept, group_ids=partition['group_id'])
}" title="Liste du groupe {gr_name}">{gr_name}</a>
"""
) )
grlink = ", ".join(grlinks)
# infos ajoutées au semestre dans le parcours (groupe, menu) # infos ajoutées au semestre dans le parcours (groupe, menu)
menu = _menuScolarite(authuser, sem, etudid) menu = _menuScolarite(authuser, sem, etudid)
if menu: if menu:
@ -296,9 +310,9 @@ def ficheEtud(etudid=None):
if not sco_permissions_check.can_suppress_annotation(a["id"]): if not sco_permissions_check.can_suppress_annotation(a["id"]):
a["dellink"] = "" a["dellink"] = ""
else: else:
a[ a["dellink"] = (
"dellink" '<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&annotation_id=%s">%s</a></td>'
] = '<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&annotation_id=%s">%s</a></td>' % ( % (
etudid, etudid,
a["id"], a["id"],
scu.icontag( scu.icontag(
@ -308,6 +322,7 @@ def ficheEtud(etudid=None):
title="Supprimer cette annotation", title="Supprimer cette annotation",
), ),
) )
)
author = sco_users.user_info(a["author"]) author = sco_users.user_info(a["author"])
alist.append( alist.append(
f"""<tr><td><span class="annodate">Le {a['date']} par {author['prenomnom']} : f"""<tr><td><span class="annodate">Le {a['date']} par {author['prenomnom']} :
@ -422,9 +437,11 @@ def ficheEtud(etudid=None):
# #
if info["groupes"].strip(): if info["groupes"].strip():
info["groupes_row"] = ( info[
'<tr><td class="fichetitre2">Groupe :</td><td>%(groupes)s</td></tr>' % info "groupes_row"
) ] = f"""<tr>
<td class="fichetitre2">Groupes :</td><td>{info['groupes']}</td>
</tr>"""
else: else:
info["groupes_row"] = "" info["groupes_row"] = ""
info["menus_etud"] = menus_etud(etudid) info["menus_etud"] = menus_etud(etudid)

View File

@ -50,10 +50,10 @@ _SCO_PERMISSIONS = (
(1 << 27, "RelationsEntreprisesCorrespondants", "Voir les correspondants"), (1 << 27, "RelationsEntreprisesCorrespondants", "Voir les correspondants"),
# 27 à 39 ... réservé pour "entreprises" # 27 à 39 ... réservé pour "entreprises"
# Api scodoc9 # Api scodoc9
(1 << 40, "APIView", "Voir"), (1 << 40, "APIView", "API: Lecture"),
(1 << 41, "APIEtudChangeGroups", "Modifier les groupes"), (1 << 41, "APIEtudChangeGroups", "API: Modifier les groupes"),
(1 << 42, "APIEditAllNotes", "Modifier toutes les notes"), (1 << 42, "APIEditAllNotes", "API: Modifier toutes les notes"),
(1 << 43, "APIAbsChange", "Saisir des absences"), (1 << 43, "APIAbsChange", "API: Saisir des absences"),
) )
@ -70,7 +70,8 @@ class Permission(object):
setattr(Permission, symbol, perm) setattr(Permission, symbol, perm)
Permission.description[symbol] = description Permission.description[symbol] = description
Permission.permission_by_name[symbol] = perm 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 @staticmethod
def get_by_name(permission_name: str) -> int: def get_by_name(permission_name: str) -> int:

View File

@ -132,8 +132,11 @@ def clear_base_preferences():
g._SCO_BASE_PREFERENCES = {} # { dept_id: BasePreferences instance } g._SCO_BASE_PREFERENCES = {} # { dept_id: BasePreferences instance }
def get_base_preferences(): def get_base_preferences(dept_id: int = None):
"""Return global preferences for the current department""" """Return global preferences for the specified department
or the current departement
"""
if dept_id is None:
dept_id = g.scodoc_dept_id dept_id = g.scodoc_dept_id
if not hasattr(g, "_SCO_BASE_PREFERENCES"): if not hasattr(g, "_SCO_BASE_PREFERENCES"):
g._SCO_BASE_PREFERENCES = {} g._SCO_BASE_PREFERENCES = {}
@ -142,12 +145,12 @@ def get_base_preferences():
return g._SCO_BASE_PREFERENCES[dept_id] 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. """Returns value of named preference.
All preferences have a sensible default value, so this All preferences have a sensible default value, so this
function always returns a usable value for all defined preferences names. 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): def _convert_pref_type(p, pref_spec):
@ -2145,9 +2148,9 @@ class BasePreferences(object):
class SemPreferences: class SemPreferences:
"""Preferences for a formsemestre""" """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.formsemestre_id = formsemestre_id
self.base_prefs = get_base_preferences() self.base_prefs = get_base_preferences(dept_id=dept_id)
def __getitem__(self, name): def __getitem__(self, name):
return self.base_prefs.get(self.formsemestre_id, name) return self.base_prefs.get(self.formsemestre_id, name)

View File

@ -32,7 +32,7 @@ import time
from xml.etree import ElementTree from xml.etree import ElementTree
from flask import g, request from flask import g, request
from flask import url_for from flask import abort, url_for
from app import log from app import log
from app.but import bulletin_but 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 force_publishing: publie les xml et json même si bulletins non publiés
selected_etudid: etudid sélectionné (pour scroller au bon endroit) 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) 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) modejury = int(modejury)
xml_with_decisions = int(xml_with_decisions) xml_with_decisions = int(xml_with_decisions)
force_publishing = int(force_publishing) force_publishing = int(force_publishing)
is_file = tabformat in {"csv", "json", "xls", "xlsx", "xlsall", "xml"}
data = _do_formsemestre_recapcomplet( data = _do_formsemestre_recapcomplet(
formsemestre_id, formsemestre_id,
format=tabformat, format=tabformat,
@ -128,6 +133,8 @@ def formsemestre_recapcomplet(
for (format, label) in ( for (format, label) in (
("html", "Tableau"), ("html", "Tableau"),
("evals", "Avec toutes les évaluations"), ("evals", "Avec toutes les évaluations"),
("xlsx", "Excel (non formaté)"),
("xlsall", "Excel avec évaluations"),
("xml", "Bulletins XML (obsolète)"), ("xml", "Bulletins XML (obsolète)"),
("json", "Bulletins JSON"), ("json", "Bulletins JSON"),
): ):

View File

@ -799,22 +799,22 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id}) evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals: if not evals:
raise ScoValueError("invalid evaluation_id") raise ScoValueError("invalid evaluation_id")
E = evals[0] eval_dict = evals[0]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=eval_dict["moduleimpl_id"])[0]
formsemestre_id = M["formsemestre_id"] formsemestre_id = M["formsemestre_id"]
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
mod_responsable = sco_users.user_info(M["responsable_id"]) mod_responsable = sco_users.user_info(M["responsable_id"])
if E["jour"]: if eval_dict["jour"]:
indication_date = ndb.DateDMYtoISO(E["jour"]) indication_date = ndb.DateDMYtoISO(eval_dict["jour"])
else: else:
indication_date = scu.sanitize_filename(E["description"])[:12] indication_date = scu.sanitize_filename(eval_dict["description"])[:12]
evalname = "%s-%s" % (Mod["code"], indication_date) eval_name = "%s-%s" % (Mod["code"], indication_date)
if E["description"]: if eval_dict["description"]:
evaltitre = "%s du %s" % (E["description"], E["jour"]) evaltitre = "%s du %s" % (eval_dict["description"], eval_dict["jour"])
else: else:
evaltitre = "évaluation du %s" % E["jour"] evaltitre = "évaluation du %s" % eval_dict["jour"]
description = "%s en %s (%s) resp. %s" % ( description = "%s en %s (%s) resp. %s" % (
evaltitre, evaltitre,
Mod["abbrev"] or "", 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 # une liste de liste de chaines: lignes de la feuille de calcul
L = [] L = []
etuds = _get_sorted_etuds(E, etudids, formsemestre_id) etuds = _get_sorted_etuds(eval_dict, etudids, formsemestre_id)
for e in etuds: for e in etuds:
etudid = e["etudid"] etudid = e["etudid"]
groups = sco_groups.get_etud_groups(etudid, formsemestre_id) 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) filename = "notes_%s_%s" % (eval_name, gr_title_filename)
xls = sco_excel.excel_feuille_saisie(E, sem["titreannee"], description, lines=L) 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 scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
# return sco_excel.send_excel_file(xls, filename) # return sco_excel.send_excel_file(xls, filename)
@ -941,7 +943,9 @@ def saisie_notes(evaluation_id, group_ids=[]):
cssstyles=sco_groups_view.CSSSTYLES, cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True, init_qtip=True,
), ),
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id), sco_evaluations.evaluation_describe(
evaluation_id=evaluation_id, link_saisie=False
),
'<div id="saisie_notes"><span class="eval_title">Saisie des notes</span>', '<div id="saisie_notes"><span class="eval_title">Saisie des notes</span>',
] ]
H.append("""<div id="group-tabs"><table><tr><td>""") H.append("""<div id="group-tabs"><table><tr><td>""")
@ -1008,10 +1012,9 @@ def saisie_notes(evaluation_id, group_ids=[]):
return "\n".join(H) return "\n".join(H)
def _get_sorted_etuds(E, etudids, formsemestre_id): def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int):
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
notes_db = sco_evaluation_db.do_evaluation_get_all_notes( notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
E["evaluation_id"] eval_dict["evaluation_id"]
) # Notes existantes ) # Notes existantes
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
etuds = [] etuds = []
@ -1028,9 +1031,9 @@ def _get_sorted_etuds(E, etudids, formsemestre_id):
e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id) e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id)
# Information sur absence (tenant compte de la demi-journée) # 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 = [] warn_abs_lst = []
if E["matin"]: if eval_dict["matin"]:
nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=1) 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) nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=1)
if nbabs: if nbabs:
@ -1038,7 +1041,7 @@ def _get_sorted_etuds(E, etudids, formsemestre_id):
warn_abs_lst.append("absent justifié le matin !") warn_abs_lst.append("absent justifié le matin !")
else: else:
warn_abs_lst.append("absent le matin !") 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) 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) nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=0)
if nbabs: if nbabs:

View File

@ -252,7 +252,7 @@ def formsemestre_synchro_etuds(
etudids_a_desinscrire = [nip2etudid(x) for x in a_desinscrire] etudids_a_desinscrire = [nip2etudid(x) for x in a_desinscrire]
etudids_a_desinscrire += a_desinscrire_without_key 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) do_import_etuds_from_portal(sem, a_importer, etudsapo_ident)
sco_inscr_passage.do_inscrit(sem, etudids_a_inscrire) sco_inscr_passage.do_inscrit(sem, etudids_a_inscrire)
sco_inscr_passage.do_desinscrit(sem, etudids_a_desinscrire) sco_inscr_passage.do_desinscrit(sem, etudids_a_desinscrire)

View File

@ -56,6 +56,7 @@ Solution proposée (nov 2014):
import flask import flask
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
from app.models.formsemestre import FormSemestre
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu 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_matiere
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue 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_evaluation_db
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
@ -85,17 +85,21 @@ def external_ue_create(
ects=0.0, ects=0.0,
): ):
"""Crée UE/matiere/module/evaluation puis saisie les notes""" """Crée UE/matiere/module/evaluation puis saisie les notes"""
log("external_ue_create( formsemestre_id=%s, titre=%s )" % (formsemestre_id, titre)) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
sem = sco_formsemestre.get_formsemestre(formsemestre_id) log(f"creating external UE in {formsemestre}: {acronyme}")
# Contrôle d'accès: # Contrôle d'accès:
if not current_user.has_permission(Permission.ScoImplement): 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") raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
# #
formation_id = sem["formation_id"] formation_id = formsemestre.formation.id
log("creating external UE in %s: %s" % (formsemestre_id, acronyme))
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( ue_id = sco_edit_ue.do_ue_create(
{ {
"formation_id": formation_id, "formation_id": formation_id,
@ -120,7 +124,8 @@ def external_ue_create(
"ue_id": ue_id, "ue_id": ue_id,
"matiere_id": matiere_id, "matiere_id": matiere_id,
"formation_id": formation_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, "module_id": module_id,
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,
# affecte le 1er responsable du semestre comme resp. du module # 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 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( log(
"external_ue_inscrit_et_note(moduleimpl_id=%s, notes_etuds=%s)" f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl_id}, notes_etuds={notes_etuds})"
% (moduleimpl_id, notes_etuds)
) )
# Inscription des étudiants # Inscription des étudiants
sco_moduleimpl.do_moduleimpl_inscrit_etuds( 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): def get_existing_external_ue(formation_id: int) -> list[dict]:
"la liste de toutes les UE externes définies dans cette formation" "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}) 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" "moduleimpl correspondant à l'UE externe indiquée de ce formsemestre"
r = ndb.SimpleDictFetch( r = ndb.SimpleDictFetch(
""" """
SELECT mi.id AS moduleimpl_id FROM notes_moduleimpl mi, notes_modules mo 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 mi.module_id = mo.id
AND mo.ue_id = %(ue_id)s AND mo.ue_id = %(ue_id)s
""", """,
@ -194,11 +205,14 @@ def get_external_moduleimpl_id(formsemestre_id, ue_id):
if r: if r:
return r[0]["moduleimpl_id"] return r[0]["moduleimpl_id"]
else: 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 # 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 """Formulaire création UE externe + inscription étudiant et saisie note
- Demande UE: peut-être existante (liste les UE externes de cette formation), - Demande UE: peut-être existante (liste les UE externes de cette formation),
ou sinon spécifier titre, acronyme, type, ECTS 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() html_footer = html_sco_header.sco_footer()
Fo = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] Fo = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"]) 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.sort()
ue_types_names = [sco_codes_parcours.UE_TYPE_NAME[k] for k in ue_types] ue_types_names = [sco_codes_parcours.UE_TYPE_NAME[k] for k in ue_types]
ue_types = [str(x) for x 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", "input_type": "menu",
"title": "UE externe existante:", "title": "UE externe existante:",
"allowed_values": [""] "allowed_values": [""]
+ [ue["ue_id"] for ue in existing_external_ue], + [str(ue["ue_id"]) for ue in existing_external_ue],
"labels": [default_label] "labels": [default_label]
+ [ + [
"%s (%s)" % (ue["titre"], ue["acronyme"]) "%s (%s)" % (ue["titre"], ue["acronyme"])
@ -337,7 +353,7 @@ def external_ue_create_form(formsemestre_id, etudid):
+ html_footer + html_footer
) )
if tf[2]["existing_ue"]: 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) moduleimpl_id = get_external_moduleimpl_id(formsemestre_id, ue_id)
else: else:
acronyme = tf[2]["acronyme"].strip() acronyme = tf[2]["acronyme"].strip()

View File

@ -191,7 +191,7 @@ def fmt_note(val, note_max=None, keep_numeric=False):
return "EXC" # excuse, note neutralise return "EXC" # excuse, note neutralise
if val == NOTES_ATTENTE: if val == NOTES_ATTENTE:
return "ATT" # attente, note neutralisee return "ATT" # attente, note neutralisee
if isinstance(val, float) or isinstance(val, int): if not isinstance(val, str):
if np.isnan(val): if np.isnan(val):
return "~" return "~"
if (note_max is not None) and note_max > 0: if (note_max is not None) and note_max > 0:

View File

@ -10,18 +10,21 @@ function toggle_new_ue_form(state) {
} }
$("#tf_extue_titre td:eq(1) input").prop("disabled", state); $("#tf_extue_titre td:eq(1) input").prop("disabled", state);
$("#tf_extue_titre td:eq(1) input").css('color', text_color) $("#tf_extue_titre").css('color', text_color)
$("#tf_extue_acronyme td:eq(1) input").prop("disabled", state); $("#tf_extue_acronyme td:eq(1) input").prop("disabled", state);
$("#tf_extue_acronyme td:eq(1) input").css('color', text_color) $("#tf_extue_acronyme").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 td:eq(1) input").prop("disabled", state);
$("#tf_extue_ects td:eq(1) input").css('color', text_color) $("#tf_extue_ects").css('color', text_color)
} }
function update_external_ue_form() { function update_external_ue_form() {
var state = (tf.existing_ue.value != "") var state = (tf.existing_ue.value != "");
toggle_new_ue_form(state); toggle_new_ue_form(state);
} }

View File

@ -15,13 +15,23 @@ $(function () {
}, },
{ {
name: "toggle_partitions", name: "toggle_partitions",
text: "Toutes les partitions", text: "Montrer groupes",
action: function (e, dt, node, config) { action: function (e, dt, node, config) {
let visible = dt.columns(".partition_aux").visible()[0]; let visible = dt.columns(".partition_aux").visible()[0];
dt.columns(".partition_aux").visible(!visible); 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")) { if (!$('table.table_recap').hasClass("jury")) {
buttons.push( buttons.push(
$('table.table_recap').hasClass("apc") ? $('table.table_recap').hasClass("apc") ?
@ -95,12 +105,12 @@ $(function () {
"columnDefs": [ "columnDefs": [
{ {
// cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides // 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, visible: false,
}, },
{ {
// Elimine les 0 à gauche pour les exports excel et les "copy" // 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) { render: function (data, type, row) {
return type === 'export' ? data.replace(/0(\d\..*)/, '$1') : data; return type === 'export' ? data.replace(/0(\d\..*)/, '$1') : data;
} }

View File

@ -45,7 +45,7 @@
{# Liste des permissions #} {# Liste des permissions #}
<div class="permissions"> <div class="permissions">
<p>Permissions de cet utilisateur dans le département {dept}:</p> <p>Permissions de cet utilisateur dans le département {{dept}}:</p>
<ul> <ul>
{% for p in Permission.description %} {% for p in Permission.description %}
<li>{{Permission.description[p]}} : <li>{{Permission.description[p]}} :

View File

@ -292,7 +292,7 @@ def formsemestre_bulletinetud(
format = format or "html" format = format or "html"
if not isinstance(formsemestre_id, int): 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) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if etudid: if etudid:
etud = models.Identite.query.get_or_404(etudid) etud = models.Identite.query.get_or_404(etudid)
@ -314,7 +314,7 @@ def formsemestre_bulletinetud(
) )
if format == "json": if format == "json":
return sco_bulletins.get_formsemestre_bulletin_etud_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": if formsemestre.formation.is_apc() and format == "html":
return render_template( 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"]) @bp.route("/formation_import_xml_form", methods=["GET", "POST"])
@scodoc @scodoc
@permission_required(Permission.ScoChangeFormation) @permission_required(Permission.ScoChangeFormation)

View File

@ -13,7 +13,7 @@ class Config:
SQLALCHEMY_DATABASE_URI = None # set in subclass SQLALCHEMY_DATABASE_URI = None # set in subclass
FLASK_ENV = 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 SQLALCHEMY_TRACK_MODIFICATIONS = False
LOG_TO_STDOUT = os.environ.get("LOG_TO_STDOUT") LOG_TO_STDOUT = os.environ.get("LOG_TO_STDOUT")
MAIL_SERVER = os.environ.get("MAIL_SERVER", "localhost") MAIL_SERVER = os.environ.get("MAIL_SERVER", "localhost")
@ -46,6 +46,7 @@ class Config:
class ProdConfig(Config): class ProdConfig(Config):
"mode production, normalement derrière nginx/gunicorn"
FLASK_ENV = "production" FLASK_ENV = "production"
DEBUG = False DEBUG = False
TESTING = False TESTING = False
@ -56,6 +57,7 @@ class ProdConfig(Config):
class DevConfig(Config): class DevConfig(Config):
"mode développement"
FLASK_ENV = "development" FLASK_ENV = "development"
DEBUG = True DEBUG = True
TESTING = False TESTING = False
@ -66,6 +68,7 @@ class DevConfig(Config):
class TestConfig(DevConfig): class TestConfig(DevConfig):
"Pour les tests unitaires"
TESTING = True TESTING = True
DEBUG = True DEBUG = True
SQLALCHEMY_DATABASE_URI = ( SQLALCHEMY_DATABASE_URI = (
@ -76,6 +79,19 @@ class TestConfig(DevConfig):
SECRET_KEY = os.environ.get("TEST_SECRET_KEY") or "c7ecff5db1594c208f573ff30e0f6bca" 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") mode = os.environ.get("FLASK_ENV", "production")
if mode == "production": if mode == "production":
RunningConfig = ProdConfig RunningConfig = ProdConfig
@ -83,3 +99,5 @@ elif mode == "development":
RunningConfig = DevConfig RunningConfig = DevConfig
elif mode == "test": elif mode == "test":
RunningConfig = TestConfig RunningConfig = TestConfig
elif mode == "test_api":
RunningConfig = TestAPIConfig

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.2.15" SCOVERSION = "9.2.24"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -496,12 +496,11 @@ def clear_cache(sanitize): # clear-cache
@app.cli.command() @app.cli.command()
def init_test_database(): def init_test_database(): # init-test-database
"""Initialise les objets en base pour les tests API """Initialise les objets en base pour les tests API
(à appliquer sur SCODOC_TEST ou SCODOC_DEV) (à appliquer sur SCODOC_TEST ou SCODOC_DEV)
""" """
click.echo("Initialisation base de test API...") click.echo("Initialisation base de test API...")
# import app as mapp # le package app
ctx = app.test_request_context() ctx = app.test_request_context()
ctx.push() ctx.push()

1
tests/api/__init__.py Normal file
View File

@ -0,0 +1 @@
# API tests

11
tests/api/dotenv_exemple Normal file
View File

@ -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

View File

@ -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). (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 from dotenv import load_dotenv
import os import os
import pdb
import requests import requests
import urllib3 import urllib3
from pprint import pprint as pp from pprint import pprint as pp
# --- Lecture configuration (variables d'env ou .env) # --- Lecture configuration (variables d'env ou .env)
try:
BASEDIR = os.path.abspath(os.path.dirname(__file__)) BASEDIR = os.path.abspath(os.path.dirname(__file__))
except NameError:
BASEDIR = "."
load_dotenv(os.path.join(BASEDIR, ".env")) load_dotenv(os.path.join(BASEDIR, ".env"))
CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) CHK_CERT = bool(int(os.environ.get("CHECK_CERTIFICATE", False)))
SCODOC_URL = os.environ["SCODOC_URL"] SCODOC_URL = os.environ["SCODOC_URL"] or "http://localhost:5000"
SCODOC_DEPT = os.environ["SCODOC_DEPT"] API_URL = SCODOC_URL + "/ScoDoc/api"
DEPT_URL = SCODOC_URL + "/ScoDoc/" + SCODOC_DEPT + "/Scolarite/"
SCODOC_USER = os.environ["SCODOC_USER"] 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"SCODOC_URL={SCODOC_URL}")
print(f"API URL={API_URL}")
# --- # ---
if not CHECK_CERTIFICATE: if not CHK_CERT:
urllib3.disable_warnings() urllib3.disable_warnings()
@ -48,9 +51,7 @@ class ScoError(Exception):
def GET(path: str, headers={}, errmsg=None): def GET(path: str, headers={}, errmsg=None):
"""Get and returns as JSON""" """Get and returns as JSON"""
r = requests.get( r = requests.get(API_URL + "/" + path, headers=headers or HEADERS, verify=CHK_CERT)
DEPT_URL + "/" + path, headers=headers or HEADERS, verify=CHECK_CERTIFICATE
)
if r.status_code != 200: if r.status_code != 200:
raise ScoError(errmsg or "erreur !") raise ScoError(errmsg or "erreur !")
return r.json() # decode la reponse JSON 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): def POST(s, path: str, data: dict, errmsg=None):
"""Post""" """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: if r.status_code != 200:
raise ScoError(errmsg or "erreur !") raise ScoError(errmsg or "erreur !")
return r.text return r.text
# --- Obtention du jeton (token) # --- Obtention du jeton (token)
r = requests.post( r = requests.post(API_URL + "/tokens", auth=(SCODOC_USER, SCODOC_PASSWORD))
SCODOC_URL + "/ScoDoc/api/tokens", auth=(SCODOC_USER, SCODOC_PASSWORD)
)
assert r.status_code == 200 assert r.status_code == 200
token = r.json()["token"] token = r.json()["token"]
HEADERS = {"Authorization": f"Bearer {token}"} HEADERS = {"Authorization": f"Bearer {token}"}
r = requests.get( r = requests.get(API_URL + "/departements", headers=HEADERS, verify=CHK_CERT)
SCODOC_URL + "/ScoDoc/api/list_depts",
headers=HEADERS,
verify=CHECK_CERTIFICATE,
)
if r.status_code != 200: if r.status_code != 200:
raise ScoError("erreur de connexion: vérifier adresse et identifiants") raise ScoError("erreur de connexion: vérifier adresse et identifiants")
pp(r.json()) pp(r.json())
# Liste des tous les étudiants en cours (de tous les depts) # Liste de tous les étudiants en cours (de tous les depts)
r = requests.get( r = requests.get(API_URL + "/etudiants/courant", headers=HEADERS, verify=CHK_CERT)
SCODOC_URL + "/ScoDoc/api/etudiants/courant",
headers=HEADERS,
verify=CHECK_CERTIFICATE,
)
if r.status_code != 200: if r.status_code != 200:
raise ScoError("erreur de connexion: vérifier adresse et identifiants") 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: # # --- Recupere la liste de tous les semestres:
# sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !") # sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !")
@ -146,15 +167,3 @@ if r.status_code != 200:
# print( # print(
# f"Pour vérifier, aller sur: {DEPT_URL}/Notes/moduleimpl_status?moduleimpl_id={mod['moduleimpl_id']}", # 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",
# },
# )

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Test Logos """Test API
Utilisation : Utilisation :
créer les variables d'environnement: (indiquer les valeurs créer les variables d'environnement: (indiquer les valeurs
pour le serveur ScoDoc que vous voulez interroger) pour le serveur ScoDoc que vous voulez interroger)
export SCODOC_URL="https://scodoc.xxx.net/" export SCODOC_URL="https://scodoc.xxx.net/"
export SCODOC_USER="xxx" export API_USER="xxx"
export SCODOC_PASSWD="xxx" export SCODOC_PASSWD="xxx"
export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valide export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valide
@ -15,23 +15,26 @@ Utilisation :
""" """
import os import os
import requests 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" @pytest.fixture
SCODOC_PASSWORD = "test" def api_headers() -> dict:
SCODOC_URL = "http://192.168.1.12:5000"
CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False)))
def get_token():
""" """
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( r0 = requests.post(API_URL + "/tokens", auth=(API_USER, API_PASSWORD))
SCODOC_URL + "/ScoDoc/api/tokens", auth=(SCODOC_USER, SCODOC_PASSWORD)
)
token = r0.json()["token"] token = r0.json()["token"]
return {"Authorization": f"Bearer {token}"} return {"Authorization": f"Bearer {token}"}
HEADERS = get_token()

View File

@ -18,63 +18,47 @@ Utilisation :
""" """
import requests 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 # absences
def test_absences(): def test_absences(api_headers):
"""
Route: /absences/etudid/<int:etudid>
"""
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/absences/etudid/<int:etudid>", f"{API_URL}/absences/etudid/{ETUDID}",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE,
)
assert r.status_code == 200
r = requests.get(
SCODOC_URL + "/ScoDoc/api/absences/nip/<int:nip>",
headers=HEADERS,
verify=CHECK_CERTIFICATE,
)
assert r.status_code == 200
r = requests.get(
SCODOC_URL + "/ScoDoc/api/absences/ine/<int:ine>",
headers=HEADERS,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
# absences_justify # absences_justify
def test_absences_justify(): def test_absences_justify(api_headers):
"""
Route: /absences/etudid/<etudid:int>/just
"""
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/absences/etudid/1/just", API_URL + f"/absences/etudid/{ETUDID}/just",
headers=HEADERS, headers=api_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,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
# TODO vérifier résultat
# abs_groupe_etat # XXX TODO
def test_abs_groupe_etat(): # def test_abs_groupe_etat(api_headers):
r = requests.get( # """
SCODOC_URL # Route:
+ "/ScoDoc/api/absences/abs_group_etat/?group_id=<int:group_id>&date_debut=date_debut&date_fin=date_fin", # """
headers=HEADERS, # r = requests.get(
verify=CHECK_CERTIFICATE, # API_URL + "/absences/abs_group_etat/?group_id=<int:group_id>&date_debut=date_debut&date_fin=date_fin",
) # headers=api_headers,
assert r.status_code == 200 # verify=CHECK_CERTIFICATE,
# )
# assert r.status_code == 200

View File

@ -19,12 +19,15 @@ Utilisation :
import requests 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 verify_fields
# departements DEPARTEMENT_FIELDS = [
def test_departements():
fields = [
"id", "id",
"acronym", "acronym",
"description", "description",
@ -32,81 +35,73 @@ def test_departements():
"date_creation", "date_creation",
] ]
def test_departements(api_headers):
""" "
Routes: /departements_ids, /departement, /departement/<string:dept>/formsemestres_ids
"""
# --- Liste des ids
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/departements", API_URL + "/departements_ids",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 1 departements_ids = r.json()
assert isinstance(departements_ids, list)
dept = r.json()[0] assert len(departements_ids) > 0
assert all(isinstance(x, int) for x in departements_ids)
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",
]
dept_id = departements_ids[0]
# --- Infos sur un département, accès par id
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/departements/TAPI/etudiants/liste", f"{API_URL}/departement/{dept_id}",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
etu = r.json()[0]
fields_OK = verify_fields(etu, fields)
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 16 dept_a = r.json()
assert fields_OK is True assert verify_fields(dept_a, DEPARTEMENT_FIELDS) is True
# --- Infos sur un département, accès par acronyme4
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/departements/TAPI/etudiants/liste/1", f"{API_URL}/departement/{dept_a['acronym']}",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
etu = r.json()[0]
fields_OK = verify_fields(etu, fields)
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 16 dept_b = r.json()
assert fields_OK is True 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 # liste_semestres_courant
def test_semestres_courant(): def test_semestres_courant(api_headers):
fields = [ fields = [
"titre", "titre",
"gestion_semestrielle", "gestion_semestrielle",
@ -130,32 +125,39 @@ def test_semestres_courant():
"block_moyennes", "block_moyennes",
"formsemestre_id", "formsemestre_id",
"titre_num", "titre_num",
"titre_formation",
"date_debut_iso", "date_debut_iso",
"date_fin_iso", "date_fin_iso",
"responsables", "responsables",
] ]
dept_id = 1
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/departements/TAPI/semestres_courants", f"{API_URL}/departement/{dept_id}",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
sem = r.json()[0]
fields_OK = verify_fields(sem, fields)
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 1 dept = r.json()
assert fields_OK is True assert dept["id"] == dept_id
# Accès via acronyme
# referenciel_competences
def test_referenciel_competences():
r = requests.get( r = requests.get(
SCODOC_URL f"{API_URL}/departement/{dept['acronym']}/formsemestres_courants",
+ "/ScoDoc/api/departements/TAPI/formations/1/referentiel_competences", headers=api_headers,
headers=HEADERS,
verify=CHECK_CERTIFICATE, 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

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Test Logos """Test API: accès aux étudiants
Utilisation : Utilisation :
créer les variables d'environnement: (indiquer les valeurs créer les variables d'environnement: (indiquer les valeurs
@ -16,281 +16,222 @@ Utilisation :
Lancer : Lancer :
pytest tests/api/test_api_etudiants.py pytest tests/api/test_api_etudiants.py
""" """
from random import randint
import requests 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 verify_fields
from tests.api.tools_test_api import ETUD_FIELDS, FSEM_FIELDS
# etudiants_courant
def test_etudiants_courant():
fields = [ def test_etudiants_courant(api_headers):
"id", """
"nip", Route: /etudiants/courant
"nom", """
"nom_usuel", fields = {"id", "nip", "ine", "nom", "nom_usuel", "prenom", "civilite"}
"prenom",
"civilite",
]
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiants/courant", API_URL + "/etudiants/courant",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 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 etud = etudiants[-1]
etu = r.json()[randint(0, len(r.json())) - 1] assert verify_fields(etud, fields) is True
fields_OK = verify_fields(etu, fields)
assert fields_OK is True
########## Version long ################ ########## 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",
]
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiants/courant/long", API_URL + "/etudiants/courant/long",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 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 etud = etudiants[-1]
etu = r.json()[randint(0, len(r.json())) - 1] assert verify_fields(etud, ETUD_FIELDS) is True
fields_OK = verify_fields(etu, fields_long)
assert fields_OK is True
# etudiant def test_etudiant(api_headers):
def test_etudiant(): """
Routes: /etudiant/etudid, /etudiant/nip, /etudiant/ine
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",
]
######### Test etudid ######### ######### Test etudid #########
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiant/etudid/1", API_URL + "/etudiant/etudid/1",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 24 etud = r.json()
etu = r.json() assert verify_fields(etud, ETUD_FIELDS) is True
fields_OK = verify_fields(etu, fields)
assert fields_OK is True
######### Test code nip ######### ######### Test code nip #########
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiant/nip/1", API_URL + "/etudiant/nip/1",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 24 etud = r.json()
fields_ok = verify_fields(etud, ETUD_FIELDS)
etu = r.json() assert fields_ok is True
assert etud["dept_acronym"] == DEPT_ACRONYM
fields_OK = verify_fields(etu, fields)
assert fields_OK is True
######### Test code ine ######### ######### Test code ine #########
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiant/ine/1", API_URL + "/etudiant/ine/INE1",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 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() # Vérifie le requetage des 3 1er étudiants
for etudid in (1, 2, 3):
fields_OK = verify_fields(etu, fields) r = requests.get(
f"{API_URL }/etudiant/etudid/{etudid}",
assert fields_OK is True 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(api_headers):
def test_etudiant_formsemestres(): """
Route: /etudiant/etudid/<etudid:int>/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",
]
######### Test etudid ######### ######### Test etudid #########
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiant/etudid/1/formsemestres", API_URL + "/etudiant/etudid/1/formsemestres",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 1 formsemestres = r.json()
assert len(formsemestres) == 1
formsemestre = r.json()[0] formsemestre = formsemestres[0]
assert verify_fields(formsemestre, FSEM_FIELDS) is True
fields_OK = verify_fields(formsemestre, fields)
assert fields_OK is True
######### Test code nip ######### ######### Test code nip #########
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiant/nip/1/formsemestres", API_URL + "/etudiant/nip/1/formsemestres",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 1 formsemestres = r.json()
assert len(formsemestres) == 1
formsemestre = r.json()[0] formsemestre = formsemestres[0]
assert verify_fields(formsemestre, FSEM_FIELDS) is True
fields_OK = verify_fields(formsemestre, fields)
assert fields_OK is True
######### Test code ine ######### ######### Test code ine #########
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiant/ine/1/formsemestres", API_URL + "/etudiant/ine/INE1/formsemestres",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 1 formsemestres = r.json()
assert len(formsemestres) == 1
formsemestre = r.json()[0] formsemestre = formsemestres[0]
assert verify_fields(formsemestre, FSEM_FIELDS) is True
fields_OK = verify_fields(formsemestre, fields)
assert fields_OK is True
# etudiant_bulletin_semestre def test_etudiant_bulletin_semestre(api_headers):
def test_etudiant_bulletin_semestre(): """
Route: /etudiant/etudid/<etudid>/formsemestre/<formsemestre_id>/bulletin
"""
######### Test etudid ######### ######### Test etudid #########
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiant/etudid/1/formsemestre/1/bulletin", API_URL + "/etudiant/etudid/1/formsemestre/1/bulletin",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 13 bul = r.json()
assert len(bul) == 13 # HARDCODED
######### Test code nip ######### ######### Test code nip #########
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiant/nip/1/formsemestre/1/bulletin", API_URL + "/etudiant/nip/1/formsemestre/1/bulletin",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 13 bul = r.json()
assert len(bul) == 13 # HARDCODED
######### Test code ine ######### ######### Test code ine #########
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiant/ine/1/formsemestre/1/bulletin", API_URL + "/etudiant/ine/INE1/formsemestre/1/bulletin",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 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(api_headers):
def test_etudiant_groups(): """
Route:
/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups
"""
fields = [ fields = [
"partition_id", "partition_id",
"id", "id",
@ -306,47 +247,39 @@ def test_etudiant_groups():
######### Test etudid ######### ######### Test etudid #########
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiant/etudid/1/semestre/1/groups", API_URL + "/etudiant/etudid/1/formsemestre/1/groups",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 1 groups = r.json()
assert len(groups) == 1 # dans un seul groupe
groups = r.json()[0] group = groups[0]
fields_ok = verify_fields(group, fields)
fields_OK = verify_fields(groups, fields) assert fields_ok is True
assert fields_OK is True
######### Test code nip ######### ######### Test code nip #########
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiant/nip/1/semestre/1/groups", API_URL + "/etudiant/nip/1/formsemestre/1/groups",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 1 groups = r.json()
assert len(groups) == 1 # dans un seul groupe
groups = r.json()[0] group = groups[0]
fields_ok = verify_fields(group, fields)
fields_OK = verify_fields(groups, fields) assert fields_ok is True
assert fields_OK is True
######### Test code ine ######### ######### Test code ine #########
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiant/ine/1/semestre/1/groups", API_URL + "/etudiant/ine/INE1/formsemestre/1/groups",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 1 groups = r.json()
assert len(groups) == 1 # dans un seul groupe
groups = r.json()[0] group = groups[0]
fields_ok = verify_fields(group, fields)
fields_OK = verify_fields(groups, fields) assert fields_ok is True
assert fields_OK is True

View File

@ -19,23 +19,31 @@ Utilisation :
import requests 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/<int:moduleimpl_id>
"""
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/evaluations/1", API_URL + "/evaluations/1",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
# TODO
# evaluation_notes # TODO car pas d'évaluations créées à ce stade
def test_evaluation_notes(): # def test_evaluation_notes(api_headers):
r = requests.get( # """
SCODOC_URL + "/ScoDoc/api/evaluations/eval_notes/1", # Route: /evaluation/eval_notes/<int:evaluation_id>
headers=HEADERS, # """
verify=CHECK_CERTIFICATE, # r = requests.get(
) # API_URL + "/evaluation/eval_notes/1",
assert r.status_code == 200 # headers=api_headers,
# verify=CHECK_CERTIFICATE,
# )
# assert r.status_code == 200
# # TODO

View File

@ -19,163 +19,91 @@ Utilisation :
import requests 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 verify_fields
from tests.api.tools_test_api import FORMATION_FIELDS, MODIMPL_FIELDS
# formations def test_formations_ids(api_headers):
def test_formations(): """
fields = [ Route: /formations_ids
"id", """
"acronyme",
"titre_officiel",
"formation_code",
"code_specialite",
"dept_id",
"titre",
"version",
"type_parcours",
"referentiel_competence_id",
"formation_id",
]
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/formations", API_URL + "/formations_ids",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
formation = r.json()[0]
fields_OK = verify_fields(formation, fields)
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 1 formations_ids = r.json()
assert fields_OK is True # 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(api_headers):
def test_formations_by_id(): """
fields = [ Route: /formation/<int:formation_id>
"id", """
"acronyme",
"titre_officiel",
"formation_code",
"code_specialite",
"dept_id",
"titre",
"version",
"type_parcours",
"referentiel_competence_id",
"formation_id",
]
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/formations/1", API_URL + "/formation/1",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200
formation = r.json() formation = r.json()
assert verify_fields(formation, FORMATION_FIELDS) is True
fields_OK = verify_fields(formation, fields) # TODO tester le contenu de certains champs
assert r.status_code == 200
assert fields_OK is True
# formation_export_by_formation_id def test_formation_export(api_headers):
def test_formation_export_by_formation_id(): """
fields = [ Route: /formation/formation_export/<int:formation_id>
"id", """
"acronyme",
"titre_officiel",
"formation_code",
"code_specialite",
"dept_id",
"titre",
"version",
"type_parcours",
"referentiel_competence_id",
"formation_id",
"ue",
]
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/formations/formation_export/1", API_URL + "/formation/formation_export/1",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
export_formation = r.json()
fields_OK = verify_fields(export_formation, fields)
assert r.status_code == 200 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 # TODO
# def test_formsemestre_apo(): # def test_formsemestre_apo(api_headers):
# r = requests.get( # r = requests.get(
# SCODOC_URL + "/ScoDoc/api/formations/apo/<string:etape_apo>", # API_URL + "/formation/apo/<string:etape_apo>",
# headers=HEADERS, # headers=api_headers,
# verify=CHECK_CERTIFICATE, # verify=CHECK_CERTIFICATE,
# ) # )
# assert r.status_code == 200 # assert r.status_code == 200
# moduleimpl def test_moduleimpl(api_headers):
def test_moduleimpl(): """
Route: /formation/moduleimpl/<int:moduleimpl_id>
fields = [ """
"id",
"formsemestre_id",
"computation_expr",
"module_id",
"responsable_id",
"moduleimpl_id",
"ens",
"module",
]
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/formations/moduleimpl/1", API_URL + "/formation/moduleimpl/1",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200
moduleimpl = r.json() moduleimpl = r.json()
assert verify_fields(moduleimpl, MODIMPL_FIELDS) is True
fields_OK = verify_fields(moduleimpl, fields) # TODO tester le contenu de certains champs
assert r.status_code == 200
assert fields_OK is True
# moduleimpls_sem def test_referentiel_competences(api_headers):
def test_moduleimpls_sem(): """
Route: "/formation/<int:formation_id>/referentiel_competences",
fields = [ """
"id",
"formsemestre_id",
"computation_expr",
"module_id",
"responsable_id",
"moduleimpl_id",
"ens",
"module",
"moduleimpl_id",
"ens",
]
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/formations/moduleimpl/formsemestre/1/liste", API_URL + "/formation/1/referentiel_competences",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
moduleimpl = r.json()[0]
fields_OK = verify_fields(moduleimpl, fields)
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 21 # XXX A compléter
assert fields_OK is True

View File

@ -18,83 +18,74 @@ Utilisation :
""" """
import requests import requests
from app.api.formsemestres import formsemestre
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 MODIMPL_FIELDS, verify_fields
from tests.api.tools_test_api import FSEM_FIELDS, UE_FIELDS, MODULE_FIELDS
# formsemestre # Etudiant pour les tests
def test_formsemestre(): ETUDID = 1
NIP = "1"
INE = "INE1"
def test_formsemestre(api_headers):
"""
Route: /formsemestre/<id>
"""
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/formsemestre/1", API_URL + "/formsemestre/1",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
formsemestre = r.json() formsemestre = r.json()
assert verify_fields(formsemestre, FSEM_FIELDS)
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
# etudiant_bulletin def test_etudiant_bulletin(api_headers):
def test_etudiant_bulletin(): """
Route:
"""
formsemestre_id = 1
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/formsemestre/1/etudiant/etudid/1/bulletin", f"{API_URL}/etudiant/etudid/1/formsemestre/{formsemestre_id}/bulletin",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
bull_a = r.json()
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/formsemestre/1/etudiant/nip/1/bulletin", f"{API_URL}/etudiant/nip/{NIP}/formsemestre/{formsemestre_id}/bulletin",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
bull_b = r.json()
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/formsemestre/1/etudiant/ine/1/bulletin", f"{API_URL}/etudiant/ine/{INE}/formsemestre/{formsemestre_id}/bulletin",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 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(api_headers):
def test_bulletins(): """
Route:
"""
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/formsemestre/1/bulletins", API_URL + "/formsemestre/1/bulletins",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
@ -103,88 +94,40 @@ def test_bulletins():
# # jury # # jury
# def test_jury(): # def test_jury():
# r = requests.get( # r = requests.get(
# SCODOC_URL + "/ScoDoc/api/formsemestre/1/jury", # API_URL + "/formsemestre/1/jury",
# headers=HEADERS, # headers=api_headers,
# verify=CHECK_CERTIFICATE, # verify=CHECK_CERTIFICATE,
# ) # )
# assert r.status_code == 200 # assert r.status_code == 200
# semestre_index
def test_semestre_index():
ue_fields = [ def test_formsemestre_programme(api_headers):
"semestre_idx", """
"type", Route: /formsemestre/1/programme
"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",
]
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/formsemestre/1/programme", API_URL + "/formsemestre/1/programme",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 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] assert verify_fields(ue, UE_FIELDS)
ressource = r.json()["ressources"][0] if len(modules) > 1:
sae = r.json()["saes"][0] assert verify_fields(modules[0], MODIMPL_FIELDS)
assert verify_fields(ressource, MODIMPL_FIELDS)
fields_ue_OK = verify_fields(ue, ue_fields) assert verify_fields(sae, MODIMPL_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

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Test Logos """Test API Jurys XXX TODO A ECRIRE
Utilisation : Utilisation :
créer les variables d'environnement: (indiquer les valeurs créer les variables d'environnement: (indiquer les valeurs
@ -19,37 +19,41 @@ Utilisation :
import requests 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( r = requests.get(
SCODOC_URL SCODOC_URL
+ "/ScoDoc/api/jury/formsemestre/<int:formsemestre_id>/preparation_jury", + "/ScoDoc/api/jury/formsemestre/<int:formsemestre_id>/preparation_jury",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
# jury_decisions def test_jury_decisions(api_headers):
def test_jury_decisions(): """
Route:
"""
r = requests.get( r = requests.get(
SCODOC_URL API_URL + "/jury/formsemestre/<int:formsemestre_id>/decisions_jury",
+ "/ScoDoc/api/jury/formsemestre/<int:formsemestre_id>/decisions_jury", headers=api_headers,
headers=HEADERS,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
# set_decision_jury # set_decision_jury
def test_set_decision_jury(): def test_set_decision_jury(api_headers):
r = requests.get( r = requests.get(
SCODOC_URL SCODOC_URL
+ "/ScoDoc/api/jury/set_decision/etudid?etudid=<int:etudid>&formsemestre_id=<int:formesemestre_id>" + "/ScoDoc/api/jury/set_decision/etudid?etudid=<int:etudid>&formsemestre_id=<int:formesemestre_id>"
"&jury=<string:decision_jury>&devenir=<string:devenir_jury>&assiduite=<bool>", "&jury=<string:decision_jury>&devenir=<string:devenir_jury>&assiduite=<bool>",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
@ -58,7 +62,7 @@ def test_set_decision_jury():
SCODOC_URL SCODOC_URL
+ "/ScoDoc/api/jury/set_decision/nip?etudid=<int:etudid>&formsemestre_id=<int:formesemestre_id>" + "/ScoDoc/api/jury/set_decision/nip?etudid=<int:etudid>&formsemestre_id=<int:formesemestre_id>"
"&jury=<string:decision_jury>&devenir=<string:devenir_jury>&assiduite=<bool>", "&jury=<string:decision_jury>&devenir=<string:devenir_jury>&assiduite=<bool>",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
@ -67,34 +71,36 @@ def test_set_decision_jury():
SCODOC_URL SCODOC_URL
+ "/ScoDoc/api/jury/set_decision/ine?etudid=<int:etudid>&formsemestre_id=<int:formesemestre_id>" + "/ScoDoc/api/jury/set_decision/ine?etudid=<int:etudid>&formsemestre_id=<int:formesemestre_id>"
"&jury=<string:decision_jury>&devenir=<string:devenir_jury>&assiduite=<bool>", "&jury=<string:decision_jury>&devenir=<string:devenir_jury>&assiduite=<bool>",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
# annule_decision_jury # def test_annule_decision_jury(api_headers):
def test_annule_decision_jury(): # """
r = requests.get( # Route:
SCODOC_URL # """
+ "/ScoDoc/api/jury/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/annule_decision", # r = requests.get(
headers=HEADERS, # SCODOC_URL
verify=CHECK_CERTIFICATE, # + "/ScoDoc/api/jury/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/annule_decision",
) # headers=api_headers,
assert r.status_code == 200 # verify=CHECK_CERTIFICATE,
# )
# assert r.status_code == 200
r = requests.get( # r = requests.get(
SCODOC_URL # SCODOC_URL
+ "/ScoDoc/api/jury/nip/<int:nip>/formsemestre/<int:formsemestre_id>/annule_decision", # + "/ScoDoc/api/jury/nip/<int:nip>/formsemestre/<int:formsemestre_id>/annule_decision",
headers=HEADERS, # headers=api_headers,
verify=CHECK_CERTIFICATE, # verify=CHECK_CERTIFICATE,
) # )
assert r.status_code == 200 # assert r.status_code == 200
r = requests.get( # r = requests.get(
SCODOC_URL # SCODOC_URL
+ "/ScoDoc/api/jury/ine/<int:ine>/formsemestre/<int:formsemestre_id>/annule_decision", # + "/ScoDoc/api/jury/ine/<int:ine>/formsemestre/<int:formsemestre_id>/annule_decision",
headers=HEADERS, # headers=api_headers,
verify=CHECK_CERTIFICATE, # verify=CHECK_CERTIFICATE,
) # )
assert r.status_code == 200 # assert r.status_code == 200

View File

@ -5,11 +5,16 @@
"""Exemple utilisation API ScoDoc 9 avec jeton obtenu par basic authentication """Exemple utilisation API ScoDoc 9 avec jeton obtenu par basic authentication
utilisation: 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 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 scodoc import app
from tests.unit.config_test_logos import ( 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): def test_super_access(create_super_token):
"""
Route:
"""
dept1, dept2, dept3, token = create_super_token dept1, dept2, dept3, token = create_super_token
HEADERS = {"Authorization": f"Bearer {token}"} headers = {"Authorization": f"Bearer {token}"}
with app.test_client() as client: 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.status_code == 200
assert response.json is not None assert response.json is not None
def test_admin_access(create_admin_token): def test_admin_access(create_admin_token):
"""
Route:
"""
dept1, dept2, dept3, token = create_admin_token dept1, dept2, dept3, token = create_admin_token
headers = {"Authorization": f"Bearer {token}"} headers = {"Authorization": f"Bearer {token}"}
with app.test_client() as client: 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 assert response.status_code == 401
def test_lambda_access(create_lambda_token): def test_lambda_access(create_lambda_token):
"""
Route:
"""
dept1, dept2, dept3, token = create_lambda_token dept1, dept2, dept3, token = create_lambda_token
headers = {"Authorization": f"Bearer {token}"} headers = {"Authorization": f"Bearer {token}"}
with app.test_client() as client: 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 assert response.status_code == 401
def test_initial_with_header_and_footer(create_super_token): def test_initial_with_header_and_footer(create_super_token):
"""
Route:
"""
dept1, dept2, dept3, token = create_super_token dept1, dept2, dept3, token = create_super_token
headers = {"Authorization": f"Bearer {token}"} headers = {"Authorization": f"Bearer {token}"}
with app.test_client() as client: 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.status_code == 200
assert response.json is not None assert response.json is not None
assert len(response.json) == 7 assert len(response.json) == 7

View File

@ -19,12 +19,14 @@ Utilisation :
import requests 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 verify_fields
# partition def test_partition(api_headers):
def test_partition(): """
Route:
"""
fields = [ fields = [
"partition_id", "partition_id",
"id", "id",
@ -36,23 +38,22 @@ def test_partition():
] ]
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/partitions/1", API_URL + "/partitions/1",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
partition = r.json()[0]
fields_OK = verify_fields(partition, fields)
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 2 partitions = r.json()
assert fields_OK is True 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(api_headers):
def test_etud_in_group(): """
Route:
"""
fields = [ fields = [
"etudid", "etudid",
"id", "id",
@ -92,33 +93,36 @@ def test_etud_in_group():
] ]
r = requests.get( r = requests.get(
SCODOC_URL + "/ScoDoc/api/partitions/groups/1", API_URL + "/partitions/groups/1",
headers=HEADERS, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
etu = r.json()[0] etu = r.json()[0]
fields_OK = verify_fields(etu, fields) fields_ok = verify_fields(etu, fields)
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 16 assert len(r.json()) == 16
assert fields_OK is True assert fields_ok is True
# r = requests.get( # r = requests.get(
# SCODOC_URL + "/ScoDoc/api/partitions/groups/1/etat/<string:etat>", # API_URL + "/partitions/groups/1/etat/<string:etat>",
# headers=HEADERS, # headers=api_headers,
# verify=CHECK_CERTIFICATE, # verify=CHECK_CERTIFICATE,
# ) # )
# assert r.status_code == 200 # assert r.status_code == 200
# # set_groups # # set_groups
# def test_set_groups(): # def test_set_groups(api_headers):
# """
# Route:
# """
# r = requests.get( # r = requests.get(
# SCODOC_URL # SCODOC_URL
# + "/partitions/set_groups/partition/<int:partition_id>/groups/<string:groups_id>" # + "/partitions/set_groups/partition/<int:partition_id>/groups/<string:groups_id>"
# "/delete/<string:groups_to_delete>/create/<string:groups_to_create>", # "/delete/<string:groups_to_delete>/create/<string:groups_to_create>",
# headers=HEADERS, # headers=api_headers,
# verify=CHECK_CERTIFICATE, # verify=CHECK_CERTIFICATE,
# ) # )
# assert r.status_code == 200 # assert r.status_code == 200

View File

@ -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

View File

@ -1,13 +1,133 @@
def verify_fields(json_response, fields): """Utilitaires pour les tests de l'API
""" """
Vérifie si les champs de la réponse json sont corrects
def verify_fields(json_response: dict, expected_fields: set) -> bool:
"""
Vérifie si les champs attendu de la réponse json sont présents
json_response : la réponse de la requête 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 Retourne True ou False
""" """
for field in json_response: return all(field in json_response for field in expected_fields)
if field not in fields:
return False
return True 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",
}

View File

@ -9,8 +9,16 @@ die() {
echo echo
exit 1 exit 1
} }
[ $# = 1 ] || die "Usage $0 db_name" [ $# = 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" db_name="$1"
fi
# Le répertoire de ce script: # Le répertoire de ce script:
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"

View File

@ -5,60 +5,59 @@
Création des départements, formations, semestres, étudiants, groupes... Création des départements, formations, semestres, étudiants, groupes...
utilisation: utilisation:
1) modifier le .env pour indiquer 1) modifier /opt/scodoc/.env pour indiquer
SCODOC_DATABASE_URI="postgresql:///SCO_TEST_API" FLASK_ENV=test_api
FLASK_DEBUG=1
2) En tant qu'utilisateur scodoc, lancer: 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 db upgrade
flask sco-db-init --erase flask sco-db-init --erase
flask init-test-database 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: 3) relancer ScoDoc:
flask run --host 0.0.0.0 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 datetime
import random import random
import sys
random.seed(12345678) # tests reproductibles from app.auth.models import Role, User
from flask_login import login_user
from app import auth
from app import models from app import models
from app.models import Departement, Formation, FormSemestre, Identite
from app import db from app import db
from app.scodoc import ( from app.scodoc import (
sco_cache,
sco_evaluation_db,
sco_formations, sco_formations,
sco_formsemestre,
sco_formsemestre_inscriptions, sco_formsemestre_inscriptions,
sco_groups, sco_groups,
) )
from app.scodoc.sco_permissions import Permission
from tools.fakeportal.gen_nomprenoms import nomprenom from tools.fakeportal.gen_nomprenoms import nomprenom
random.seed(12345678) # tests reproductibles
# La formation à utiliser: # La formation à utiliser:
FORMATION_XML_FILENAME = "tests/ressources/formations/scodoc_formation_RT_BUT_RT_v1.xml" 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." "Create dept, and switch context into it."
import app as mapp import app as mapp
dept = models.Departement(acronym=acronym) dept = Departement(acronym=acronym)
db.session.add(dept) db.session.add(dept)
mapp.set_sco_dept(acronym) mapp.set_sco_dept(acronym)
db.session.commit() db.session.commit()
return dept return dept
def import_formation() -> models.Formation: def import_formation() -> Formation:
"""Import formation from XML. """Import formation from XML.
Returns formation_id Returns formation_id
""" """
@ -66,28 +65,48 @@ def import_formation() -> models.Formation:
doc = f.read() doc = f.read()
# --- Création de la formation # --- Création de la formation
f = sco_formations.formation_import_xml(doc) f = sco_formations.formation_import_xml(doc)
return models.Formation.query.get(f[0]) return Formation.query.get(f[0])
def create_user(dept): def create_users(dept: Departement) -> tuple:
"""créé les utilisaterurs nécessaires aux tests""" """créé les utilisateurs nécessaires aux tests"""
user = auth.models.User( # Un utilisateur "test" (passwd test) pouvant lire l'API
user_name="test", nom="Doe", prenom="John", dept=dept.acronym user = User(user_name="test", nom="Doe", prenom="John", dept=dept.acronym)
) user.set_password("test")
db.session.add(user) 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() db.session.commit()
return user return user, other
def create_fake_etud(dept): def create_fake_etud(dept: Departement) -> Identite:
"""Créé un faux étudiant et l'insère dans la base""" """Créé un faux étudiant et l'insère dans la base."""
civilite = random.choice(("M", "F", "X")) civilite = random.choice(("M", "F", "X"))
nom, prenom = nomprenom(civilite) 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.add(etud)
db.session.commit() db.session.commit()
etud.code_nip = etud.id # créé un étudiant sur deux avec un NIP et INE alphanumérique
etud.code_ine = etud.id 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.add(etud)
db.session.commit() db.session.commit()
adresse = models.Adresse( adresse = models.Adresse(
@ -100,14 +119,18 @@ def create_fake_etud(dept):
return etud return etud
def create_etuds(dept, nb=16): def create_etuds(dept: Departement, nb=16) -> list:
"create nb etuds" "create nb etuds"
return [create_fake_etud(dept) for _ in range(nb)] return [create_fake_etud(dept) for _ in range(nb)]
def create_formsemestre(formation, user, semestre_idx=1): def create_formsemestre(
"""Create formsemestre and moduleimpls""" formation: Formation, responsable: User, semestre_idx=1
formsemestre = models.FormSemestre( ) -> FormSemestre:
"""Create formsemestre and moduleimpls
responsable: resp. du formsemestre
"""
formsemestre = FormSemestre(
dept_id=formation.dept_id, dept_id=formation.dept_id,
semestre_id=semestre_idx, semestre_id=semestre_idx,
titre="Semestre test", titre="Semestre test",
@ -121,7 +144,9 @@ def create_formsemestre(formation, user, semestre_idx=1):
# Crée un modulimpl par module de ce semestre: # Crée un modulimpl par module de ce semestre:
for module in formation.modules.filter_by(semestre_id=semestre_idx): for module in formation.modules.filter_by(semestre_id=semestre_idx):
modimpl = models.ModuleImpl( 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.add(modimpl)
db.session.commit() db.session.commit()
@ -132,7 +157,7 @@ def create_formsemestre(formation, user, semestre_idx=1):
return formsemestre return formsemestre
def inscrit_etudiants(etuds, formsemestre): def inscrit_etudiants(etuds: list, formsemestre: FormSemestre):
"""Inscrit les etudiants aux semestres et à tous ses modules""" """Inscrit les etudiants aux semestres et à tous ses modules"""
for etud in etuds: for etud in etuds:
sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
@ -144,13 +169,37 @@ 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") dept = init_departement("TAPI")
user = create_user(dept) user_lecteur, user_autre = create_users(dept)
with sco_cache.DeferredSemCacheManager():
etuds = create_etuds(dept) etuds = create_etuds(dept)
formation = import_formation() formation = import_formation()
formsemestre = create_formsemestre(formation, user) formsemestre = create_formsemestre(formation, user_lecteur)
create_evaluations(formsemestre)
inscrit_etudiants(etuds, formsemestre) inscrit_etudiants(etuds, formsemestre)
# à compléter # à compléter
# - groupes # - groupes