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

This commit is contained in:
Emmanuel Viennet 2022-05-21 07:41:09 +02:00
commit 539041fd0d
38 changed files with 740 additions and 454 deletions

View File

@ -5,73 +5,62 @@ 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_auth, token_permission_required from app.api.auth import token_auth, token_permission_required
from app.api.tools import get_etud_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/<string:nip>", methods=["GET"])
@bp.route("/absences/ine/<string:ine>", methods=["GET"])
@token_auth.login_required @token_auth.login_required
@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:
[ [
{ {
"jour": "2022-04-15", "jour": "2022-04-15",
"matin": true, "matin": true,
"estabs": true, "estabs": true,
"estjust": true, "estjust": true,
"description": "", "description": "",
"begin": "2022-04-15 08:00:00", "begin": "2022-04-15 08:00:00",
"end": "2022-04-15 11:59:59" "end": "2022-04-15 11:59:59"
}, },
{ {
"jour": "2022-04-15", "jour": "2022-04-15",
"matin": false, "matin": false,
"estabs": true, "estabs": true,
"estjust": false, "estjust": false,
"description": "", "description": "",
"begin": "2022-04-15 12:00:00", "begin": "2022-04-15 12:00:00",
"end": "2022-04-15 17:59:59" "end": "2022-04-15 17:59:59"
} }
] ]
""" """
if etudid is None: etud = Identite.query.get(etudid)
# Récupération de l'étudiant if etud is None:
etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) return error_response(
if etud is None: 404,
return error_response( message="id de l'étudiant (etudid, nip, ine) inconnu",
404, )
message="id de l'étudiant (etudid, nip, ine) inconnu", # Absences de l'étudiant
)
etudid = etud.etudid
# 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"])
@bp.route("/absences/ine/<int:ine>/just", methods=["GET"])
@token_auth.login_required @token_auth.login_required
@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é
@ -80,39 +69,37 @@ def absences_just(etudid: int = None, nip: int = None, ine: int = None):
ine : le code ine d'un étudiant ine : le code ine d'un étudiant
Exemple de résultat : Exemple de résultat :
[ [
{ {
"jour": "2022-04-15", "jour": "2022-04-15",
"matin": true, "matin": true,
"estabs": true, "estabs": true,
"estjust": true, "estjust": true,
"description": "", "description": "",
"begin": "2022-04-15 08:00:00", "begin": "2022-04-15 08:00:00",
"end": "2022-04-15 11:59:59" "end": "2022-04-15 11:59:59"
}, },
{ {
"jour": "Fri, 15 Apr 2022 00:00:00 GMT", "jour": "Fri, 15 Apr 2022 00:00:00 GMT",
"matin": false, "matin": false,
"estabs": true, "estabs": true,
"estjust": true, "estjust": true,
"description": "", "description": "",
"begin": "2022-04-15 12:00:00", "begin": "2022-04-15 12:00:00",
"end": "2022-04-15 17:59:59" "end": "2022-04-15 17:59:59"
} }
] ]
""" """
if etudid is None: etud = Identite.query.get(etudid)
etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) if etud is None:
if etud is None: return error_response(
return error_response( 404,
404, message="id de l'étudiant (etudid, nip, ine) inconnu",
message="id de l'étudiant (etudid, nip, ine) inconnu", )
)
etudid = etud.etudid
# Récupération des absences justifiées de l'étudiant # Absences justifiées de l'étudiant
abs_just = [ abs_just = [
absence for absence in sco_abs.list_abs_date(etudid) if absence["estjust"] absence for absence in sco_abs.list_abs_date(etud.id) if absence["estjust"]
] ]
for absence in abs_just: for absence in abs_just:
absence["jour"] = absence["jour"].isoformat() absence["jour"] = absence["jour"].isoformat()

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_auth, token_permission_required from app.api.auth import token_auth, token_permission_required
from app.api.tools import get_etud_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
@ -59,53 +65,102 @@ def etudiants_courant(long=False):
@bp.route("/etudiant/ine/<string:ine>", methods=["GET"]) @bp.route("/etudiant/ine/<string:ine>", methods=["GET"])
@token_auth.login_required @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 :
{ {
"civilite": "X", "civilite": "X",
"code_ine": "1", "code_ine": "1",
"code_nip": "1", "code_nip": "1",
"date_naissance": "", "date_naissance": "",
"email": "SACHA.COSTA@example.com", "email": "SACHA.COSTA@example.com",
"emailperso": "", "emailperso": "",
"etudid": 1, "etudid": 1,
"nom": "COSTA", "nom": "COSTA",
"prenom": "SACHA", "prenom": "SACHA",
"nomprenom": "Sacha COSTA", "nomprenom": "Sacha COSTA",
"lieu_naissance": "", "lieu_naissance": "",
"dept_naissance": "", "dept_naissance": "",
"nationalite": "", "nationalite": "",
"boursier": "", "boursier": "",
"id": 1, "id": 1,
"codepostaldomicile": "", "codepostaldomicile": "",
"paysdomicile": "", "paysdomicile": "",
"telephonemobile": "", "telephonemobile": "",
"typeadresse": "domicile", "typeadresse": "domicile",
"domicile": "", "domicile": "",
"villedomicile": "", "villedomicile": "",
"telephone": "", "telephone": "",
"fax": "", "fax": "",
"description": "" "description": ""
} }
""" """
# Récupération de l'étudiant if etudid is not None:
etud = get_etud_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()
if etud is None: if etud is None:
return error_response( return error_response(
404, 404,
message="id de l'étudiant (etudid, nip, ine) inconnu", message="étudiant inconnu",
) )
# Mise en forme des données
data = etud.to_dict_bul(include_urls=False)
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")
@ -115,56 +170,65 @@ def etudiant(etudid: int = None, nip: int = None, ine: int = None):
@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 :
[ [
{ {
"date_fin": "31/08/2022", "date_fin": "31/08/2022",
"resp_can_edit": false, "resp_can_edit": false,
"dept_id": 1, "dept_id": 1,
"etat": true, "etat": true,
"resp_can_change_ens": true, "resp_can_change_ens": true,
"id": 1, "id": 1,
"modalite": "FI", "modalite": "FI",
"ens_can_edit_eval": false, "ens_can_edit_eval": false,
"formation_id": 1, "formation_id": 1,
"gestion_compensation": false, "gestion_compensation": false,
"elt_sem_apo": null, "elt_sem_apo": null,
"semestre_id": 1, "semestre_id": 1,
"bul_hide_xml": false, "bul_hide_xml": false,
"elt_annee_apo": null, "elt_annee_apo": null,
"titre": "Semestre test", "titre": "Semestre test",
"block_moyennes": false, "block_moyennes": false,
"scodoc7_id": null, "scodoc7_id": null,
"date_debut": "01/09/2021", "date_debut": "01/09/2021",
"gestion_semestrielle": false, "gestion_semestrielle": false,
"bul_bgcolor": "white", "bul_bgcolor": "white",
"formsemestre_id": 1, "formsemestre_id": 1,
"titre_num": "Semestre test semestre 1", "titre_num": "Semestre test semestre 1",
"date_debut_iso": "2021-09-01", "date_debut_iso": "2021-09-01",
"date_fin_iso": "2022-08-31", "date_fin_iso": "2022-08-31",
"responsables": [] "responsables": []
}, },
... ...
] ]
""" """
# Récupération de l'étudiant if etudid is not None:
etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) query = FormSemestre.query.filter(
if etud is None: 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( return error_response(
404, 404,
message="id de l'étudiant (etudid, nip, ine) inconnu", 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])
@ -204,8 +268,8 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
def etudiant_bulletin_semestre( def etudiant_bulletin_semestre(
formsemestre_id, formsemestre_id,
etudid: int = None, etudid: int = None,
nip: int = None, nip: str = None,
ine: int = None, ine: str = None,
version="long", version="long",
): ):
""" """
@ -216,12 +280,12 @@ def etudiant_bulletin_semestre(
nip : le code nip d'un étudiant nip : le code nip d'un étudiant
ine : le code ine d'un étudiant ine : le code ine d'un étudiant
Exemple de résultat : Exemple de résultat :
{ {
"version": "0", "version": "0",
"type": "BUT", "type": "BUT",
"date": "2022-04-27T07:18:16.450634Z", "date": "2022-04-27T07:18:16.450634Z",
"publie": true, "publie": true,
"etudiant": { "etudiant": {
"civilite": "X", "civilite": "X",
"code_ine": "1", "code_ine": "1",
"code_nip": "1", "code_nip": "1",
@ -247,17 +311,17 @@ 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",
"options": { "options": {
"show_abs": true, "show_abs": true,
"show_abs_modules": false, "show_abs_modules": false,
"show_ects": true, "show_ects": true,
@ -276,128 +340,113 @@ 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": {
"id": 1, "id": 1,
"titre": "Initiation aux r\u00e9seaux informatiques", "titre": "Initiation aux r\u00e9seaux informatiques",
"code_apogee": null, "code_apogee": null,
"url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=1", "url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=1",
"moyenne": {}, "moyenne": {},
"evaluations": [ "evaluations": [
{ {
"id": 1, "id": 1,
"description": "eval1", "description": "eval1",
"date": "2022-04-20", "date": "2022-04-20",
"heure_debut": "08:00", "heure_debut": "08:00",
"heure_fin": "09:00", "heure_fin": "09:00",
"coef": "01.00", "coef": "01.00",
"poids": { "poids": {
"RT1.1": 1.0, "RT1.1": 1.0,
}, },
"note": { "note": {
"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": {
"SAE11": { "SAE11": {
"id": 2, "id": 2,
"titre": "Se sensibiliser \u00e0 l&apos;hygi\u00e8ne informatique et \u00e0 la cybers\u00e9curit\u00e9", "titre": "Se sensibiliser \u00e0 l&apos;hygi\u00e8ne informatique et \u00e0 la cybers\u00e9curit\u00e9",
"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": {
"RT1.1": { "RT1.1": {
"id": 1, "id": 1,
"titre": "Administrer les r\u00e9seaux et l\u2019Internet", "titre": "Administrer les r\u00e9seaux et l\u2019Internet",
"numero": 1, "numero": 1,
"type": 0, "type": 0,
"color": "#B80004", "color": "#B80004",
"competence": null, "competence": null,
"moyenne": { "moyenne": {
"value": "08.50", "value": "08.50",
"min": "06.00", "min": "06.00",
"max": "16.50", "max": "16.50",
"moy": "11.31", "moy": "11.31",
"rang": "12", "rang": "12",
"total": 16 "total": 16,
},
"bonus": "00.00",
"malus": "00.00",
"capitalise": null,
"ressources": {
"R101": {
"id": 1,
"coef": 12.0,
"moyenne": "12.00"
}, },
}, "bonus": "00.00",
"saes": { "malus": "00.00",
"SAE11": { "capitalise": null,
"id": 2, "ressources": {
"coef": 16.0, "R101": {"id": 1, "coef": 12.0, "moyenne": "12.00"},
"moyenne": "~"
}, },
}, "saes": {
"ECTS": { "SAE11": {"id": 2, "coef": 16.0, "moyenne": "~"},
"acquis": 0.0, },
"total": 12.0 "ECTS": {"acquis": 0.0, "total": 12.0},
}
}, },
"semestre": { "semestre": {
"etapes": [], "etapes": [],
"date_debut": "2021-09-01", "date_debut": "2021-09-01",
"date_fin": "2022-08-31", "date_fin": "2022-08-31",
"annee_universitaire": "2021 - 2022", "annee_universitaire": "2021 - 2022",
"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)
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",
)
app.set_sco_dept(dept.acronym) etud = query.first()
etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine)
if etud is None: if etud is None:
return error_response( return error_response(
404, 404,
message="id de l'étudiant (etudid, nip, ine) inconnu", message="id de l'étudiant (etudid, nip, ine) inconnu",
) )
app.set_sco_dept(dept.acronym)
return sco_bulletins.get_formsemestre_bulletin_etud_json( return sco_bulletins.get_formsemestre_bulletin_etud_json(
formsemestre, etud, version formsemestre, etud, version
) )
@ -429,44 +478,57 @@ def etudiant_groups(
ine : le code ine d'un étudiant ine : le code ine d'un étudiant
Exemple de résultat : Exemple de résultat :
[ [
{ {
"partition_id": 1, "partition_id": 1,
"id": 1, "id": 1,
"formsemestre_id": 1, "formsemestre_id": 1,
"partition_name": null, "partition_name": null,
"numero": 0, "numero": 0,
"bul_show_rank": false, "bul_show_rank": false,
"show_in_lists": true, "show_in_lists": true,
"group_id": 1, "group_id": 1,
"group_name": null "group_name": null
}, },
{ {
"partition_id": 2, "partition_id": 2,
"id": 2, "id": 2,
"formsemestre_id": 1, "formsemestre_id": 1,
"partition_name": "TD", "partition_name": "TD",
"numero": 1, "numero": 1,
"bul_show_rank": false, "bul_show_rank": false,
"show_in_lists": true, "show_in_lists": true,
"group_id": 2, "group_id": 2,
"group_name": "A" "group_name": "A"
} }
] ]
""" """
if etudid is None:
etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine)
if etud is None:
return error_response(
404,
message="id de l'étudiant (etudid, nip, ine) inconnu",
)
etudid = etud.etudid
# Récupération du formsemestre formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
sem = models.FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() if formsemestre is None:
dept = models.Departement.query.get(sem.dept_id) return error_response(
404,
message="formsemestre inconnu",
)
dept = Departement.query.get(formsemestre.dept_id)
if etudid is not None:
query = Identite.query.filter_by(id=etudid)
elif nip is not None:
query = Identite.query.filter_by(code_nip=nip, dept_id=dept.id)
elif ine is not None:
query = Identite.query.filter_by(code_ine=ine, dept_id=dept.id)
else:
return error_response(
404,
message="parametre manquant",
)
etud = query.first()
if etud is None:
return error_response(
404,
message="etudiant inconnu",
)
app.set_sco_dept(dept.acronym) 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,6 +4,7 @@ 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_auth, 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
@ -46,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]

View File

@ -59,6 +59,7 @@ def formsemestre(formsemestre_id: int):
# pour accéder aux préferences # pour accéder aux préferences
dept = Departement.query.get(formsemestre.dept_id) dept = Departement.query.get(formsemestre.dept_id)
app.set_sco_dept(dept.acronym) app.set_sco_dept(dept.acronym)
data["annee_scolaire"] = formsemestre.annee_scolaire_str()
data["session_id"] = formsemestre.session_id() data["session_id"] = formsemestre.session_id()
return jsonify(data) return jsonify(data)

View File

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

@ -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,7 +113,18 @@ 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,
# en tous cas elles auront la même description # en tous cas elles auront la même description
@ -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

@ -481,6 +481,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,6 +703,51 @@ 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, St Malo).
<ul>
<li>Les étudiants peuvent 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.
</li>
<li>Le vingtième des points au dessus de 10 est ajouté à la moyenne des UE.
</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, St Malo)"
seuil_moy_gen = 10.0
proportion_point = 1 / 20.0
classic_use_bonus_ues = True
# Adapté de BonusTarbes, mais s'applique aussi en classic
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)
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.

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
@ -151,6 +151,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

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
""" """
rangs, effectif = self.ue_rangs[ue_id] if group_id is None:
if rangs is not None: rangs, effectif = self.ue_rangs[ue_id]
rang = rangs[etudid] if rangs is not None:
rang = rangs[etudid]
else:
return "", ""
else: else:
return "", "" 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,13 @@ 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!r} {self.prenom!r}>" return (
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
)
@classmethod @classmethod
def from_request(cls, etudid=None, code_nip=None): def from_request(cls, etudid=None, code_nip=None):
@ -178,6 +180,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

View File

@ -287,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"
@ -449,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

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

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

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

@ -204,13 +204,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} ({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,

View File

@ -157,6 +157,8 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
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)
formation_id = ue.formation_id
semestre_idx = ue.semestre_idx
if not can_delete_ue(ue): if not can_delete_ue(ue):
raise ScoNonEmptyFormationObject( raise ScoNonEmptyFormationObject(
"UE", "UE",
@ -164,8 +166,8 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
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,
), ),
) )
@ -188,13 +190,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},
@ -216,10 +218,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,
) )
@ -229,8 +231,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

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,6 +413,34 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"):
return R return R
def get_etud_formsemestre_groups(
etud: Identite, formsemestre: FormSemestre, only_to_show=True
) -> list[GroupDescr]:
"""Liste les groupes auxquels est inscrit"""
# Note: je n'ai pas réussi à cosntruire une requete SQLAlechemy avec
# la Table d'association group_membership
cursor = db.session.execute(
text(
"""
SELECT g.id
FROM group_descr g, group_membership gm, partition p
WHERE gm.etudid = :etudid
AND gm.group_id = g.id
AND g.partition_id = p.id
AND p.formsemestre_id = :formsemestre_id
AND p.partition_name is not NULL
"""
+ (" and (p.show_in_lists is True) " if only_to_show else "")
+ """
ORDER BY p.numero
"""
),
{"etudid": etud.id, "formsemestre_id": formsemestre.id},
)
return [GroupDescr.query.get(group_id) for group_id in cursor]
# Ancienne fonction:
def etud_add_group_infos(etud, formsemestre_id, sep=" ", only_to_show=False): def etud_add_group_infos(etud, formsemestre_id, sep=" ", only_to_show=False):
"""Add informations on partitions and group memberships to etud """Add informations on partitions and group memberships to etud
(a dict with an etudid) (a dict with an etudid)
@ -453,7 +481,7 @@ def etud_add_group_infos(etud, formsemestre_id, sep=" ", only_to_show=False):
) )
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
] ]

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"),
) )

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,9 +83,11 @@ 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"} file_formats = {"csv", "json", "xls", "xlsx", "xlsall", "xml"}
supported_formats = file_formats | {"html"} supported_formats = file_formats | {"html", "evals"}
if tabformat not in supported_formats: if tabformat not in supported_formats:
raise ScoValueError(f"Format non supporté: {tabformat}") raise ScoValueError(f"Format non supporté: {tabformat}")
is_file = tabformat in file_formats is_file = tabformat in file_formats
@ -131,7 +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 formatté"), ("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

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

@ -100,7 +100,7 @@ $(function () {
}, },
{ {
// 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

@ -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)
@ -662,17 +662,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

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

View File

@ -26,13 +26,17 @@ 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)
BASEDIR = os.path.abspath(os.path.dirname(__file__)) try:
BASEDIR = os.path.abspath(os.path.dirname(__file__))
except NameError:
BASEDIR = "."
load_dotenv(os.path.join(BASEDIR, ".env")) load_dotenv(os.path.join(BASEDIR, ".env"))
CHK_CERT = 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"
API_URL = SCODOC_URL + "/ScoDoc/api" API_URL = SCODOC_URL + "/ScoDoc/api"
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}") print(f"API URL={API_URL}")
@ -90,6 +94,25 @@ formsemestre_id = 1028 # A adapter
etudid = 14721 etudid = 14721
bul_dut = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin") 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 !")

View File

@ -25,6 +25,7 @@ SCODOC_URL = os.environ["SCODOC_URL"]
API_URL = SCODOC_URL + "/ScoDoc/api" API_URL = SCODOC_URL + "/ScoDoc/api"
API_USER = os.environ.get("API_USER", "test") API_USER = os.environ.get("API_USER", "test")
API_PASSWORD = os.environ.get("API_PASSWD", "test") API_PASSWORD = os.environ.get("API_PASSWD", "test")
DEPT_ACRONYM = "TAPI"
print(f"SCODOC_URL={SCODOC_URL}") print(f"SCODOC_URL={SCODOC_URL}")
print(f"API URL={API_URL}") print(f"API URL={API_URL}")

View File

@ -22,8 +22,7 @@ from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers
# Etudiant pour les tests # Etudiant pour les tests
ETUDID = 1 ETUDID = 1
INE = "1"
NIP = "1"
# absences # absences
def test_absences(api_headers): def test_absences(api_headers):
@ -37,20 +36,6 @@ def test_absences(api_headers):
) )
assert r.status_code == 200 assert r.status_code == 200
r = requests.get(
f"{API_URL}/absences/nip/{NIP}",
headers=api_headers,
verify=CHECK_CERTIFICATE,
)
assert r.status_code == 200
r = requests.get(
f"{API_URL}/absences/ine/{INE}",
headers=api_headers,
verify=CHECK_CERTIFICATE,
)
assert r.status_code == 200
# absences_justify # absences_justify
def test_absences_justify(api_headers): def test_absences_justify(api_headers):
@ -65,22 +50,6 @@ def test_absences_justify(api_headers):
assert r.status_code == 200 assert r.status_code == 200
# TODO vérifier résultat # TODO vérifier résultat
r = requests.get(
API_URL + f"/absences/nip/{NIP}/just",
headers=api_headers,
verify=CHECK_CERTIFICATE,
)
assert r.status_code == 200
# TODO vérifier résultat
r = requests.get(
API_URL + f"/absences/ine/{INE}/just",
headers=api_headers,
verify=CHECK_CERTIFICATE,
)
assert r.status_code == 200
# TODO vérifier résultat
# XXX TODO # XXX TODO
# def test_abs_groupe_etat(api_headers): # def test_abs_groupe_etat(api_headers):

View File

@ -19,7 +19,12 @@ Utilisation :
import requests import requests
from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_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
DEPARTEMENT_FIELDS = [ DEPARTEMENT_FIELDS = [
@ -86,7 +91,7 @@ def test_list_etudiants(api_headers):
fields = {"id", "nip", "ine", "nom", "nom_usuel", "prenom", "civilite"} fields = {"id", "nip", "ine", "nom", "nom_usuel", "prenom", "civilite"}
r = requests.get( r = requests.get(
API_URL + "/departement/TAPI/etudiants", f"{API_URL}/departement/{DEPT_ACRONYM}/etudiants",
headers=api_headers, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )

View File

@ -19,7 +19,12 @@ Utilisation :
import requests import requests
from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_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 from tests.api.tools_test_api import ETUD_FIELDS, FSEM_FIELDS
@ -83,7 +88,7 @@ def test_etudiant(api_headers):
etud = r.json() etud = r.json()
fields_ok = verify_fields(etud, ETUD_FIELDS) fields_ok = verify_fields(etud, ETUD_FIELDS)
assert fields_ok is True assert fields_ok is True
assert etud["dept_acronym"] == DEPT_ACRONYM
######### Test code ine ######### ######### Test code ine #########
r = requests.get( r = requests.get(
@ -93,7 +98,7 @@ def test_etudiant(api_headers):
) )
assert r.status_code == 200 assert r.status_code == 200
etud = r.json() etud = r.json()
assert len(etud) == 24 assert len(etud) == 25
fields_ok = verify_fields(etud, ETUD_FIELDS) fields_ok = verify_fields(etud, ETUD_FIELDS)
assert fields_ok is True assert fields_ok is True

View File

@ -24,6 +24,11 @@ from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers
from tests.api.tools_test_api import MODIMPL_FIELDS, verify_fields from tests.api.tools_test_api import MODIMPL_FIELDS, verify_fields
from tests.api.tools_test_api import FSEM_FIELDS, UE_FIELDS, MODULE_FIELDS from tests.api.tools_test_api import FSEM_FIELDS, UE_FIELDS, MODULE_FIELDS
# Etudiant pour les tests
ETUDID = 1
NIP = "1"
INE = "INE1"
def test_formsemestre(api_headers): def test_formsemestre(api_headers):
""" """
@ -53,7 +58,7 @@ def test_etudiant_bulletin(api_headers):
bull_a = r.json() bull_a = r.json()
r = requests.get( r = requests.get(
f"{API_URL}/etudiant/nip/1/formsemestre/{formsemestre_id}/bulletin", f"{API_URL}/etudiant/nip/{NIP}/formsemestre/{formsemestre_id}/bulletin",
headers=api_headers, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
@ -61,7 +66,7 @@ def test_etudiant_bulletin(api_headers):
bull_b = r.json() bull_b = r.json()
r = requests.get( r = requests.get(
f"{API_URL}/etudiant/ine/1/formsemestre/{formsemestre_id}/bulletin", f"{API_URL}/etudiant/ine/{INE}/formsemestre/{formsemestre_id}/bulletin",
headers=api_headers, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )

View File

@ -21,6 +21,8 @@ ETUD_FIELDS = {
"code_nip", "code_nip",
"codepostaldomicile", "codepostaldomicile",
"date_naissance", "date_naissance",
"dept_acronym",
"dept_id",
"dept_naissance", "dept_naissance",
"description", "description",
"domicile", "domicile",