Compare commits
1 Commits
master
...
revamp-jqu
Author | SHA1 | Date | |
---|---|---|---|
ec11eaf44b |
3
.gitignore
vendored
3
.gitignore
vendored
@ -176,6 +176,3 @@ copy
|
|||||||
|
|
||||||
# Symlinks static ScoDoc
|
# Symlinks static ScoDoc
|
||||||
app/static/links/[0-9]*.*[0-9]
|
app/static/links/[0-9]*.*[0-9]
|
||||||
|
|
||||||
# Essais locaux
|
|
||||||
xp/
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# ScoDoc - Gestion de la scolarité - Version ScoDoc 9
|
# ScoDoc - Gestion de la scolarité - Version ScoDoc 9
|
||||||
|
|
||||||
(c) Emmanuel Viennet 1999 - 2024 (voir LICENCE.txt).
|
(c) Emmanuel Viennet 1999 - 2022 (voir LICENCE.txt).
|
||||||
|
|
||||||
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian11>
|
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian11>
|
||||||
|
|
||||||
|
@ -86,9 +86,8 @@ def handle_invalid_csrf(exc):
|
|||||||
return render_template("error_csrf.j2", exc=exc), 404
|
return render_template("error_csrf.j2", exc=exc), 404
|
||||||
|
|
||||||
|
|
||||||
# def handle_pdf_format_error(exc):
|
def handle_pdf_format_error(exc):
|
||||||
# return "ay ay ay"
|
return "ay ay ay"
|
||||||
handle_pdf_format_error = handle_sco_value_error
|
|
||||||
|
|
||||||
|
|
||||||
def internal_server_error(exc):
|
def internal_server_error(exc):
|
||||||
|
@ -3,11 +3,9 @@
|
|||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask import request, g
|
from flask import request, g
|
||||||
from flask_login import current_user
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoException
|
from app.scodoc.sco_exceptions import AccessDenied, ScoException
|
||||||
from app.scodoc.sco_permissions import Permission
|
|
||||||
|
|
||||||
api_bp = Blueprint("api", __name__)
|
api_bp = Blueprint("api", __name__)
|
||||||
api_web_bp = Blueprint("apiweb", __name__)
|
api_web_bp = Blueprint("apiweb", __name__)
|
||||||
@ -50,35 +48,20 @@ def requested_format(default_format="json", allowed_formats=None):
|
|||||||
|
|
||||||
|
|
||||||
@as_json
|
@as_json
|
||||||
def get_model_api_object(
|
def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model = None):
|
||||||
model_cls: db.Model,
|
|
||||||
model_id: int,
|
|
||||||
join_cls: db.Model = None,
|
|
||||||
restrict: bool | None = None,
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Retourne une réponse contenant la représentation api de l'objet "Model[model_id]"
|
Retourne une réponse contenant la représentation api de l'objet "Model[model_id]"
|
||||||
|
|
||||||
Filtrage du département en fonction d'une classe de jointure (eg: Identite, Formsemestre) -> join_cls
|
Filtrage du département en fonction d'une classe de jointure (eg: Identite, Formsemstre) -> join_cls
|
||||||
|
|
||||||
exemple d'utilisation : fonction "justificatif()" -> app/api/justificatifs.py
|
exemple d'utilisation : fonction "justificatif()" -> app/api/justificatifs.py
|
||||||
|
|
||||||
L'agument restrict est passé to_dict, est signale que l'on veut une version restreinte
|
|
||||||
(sans données personnelles, ou sans informations sur le justificatif d'absence)
|
|
||||||
"""
|
"""
|
||||||
query = model_cls.query.filter_by(id=model_id)
|
query = model_cls.query.filter_by(id=model_id)
|
||||||
if g.scodoc_dept and join_cls is not None:
|
if g.scodoc_dept and join_cls is not None:
|
||||||
query = query.join(join_cls).filter_by(dept_id=g.scodoc_dept_id)
|
query = query.join(join_cls).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
unique: model_cls = query.first()
|
unique: model_cls = query.first_or_404()
|
||||||
|
|
||||||
if unique is None:
|
|
||||||
return scu.json_error(
|
|
||||||
404,
|
|
||||||
message=f"{model_cls.__name__} inexistant(e)",
|
|
||||||
)
|
|
||||||
if restrict is None:
|
|
||||||
return unique.to_dict(format_api=True)
|
return unique.to_dict(format_api=True)
|
||||||
return unique.to_dict(format_api=True, restrict=restrict)
|
|
||||||
|
|
||||||
|
|
||||||
from app.api import tokens
|
from app.api import tokens
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
"""ScoDoc 9 API : Assiduités
|
"""ScoDoc 9 API : Assiduités
|
||||||
@ -10,7 +10,6 @@ from datetime import datetime
|
|||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from flask_sqlalchemy.query import Query
|
|
||||||
|
|
||||||
from app import db, log, set_sco_dept
|
from app import db, log, set_sco_dept
|
||||||
import app.scodoc.sco_assiduites as scass
|
import app.scodoc.sco_assiduites as scass
|
||||||
@ -25,6 +24,7 @@ from app.models import (
|
|||||||
ModuleImpl,
|
ModuleImpl,
|
||||||
Scolog,
|
Scolog,
|
||||||
)
|
)
|
||||||
|
from flask_sqlalchemy.query import Query
|
||||||
from app.models.assiduites import (
|
from app.models.assiduites import (
|
||||||
get_assiduites_justif,
|
get_assiduites_justif,
|
||||||
get_justifs_from_date,
|
get_justifs_from_date,
|
||||||
@ -39,7 +39,6 @@ from app.scodoc.sco_utils import json_error
|
|||||||
@api_web_bp.route("/assiduite/<int:assiduite_id>")
|
@api_web_bp.route("/assiduite/<int:assiduite_id>")
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
|
||||||
def assiduite(assiduite_id: int = None):
|
def assiduite(assiduite_id: int = None):
|
||||||
"""Retourne un objet assiduité à partir de son id
|
"""Retourne un objet assiduité à partir de son id
|
||||||
|
|
||||||
@ -85,7 +84,7 @@ def assiduite_justificatifs(assiduite_id: int = None, long: bool = False):
|
|||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return get_assiduites_justif(assiduite_id, long)
|
return get_assiduites_justif(assiduite_id, True)
|
||||||
|
|
||||||
|
|
||||||
# etudid
|
# etudid
|
||||||
@ -173,7 +172,6 @@ def count_assiduites(
|
|||||||
404,
|
404,
|
||||||
message="étudiant inconnu",
|
message="étudiant inconnu",
|
||||||
)
|
)
|
||||||
set_sco_dept(etud.departement.acronym)
|
|
||||||
|
|
||||||
# Les filtres qui seront appliqués au comptage (type, date, etudid...)
|
# Les filtres qui seront appliqués au comptage (type, date, etudid...)
|
||||||
filtered: dict[str, object] = {}
|
filtered: dict[str, object] = {}
|
||||||
@ -337,7 +335,7 @@ def assiduites_group(with_query: bool = False):
|
|||||||
try:
|
try:
|
||||||
etuds = [int(etu) for etu in etuds]
|
etuds = [int(etu) for etu in etuds]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return json_error(404, "Le champ etudids n'est pas correctement formé")
|
return json_error(404, "Le champs etudids n'est pas correctement formé")
|
||||||
|
|
||||||
# Vérification que tous les étudiants sont du même département
|
# Vérification que tous les étudiants sont du même département
|
||||||
query = Identite.query.filter(Identite.id.in_(etuds))
|
query = Identite.query.filter(Identite.id.in_(etuds))
|
||||||
@ -446,8 +444,6 @@ def count_assiduites_formsemestre(
|
|||||||
if formsemestre is None:
|
if formsemestre is None:
|
||||||
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
||||||
|
|
||||||
set_sco_dept(formsemestre.departement.acronym)
|
|
||||||
|
|
||||||
# Récupération des étudiants du formsemestre
|
# Récupération des étudiants du formsemestre
|
||||||
etuds = formsemestre.etuds.all()
|
etuds = formsemestre.etuds.all()
|
||||||
etuds_id = [etud.id for etud in etuds]
|
etuds_id = [etud.id for etud in etuds]
|
||||||
@ -609,9 +605,9 @@ def _create_one(
|
|||||||
etud: Identite,
|
etud: Identite,
|
||||||
) -> tuple[int, object]:
|
) -> tuple[int, object]:
|
||||||
"""
|
"""
|
||||||
Création d'une assiduité à partir d'un dict
|
_create_one Création d'une assiduité à partir d'une représentation JSON
|
||||||
|
|
||||||
Cette fonction vérifie les données du dict (qui vient du JSON API)
|
Cette fonction vérifie la représentation JSON
|
||||||
|
|
||||||
Puis crée l'assiduité si la représentation est valide.
|
Puis crée l'assiduité si la représentation est valide.
|
||||||
|
|
||||||
@ -837,12 +833,15 @@ def assiduite_edit(assiduite_id: int):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Récupération de l'assiduité à modifier
|
# Récupération de l'assiduité à modifier
|
||||||
assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
|
assiduite_unique: Assiduite = Assiduite.query.filter_by(
|
||||||
if assiduite_unique is None:
|
id=assiduite_id
|
||||||
return json_error(404, "Assiduité non existante")
|
).first_or_404()
|
||||||
# Récupération des valeurs à modifier
|
# Récupération des valeurs à modifier
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
|
|
||||||
|
# Préparation du retour
|
||||||
|
errors: list[str] = []
|
||||||
|
|
||||||
# Code 200 si modification réussie
|
# Code 200 si modification réussie
|
||||||
# Code 404 si raté + message d'erreur
|
# Code 404 si raté + message d'erreur
|
||||||
code, obj = _edit_one(assiduite_unique, data)
|
code, obj = _edit_one(assiduite_unique, data)
|
||||||
@ -989,7 +988,9 @@ def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]:
|
|||||||
if moduleimpl is None:
|
if moduleimpl is None:
|
||||||
errors.append("param 'moduleimpl_id': invalide")
|
errors.append("param 'moduleimpl_id': invalide")
|
||||||
else:
|
else:
|
||||||
if not moduleimpl.est_inscrit(assiduite_unique.etudiant):
|
if not moduleimpl.est_inscrit(
|
||||||
|
Identite.query.filter_by(id=assiduite_unique.etudid).first()
|
||||||
|
):
|
||||||
errors.append("param 'moduleimpl_id': etud non inscrit")
|
errors.append("param 'moduleimpl_id': etud non inscrit")
|
||||||
else:
|
else:
|
||||||
# Mise à jour du moduleimpl
|
# Mise à jour du moduleimpl
|
||||||
@ -1005,9 +1006,7 @@ def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]:
|
|||||||
if formsemestre:
|
if formsemestre:
|
||||||
force = scu.is_assiduites_module_forced(formsemestre_id=formsemestre.id)
|
force = scu.is_assiduites_module_forced(formsemestre_id=formsemestre.id)
|
||||||
else:
|
else:
|
||||||
force = scu.is_assiduites_module_forced(
|
force = scu.is_assiduites_module_forced(dept_id=etud.dept_id)
|
||||||
dept_id=assiduite_unique.etudiant.dept_id
|
|
||||||
)
|
|
||||||
|
|
||||||
external_data = (
|
external_data = (
|
||||||
external_data
|
external_data
|
||||||
@ -1015,9 +1014,7 @@ def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]:
|
|||||||
else assiduite_unique.external_data
|
else assiduite_unique.external_data
|
||||||
)
|
)
|
||||||
|
|
||||||
if force and not (
|
if force and not (external_data is not None and external_data.get("module", False) != ""):
|
||||||
external_data is not None and external_data.get("module", False) != ""
|
|
||||||
):
|
|
||||||
errors.append(
|
errors.append(
|
||||||
"param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul"
|
"param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul"
|
||||||
)
|
)
|
||||||
@ -1235,8 +1232,8 @@ def _filter_manager(requested, assiduites_query: Query) -> Query:
|
|||||||
annee: int = scu.annee_scolaire()
|
annee: int = scu.annee_scolaire()
|
||||||
|
|
||||||
assiduites_query: Query = assiduites_query.filter(
|
assiduites_query: Query = assiduites_query.filter(
|
||||||
Assiduite.date_debut >= scu.date_debut_annee_scolaire(annee),
|
Assiduite.date_debut >= scu.date_debut_anne_scolaire(annee),
|
||||||
Assiduite.date_fin <= scu.date_fin_annee_scolaire(annee),
|
Assiduite.date_fin <= scu.date_fin_anne_scolaire(annee),
|
||||||
)
|
)
|
||||||
|
|
||||||
return assiduites_query
|
return assiduites_query
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -271,11 +271,23 @@ def dept_formsemestres_courants(acronym: str):
|
|||||||
"""
|
"""
|
||||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||||
date_courante = request.args.get("date_courante")
|
date_courante = request.args.get("date_courante")
|
||||||
date_courante = datetime.fromisoformat(date_courante) if date_courante else None
|
if date_courante:
|
||||||
|
test_date = datetime.fromisoformat(date_courante)
|
||||||
|
else:
|
||||||
|
test_date = app.db.func.now()
|
||||||
|
# Les semestres en cours de ce département
|
||||||
|
formsemestres = FormSemestre.query.filter(
|
||||||
|
FormSemestre.dept_id == dept.id,
|
||||||
|
FormSemestre.date_debut <= test_date,
|
||||||
|
FormSemestre.date_fin >= test_date,
|
||||||
|
)
|
||||||
return [
|
return [
|
||||||
formsemestre.to_dict_api()
|
d.to_dict_api()
|
||||||
for formsemestre in FormSemestre.get_dept_formsemestres_courants(
|
for d in formsemestres.order_by(
|
||||||
dept, date_courante
|
FormSemestre.date_debut.desc(),
|
||||||
|
FormSemestre.modalite,
|
||||||
|
FormSemestre.semestre_id,
|
||||||
|
FormSemestre.titre,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -295,7 +307,7 @@ def dept_formsemestres_courants_by_id(dept_id: int):
|
|||||||
if date_courante:
|
if date_courante:
|
||||||
test_date = datetime.fromisoformat(date_courante)
|
test_date = datetime.fromisoformat(date_courante)
|
||||||
else:
|
else:
|
||||||
test_date = db.func.current_date()
|
test_date = app.db.func.now()
|
||||||
# Les semestres en cours de ce département
|
# Les semestres en cours de ce département
|
||||||
formsemestres = FormSemestre.query.filter(
|
formsemestres = FormSemestre.query.filter(
|
||||||
FormSemestre.dept_id == dept.id,
|
FormSemestre.dept_id == dept.id,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -10,7 +10,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from flask import g, request, Response
|
from flask import g, request
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
@ -18,7 +18,6 @@ from sqlalchemy import desc, func, or_
|
|||||||
from sqlalchemy.dialects.postgresql import VARCHAR
|
from sqlalchemy.dialects.postgresql import VARCHAR
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db, log
|
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp
|
||||||
from app.api import tools
|
from app.api import tools
|
||||||
from app.but import bulletin_but_court
|
from app.but import bulletin_but_court
|
||||||
@ -26,16 +25,13 @@ from app.decorators import scodoc, permission_required
|
|||||||
from app.models import (
|
from app.models import (
|
||||||
Admission,
|
Admission,
|
||||||
Departement,
|
Departement,
|
||||||
EtudAnnotation,
|
|
||||||
FormSemestreInscription,
|
FormSemestreInscription,
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
Identite,
|
Identite,
|
||||||
ScolarNews,
|
|
||||||
)
|
)
|
||||||
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_bulletins import do_formsemestre_bulletinetud
|
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
|
||||||
from app.scodoc import sco_etud
|
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_utils import json_error, suppress_accents
|
from app.scodoc.sco_utils import json_error, suppress_accents
|
||||||
|
|
||||||
@ -55,32 +51,6 @@ import app.scodoc.sco_utils as scu
|
|||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
def _get_etud_by_code(
|
|
||||||
code_type: str, code: str, dept: Departement
|
|
||||||
) -> tuple[bool, Response | Identite]:
|
|
||||||
"""Get etud, using etudid, NIP or INE
|
|
||||||
Returns True, etud if ok, or False, error response.
|
|
||||||
"""
|
|
||||||
if code_type == "nip":
|
|
||||||
query = Identite.query.filter_by(code_nip=code)
|
|
||||||
elif code_type == "etudid":
|
|
||||||
try:
|
|
||||||
etudid = int(code)
|
|
||||||
except ValueError:
|
|
||||||
return False, json_error(404, "invalid etudid type")
|
|
||||||
query = Identite.query.filter_by(id=etudid)
|
|
||||||
elif code_type == "ine":
|
|
||||||
query = Identite.query.filter_by(code_ine=code)
|
|
||||||
else:
|
|
||||||
return False, json_error(404, "invalid code_type")
|
|
||||||
if dept:
|
|
||||||
query = query.filter_by(dept_id=dept.id)
|
|
||||||
etud = query.first()
|
|
||||||
if etud is None:
|
|
||||||
return False, json_error(404, message="etudiant inexistant")
|
|
||||||
return True, etud
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiants/courants", defaults={"long": False})
|
@bp.route("/etudiants/courants", defaults={"long": False})
|
||||||
@bp.route("/etudiants/courants/long", defaults={"long": True})
|
@bp.route("/etudiants/courants/long", defaults={"long": True})
|
||||||
@api_web_bp.route("/etudiants/courants", defaults={"long": False})
|
@api_web_bp.route("/etudiants/courants", defaults={"long": False})
|
||||||
@ -131,10 +101,7 @@ def etudiants_courants(long=False):
|
|||||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||||
)
|
)
|
||||||
if long:
|
if long:
|
||||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
data = [etud.to_dict_api() for etud in etuds]
|
||||||
data = [
|
|
||||||
etud.to_dict_api(restrict=restrict, with_annotations=True) for etud in etuds
|
|
||||||
]
|
|
||||||
else:
|
else:
|
||||||
data = [etud.to_dict_short() for etud in etuds]
|
data = [etud.to_dict_short() for etud in etuds]
|
||||||
return data
|
return data
|
||||||
@ -168,8 +135,8 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
|||||||
404,
|
404,
|
||||||
message="étudiant inconnu",
|
message="étudiant inconnu",
|
||||||
)
|
)
|
||||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
|
||||||
return etud.to_dict_api(restrict=restrict, with_annotations=True)
|
return etud.to_dict_api()
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiant/etudid/<int:etudid>/photo")
|
@bp.route("/etudiant/etudid/<int:etudid>/photo")
|
||||||
@ -281,10 +248,7 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
|||||||
query = query.join(Departement).filter(
|
query = query.join(Departement).filter(
|
||||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||||
)
|
)
|
||||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
return [etud.to_dict_api() for etud in query]
|
||||||
return [
|
|
||||||
etud.to_dict_api(restrict=restrict, with_annotations=True) for etud in query
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiants/name/<string:start>")
|
@bp.route("/etudiants/name/<string:start>")
|
||||||
@ -311,11 +275,7 @@ def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
|||||||
)
|
)
|
||||||
etuds = query.order_by(Identite.nom, Identite.prenom).limit(limit)
|
etuds = query.order_by(Identite.nom, Identite.prenom).limit(limit)
|
||||||
# Note: on raffine le tri pour les caractères spéciaux et nom usuel ici:
|
# Note: on raffine le tri pour les caractères spéciaux et nom usuel ici:
|
||||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
return [etud.to_dict_api() for etud in sorted(etuds, key=attrgetter("sort_key"))]
|
||||||
return [
|
|
||||||
etud.to_dict_api(restrict=restrict)
|
|
||||||
for etud in sorted(etuds, key=attrgetter("sort_key"))
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
||||||
@ -396,7 +356,7 @@ def bulletin(
|
|||||||
code_type: str = "etudid",
|
code_type: str = "etudid",
|
||||||
code: str = None,
|
code: str = None,
|
||||||
formsemestre_id: int = None,
|
formsemestre_id: int = None,
|
||||||
version: str = "selectedevals",
|
version: str = "long",
|
||||||
pdf: bool = False,
|
pdf: bool = False,
|
||||||
with_img_signatures_pdf: bool = True,
|
with_img_signatures_pdf: bool = True,
|
||||||
):
|
):
|
||||||
@ -406,7 +366,7 @@ def bulletin(
|
|||||||
formsemestre_id : l'id d'un formsemestre
|
formsemestre_id : l'id d'un formsemestre
|
||||||
code_type : "etudid", "nip" ou "ine"
|
code_type : "etudid", "nip" ou "ine"
|
||||||
code : valeur du code INE, NIP ou etudid, selon code_type.
|
code : valeur du code INE, NIP ou etudid, selon code_type.
|
||||||
version : type de bulletin (par défaut, "selectedevals"): short, long, selectedevals, butcourt
|
version : type de bulletin (par défaut, "long"): short, long, selectedevals, butcourt
|
||||||
pdf : si spécifié, bulletin au format PDF (et non JSON).
|
pdf : si spécifié, bulletin au format PDF (et non JSON).
|
||||||
|
|
||||||
Exemple de résultat : voir https://scodoc.org/ScoDoc9API/#bulletin
|
Exemple de résultat : voir https://scodoc.org/ScoDoc9API/#bulletin
|
||||||
@ -416,15 +376,28 @@ def bulletin(
|
|||||||
pdf = True
|
pdf = True
|
||||||
if version not in scu.BULLETINS_VERSIONS_BUT:
|
if version not in scu.BULLETINS_VERSIONS_BUT:
|
||||||
return json_error(404, "version invalide")
|
return json_error(404, "version invalide")
|
||||||
|
# return f"{code_type}={code}, version={version}, pdf={pdf}"
|
||||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
||||||
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
||||||
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
||||||
return json_error(404, "formsemestre inexistant")
|
return json_error(404, "formsemestre inexistant")
|
||||||
app.set_sco_dept(dept.acronym)
|
app.set_sco_dept(dept.acronym)
|
||||||
|
|
||||||
ok, etud = _get_etud_by_code(code_type, code, dept)
|
if code_type == "nip":
|
||||||
if not ok:
|
query = Identite.query.filter_by(code_nip=code, dept_id=dept.id)
|
||||||
return etud # json error
|
elif code_type == "etudid":
|
||||||
|
try:
|
||||||
|
etudid = int(code)
|
||||||
|
except ValueError:
|
||||||
|
return json_error(404, "invalid etudid type")
|
||||||
|
query = Identite.query.filter_by(id=etudid)
|
||||||
|
elif code_type == "ine":
|
||||||
|
query = Identite.query.filter_by(code_ine=code, dept_id=dept.id)
|
||||||
|
else:
|
||||||
|
return json_error(404, "invalid code_type")
|
||||||
|
etud = query.first()
|
||||||
|
if etud is None:
|
||||||
|
return json_error(404, message="etudiant inexistant")
|
||||||
|
|
||||||
if version == "butcourt":
|
if version == "butcourt":
|
||||||
if pdf:
|
if pdf:
|
||||||
@ -445,9 +418,9 @@ def bulletin(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups")
|
@bp.route(
|
||||||
@api_web_bp.route(
|
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups",
|
||||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups"
|
methods=["GET"],
|
||||||
)
|
)
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@ -485,6 +458,7 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
@ -501,173 +475,3 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
|||||||
data = sco_groups.get_etud_groups(etud.id, formsemestre.id)
|
data = sco_groups.get_etud_groups(etud.id, formsemestre.id)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiant/create", methods=["POST"], defaults={"force": False})
|
|
||||||
@bp.route("/etudiant/create/force", methods=["POST"], defaults={"force": True})
|
|
||||||
@api_web_bp.route("/etudiant/create", methods=["POST"], defaults={"force": False})
|
|
||||||
@api_web_bp.route("/etudiant/create/force", methods=["POST"], defaults={"force": True})
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.EtudInscrit)
|
|
||||||
@as_json
|
|
||||||
def etudiant_create(force=False):
|
|
||||||
"""Création d'un nouvel étudiant
|
|
||||||
Si force, crée même si homonymie détectée.
|
|
||||||
L'étudiant créé n'est pas inscrit à un semestre.
|
|
||||||
Champs requis: nom, prenom (sauf si config sans prénom), dept (string:acronyme)
|
|
||||||
"""
|
|
||||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
|
||||||
dept = args.get("dept", None)
|
|
||||||
if not dept:
|
|
||||||
return scu.json_error(400, "dept requis")
|
|
||||||
dept_o = Departement.query.filter_by(acronym=dept).first()
|
|
||||||
if not dept_o:
|
|
||||||
return scu.json_error(400, "dept invalide")
|
|
||||||
if g.scodoc_dept and g.scodoc_dept_id != dept_o.id:
|
|
||||||
return scu.json_error(400, "dept invalide (route departementale)")
|
|
||||||
else:
|
|
||||||
app.set_sco_dept(dept)
|
|
||||||
args["dept_id"] = dept_o.id
|
|
||||||
# vérifie que le département de création est bien autorisé
|
|
||||||
if not current_user.has_permission(Permission.EtudInscrit, dept):
|
|
||||||
return json_error(403, "departement non autorisé")
|
|
||||||
nom = args.get("nom", None)
|
|
||||||
prenom = args.get("prenom", None)
|
|
||||||
ok, homonyms = sco_etud.check_nom_prenom_homonyms(nom=nom, prenom=prenom)
|
|
||||||
if not ok:
|
|
||||||
return scu.json_error(400, "nom ou prénom invalide")
|
|
||||||
if len(homonyms) > 0 and not force:
|
|
||||||
return scu.json_error(
|
|
||||||
400, f"{len(homonyms)} homonymes détectés. Vous pouvez utiliser /force."
|
|
||||||
)
|
|
||||||
etud = Identite.create_etud(**args)
|
|
||||||
db.session.flush()
|
|
||||||
# --- Données admission
|
|
||||||
admission_args = args.get("admission", None)
|
|
||||||
if admission_args:
|
|
||||||
etud.admission.from_dict(admission_args)
|
|
||||||
# --- Adresse
|
|
||||||
adresses = args.get("adresses", [])
|
|
||||||
if adresses:
|
|
||||||
# ne prend en compte que la première adresse
|
|
||||||
# car si la base est concue pour avoir plusieurs adresses par étudiant,
|
|
||||||
# l'application n'en gère plus qu'une seule.
|
|
||||||
adresse = etud.adresses.first()
|
|
||||||
adresse.from_dict(adresses[0])
|
|
||||||
|
|
||||||
# Poste une nouvelle dans le département concerné:
|
|
||||||
ScolarNews.add(
|
|
||||||
typ=ScolarNews.NEWS_INSCR,
|
|
||||||
text=f"Nouvel étudiant {etud.html_link_fiche()}",
|
|
||||||
url=etud.url_fiche(),
|
|
||||||
max_frequency=0,
|
|
||||||
dept_id=dept_o.id,
|
|
||||||
)
|
|
||||||
db.session.commit()
|
|
||||||
# Note: je ne comprends pas pourquoi un refresh est nécessaire ici
|
|
||||||
# sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
|
|
||||||
db.session.refresh(etud)
|
|
||||||
|
|
||||||
r = etud.to_dict_api(restrict=False) # pas de restriction, on vient de le créer
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiant/<string:code_type>/<string:code>/edit", methods=["POST"])
|
|
||||||
@api_web_bp.route("/etudiant/<string:code_type>/<string:code>/edit", methods=["POST"])
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.EtudInscrit)
|
|
||||||
@as_json
|
|
||||||
def etudiant_edit(
|
|
||||||
code_type: str = "etudid",
|
|
||||||
code: str = None,
|
|
||||||
):
|
|
||||||
"""Edition des données étudiant (identité, admission, adresses)"""
|
|
||||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
|
||||||
if not ok:
|
|
||||||
return etud # json error
|
|
||||||
#
|
|
||||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
|
||||||
etud.from_dict(args)
|
|
||||||
admission_args = args.get("admission", None)
|
|
||||||
if admission_args:
|
|
||||||
etud.admission.from_dict(admission_args)
|
|
||||||
# --- Adresse
|
|
||||||
adresses = args.get("adresses", [])
|
|
||||||
if adresses:
|
|
||||||
# ne prend en compte que la première adresse
|
|
||||||
# car si la base est concue pour avoir plusieurs adresses par étudiant,
|
|
||||||
# l'application n'en gère plus qu'une seule.
|
|
||||||
adresse = etud.adresses.first()
|
|
||||||
adresse.from_dict(adresses[0])
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
# Note: je ne comprends pas pourquoi un refresh est nécessaire ici
|
|
||||||
# sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
|
|
||||||
db.session.refresh(etud)
|
|
||||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
|
||||||
r = etud.to_dict_api(restrict=restrict)
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/etudiant/<string:code_type>/<string:code>/annotation", methods=["POST"])
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/<string:code_type>/<string:code>/annotation", methods=["POST"]
|
|
||||||
)
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.EtudInscrit) # il faut en plus ViewEtudData
|
|
||||||
@as_json
|
|
||||||
def etudiant_annotation(
|
|
||||||
code_type: str = "etudid",
|
|
||||||
code: str = None,
|
|
||||||
):
|
|
||||||
"""Ajout d'une annotation sur un étudiant"""
|
|
||||||
if not current_user.has_permission(Permission.ViewEtudData):
|
|
||||||
return json_error(403, "non autorisé (manque ViewEtudData)")
|
|
||||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
|
||||||
if not ok:
|
|
||||||
return etud # json error
|
|
||||||
#
|
|
||||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
|
||||||
comment = args.get("comment", None)
|
|
||||||
if not isinstance(comment, str):
|
|
||||||
return json_error(404, "invalid comment (expected string)")
|
|
||||||
if len(comment) > scu.MAX_TEXT_LEN:
|
|
||||||
return json_error(404, "invalid comment (too large)")
|
|
||||||
annotation = EtudAnnotation(comment=comment, author=current_user.user_name)
|
|
||||||
etud.annotations.append(annotation)
|
|
||||||
db.session.add(etud)
|
|
||||||
db.session.commit()
|
|
||||||
log(f"etudiant_annotation/{etud.id}/{annotation.id}")
|
|
||||||
return annotation.to_dict()
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/<string:code_type>/<string:code>/annotation/<int:annotation_id>/delete",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/<string:code_type>/<string:code>/annotation/<int:annotation_id>/delete",
|
|
||||||
methods=["POST"],
|
|
||||||
)
|
|
||||||
@login_required
|
|
||||||
@scodoc
|
|
||||||
@as_json
|
|
||||||
@permission_required(Permission.EtudInscrit)
|
|
||||||
def etudiant_annotation_delete(
|
|
||||||
code_type: str = "etudid", code: str = None, annotation_id: int = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Suppression d'une annotation
|
|
||||||
"""
|
|
||||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
|
||||||
if not ok:
|
|
||||||
return etud # json error
|
|
||||||
annotation = EtudAnnotation.query.filter_by(
|
|
||||||
etudid=etud.id, id=annotation_id
|
|
||||||
).first()
|
|
||||||
if annotation is None:
|
|
||||||
return json_error(404, "annotation not found")
|
|
||||||
log(f"etudiant_annotation_delete/{etud.id}/{annotation.id}")
|
|
||||||
db.session.delete(annotation)
|
|
||||||
db.session.commit()
|
|
||||||
return "ok"
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ def get_evaluation(evaluation_id: int):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@as_json
|
@as_json
|
||||||
def moduleimpl_evaluations(moduleimpl_id: int):
|
def evaluations(moduleimpl_id: int):
|
||||||
"""
|
"""
|
||||||
Retourne la liste des évaluations d'un moduleimpl
|
Retourne la liste des évaluations d'un moduleimpl
|
||||||
|
|
||||||
@ -75,8 +75,14 @@ def moduleimpl_evaluations(moduleimpl_id: int):
|
|||||||
|
|
||||||
Exemple de résultat : voir /evaluation
|
Exemple de résultat : voir /evaluation
|
||||||
"""
|
"""
|
||||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
query = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id)
|
||||||
return [evaluation.to_dict_api() for evaluation in modimpl.evaluations]
|
if g.scodoc_dept:
|
||||||
|
query = (
|
||||||
|
query.join(ModuleImpl)
|
||||||
|
.join(FormSemestre)
|
||||||
|
.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
)
|
||||||
|
return [e.to_dict_api() for e in query]
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/evaluation/<int:evaluation_id>/notes")
|
@bp.route("/evaluation/<int:evaluation_id>/notes")
|
||||||
@ -142,7 +148,7 @@ def evaluation_notes(evaluation_id: int):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.EnsView)
|
@permission_required(Permission.EnsView)
|
||||||
@as_json
|
@as_json
|
||||||
def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set
|
def evaluation_set_notes(evaluation_id: int):
|
||||||
"""Écriture de notes dans une évaluation.
|
"""Écriture de notes dans une évaluation.
|
||||||
The request content type should be "application/json",
|
The request content type should be "application/json",
|
||||||
and contains:
|
and contains:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ from operator import attrgetter, itemgetter
|
|||||||
|
|
||||||
from flask import g, make_response, request
|
from flask import g, make_response, request
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import current_user, login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db
|
from app import db
|
||||||
@ -124,8 +124,8 @@ def formsemestres_query():
|
|||||||
annee_scolaire_int = int(annee_scolaire)
|
annee_scolaire_int = int(annee_scolaire)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return json_error(API_CLIENT_ERROR, "invalid annee_scolaire: not int")
|
return json_error(API_CLIENT_ERROR, "invalid annee_scolaire: not int")
|
||||||
debut_annee = scu.date_debut_annee_scolaire(annee_scolaire_int)
|
debut_annee = scu.date_debut_anne_scolaire(annee_scolaire_int)
|
||||||
fin_annee = scu.date_fin_annee_scolaire(annee_scolaire_int)
|
fin_annee = scu.date_fin_anne_scolaire(annee_scolaire_int)
|
||||||
formsemestres = formsemestres.filter(
|
formsemestres = formsemestres.filter(
|
||||||
FormSemestre.date_fin >= debut_annee, FormSemestre.date_debut <= fin_annee
|
FormSemestre.date_fin >= debut_annee, FormSemestre.date_debut <= fin_annee
|
||||||
)
|
)
|
||||||
@ -360,8 +360,7 @@ def formsemestre_etudiants(
|
|||||||
inscriptions = formsemestre.inscriptions
|
inscriptions = formsemestre.inscriptions
|
||||||
|
|
||||||
if long:
|
if long:
|
||||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
etuds = [ins.etud.to_dict_api() for ins in inscriptions]
|
||||||
etuds = [ins.etud.to_dict_api(restrict=restrict) for ins in inscriptions]
|
|
||||||
else:
|
else:
|
||||||
etuds = [ins.etud.to_dict_short() for ins in inscriptions]
|
etuds = [ins.etud.to_dict_short() for ins in inscriptions]
|
||||||
# Ajout des groupes de chaque étudiants
|
# Ajout des groupes de chaque étudiants
|
||||||
@ -426,7 +425,7 @@ def etat_evals(formsemestre_id: int):
|
|||||||
for modimpl_id in nt.modimpls_results:
|
for modimpl_id in nt.modimpls_results:
|
||||||
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id]
|
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id]
|
||||||
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id)
|
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id)
|
||||||
modimpl_dict = modimpl.to_dict(convert_objects=True, with_module=False)
|
modimpl_dict = modimpl.to_dict(convert_objects=True)
|
||||||
|
|
||||||
list_eval = []
|
list_eval = []
|
||||||
for evaluation_id in modimpl_results.evaluations_etat:
|
for evaluation_id in modimpl_results.evaluations_etat:
|
||||||
@ -570,14 +569,10 @@ def formsemestre_edt(formsemestre_id: int):
|
|||||||
Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
|
Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
|
||||||
|
|
||||||
group_ids permet de filtrer sur les groupes ScoDoc.
|
group_ids permet de filtrer sur les groupes ScoDoc.
|
||||||
show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
|
|
||||||
"""
|
"""
|
||||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||||
group_ids = request.args.getlist("group_ids", int)
|
group_ids = request.args.getlist("group_ids", int)
|
||||||
show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False))
|
return sco_edt_cal.formsemestre_edt_dict(formsemestre, group_ids=group_ids)
|
||||||
return sco_edt_cal.formsemestre_edt_dict(
|
|
||||||
formsemestre, group_ids=group_ids, show_modules_titles=show_modules_titles
|
|
||||||
)
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -49,11 +49,6 @@ def decisions_jury(formsemestre_id: int):
|
|||||||
"""Décisions du jury des étudiants du formsemestre."""
|
"""Décisions du jury des étudiants du formsemestre."""
|
||||||
# APC, pair:
|
# APC, pair:
|
||||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||||
if formsemestre is None:
|
|
||||||
return json_error(
|
|
||||||
404,
|
|
||||||
message="formsemestre inconnu",
|
|
||||||
)
|
|
||||||
if formsemestre.formation.is_apc():
|
if formsemestre.formation.is_apc():
|
||||||
app.set_sco_dept(formsemestre.departement.acronym)
|
app.set_sco_dept(formsemestre.departement.acronym)
|
||||||
rows = jury_but_results.get_jury_but_results(formsemestre)
|
rows = jury_but_results.get_jury_but_results(formsemestre)
|
||||||
@ -66,7 +61,7 @@ def _news_delete_jury_etud(etud: Identite):
|
|||||||
"génère news sur effacement décision"
|
"génère news sur effacement décision"
|
||||||
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
|
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
|
||||||
url = url_for(
|
url = url_for(
|
||||||
"scolar.fiche_etud", scodoc_dept=etud.departement.acronym, etudid=etud.id
|
"scolar.ficheEtud", scodoc_dept=etud.departement.acronym, etudid=etud.id
|
||||||
)
|
)
|
||||||
ScolarNews.add(
|
ScolarNews.add(
|
||||||
typ=ScolarNews.NEWS_JURY,
|
typ=ScolarNews.NEWS_JURY,
|
||||||
|
@ -11,11 +11,10 @@ from flask_json import as_json
|
|||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
from werkzeug.exceptions import NotFound
|
|
||||||
|
|
||||||
import app.scodoc.sco_assiduites as scass
|
import app.scodoc.sco_assiduites as scass
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app import db, set_sco_dept
|
from app import db
|
||||||
from app.api import api_bp as bp
|
from app.api import api_bp as bp
|
||||||
from app.api import api_web_bp
|
from app.api import api_web_bp
|
||||||
from app.api import get_model_api_object, tools
|
from app.api import get_model_api_object, tools
|
||||||
@ -25,6 +24,7 @@ from app.models import (
|
|||||||
Justificatif,
|
Justificatif,
|
||||||
Departement,
|
Departement,
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
|
FormSemestreInscription,
|
||||||
)
|
)
|
||||||
from app.models.assiduites import (
|
from app.models.assiduites import (
|
||||||
compute_assiduites_justified,
|
compute_assiduites_justified,
|
||||||
@ -53,19 +53,14 @@ def justificatif(justif_id: int = None):
|
|||||||
"date_fin": "2022-10-31T10:00+01:00",
|
"date_fin": "2022-10-31T10:00+01:00",
|
||||||
"etat": "valide",
|
"etat": "valide",
|
||||||
"fichier": "archive_id",
|
"fichier": "archive_id",
|
||||||
"raison": "une raison", // VIDE si pas le droit
|
"raison": "une raison",
|
||||||
"entry_date": "2022-10-31T08:00+01:00",
|
"entry_date": "2022-10-31T08:00+01:00",
|
||||||
"user_id": 1 or null,
|
"user_id": 1 or null,
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return get_model_api_object(
|
return get_model_api_object(Justificatif, justif_id, Identite)
|
||||||
Justificatif,
|
|
||||||
justif_id,
|
|
||||||
Identite,
|
|
||||||
restrict=not current_user.has_permission(Permission.AbsJustifView),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# etudid
|
# etudid
|
||||||
@ -138,9 +133,8 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
|
|||||||
|
|
||||||
# Mise en forme des données puis retour en JSON
|
# Mise en forme des données puis retour en JSON
|
||||||
data_set: list[dict] = []
|
data_set: list[dict] = []
|
||||||
restrict = not current_user.has_permission(Permission.AbsJustifView)
|
|
||||||
for just in justificatifs_query.all():
|
for just in justificatifs_query.all():
|
||||||
data = just.to_dict(format_api=True, restrict=restrict)
|
data = just.to_dict(format_api=True)
|
||||||
data_set.append(data)
|
data_set.append(data)
|
||||||
|
|
||||||
return data_set
|
return data_set
|
||||||
@ -157,15 +151,10 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
|
|||||||
@as_json
|
@as_json
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
def justificatifs_dept(dept_id: int = None, with_query: bool = False):
|
def justificatifs_dept(dept_id: int = None, with_query: bool = False):
|
||||||
"""
|
""" """
|
||||||
Renvoie tous les justificatifs d'un département
|
|
||||||
(en ajoutant un champ "formsemestre" si possible)
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Récupération du département et des étudiants du département
|
# Récupération du département et des étudiants du département
|
||||||
dept: Departement = Departement.query.get(dept_id)
|
dept: Departement = Departement.query.get_or_404(dept_id)
|
||||||
if dept is None:
|
|
||||||
return json_error(404, "Assiduité non existante")
|
|
||||||
etuds: list[int] = [etud.id for etud in dept.etudiants]
|
etuds: list[int] = [etud.id for etud in dept.etudiants]
|
||||||
|
|
||||||
# Récupération des justificatifs des étudiants du département
|
# Récupération des justificatifs des étudiants du département
|
||||||
@ -178,15 +167,14 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
|
|||||||
justificatifs_query: Query = _filter_manager(request, justificatifs_query)
|
justificatifs_query: Query = _filter_manager(request, justificatifs_query)
|
||||||
|
|
||||||
# Mise en forme des données et retour JSON
|
# Mise en forme des données et retour JSON
|
||||||
restrict = not current_user.has_permission(Permission.AbsJustifView)
|
|
||||||
data_set: list[dict] = []
|
data_set: list[dict] = []
|
||||||
for just in justificatifs_query:
|
for just in justificatifs_query:
|
||||||
data_set.append(_set_sems(just, restrict=restrict))
|
data_set.append(_set_sems(just))
|
||||||
|
|
||||||
return data_set
|
return data_set
|
||||||
|
|
||||||
|
|
||||||
def _set_sems(justi: Justificatif, restrict: bool) -> dict:
|
def _set_sems(justi: Justificatif) -> dict:
|
||||||
"""
|
"""
|
||||||
_set_sems Ajoute le formsemestre associé au justificatif s'il existe
|
_set_sems Ajoute le formsemestre associé au justificatif s'il existe
|
||||||
|
|
||||||
@ -199,7 +187,7 @@ def _set_sems(justi: Justificatif, restrict: bool) -> dict:
|
|||||||
dict: La représentation de l'assiduité en dictionnaire
|
dict: La représentation de l'assiduité en dictionnaire
|
||||||
"""
|
"""
|
||||||
# Conversion du justificatif en dictionnaire
|
# Conversion du justificatif en dictionnaire
|
||||||
data = justi.to_dict(format_api=True, restrict=restrict)
|
data = justi.to_dict(format_api=True)
|
||||||
|
|
||||||
# Récupération du formsemestre de l'assiduité
|
# Récupération du formsemestre de l'assiduité
|
||||||
formsemestre: FormSemestre = get_formsemestre_from_data(justi.to_dict())
|
formsemestre: FormSemestre = get_formsemestre_from_data(justi.to_dict())
|
||||||
@ -253,10 +241,9 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
|
|||||||
justificatifs_query: Query = _filter_manager(request, justificatifs_query)
|
justificatifs_query: Query = _filter_manager(request, justificatifs_query)
|
||||||
|
|
||||||
# Retour des justificatifs en JSON
|
# Retour des justificatifs en JSON
|
||||||
restrict = not current_user.has_permission(Permission.AbsJustifView)
|
|
||||||
data_set: list[dict] = []
|
data_set: list[dict] = []
|
||||||
for justi in justificatifs_query.all():
|
for justi in justificatifs_query.all():
|
||||||
data = justi.to_dict(format_api=True, restrict=restrict)
|
data = justi.to_dict(format_api=True)
|
||||||
data_set.append(data)
|
data_set.append(data)
|
||||||
|
|
||||||
return data_set
|
return data_set
|
||||||
@ -305,7 +292,6 @@ def justif_create(etudid: int = None, nip=None, ine=None):
|
|||||||
404,
|
404,
|
||||||
message="étudiant inconnu",
|
message="étudiant inconnu",
|
||||||
)
|
)
|
||||||
set_sco_dept(etud.departement.acronym)
|
|
||||||
|
|
||||||
# Récupération des justificatifs à créer
|
# Récupération des justificatifs à créer
|
||||||
create_list: list[object] = request.get_json(force=True)
|
create_list: list[object] = request.get_json(force=True)
|
||||||
@ -388,7 +374,7 @@ def _create_one(
|
|||||||
date_debut=deb,
|
date_debut=deb,
|
||||||
date_fin=fin,
|
date_fin=fin,
|
||||||
etat=etat,
|
etat=etat,
|
||||||
etudiant=etud,
|
etud=etud,
|
||||||
raison=raison,
|
raison=raison,
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
external_data=external_data,
|
external_data=external_data,
|
||||||
@ -434,7 +420,9 @@ def justif_edit(justif_id: int):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Récupération du justificatif à modifier
|
# Récupération du justificatif à modifier
|
||||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
justificatif_unique: Query = Justificatif.query.filter_by(
|
||||||
|
id=justif_id
|
||||||
|
).first_or_404()
|
||||||
|
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
data = request.get_json(force=True)
|
data = request.get_json(force=True)
|
||||||
@ -510,7 +498,7 @@ def justif_edit(justif_id: int):
|
|||||||
retour = {
|
retour = {
|
||||||
"couverture": {
|
"couverture": {
|
||||||
"avant": avant_ids,
|
"avant": avant_ids,
|
||||||
"apres": compute_assiduites_justified(
|
"après": compute_assiduites_justified(
|
||||||
justificatif_unique.etudid,
|
justificatif_unique.etudid,
|
||||||
[justificatif_unique],
|
[justificatif_unique],
|
||||||
True,
|
True,
|
||||||
@ -574,10 +562,12 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
|
|||||||
message : OK si réussi, message d'erreur sinon
|
message : OK si réussi, message d'erreur sinon
|
||||||
"""
|
"""
|
||||||
# Récupération du justificatif à supprimer
|
# Récupération du justificatif à supprimer
|
||||||
try:
|
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
id=justif_id
|
||||||
except NotFound:
|
).first()
|
||||||
|
if justificatif_unique is None:
|
||||||
return (404, "Justificatif non existant")
|
return (404, "Justificatif non existant")
|
||||||
|
|
||||||
# Récupération de l'archive du justificatif
|
# Récupération de l'archive du justificatif
|
||||||
archive_name: str = justificatif_unique.fichier
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
|
||||||
@ -623,7 +613,10 @@ def justif_import(justif_id: int = None):
|
|||||||
return json_error(404, "Il n'y a pas de fichier joint")
|
return json_error(404, "Il n'y a pas de fichier joint")
|
||||||
|
|
||||||
# On récupère le justificatif auquel on va importer le fichier
|
# On récupère le justificatif auquel on va importer le fichier
|
||||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
query: Query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
# Récupération de l'archive si elle existe
|
# Récupération de l'archive si elle existe
|
||||||
archive_name: str = justificatif_unique.fichier
|
archive_name: str = justificatif_unique.fichier
|
||||||
@ -648,32 +641,26 @@ def justif_import(justif_id: int = None):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return {"filename": fname}
|
return {"filename": fname}
|
||||||
except ScoValueError as exc:
|
except ScoValueError as err:
|
||||||
# Si cela ne fonctionne pas on renvoie une erreur
|
# Si cela ne fonctionne pas on renvoie une erreur
|
||||||
return json_error(404, exc.args[0])
|
return json_error(404, err.args[0])
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["GET", "POST"])
|
@bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["POST"])
|
||||||
@api_web_bp.route(
|
@api_web_bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["POST"])
|
||||||
"/justificatif/<int:justif_id>/export/<filename>", methods=["GET", "POST"]
|
|
||||||
)
|
|
||||||
@scodoc
|
@scodoc
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.AbsChange)
|
||||||
def justif_export(justif_id: int | None = None, filename: str | None = None):
|
def justif_export(justif_id: int = None, filename: str = None):
|
||||||
"""
|
"""
|
||||||
Retourne un fichier d'une archive d'un justificatif.
|
Retourne un fichier d'une archive d'un justificatif
|
||||||
La permission est ScoView + (AbsJustifView ou être l'auteur du justifcatif)
|
|
||||||
"""
|
"""
|
||||||
# On récupère le justificatif concerné
|
|
||||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
|
||||||
|
|
||||||
# Vérification des permissions
|
# On récupère le justificatif concerné
|
||||||
if not (
|
query: Query = Justificatif.query.filter_by(id=justif_id)
|
||||||
current_user.has_permission(Permission.AbsJustifView)
|
if g.scodoc_dept:
|
||||||
or justificatif_unique.user_id == current_user.id
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
):
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
return json_error(401, "non autorisé à voir ce fichier")
|
|
||||||
|
|
||||||
# On récupère l'archive concernée
|
# On récupère l'archive concernée
|
||||||
archive_name: str = justificatif_unique.fichier
|
archive_name: str = justificatif_unique.fichier
|
||||||
@ -699,7 +686,6 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
|
|||||||
@as_json
|
@as_json
|
||||||
@permission_required(Permission.AbsChange)
|
@permission_required(Permission.AbsChange)
|
||||||
def justif_remove(justif_id: int = None):
|
def justif_remove(justif_id: int = None):
|
||||||
# XXX TODO pas de test unitaire
|
|
||||||
"""
|
"""
|
||||||
Supression d'un fichier ou d'une archive
|
Supression d'un fichier ou d'une archive
|
||||||
{
|
{
|
||||||
@ -716,7 +702,10 @@ def justif_remove(justif_id: int = None):
|
|||||||
data: dict = request.get_json(force=True)
|
data: dict = request.get_json(force=True)
|
||||||
|
|
||||||
# On récupère le justificatif concerné
|
# On récupère le justificatif concerné
|
||||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
query: Query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
# On récupère l'archive
|
# On récupère l'archive
|
||||||
archive_name: str = justificatif_unique.fichier
|
archive_name: str = justificatif_unique.fichier
|
||||||
@ -778,7 +767,10 @@ def justif_list(justif_id: int = None):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Récupération du justificatif concerné
|
# Récupération du justificatif concerné
|
||||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
query: Query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
# Récupération de l'archive avec l'archiver
|
# Récupération de l'archive avec l'archiver
|
||||||
archive_name: str = justificatif_unique.fichier
|
archive_name: str = justificatif_unique.fichier
|
||||||
@ -820,7 +812,10 @@ def justif_justifies(justif_id: int = None):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# On récupère le justificatif concerné
|
# On récupère le justificatif concerné
|
||||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
query: Query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
# On récupère la liste des assiduités justifiées par le justificatif
|
# On récupère la liste des assiduités justifiées par le justificatif
|
||||||
assiduites_list: list[int] = scass.justifies(justificatif_unique)
|
assiduites_list: list[int] = scass.justifies(justificatif_unique)
|
||||||
@ -834,7 +829,6 @@ def justif_justifies(justif_id: int = None):
|
|||||||
def _filter_manager(requested, justificatifs_query: Query):
|
def _filter_manager(requested, justificatifs_query: Query):
|
||||||
"""
|
"""
|
||||||
Retourne les justificatifs entrés filtrés en fonction de la request
|
Retourne les justificatifs entrés filtrés en fonction de la request
|
||||||
et du département courant s'il y en a un
|
|
||||||
"""
|
"""
|
||||||
# cas 1 : etat justificatif
|
# cas 1 : etat justificatif
|
||||||
etat: str = requested.args.get("etat")
|
etat: str = requested.args.get("etat")
|
||||||
@ -869,7 +863,7 @@ def _filter_manager(requested, justificatifs_query: Query):
|
|||||||
formsemestre: FormSemestre = None
|
formsemestre: FormSemestre = None
|
||||||
try:
|
try:
|
||||||
formsemestre_id = int(formsemestre_id)
|
formsemestre_id = int(formsemestre_id)
|
||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||||
justificatifs_query = scass.filter_by_formsemestre(
|
justificatifs_query = scass.filter_by_formsemestre(
|
||||||
justificatifs_query, Justificatif, formsemestre
|
justificatifs_query, Justificatif, formsemestre
|
||||||
)
|
)
|
||||||
@ -888,8 +882,8 @@ def _filter_manager(requested, justificatifs_query: Query):
|
|||||||
annee: int = scu.annee_scolaire()
|
annee: int = scu.annee_scolaire()
|
||||||
|
|
||||||
justificatifs_query: Query = justificatifs_query.filter(
|
justificatifs_query: Query = justificatifs_query.filter(
|
||||||
Justificatif.date_debut >= scu.date_debut_annee_scolaire(annee),
|
Justificatif.date_debut >= scu.date_debut_anne_scolaire(annee),
|
||||||
Justificatif.date_fin <= scu.date_fin_annee_scolaire(annee),
|
Justificatif.date_fin <= scu.date_fin_anne_scolaire(annee),
|
||||||
)
|
)
|
||||||
|
|
||||||
# cas 8 : group_id filtre les justificatifs d'un groupe d'étudiant
|
# cas 8 : group_id filtre les justificatifs d'un groupe d'étudiant
|
||||||
@ -904,10 +898,4 @@ def _filter_manager(requested, justificatifs_query: Query):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
group_id = None
|
group_id = None
|
||||||
|
|
||||||
# Département
|
|
||||||
if g.scodoc_dept:
|
|
||||||
justificatifs_query = justificatifs_query.join(Identite).filter_by(
|
|
||||||
dept_id=g.scodoc_dept_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return justificatifs_query
|
return justificatifs_query
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -8,14 +8,16 @@
|
|||||||
ScoDoc 9 API : accès aux moduleimpl
|
ScoDoc 9 API : accès aux moduleimpl
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from flask import g
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
import app
|
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.models import ModuleImpl
|
from app.models import (
|
||||||
from app.scodoc import sco_liste_notes
|
FormSemestre,
|
||||||
|
ModuleImpl,
|
||||||
|
)
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
|
||||||
|
|
||||||
@ -60,7 +62,10 @@ def moduleimpl(moduleimpl_id: int):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
query = ModuleImpl.query.filter_by(id=moduleimpl_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
modimpl: ModuleImpl = query.first_or_404()
|
||||||
return modimpl.to_dict(convert_objects=True)
|
return modimpl.to_dict(convert_objects=True)
|
||||||
|
|
||||||
|
|
||||||
@ -82,36 +87,8 @@ def moduleimpl_inscriptions(moduleimpl_id: int):
|
|||||||
...
|
...
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
query = ModuleImpl.query.filter_by(id=moduleimpl_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
modimpl: ModuleImpl = query.first_or_404()
|
||||||
return [i.to_dict() for i in modimpl.inscriptions]
|
return [i.to_dict() for i in modimpl.inscriptions]
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/moduleimpl/<int:moduleimpl_id>/notes")
|
|
||||||
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/notes")
|
|
||||||
@login_required
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.ScoView)
|
|
||||||
def moduleimpl_notes(moduleimpl_id: int):
|
|
||||||
"""Liste des notes dans ce moduleimpl
|
|
||||||
Exemple de résultat :
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"etudid": 17776, // code de l'étudiant
|
|
||||||
"nom": "DUPONT",
|
|
||||||
"prenom": "Luz",
|
|
||||||
"38411": 16.0, // Note dans l'évaluation d'id 38411
|
|
||||||
"38410": 15.0,
|
|
||||||
"moymod": 15.5, // Moyenne INDICATIVE module
|
|
||||||
"moy_ue_2875": 15.5, // Moyenne vers l'UE 2875
|
|
||||||
"moy_ue_2876": 15.5, // Moyenne vers l'UE 2876
|
|
||||||
"moy_ue_2877": 15.5 // Moyenne vers l'UE 2877
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
|
||||||
app.set_sco_dept(modimpl.formsemestre.departement.acronym)
|
|
||||||
table, _ = sco_liste_notes.do_evaluation_listenotes(
|
|
||||||
moduleimpl_id=modimpl.id, fmt="json"
|
|
||||||
)
|
|
||||||
return table
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -303,26 +303,15 @@ def group_create(partition_id: int): # partition-group-create
|
|||||||
return json_error(403, "partition non editable")
|
return json_error(403, "partition non editable")
|
||||||
if not partition.formsemestre.can_change_groups():
|
if not partition.formsemestre.can_change_groups():
|
||||||
return json_error(401, "opération non autorisée")
|
return json_error(401, "opération non autorisée")
|
||||||
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
group_name = data.get("group_name")
|
||||||
group_name = args.get("group_name")
|
if group_name is None:
|
||||||
if not isinstance(group_name, str):
|
|
||||||
return json_error(API_CLIENT_ERROR, "missing group name or invalid data format")
|
return json_error(API_CLIENT_ERROR, "missing group name or invalid data format")
|
||||||
args["group_name"] = args["group_name"].strip()
|
if not GroupDescr.check_name(partition, group_name):
|
||||||
if not GroupDescr.check_name(partition, args["group_name"]):
|
|
||||||
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||||
|
group_name = group_name.strip()
|
||||||
|
|
||||||
# le numero est optionnel
|
group = GroupDescr(group_name=group_name, partition_id=partition_id)
|
||||||
numero = args.get("numero")
|
|
||||||
if numero is None:
|
|
||||||
numeros = [gr.numero or 0 for gr in partition.groups]
|
|
||||||
numero = (max(numeros) + 1) if numeros else 0
|
|
||||||
args["numero"] = numero
|
|
||||||
args["partition_id"] = partition_id
|
|
||||||
try:
|
|
||||||
group = GroupDescr(**args)
|
|
||||||
except TypeError:
|
|
||||||
return json_error(API_CLIENT_ERROR, "invalid arguments")
|
|
||||||
db.session.add(group)
|
db.session.add(group)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
log(f"created group {group}")
|
log(f"created group {group}")
|
||||||
@ -380,53 +369,21 @@ def group_edit(group_id: int):
|
|||||||
return json_error(403, "partition non editable")
|
return json_error(403, "partition non editable")
|
||||||
if not group.partition.formsemestre.can_change_groups():
|
if not group.partition.formsemestre.can_change_groups():
|
||||||
return json_error(401, "opération non autorisée")
|
return json_error(401, "opération non autorisée")
|
||||||
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
group_name = data.get("group_name")
|
||||||
if "group_name" in args:
|
if group_name is not None:
|
||||||
if not isinstance(args["group_name"], str):
|
group_name = group_name.strip()
|
||||||
return json_error(API_CLIENT_ERROR, "invalid data format for group_name")
|
if not GroupDescr.check_name(group.partition, group_name, existing=True):
|
||||||
args["group_name"] = args["group_name"].strip() if args["group_name"] else ""
|
|
||||||
if not GroupDescr.check_name(
|
|
||||||
group.partition, args["group_name"], existing=True
|
|
||||||
):
|
|
||||||
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||||
|
group.group_name = group_name
|
||||||
group.from_dict(args)
|
|
||||||
db.session.add(group)
|
db.session.add(group)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
log(f"modified {group}")
|
log(f"modified {group}")
|
||||||
|
|
||||||
app.set_sco_dept(group.partition.formsemestre.departement.acronym)
|
app.set_sco_dept(group.partition.formsemestre.departement.acronym)
|
||||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
||||||
return group.to_dict(with_partition=True)
|
return group.to_dict(with_partition=True)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/group/<int:group_id>/set_edt_id/<string:edt_id>", methods=["POST"])
|
|
||||||
@api_web_bp.route("/group/<int:group_id>/set_edt_id/<string:edt_id>", methods=["POST"])
|
|
||||||
@login_required
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.ScoView)
|
|
||||||
@as_json
|
|
||||||
def group_set_edt_id(group_id: int, edt_id: str):
|
|
||||||
"""Set edt_id for this group.
|
|
||||||
Contrairement à /edit, peut-être changé pour toute partition
|
|
||||||
ou formsemestre non verrouillé.
|
|
||||||
"""
|
|
||||||
query = GroupDescr.query.filter_by(id=group_id)
|
|
||||||
if g.scodoc_dept:
|
|
||||||
query = (
|
|
||||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
|
||||||
)
|
|
||||||
group: GroupDescr = query.first_or_404()
|
|
||||||
if not group.partition.formsemestre.can_change_groups():
|
|
||||||
return json_error(401, "opération non autorisée")
|
|
||||||
log(f"group_set_edt_id( {group_id}, '{edt_id}' )")
|
|
||||||
group.edt_id = edt_id
|
|
||||||
db.session.add(group)
|
|
||||||
db.session.commit()
|
|
||||||
return group.to_dict(with_partition=True)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"])
|
@bp.route("/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"])
|
||||||
@api_web_bp.route(
|
@api_web_bp.route(
|
||||||
"/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"]
|
"/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"]
|
||||||
@ -527,7 +484,6 @@ def formsemestre_order_partitions(formsemestre_id: int):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
app.set_sco_dept(formsemestre.departement.acronym)
|
app.set_sco_dept(formsemestre.departement.acronym)
|
||||||
sco_cache.invalidate_formsemestre(formsemestre_id)
|
sco_cache.invalidate_formsemestre(formsemestre_id)
|
||||||
log(f"formsemestre_order_partitions({partition_ids})")
|
|
||||||
return [
|
return [
|
||||||
partition.to_dict()
|
partition.to_dict()
|
||||||
for partition in formsemestre.partitions.order_by(Partition.numero)
|
for partition in formsemestre.partitions.order_by(Partition.numero)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
"""ScoDoc 9 API : outils
|
"""ScoDoc 9 API : outils
|
||||||
|
115
app/api/users.py
115
app/api/users.py
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -8,20 +8,20 @@
|
|||||||
ScoDoc 9 API : accès aux utilisateurs
|
ScoDoc 9 API : accès aux utilisateurs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||||
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.auth.models import User, Role, UserRole
|
from app.auth.models import User, Role, UserRole
|
||||||
from app.auth.models import is_valid_password
|
from app.auth.models import is_valid_password
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.models import Departement, ScoDocSiteConfig
|
from app.models import Departement
|
||||||
from app.scodoc import sco_edt_cal
|
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_utils import json_error
|
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
@ -85,20 +85,6 @@ def users_info_query():
|
|||||||
return [user.to_dict() for user in query]
|
return [user.to_dict() for user in query]
|
||||||
|
|
||||||
|
|
||||||
def _is_allowed_user_edit(args: dict) -> tuple[bool, str]:
|
|
||||||
"Vrai si on peut"
|
|
||||||
if "cas_id" in args and not current_user.has_permission(
|
|
||||||
Permission.UsersChangeCASId
|
|
||||||
):
|
|
||||||
return False, "non autorise a changer cas_id"
|
|
||||||
|
|
||||||
if not current_user.is_administrator():
|
|
||||||
for field in ("cas_allow_login", "cas_allow_scodoc_login"):
|
|
||||||
if field in args:
|
|
||||||
return False, f"non autorise a changer {field}"
|
|
||||||
return True, ""
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/user/create", methods=["POST"])
|
@bp.route("/user/create", methods=["POST"])
|
||||||
@api_web_bp.route("/user/create", methods=["POST"])
|
@api_web_bp.route("/user/create", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@ -109,22 +95,21 @@ def user_create():
|
|||||||
"""Création d'un utilisateur
|
"""Création d'un utilisateur
|
||||||
The request content type should be "application/json":
|
The request content type should be "application/json":
|
||||||
{
|
{
|
||||||
"active":bool (default True),
|
"user_name": str,
|
||||||
"dept": str or null,
|
"dept": str or null,
|
||||||
"nom": str,
|
"nom": str,
|
||||||
"prenom": str,
|
"prenom": str,
|
||||||
"user_name": str,
|
"active":bool (default True)
|
||||||
...
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
user_name = args.get("user_name")
|
user_name = data.get("user_name")
|
||||||
if not user_name:
|
if not user_name:
|
||||||
return json_error(404, "empty user_name")
|
return json_error(404, "empty user_name")
|
||||||
user = User.query.filter_by(user_name=user_name).first()
|
user = User.query.filter_by(user_name=user_name).first()
|
||||||
if user:
|
if user:
|
||||||
return json_error(404, f"user_create: user {user} already exists\n")
|
return json_error(404, f"user_create: user {user} already exists\n")
|
||||||
dept = args.get("dept")
|
dept = data.get("dept")
|
||||||
if dept == "@all":
|
if dept == "@all":
|
||||||
dept = None
|
dept = None
|
||||||
allowed_depts = current_user.get_depts_with_permission(Permission.UsersAdmin)
|
allowed_depts = current_user.get_depts_with_permission(Permission.UsersAdmin)
|
||||||
@ -134,12 +119,10 @@ def user_create():
|
|||||||
Departement.query.filter_by(acronym=dept).first() is None
|
Departement.query.filter_by(acronym=dept).first() is None
|
||||||
):
|
):
|
||||||
return json_error(404, "user_create: departement inexistant")
|
return json_error(404, "user_create: departement inexistant")
|
||||||
args["dept"] = dept
|
nom = data.get("nom")
|
||||||
ok, msg = _is_allowed_user_edit(args)
|
prenom = data.get("prenom")
|
||||||
if not ok:
|
active = scu.to_bool(data.get("active", True))
|
||||||
return json_error(403, f"user_create: {msg}")
|
user = User(user_name=user_name, active=active, dept=dept, nom=nom, prenom=prenom)
|
||||||
user = User(user_name=user_name)
|
|
||||||
user.from_dict(args, new_user=True)
|
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user.to_dict()
|
return user.to_dict()
|
||||||
@ -159,14 +142,13 @@ def user_edit(uid: int):
|
|||||||
"nom": str,
|
"nom": str,
|
||||||
"prenom": str,
|
"prenom": str,
|
||||||
"active":bool
|
"active":bool
|
||||||
...
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
user: User = User.query.get_or_404(uid)
|
user: User = User.query.get_or_404(uid)
|
||||||
# L'utilisateur doit avoir le droit dans le département de départ et celui d'arrivée
|
# L'utilisateur doit avoir le droit dans le département de départ et celui d'arrivée
|
||||||
orig_dept = user.dept
|
orig_dept = user.dept
|
||||||
dest_dept = args.get("dept", False)
|
dest_dept = data.get("dept", False)
|
||||||
if dest_dept is not False:
|
if dest_dept is not False:
|
||||||
if dest_dept == "@all":
|
if dest_dept == "@all":
|
||||||
dest_dept = None
|
dest_dept = None
|
||||||
@ -182,11 +164,10 @@ def user_edit(uid: int):
|
|||||||
return json_error(404, "user_edit: departement inexistant")
|
return json_error(404, "user_edit: departement inexistant")
|
||||||
user.dept = dest_dept
|
user.dept = dest_dept
|
||||||
|
|
||||||
ok, msg = _is_allowed_user_edit(args)
|
user.nom = data.get("nom", user.nom)
|
||||||
if not ok:
|
user.prenom = data.get("prenom", user.prenom)
|
||||||
return json_error(403, f"user_edit: {msg}")
|
user.active = scu.to_bool(data.get("active", user.active))
|
||||||
|
|
||||||
user.from_dict(args)
|
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user.to_dict()
|
return user.to_dict()
|
||||||
@ -441,63 +422,3 @@ def role_delete(role_name: str):
|
|||||||
db.session.delete(role)
|
db.session.delete(role)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return {"OK": True}
|
return {"OK": True}
|
||||||
|
|
||||||
|
|
||||||
# @bp.route("/user/<int:uid>/edt")
|
|
||||||
# @api_web_bp.route("/user/<int:uid>/edt")
|
|
||||||
# @login_required
|
|
||||||
# @scodoc
|
|
||||||
# @permission_required(Permission.ScoView)
|
|
||||||
# @as_json
|
|
||||||
# def user_edt(uid: int):
|
|
||||||
# """L'emploi du temps de l'utilisateur.
|
|
||||||
# Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
|
|
||||||
|
|
||||||
# show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
|
|
||||||
|
|
||||||
# Il faut la permission ScoView + (UsersView ou bien être connecté comme l'utilisateur demandé)
|
|
||||||
# """
|
|
||||||
# if g.scodoc_dept is None: # route API non départementale
|
|
||||||
# if not current_user.has_permission(Permission.UsersView):
|
|
||||||
# return scu.json_error(403, "accès non autorisé")
|
|
||||||
# user: User = db.session.get(User, uid)
|
|
||||||
# if user is None:
|
|
||||||
# return json_error(404, "user not found")
|
|
||||||
# # Check permission
|
|
||||||
# if current_user.id != user.id:
|
|
||||||
# if g.scodoc_dept:
|
|
||||||
# allowed_depts = current_user.get_depts_with_permission(Permission.UsersView)
|
|
||||||
# if (None not in allowed_depts) and (user.dept not in allowed_depts):
|
|
||||||
# return json_error(404, "user not found")
|
|
||||||
|
|
||||||
# show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False))
|
|
||||||
|
|
||||||
# # Cherche ics
|
|
||||||
# if not user.edt_id:
|
|
||||||
# return json_error(404, "user not configured")
|
|
||||||
# ics_filename = sco_edt_cal.get_ics_user_edt_filename(user.edt_id)
|
|
||||||
# if not ics_filename:
|
|
||||||
# return json_error(404, "no calendar for this user")
|
|
||||||
|
|
||||||
# _, calendar = sco_edt_cal.load_calendar(ics_filename)
|
|
||||||
|
|
||||||
# # TODO:
|
|
||||||
# # - Construire mapping edt2modimpl: edt_id -> modimpl
|
|
||||||
# # pour cela, considérer tous les formsemestres de la période de l'edt
|
|
||||||
# # (soit on considère l'année scolaire du 1er event, ou celle courante,
|
|
||||||
# # soit on cherche min, max des dates des events)
|
|
||||||
# # - Modifier décodage des groupes dans convert_ics pour avoi run mapping
|
|
||||||
# # de groupe par semestre (retrouvé grâce au modimpl associé à l'event)
|
|
||||||
|
|
||||||
# raise NotImplementedError() # TODO XXX WIP
|
|
||||||
|
|
||||||
# events_scodoc, _ = sco_edt_cal.convert_ics(
|
|
||||||
# calendar,
|
|
||||||
# edt2group=edt2group,
|
|
||||||
# default_group=default_group,
|
|
||||||
# edt2modimpl=edt2modimpl,
|
|
||||||
# )
|
|
||||||
# edt_dict = sco_edt_cal.translate_calendar(
|
|
||||||
# events_scodoc, group_ids, show_modules_titles=show_modules_titles
|
|
||||||
# )
|
|
||||||
# return edt_dict
|
|
||||||
|
@ -12,6 +12,7 @@ from typing import Optional
|
|||||||
|
|
||||||
import cracklib # pylint: disable=import-error
|
import cracklib # pylint: disable=import-error
|
||||||
|
|
||||||
|
import flask
|
||||||
from flask import current_app, g
|
from flask import current_app, g
|
||||||
from flask_login import UserMixin, AnonymousUserMixin
|
from flask_login import UserMixin, AnonymousUserMixin
|
||||||
|
|
||||||
@ -20,13 +21,14 @@ from werkzeug.security import generate_password_hash, check_password_hash
|
|||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
from app import db, email, log, login
|
from app import db, email, log, login
|
||||||
from app.models import Departement, ScoDocModel
|
from app.models import Departement
|
||||||
from app.models import SHORT_STR_LEN, USERNAME_STR_LEN
|
from app.models import SHORT_STR_LEN, USERNAME_STR_LEN
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
|
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app.scodoc import sco_etud # a deplacer dans scu
|
||||||
|
|
||||||
VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$")
|
VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$")
|
||||||
|
|
||||||
@ -51,14 +53,13 @@ def is_valid_password(cleartxt) -> bool:
|
|||||||
def invalid_user_name(user_name: str) -> bool:
|
def invalid_user_name(user_name: str) -> bool:
|
||||||
"Check that user_name (aka login) is invalid"
|
"Check that user_name (aka login) is invalid"
|
||||||
return (
|
return (
|
||||||
not user_name
|
(len(user_name) < 2)
|
||||||
or (len(user_name) < 2)
|
|
||||||
or (len(user_name) >= USERNAME_STR_LEN)
|
or (len(user_name) >= USERNAME_STR_LEN)
|
||||||
or not VALID_LOGIN_EXP.match(user_name)
|
or not VALID_LOGIN_EXP.match(user_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin, ScoDocModel):
|
class User(UserMixin, db.Model):
|
||||||
"""ScoDoc users, handled by Flask / SQLAlchemy"""
|
"""ScoDoc users, handled by Flask / SQLAlchemy"""
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@ -90,7 +91,7 @@ class User(UserMixin, ScoDocModel):
|
|||||||
"""date du dernier login via CAS"""
|
"""date du dernier login via CAS"""
|
||||||
edt_id = db.Column(db.Text(), index=True, nullable=True)
|
edt_id = db.Column(db.Text(), index=True, nullable=True)
|
||||||
"identifiant emplois du temps (unicité non imposée)"
|
"identifiant emplois du temps (unicité non imposée)"
|
||||||
password_hash = db.Column(db.Text()) # les hashs modernes peuvent être très longs
|
password_hash = db.Column(db.String(128))
|
||||||
password_scodoc7 = db.Column(db.String(42))
|
password_scodoc7 = db.Column(db.String(42))
|
||||||
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
|
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
date_modif_passwd = db.Column(db.DateTime, default=datetime.utcnow)
|
date_modif_passwd = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
@ -102,8 +103,6 @@ class User(UserMixin, ScoDocModel):
|
|||||||
token = db.Column(db.Text(), index=True, unique=True)
|
token = db.Column(db.Text(), index=True, unique=True)
|
||||||
token_expiration = db.Column(db.DateTime)
|
token_expiration = db.Column(db.DateTime)
|
||||||
|
|
||||||
# Define the back reference from User to ModuleImpl
|
|
||||||
modimpls = db.relationship("ModuleImpl", back_populates="responsable")
|
|
||||||
roles = db.relationship("Role", secondary="user_role", viewonly=True)
|
roles = db.relationship("Role", secondary="user_role", viewonly=True)
|
||||||
Permission = Permission
|
Permission = Permission
|
||||||
|
|
||||||
@ -117,17 +116,12 @@ class User(UserMixin, ScoDocModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"user_name:str is mandatory"
|
|
||||||
self.roles = []
|
self.roles = []
|
||||||
self.user_roles = []
|
self.user_roles = []
|
||||||
# check login:
|
# check login:
|
||||||
if not "user_name" in kwargs:
|
if kwargs.get("user_name") and invalid_user_name(kwargs["user_name"]):
|
||||||
raise ValueError("missing user_name argument")
|
|
||||||
if invalid_user_name(kwargs["user_name"]):
|
|
||||||
raise ValueError(f"invalid user_name: {kwargs['user_name']}")
|
raise ValueError(f"invalid user_name: {kwargs['user_name']}")
|
||||||
kwargs["nom"] = kwargs.get("nom", "") or ""
|
super(User, self).__init__(**kwargs)
|
||||||
kwargs["prenom"] = kwargs.get("prenom", "") or ""
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
# Ajoute roles:
|
# Ajoute roles:
|
||||||
if (
|
if (
|
||||||
not self.roles
|
not self.roles
|
||||||
@ -236,44 +230,33 @@ class User(UserMixin, ScoDocModel):
|
|||||||
return None
|
return None
|
||||||
return db.session.get(User, user_id)
|
return db.session.get(User, user_id)
|
||||||
|
|
||||||
def sort_key(self) -> tuple:
|
|
||||||
"sort key"
|
|
||||||
return (
|
|
||||||
(self.nom or "").upper(),
|
|
||||||
(self.prenom or "").upper(),
|
|
||||||
(self.user_name or "").upper(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def to_dict(self, include_email=True):
|
def to_dict(self, include_email=True):
|
||||||
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
|
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
|
||||||
data = {
|
data = {
|
||||||
"date_expiration": (
|
"date_expiration": self.date_expiration.isoformat() + "Z"
|
||||||
self.date_expiration.isoformat() + "Z" if self.date_expiration else None
|
if self.date_expiration
|
||||||
),
|
else None,
|
||||||
"date_modif_passwd": (
|
"date_modif_passwd": self.date_modif_passwd.isoformat() + "Z"
|
||||||
self.date_modif_passwd.isoformat() + "Z"
|
|
||||||
if self.date_modif_passwd
|
if self.date_modif_passwd
|
||||||
else None
|
else None,
|
||||||
),
|
"date_created": self.date_created.isoformat() + "Z"
|
||||||
"date_created": (
|
if self.date_created
|
||||||
self.date_created.isoformat() + "Z" if self.date_created else None
|
else None,
|
||||||
),
|
|
||||||
"dept": self.dept,
|
"dept": self.dept,
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"active": self.active,
|
"active": self.active,
|
||||||
"cas_id": self.cas_id,
|
"cas_id": self.cas_id,
|
||||||
"cas_allow_login": self.cas_allow_login,
|
"cas_allow_login": self.cas_allow_login,
|
||||||
"cas_allow_scodoc_login": self.cas_allow_scodoc_login,
|
"cas_allow_scodoc_login": self.cas_allow_scodoc_login,
|
||||||
"cas_last_login": (
|
"cas_last_login": self.cas_last_login.isoformat() + "Z"
|
||||||
self.cas_last_login.isoformat() + "Z" if self.cas_last_login else None
|
if self.cas_last_login
|
||||||
),
|
else None,
|
||||||
"edt_id": self.edt_id,
|
|
||||||
"status_txt": "actif" if self.active else "fermé",
|
"status_txt": "actif" if self.active else "fermé",
|
||||||
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None,
|
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None,
|
||||||
"nom": self.nom or "",
|
"nom": (self.nom or ""), # sco8
|
||||||
"prenom": self.prenom or "",
|
"prenom": (self.prenom or ""), # sco8
|
||||||
"roles_string": self.get_roles_string(), # eg "Ens_RT, Ens_Info"
|
"roles_string": self.get_roles_string(), # eg "Ens_RT, Ens_Info"
|
||||||
"user_name": self.user_name,
|
"user_name": self.user_name, # sco8
|
||||||
# Les champs calculés:
|
# Les champs calculés:
|
||||||
"nom_fmt": self.get_nom_fmt(),
|
"nom_fmt": self.get_nom_fmt(),
|
||||||
"prenom_fmt": self.get_prenom_fmt(),
|
"prenom_fmt": self.get_prenom_fmt(),
|
||||||
@ -287,54 +270,37 @@ class User(UserMixin, ScoDocModel):
|
|||||||
data["email_institutionnel"] = self.email_institutionnel or ""
|
data["email_institutionnel"] = self.email_institutionnel or ""
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def convert_dict_fields(cls, args: dict) -> dict:
|
|
||||||
"""Convert fields in the given dict. No other side effect.
|
|
||||||
args: dict with args in application.
|
|
||||||
returns: dict to store in model's db.
|
|
||||||
Convert boolean values to bools.
|
|
||||||
"""
|
|
||||||
args_dict = args
|
|
||||||
# Dates
|
|
||||||
if "date_expiration" in args:
|
|
||||||
date_expiration = args.get("date_expiration")
|
|
||||||
if isinstance(date_expiration, str):
|
|
||||||
args["date_expiration"] = (
|
|
||||||
datetime.datetime.fromisoformat(date_expiration)
|
|
||||||
if date_expiration
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
# booléens:
|
|
||||||
for field in ("active", "cas_allow_login", "cas_allow_scodoc_login"):
|
|
||||||
if field in args:
|
|
||||||
args_dict[field] = scu.to_bool(args.get(field))
|
|
||||||
|
|
||||||
# chaines ne devant pas être NULLs
|
|
||||||
for field in ("nom", "prenom"):
|
|
||||||
if field in args:
|
|
||||||
args[field] = args[field] or ""
|
|
||||||
|
|
||||||
# chaines ne devant pas être vides mais au contraire null (unicité)
|
|
||||||
if "cas_id" in args:
|
|
||||||
args["cas_id"] = args["cas_id"] or None
|
|
||||||
|
|
||||||
return args_dict
|
|
||||||
|
|
||||||
def from_dict(self, data: dict, new_user=False):
|
def from_dict(self, data: dict, new_user=False):
|
||||||
"""Set users' attributes from given dict values.
|
"""Set users' attributes from given dict values.
|
||||||
- roles_string : roles, encoded like "Ens_RT, Secr_CJ"
|
Roles must be encoded as "roles_string", like "Ens_RT, Secr_CJ"
|
||||||
- date_expiration is a dateime object.
|
|
||||||
Does not check permissions here.
|
|
||||||
"""
|
"""
|
||||||
|
for field in [
|
||||||
|
"nom",
|
||||||
|
"prenom",
|
||||||
|
"dept",
|
||||||
|
"active",
|
||||||
|
"email",
|
||||||
|
"email_institutionnel",
|
||||||
|
"date_expiration",
|
||||||
|
"cas_id",
|
||||||
|
]:
|
||||||
|
if field in data:
|
||||||
|
setattr(self, field, data[field] or None)
|
||||||
|
# required boolean fields
|
||||||
|
for field in [
|
||||||
|
"cas_allow_login",
|
||||||
|
"cas_allow_scodoc_login",
|
||||||
|
]:
|
||||||
|
setattr(self, field, scu.to_bool(data.get(field, False)))
|
||||||
|
|
||||||
if new_user:
|
if new_user:
|
||||||
if "user_name" in data:
|
if "user_name" in data:
|
||||||
# never change name of existing users
|
# never change name of existing users
|
||||||
if invalid_user_name(data["user_name"]):
|
|
||||||
raise ValueError(f"invalid user_name: {data['user_name']}")
|
|
||||||
self.user_name = data["user_name"]
|
self.user_name = data["user_name"]
|
||||||
if "password" in data:
|
if "password" in data:
|
||||||
self.set_password(data["password"])
|
self.set_password(data["password"])
|
||||||
|
if invalid_user_name(self.user_name):
|
||||||
|
raise ValueError(f"invalid user_name: {self.user_name}")
|
||||||
# Roles: roles_string is "Ens_RT, Secr_RT, ..."
|
# Roles: roles_string is "Ens_RT, Secr_RT, ..."
|
||||||
if "roles_string" in data:
|
if "roles_string" in data:
|
||||||
self.user_roles = []
|
self.user_roles = []
|
||||||
@ -343,13 +309,11 @@ class User(UserMixin, ScoDocModel):
|
|||||||
role, dept = UserRole.role_dept_from_string(r_d)
|
role, dept = UserRole.role_dept_from_string(r_d)
|
||||||
self.add_role(role, dept)
|
self.add_role(role, dept)
|
||||||
|
|
||||||
super().from_dict(data, excluded={"user_name", "roles_string", "roles"})
|
|
||||||
|
|
||||||
# Set cas_id using regexp if configured:
|
# Set cas_id using regexp if configured:
|
||||||
exp = ScoDocSiteConfig.get("cas_uid_from_mail_regexp")
|
exp = ScoDocSiteConfig.get("cas_uid_from_mail_regexp")
|
||||||
if exp and self.email_institutionnel:
|
if exp and self.email_institutionnel:
|
||||||
cas_id = ScoDocSiteConfig.extract_cas_id(self.email_institutionnel)
|
cas_id = ScoDocSiteConfig.extract_cas_id(self.email_institutionnel)
|
||||||
if cas_id:
|
if cas_id is not None:
|
||||||
self.cas_id = cas_id
|
self.cas_id = cas_id
|
||||||
|
|
||||||
def get_token(self, expires_in=3600):
|
def get_token(self, expires_in=3600):
|
||||||
@ -477,12 +441,12 @@ class User(UserMixin, ScoDocModel):
|
|||||||
"""nomplogin est le nom en majuscules suivi du prénom et du login
|
"""nomplogin est le nom en majuscules suivi du prénom et du login
|
||||||
e.g. Dupont Pierre (dupont)
|
e.g. Dupont Pierre (dupont)
|
||||||
"""
|
"""
|
||||||
nom = scu.format_nom(self.nom) if self.nom else self.user_name.upper()
|
nom = sco_etud.format_nom(self.nom) if self.nom else self.user_name.upper()
|
||||||
return f"{nom} {scu.format_prenom(self.prenom)} ({self.user_name})"
|
return f"{nom} {sco_etud.format_prenom(self.prenom)} ({self.user_name})"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_user_from_nomplogin(nomplogin: str) -> Optional["User"]:
|
def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]:
|
||||||
"""Returns User instance from the string "Dupont Pierre (dupont)"
|
"""Returns id from the string "Dupont Pierre (dupont)"
|
||||||
or None if user does not exist
|
or None if user does not exist
|
||||||
"""
|
"""
|
||||||
match = re.match(r".*\((.*)\)", nomplogin.strip())
|
match = re.match(r".*\((.*)\)", nomplogin.strip())
|
||||||
@ -490,35 +454,35 @@ class User(UserMixin, ScoDocModel):
|
|||||||
user_name = match.group(1)
|
user_name = match.group(1)
|
||||||
u = User.query.filter_by(user_name=user_name).first()
|
u = User.query.filter_by(user_name=user_name).first()
|
||||||
if u:
|
if u:
|
||||||
return u
|
return u.id
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_nom_fmt(self):
|
def get_nom_fmt(self):
|
||||||
"""Nom formaté: "Martin" """
|
"""Nom formaté: "Martin" """
|
||||||
if self.nom:
|
if self.nom:
|
||||||
return scu.format_nom(self.nom, uppercase=False)
|
return sco_etud.format_nom(self.nom, uppercase=False)
|
||||||
else:
|
else:
|
||||||
return self.user_name
|
return self.user_name
|
||||||
|
|
||||||
def get_prenom_fmt(self):
|
def get_prenom_fmt(self):
|
||||||
"""Prénom formaté (minuscule capitalisées)"""
|
"""Prénom formaté (minuscule capitalisées)"""
|
||||||
return scu.format_prenom(self.prenom)
|
return sco_etud.format_prenom(self.prenom)
|
||||||
|
|
||||||
def get_nomprenom(self):
|
def get_nomprenom(self):
|
||||||
"""Nom capitalisé suivi de l'initiale du prénom:
|
"""Nom capitalisé suivi de l'initiale du prénom:
|
||||||
Viennet E.
|
Viennet E.
|
||||||
"""
|
"""
|
||||||
prenom_abbrv = scu.abbrev_prenom(scu.format_prenom(self.prenom))
|
prenom_abbrv = scu.abbrev_prenom(sco_etud.format_prenom(self.prenom))
|
||||||
return (self.get_nom_fmt() + " " + prenom_abbrv).strip()
|
return (self.get_nom_fmt() + " " + prenom_abbrv).strip()
|
||||||
|
|
||||||
def get_prenomnom(self):
|
def get_prenomnom(self):
|
||||||
"""L'initiale du prénom suivie du nom: "J.-C. Dupont" """
|
"""L'initiale du prénom suivie du nom: "J.-C. Dupont" """
|
||||||
prenom_abbrv = scu.abbrev_prenom(scu.format_prenom(self.prenom))
|
prenom_abbrv = scu.abbrev_prenom(sco_etud.format_prenom(self.prenom))
|
||||||
return (prenom_abbrv + " " + self.get_nom_fmt()).strip()
|
return (prenom_abbrv + " " + self.get_nom_fmt()).strip()
|
||||||
|
|
||||||
def get_nomcomplet(self):
|
def get_nomcomplet(self):
|
||||||
"Prénom et nom complets"
|
"Prénom et nom complets"
|
||||||
return scu.format_prenom(self.prenom) + " " + self.get_nom_fmt()
|
return sco_etud.format_prenom(self.prenom) + " " + self.get_nom_fmt()
|
||||||
|
|
||||||
# nomnoacc était le nom en minuscules sans accents (inutile)
|
# nomnoacc était le nom en minuscules sans accents (inutile)
|
||||||
|
|
||||||
|
@ -54,7 +54,6 @@ def _login_form():
|
|||||||
title=_("Sign In"),
|
title=_("Sign In"),
|
||||||
form=form,
|
form=form,
|
||||||
is_cas_enabled=ScoDocSiteConfig.is_cas_enabled(),
|
is_cas_enabled=ScoDocSiteConfig.is_cas_enabled(),
|
||||||
is_cas_forced=ScoDocSiteConfig.is_cas_forced(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -209,3 +208,5 @@ def cas_users_import_config():
|
|||||||
title=_("Importation configuration CAS utilisateurs"),
|
title=_("Importation configuration CAS utilisateurs"),
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -345,16 +345,23 @@ class BulletinBUT:
|
|||||||
- Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
|
- Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
|
||||||
(bulletins non publiés).
|
(bulletins non publiés).
|
||||||
"""
|
"""
|
||||||
if version not in scu.BULLETINS_VERSIONS_BUT:
|
if version not in scu.BULLETINS_VERSIONS:
|
||||||
raise ScoValueError("bulletin_etud: version de bulletin demandée invalide")
|
raise ScoValueError("version de bulletin demandée invalide")
|
||||||
res = self.res
|
res = self.res
|
||||||
formsemestre = res.formsemestre
|
formsemestre = res.formsemestre
|
||||||
|
etat_inscription = etud.inscription_etat(formsemestre.id)
|
||||||
|
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
|
||||||
|
published = (not formsemestre.bul_hide_xml) or force_publishing
|
||||||
|
if formsemestre.formation.referentiel_competence is None:
|
||||||
|
etud_ues_ids = {ue.id for ue in res.ues if res.modimpls_in_ue(ue, etud.id)}
|
||||||
|
else:
|
||||||
|
etud_ues_ids = res.etud_ues_ids(etud.id)
|
||||||
|
|
||||||
d = {
|
d = {
|
||||||
"version": "0",
|
"version": "0",
|
||||||
"type": "BUT",
|
"type": "BUT",
|
||||||
"date": datetime.datetime.utcnow().isoformat() + "Z",
|
"date": datetime.datetime.utcnow().isoformat() + "Z",
|
||||||
"publie": not formsemestre.bul_hide_xml,
|
"publie": not formsemestre.bul_hide_xml,
|
||||||
"etat_inscription": etud.inscription_etat(formsemestre.id),
|
|
||||||
"etudiant": etud.to_dict_bul(),
|
"etudiant": etud.to_dict_bul(),
|
||||||
"formation": {
|
"formation": {
|
||||||
"id": formsemestre.formation.id,
|
"id": formsemestre.formation.id,
|
||||||
@ -363,20 +370,14 @@ class BulletinBUT:
|
|||||||
"titre": formsemestre.formation.titre,
|
"titre": formsemestre.formation.titre,
|
||||||
},
|
},
|
||||||
"formsemestre_id": formsemestre.id,
|
"formsemestre_id": formsemestre.id,
|
||||||
|
"etat_inscription": etat_inscription,
|
||||||
"options": sco_preferences.bulletin_option_affichage(
|
"options": sco_preferences.bulletin_option_affichage(
|
||||||
formsemestre, self.prefs
|
formsemestre, self.prefs
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
published = (not formsemestre.bul_hide_xml) or force_publishing
|
if not published:
|
||||||
if not published or d["etat_inscription"] is False:
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
|
|
||||||
if formsemestre.formation.referentiel_competence is None:
|
|
||||||
etud_ues_ids = {ue.id for ue in res.ues if res.modimpls_in_ue(ue, etud.id)}
|
|
||||||
else:
|
|
||||||
etud_ues_ids = res.etud_ues_ids(etud.id)
|
|
||||||
|
|
||||||
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_groups = sco_groups.get_etud_formsemestre_groups(
|
||||||
etud, formsemestre, only_to_show=True
|
etud, formsemestre, only_to_show=True
|
||||||
@ -409,7 +410,7 @@ class BulletinBUT:
|
|||||||
semestre_infos.update(
|
semestre_infos.update(
|
||||||
sco_bulletins_json.dict_decision_jury(etud, formsemestre)
|
sco_bulletins_json.dict_decision_jury(etud, formsemestre)
|
||||||
)
|
)
|
||||||
if d["etat_inscription"] == scu.INSCRIT:
|
if etat_inscription == scu.INSCRIT:
|
||||||
# moyenne des moyennes générales du semestre
|
# moyenne des moyennes générales du semestre
|
||||||
semestre_infos["notes"] = {
|
semestre_infos["notes"] = {
|
||||||
"value": fmt_note(res.etud_moy_gen[etud.id]),
|
"value": fmt_note(res.etud_moy_gen[etud.id]),
|
||||||
@ -498,8 +499,10 @@ class BulletinBUT:
|
|||||||
d["etud"]["etat_civil"] = etud.etat_civil
|
d["etud"]["etat_civil"] = etud.etat_civil
|
||||||
d.update(self.res.sem)
|
d.update(self.res.sem)
|
||||||
etud_etat = self.res.get_etud_etat(etud.id)
|
etud_etat = self.res.get_etud_etat(etud.id)
|
||||||
d["filigranne"] = sco_bulletins_pdf.get_filigranne_apc(
|
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
|
||||||
etud_etat, self.prefs, etud.id, res=self.res
|
etud_etat,
|
||||||
|
self.prefs,
|
||||||
|
decision_sem=d["semestre"].get("decision"),
|
||||||
)
|
)
|
||||||
if etud_etat == scu.DEMISSION:
|
if etud_etat == scu.DEMISSION:
|
||||||
d["demission"] = "(Démission)"
|
d["demission"] = "(Démission)"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -65,49 +65,11 @@ def bulletin_but(formsemestre_id: int, etudid: int = None, fmt="html"):
|
|||||||
)
|
)
|
||||||
if not formsemestre.formation.is_apc():
|
if not formsemestre.formation.is_apc():
|
||||||
raise ScoValueError("formation non BUT")
|
raise ScoValueError("formation non BUT")
|
||||||
|
|
||||||
args = _build_bulletin_but_infos(etud, formsemestre, fmt=fmt)
|
|
||||||
|
|
||||||
if fmt == "pdf":
|
|
||||||
filename = scu.bul_filename(formsemestre, etud, prefix="bul-but")
|
|
||||||
bul_pdf = bulletin_but_court_pdf.make_bulletin_but_court_pdf(args)
|
|
||||||
return scu.sendPDFFile(bul_pdf, filename + ".pdf")
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
"but/bulletin_court_page.j2",
|
|
||||||
datetime=datetime,
|
|
||||||
sco=ScoData(formsemestre=formsemestre, etud=etud),
|
|
||||||
time=time,
|
|
||||||
version="butcourt",
|
|
||||||
**args,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def bulletin_but_court_pdf_frag(
|
|
||||||
etud: Identite, formsemestre: FormSemestre, stand_alone=False
|
|
||||||
) -> bytes:
|
|
||||||
"""Le code PDF d'un bulletin BUT court, à intégrer dans un document
|
|
||||||
(pour les classeurs de tous les bulletins)
|
|
||||||
"""
|
|
||||||
args = _build_bulletin_but_infos(etud, formsemestre)
|
|
||||||
return bulletin_but_court_pdf.make_bulletin_but_court_pdf(
|
|
||||||
args, stand_alone=stand_alone
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _build_bulletin_but_infos(
|
|
||||||
etud: Identite, formsemestre: FormSemestre, fmt="pdf"
|
|
||||||
) -> dict:
|
|
||||||
"""Réuni toutes les information pour le contenu d'un bulletin BUT court.
|
|
||||||
On indique le format ("html" ou "pdf") car il y a moins d'infos en HTML.
|
|
||||||
"""
|
|
||||||
bulletins_sem = BulletinBUT(formsemestre)
|
bulletins_sem = BulletinBUT(formsemestre)
|
||||||
if fmt == "pdf":
|
if fmt == "pdf":
|
||||||
bul: dict = bulletins_sem.bulletin_etud_complet(etud)
|
bul: dict = bulletins_sem.bulletin_etud_complet(etud)
|
||||||
filigranne = bul["filigranne"]
|
|
||||||
else: # la même chose avec un peu moins d'infos
|
else: # la même chose avec un peu moins d'infos
|
||||||
bul: dict = bulletins_sem.bulletin_etud(etud, force_publishing=True)
|
bul: dict = bulletins_sem.bulletin_etud(etud, force_publishing=True)
|
||||||
filigranne = ""
|
|
||||||
decision_ues = (
|
decision_ues = (
|
||||||
{x["acronyme"]: x for x in bul["semestre"]["decision_ue"]}
|
{x["acronyme"]: x for x in bul["semestre"]["decision_ue"]}
|
||||||
if "semestre" in bul and "decision_ue" in bul["semestre"]
|
if "semestre" in bul and "decision_ue" in bul["semestre"]
|
||||||
@ -133,7 +95,6 @@ def _build_bulletin_but_infos(
|
|||||||
"decision_ues": decision_ues,
|
"decision_ues": decision_ues,
|
||||||
"ects_total": ects_total,
|
"ects_total": ects_total,
|
||||||
"etud": etud,
|
"etud": etud,
|
||||||
"filigranne": filigranne,
|
|
||||||
"formsemestre": formsemestre,
|
"formsemestre": formsemestre,
|
||||||
"logo": logo,
|
"logo": logo,
|
||||||
"prefs": bulletins_sem.prefs,
|
"prefs": bulletins_sem.prefs,
|
||||||
@ -145,4 +106,16 @@ def _build_bulletin_but_infos(
|
|||||||
if ue.type == UE_STANDARD and ue.acronyme in ue_acronyms
|
if ue.type == UE_STANDARD and ue.acronyme in ue_acronyms
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
return args
|
if fmt == "pdf":
|
||||||
|
filename = scu.bul_filename(formsemestre, etud, prefix="bul-but")
|
||||||
|
bul_pdf = bulletin_but_court_pdf.make_bulletin_but_court_pdf(**args)
|
||||||
|
return scu.sendPDFFile(bul_pdf, filename + ".pdf")
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"but/bulletin_court_page.j2",
|
||||||
|
datetime=datetime,
|
||||||
|
sco=ScoData(formsemestre=formsemestre, etud=etud),
|
||||||
|
time=time,
|
||||||
|
version="butcourt",
|
||||||
|
**args,
|
||||||
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -34,33 +34,25 @@ from app.scodoc.sco_preferences import SemPreferences
|
|||||||
|
|
||||||
|
|
||||||
def make_bulletin_but_court_pdf(
|
def make_bulletin_but_court_pdf(
|
||||||
args: dict,
|
|
||||||
stand_alone: bool = True,
|
|
||||||
) -> bytes:
|
|
||||||
"""génère le bulletin court BUT en pdf.
|
|
||||||
Si stand_alone, génère un doc pdf complet (une page ici),
|
|
||||||
sinon un morceau (fragment) à intégrer dans un autre document.
|
|
||||||
|
|
||||||
args donne toutes les infos du contenu du bulletin:
|
|
||||||
bul: dict = None,
|
bul: dict = None,
|
||||||
cursus: cursus_but.EtudCursusBUT = None,
|
cursus: cursus_but.EtudCursusBUT = None,
|
||||||
decision_ues: dict = None,
|
decision_ues: dict = None,
|
||||||
ects_total: float = 0.0,
|
ects_total: float = 0.0,
|
||||||
etud: Identite = None,
|
etud: Identite = None,
|
||||||
formsemestre: FormSemestre = None,
|
formsemestre: FormSemestre = None,
|
||||||
filigranne=""
|
|
||||||
logo: Logo = None,
|
logo: Logo = None,
|
||||||
prefs: SemPreferences = None,
|
prefs: SemPreferences = None,
|
||||||
title: str = "",
|
title: str = "",
|
||||||
ue_validation_by_niveau: dict[tuple[int, str], ScolarFormSemestreValidation] = None,
|
ue_validation_by_niveau: dict[tuple[int, str], ScolarFormSemestreValidation] = None,
|
||||||
ues_acronyms: list[str] = None,
|
ues_acronyms: list[str] = None,
|
||||||
"""
|
) -> bytes:
|
||||||
|
"génère le bulletin court BUT en pdf"
|
||||||
# A priori ce verrou n'est plus nécessaire avec Flask (multi-process)
|
# A priori ce verrou n'est plus nécessaire avec Flask (multi-process)
|
||||||
# mais...
|
# mais...
|
||||||
try:
|
try:
|
||||||
PDFLOCK.acquire()
|
PDFLOCK.acquire()
|
||||||
bul_generator = BulletinGeneratorBUTCourt(**args)
|
bul_generator = BulletinGeneratorBUTCourt(**locals())
|
||||||
bul_pdf = bul_generator.generate(fmt="pdf", stand_alone=stand_alone)
|
bul_pdf = bul_generator.generate(fmt="pdf")
|
||||||
finally:
|
finally:
|
||||||
PDFLOCK.release()
|
PDFLOCK.release()
|
||||||
return bul_pdf
|
return bul_pdf
|
||||||
@ -87,7 +79,6 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
decision_ues: dict = None,
|
decision_ues: dict = None,
|
||||||
ects_total: float = 0.0,
|
ects_total: float = 0.0,
|
||||||
etud: Identite = None,
|
etud: Identite = None,
|
||||||
filigranne="",
|
|
||||||
formsemestre: FormSemestre = None,
|
formsemestre: FormSemestre = None,
|
||||||
logo: Logo = None,
|
logo: Logo = None,
|
||||||
prefs: SemPreferences = None,
|
prefs: SemPreferences = None,
|
||||||
@ -97,7 +88,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
] = None,
|
] = None,
|
||||||
ues_acronyms: list[str] = None,
|
ues_acronyms: list[str] = None,
|
||||||
):
|
):
|
||||||
super().__init__(bul, authuser=current_user, filigranne=filigranne)
|
super().__init__(bul, authuser=current_user)
|
||||||
self.bul = bul
|
self.bul = bul
|
||||||
self.cursus = cursus
|
self.cursus = cursus
|
||||||
self.decision_ues = decision_ues
|
self.decision_ues = decision_ues
|
||||||
@ -194,7 +185,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
"""Génère la partie "titre" du bulletin de notes.
|
"""Génère la partie "titre" du bulletin de notes.
|
||||||
Renvoie une liste d'objets platypus
|
Renvoie une liste d'objets platypus
|
||||||
"""
|
"""
|
||||||
# comme les bulletins standards, mais avec notre préférence
|
# comme les bulletins standard, mais avec notre préférence
|
||||||
return super().bul_title_pdf(preference_field=preference_field)
|
return super().bul_title_pdf(preference_field=preference_field)
|
||||||
|
|
||||||
def bul_part_below(self, fmt="pdf") -> list:
|
def bul_part_below(self, fmt="pdf") -> list:
|
||||||
@ -406,8 +397,6 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
|
|
||||||
def boite_identite(self) -> list:
|
def boite_identite(self) -> list:
|
||||||
"Les informations sur l'identité et l'inscription de l'étudiant"
|
"Les informations sur l'identité et l'inscription de l'étudiant"
|
||||||
parcour = self.formsemestre.etuds_inscriptions[self.etud.id].parcour
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Paragraph(
|
Paragraph(
|
||||||
SU(f"""{self.etud.nomprenom}"""),
|
SU(f"""{self.etud.nomprenom}"""),
|
||||||
@ -418,8 +407,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
|||||||
f"""
|
f"""
|
||||||
<b>{self.bul["demission"]}</b><br/>
|
<b>{self.bul["demission"]}</b><br/>
|
||||||
Formation: {self.formsemestre.titre_num()}<br/>
|
Formation: {self.formsemestre.titre_num()}<br/>
|
||||||
{'Parcours ' + parcour.code + '<br/>' if parcour else ''}
|
Année scolaire: {self.formsemestre.annee_scolaire_str()}<br/>
|
||||||
Année universitaire: {self.formsemestre.annee_scolaire_str()}<br/>
|
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
style=self.style_base,
|
style=self.style_base,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -119,13 +119,9 @@ class EtudCursusBUT:
|
|||||||
|
|
||||||
self.validation_par_competence_et_annee = {}
|
self.validation_par_competence_et_annee = {}
|
||||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||||
validation_rcue: ApcValidationRCUE
|
|
||||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||||
niveau = validation_rcue.niveau()
|
niveau = validation_rcue.niveau()
|
||||||
if (
|
if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||||
niveau is None
|
|
||||||
or not niveau.competence.id in self.validation_par_competence_et_annee
|
|
||||||
):
|
|
||||||
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||||
previous_validation = self.validation_par_competence_et_annee.get(
|
previous_validation = self.validation_par_competence_et_annee.get(
|
||||||
niveau.competence.id
|
niveau.competence.id
|
||||||
@ -447,24 +443,8 @@ def formsemestre_warning_apc_setup(
|
|||||||
}">formation n'est pas associée à un référentiel de compétence.</a>
|
}">formation n'est pas associée à un référentiel de compétence.</a>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
H = []
|
|
||||||
# Le semestre n'a pas de parcours, mais les UE ont des parcours ?
|
|
||||||
if not formsemestre.parcours:
|
|
||||||
nb_ues_sans_parcours = len(
|
|
||||||
formsemestre.formation.query_ues_parcour(None)
|
|
||||||
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
nb_ues_tot = (
|
|
||||||
UniteEns.query.filter_by(formation=formsemestre.formation, type=UE_STANDARD)
|
|
||||||
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
|
|
||||||
.count()
|
|
||||||
)
|
|
||||||
if nb_ues_sans_parcours != nb_ues_tot:
|
|
||||||
H.append(
|
|
||||||
f"""Le semestre n'est associé à aucun parcours, mais les UEs de la formation ont des parcours"""
|
|
||||||
)
|
|
||||||
# Vérifie les niveaux de chaque parcours
|
# Vérifie les niveaux de chaque parcours
|
||||||
|
H = []
|
||||||
for parcour in formsemestre.parcours or [None]:
|
for parcour in formsemestre.parcours or [None]:
|
||||||
annee = (formsemestre.semestre_id + 1) // 2
|
annee = (formsemestre.semestre_id + 1) // 2
|
||||||
niveaux_ids = {
|
niveaux_ids = {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -380,24 +380,14 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
sco_codes.ADJ,
|
sco_codes.ADJ,
|
||||||
] + self.codes
|
] + self.codes
|
||||||
explanation += f" et {self.nb_rcues_under_8} < 8"
|
explanation += f" et {self.nb_rcues_under_8} < 8"
|
||||||
else: # autres cas: non admis, non passage, non dem, pas la moitié des rcue:
|
|
||||||
if formsemestre.semestre_id % 2 and self.formsemestre_pair is None:
|
|
||||||
# Si jury sur un seul semestre impair, ne propose pas redoublement
|
|
||||||
# et efface décision éventuellement existante
|
|
||||||
codes = [None]
|
|
||||||
else:
|
else:
|
||||||
codes = []
|
self.codes = [
|
||||||
self.codes = (
|
|
||||||
codes
|
|
||||||
+ [
|
|
||||||
sco_codes.RED,
|
sco_codes.RED,
|
||||||
sco_codes.NAR,
|
sco_codes.NAR,
|
||||||
sco_codes.PAS1NCI,
|
sco_codes.PAS1NCI,
|
||||||
sco_codes.ADJ,
|
sco_codes.ADJ,
|
||||||
sco_codes.PASD, # voir #488 (discutable, conventions locales)
|
sco_codes.PASD, # voir #488 (discutable, conventions locales)
|
||||||
]
|
] + self.codes
|
||||||
+ self.codes
|
|
||||||
)
|
|
||||||
explanation += f""" et {self.nb_rcues_under_8
|
explanation += f""" et {self.nb_rcues_under_8
|
||||||
} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
|
} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
|
||||||
|
|
||||||
@ -417,7 +407,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
+ '</div><div class="warning">'.join(messages)
|
+ '</div><div class="warning">'.join(messages)
|
||||||
+ "</div>"
|
+ "</div>"
|
||||||
)
|
)
|
||||||
self.codes = [self.codes[0]] + sorted((c or "") for c in self.codes[1:])
|
self.codes = [self.codes[0]] + sorted(self.codes[1:])
|
||||||
|
|
||||||
def passage_de_droit_en_but3(self) -> tuple[bool, str]:
|
def passage_de_droit_en_but3(self) -> tuple[bool, str]:
|
||||||
"""Vérifie si les conditions supplémentaires de passage BUT2 vers BUT3 sont satisfaites"""
|
"""Vérifie si les conditions supplémentaires de passage BUT2 vers BUT3 sont satisfaites"""
|
||||||
@ -524,21 +514,19 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
"""Les deux formsemestres auquel est inscrit l'étudiant (ni DEM ni DEF)
|
"""Les deux formsemestres auquel est inscrit l'étudiant (ni DEM ni DEF)
|
||||||
du niveau auquel appartient formsemestre.
|
du niveau auquel appartient formsemestre.
|
||||||
|
|
||||||
-> S_impair, S_pair (de la même année scolaire)
|
-> S_impair, S_pair
|
||||||
|
|
||||||
Si l'origine est impair, S_impair est l'origine et S_pair est None
|
Si l'origine est impair, S_impair est l'origine et S_pair est None
|
||||||
Si l'origine est paire, S_pair est l'origine, et S_impair l'antérieur
|
Si l'origine est paire, S_pair est l'origine, et S_impair l'antérieur
|
||||||
suivi par cet étudiant (ou None).
|
suivi par cet étudiant (ou None).
|
||||||
|
|
||||||
Note: si l'option "block_moyennes" est activée, ne prend pas en compte le semestre.
|
|
||||||
"""
|
"""
|
||||||
if not formsemestre.formation.is_apc(): # garde fou
|
if not formsemestre.formation.is_apc(): # garde fou
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
if formsemestre.semestre_id % 2:
|
if formsemestre.semestre_id % 2:
|
||||||
idx_autre = formsemestre.semestre_id + 1 # impair, autre = suivant
|
idx_autre = formsemestre.semestre_id + 1
|
||||||
else:
|
else:
|
||||||
idx_autre = formsemestre.semestre_id - 1 # pair: autre = précédent
|
idx_autre = formsemestre.semestre_id - 1
|
||||||
|
|
||||||
# Cherche l'autre semestre de la même année scolaire:
|
# Cherche l'autre semestre de la même année scolaire:
|
||||||
autre_formsemestre = None
|
autre_formsemestre = None
|
||||||
@ -551,8 +539,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
inscr.formsemestre.formation.referentiel_competence
|
inscr.formsemestre.formation.referentiel_competence
|
||||||
== formsemestre.formation.referentiel_competence
|
== formsemestre.formation.referentiel_competence
|
||||||
)
|
)
|
||||||
# Non bloqué
|
|
||||||
and not inscr.formsemestre.block_moyennes
|
|
||||||
# L'autre semestre
|
# L'autre semestre
|
||||||
and (inscr.formsemestre.semestre_id == idx_autre)
|
and (inscr.formsemestre.semestre_id == idx_autre)
|
||||||
# de la même année scolaire
|
# de la même année scolaire
|
||||||
@ -624,7 +610,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
def next_semestre_ids(self, code: str) -> set[int]:
|
def next_semestre_ids(self, code: str) -> set[int]:
|
||||||
"""Les indices des semestres dans lequels l'étudiant est autorisé
|
"""Les indices des semestres dans lequels l'étudiant est autorisé
|
||||||
à poursuivre après le semestre courant.
|
à poursuivre après le semestre courant.
|
||||||
code: code jury sur année BUT
|
|
||||||
"""
|
"""
|
||||||
# La poursuite d'études dans un semestre pair d'une même année
|
# La poursuite d'études dans un semestre pair d'une même année
|
||||||
# est de droit pour tout étudiant.
|
# est de droit pour tout étudiant.
|
||||||
@ -668,8 +653,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
|
|
||||||
Si les code_rcue et le code_annee ne sont pas fournis,
|
Si les code_rcue et le code_annee ne sont pas fournis,
|
||||||
et qu'il n'y en a pas déjà, enregistre ceux par défaut.
|
et qu'il n'y en a pas déjà, enregistre ceux par défaut.
|
||||||
|
|
||||||
Si le code_annee est None, efface le code déjà enregistré.
|
|
||||||
"""
|
"""
|
||||||
log("jury_but.DecisionsProposeesAnnee.record_form")
|
log("jury_but.DecisionsProposeesAnnee.record_form")
|
||||||
code_annee = self.codes[0] # si pas dans le form, valeur par defaut
|
code_annee = self.codes[0] # si pas dans le form, valeur par defaut
|
||||||
@ -714,7 +697,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
def record(self, code: str, mark_recorded: bool = True) -> bool:
|
def record(self, code: str, mark_recorded: bool = True) -> bool:
|
||||||
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
|
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
|
||||||
Si l'étudiant est DEM ou DEF, ne fait rien.
|
Si l'étudiant est DEM ou DEF, ne fait rien.
|
||||||
Si le code est None, efface le code déjà enregistré.
|
|
||||||
Si mark_recorded est vrai, positionne self.recorded
|
Si mark_recorded est vrai, positionne self.recorded
|
||||||
"""
|
"""
|
||||||
if self.inscription_etat != scu.INSCRIT:
|
if self.inscription_etat != scu.INSCRIT:
|
||||||
@ -764,9 +746,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def record_autorisation_inscription(self, code: str):
|
def record_autorisation_inscription(self, code: str):
|
||||||
"""Autorisation d'inscription dans semestre suivant.
|
"""Autorisation d'inscription dans semestre suivant"""
|
||||||
code: code jury sur année BUT
|
|
||||||
"""
|
|
||||||
if self.autorisations_recorded:
|
if self.autorisations_recorded:
|
||||||
return
|
return
|
||||||
if self.inscription_etat != scu.INSCRIT:
|
if self.inscription_etat != scu.INSCRIT:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -154,7 +154,7 @@ def pvjury_table_but(
|
|||||||
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
|
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
|
||||||
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
|
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
|
||||||
"_nom_target": url_for(
|
"_nom_target": url_for(
|
||||||
"scolar.fiche_etud",
|
"scolar.ficheEtud",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
etudid=etud.id,
|
etudid=etud.id,
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
|||||||
<span class="avertissement_redoublement">{formsemestre_2.annee_scolaire_str()
|
<span class="avertissement_redoublement">{formsemestre_2.annee_scolaire_str()
|
||||||
if formsemestre_2 else ""}</span>
|
if formsemestre_2 else ""}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="titre" title="Décisions sur RCUEs enregistrées sur l'ensemble du cursus">RCUE</div>
|
<div class="titre">RCUE</div>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
for dec_rcue in deca.get_decisions_rcues_annee():
|
for dec_rcue in deca.get_decisions_rcues_annee():
|
||||||
@ -256,7 +256,7 @@ def _gen_but_niveau_ue(
|
|||||||
return f"""<div class="but_niveau_ue {ue_class}
|
return f"""<div class="but_niveau_ue {ue_class}
|
||||||
{'annee_prec' if annee_prec else ''}
|
{'annee_prec' if annee_prec else ''}
|
||||||
">
|
">
|
||||||
<div title="{ue.titre or ''}">{ue.acronyme}</div>
|
<div title="{ue.titre}">{ue.acronyme}</div>
|
||||||
<div class="but_note with_scoplement">
|
<div class="but_note with_scoplement">
|
||||||
<div>{moy_ue_str}</div>
|
<div>{moy_ue_str}</div>
|
||||||
{scoplement}
|
{scoplement}
|
||||||
@ -447,7 +447,7 @@ def jury_but_semestriel(
|
|||||||
<div class="nom_etud">{etud.nomprenom}</div>
|
<div class="nom_etud">{etud.nomprenom}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bull_photo"><a href="{
|
<div class="bull_photo"><a href="{
|
||||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||||
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
|
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -30,9 +30,7 @@ class StatsMoyenne:
|
|||||||
self.max = np.nanmax(vals)
|
self.max = np.nanmax(vals)
|
||||||
self.size = len(vals)
|
self.size = len(vals)
|
||||||
self.nb_vals = self.size - np.count_nonzero(np.isnan(vals))
|
self.nb_vals = self.size - np.count_nonzero(np.isnan(vals))
|
||||||
except (
|
except TypeError: # que des NaN dans un array d'objets, ou ce genre de choses exotiques...
|
||||||
TypeError
|
|
||||||
): # que des NaN dans un array d'objets, ou ce genre de choses exotiques...
|
|
||||||
self.moy = self.min = self.max = self.size = self.nb_vals = 0
|
self.moy = self.min = self.max = self.size = self.nb_vals = 0
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -667,12 +667,10 @@ class BonusCalais(BonusSportAdditif):
|
|||||||
sur 20 obtenus dans chacune des matières optionnelles sont cumulés
|
sur 20 obtenus dans chacune des matières optionnelles sont cumulés
|
||||||
dans la limite de 10 points. 6% de ces points cumulés s'ajoutent :
|
dans la limite de 10 points. 6% de ces points cumulés s'ajoutent :
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>en BUT</b> à la moyenne de chaque UE;
|
<li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||||||
</li>
|
</li>
|
||||||
<li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant;
|
<li><b>en BUT et LP</b> à la moyenne des UE dont l'acronyme fini par <b>BS</b>
|
||||||
</li>
|
(ex : UE2.1BS, UE32BS)
|
||||||
<li><b>en LP</b>, et en BUT avant 2023-2024, à la moyenne de chaque UE dont
|
|
||||||
l'acronyme termine par <b>BS</b> (comme UE2.1BS, UE32BS).
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
"""
|
"""
|
||||||
@ -694,11 +692,6 @@ class BonusCalais(BonusSportAdditif):
|
|||||||
else:
|
else:
|
||||||
self.classic_use_bonus_ues = True # pour les LP
|
self.classic_use_bonus_ues = True # pour les LP
|
||||||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||||
if (
|
|
||||||
self.formsemestre.annee_scolaire() < 2023
|
|
||||||
or not self.formsemestre.formation.is_apc()
|
|
||||||
):
|
|
||||||
# LP et anciens semestres: ne s'applique qu'aux UE dont l'acronyme termine par BS
|
|
||||||
ues = self.formsemestre.get_ues(with_sport=False)
|
ues = self.formsemestre.get_ues(with_sport=False)
|
||||||
ues_sans_bs = [
|
ues_sans_bs = [
|
||||||
ue for ue in ues if ue.acronyme[-2:].upper() != "BS"
|
ue for ue in ues if ue.acronyme[-2:].upper() != "BS"
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -56,7 +56,6 @@ class EvaluationEtat:
|
|||||||
|
|
||||||
evaluation_id: int
|
evaluation_id: int
|
||||||
nb_attente: int
|
nb_attente: int
|
||||||
nb_notes: int # nb notes d'étudiants inscrits au semestre et au modimpl
|
|
||||||
is_complete: bool
|
is_complete: bool
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
@ -169,34 +168,25 @@ class ModuleImplResults:
|
|||||||
# NULL en base => ABS (= -999)
|
# NULL en base => ABS (= -999)
|
||||||
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
|
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
|
||||||
# Ce merge ne garde que les étudiants inscrits au module
|
# Ce merge ne garde que les étudiants inscrits au module
|
||||||
# et met à NULL (NaN) les notes non présentes
|
# et met à NULL les notes non présentes
|
||||||
# (notes non saisies ou etuds non inscrits au module):
|
# (notes non saisies ou etuds non inscrits au module):
|
||||||
evals_notes = evals_notes.merge(
|
evals_notes = evals_notes.merge(
|
||||||
eval_df, how="left", left_index=True, right_index=True
|
eval_df, how="left", left_index=True, right_index=True
|
||||||
)
|
)
|
||||||
# Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
|
# Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
|
||||||
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
|
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
|
||||||
# Nombre de notes (non vides, incluant ATT etc) des inscrits:
|
|
||||||
nb_notes = eval_notes_inscr.notna().sum()
|
|
||||||
# Etudiants avec notes en attente:
|
|
||||||
# = ceux avec note ATT
|
|
||||||
eval_etudids_attente = set(
|
eval_etudids_attente = set(
|
||||||
eval_notes_inscr.iloc[
|
eval_notes_inscr.iloc[
|
||||||
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
|
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
|
||||||
].index
|
].index
|
||||||
)
|
)
|
||||||
if evaluation.publish_incomplete:
|
|
||||||
# et en "imédiat", tous ceux sans note
|
|
||||||
eval_etudids_attente |= etudids_sans_note
|
|
||||||
# Synthèse pour état du module:
|
|
||||||
self.etudids_attente |= eval_etudids_attente
|
self.etudids_attente |= eval_etudids_attente
|
||||||
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
||||||
evaluation_id=evaluation.id,
|
evaluation_id=evaluation.id,
|
||||||
nb_attente=len(eval_etudids_attente),
|
nb_attente=len(eval_etudids_attente),
|
||||||
nb_notes=int(nb_notes),
|
|
||||||
is_complete=is_complete,
|
is_complete=is_complete,
|
||||||
)
|
)
|
||||||
# au moins une note en attente (ATT ou manquante en mode "immédiat") dans ce modimpl:
|
# au moins une note en ATT dans ce modimpl:
|
||||||
self.en_attente = bool(self.etudids_attente)
|
self.en_attente = bool(self.etudids_attente)
|
||||||
|
|
||||||
# Force columns names to integers (evaluation ids)
|
# Force columns names to integers (evaluation ids)
|
||||||
@ -429,10 +419,11 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
|||||||
|
|
||||||
Résultat: (evals_poids, liste de UEs du semestre sauf le sport)
|
Résultat: (evals_poids, liste de UEs du semestre sauf le sport)
|
||||||
"""
|
"""
|
||||||
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||||
|
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
|
||||||
ues = modimpl.formsemestre.get_ues(with_sport=False)
|
ues = modimpl.formsemestre.get_ues(with_sport=False)
|
||||||
ue_ids = [ue.id for ue in ues]
|
ue_ids = [ue.id for ue in ues]
|
||||||
evaluation_ids = [evaluation.id for evaluation in modimpl.evaluations]
|
evaluation_ids = [evaluation.id for evaluation in evaluations]
|
||||||
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
|
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
|
||||||
if (
|
if (
|
||||||
modimpl.module.module_type == ModuleType.RESSOURCE
|
modimpl.module.module_type == ModuleType.RESSOURCE
|
||||||
@ -443,7 +434,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
|||||||
).filter_by(moduleimpl_id=moduleimpl_id):
|
).filter_by(moduleimpl_id=moduleimpl_id):
|
||||||
try:
|
try:
|
||||||
evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids
|
evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids
|
||||||
except KeyError:
|
except KeyError as exc:
|
||||||
pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre...
|
pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre...
|
||||||
|
|
||||||
# Initialise poids non enregistrés:
|
# Initialise poids non enregistrés:
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -100,7 +100,7 @@ def compute_sem_moys_apc_using_ects(
|
|||||||
|
|
||||||
|
|
||||||
def comp_ranks_series(notes: pd.Series) -> tuple[pd.Series, pd.Series]:
|
def comp_ranks_series(notes: pd.Series) -> tuple[pd.Series, pd.Series]:
|
||||||
"""Calcul rangs à partir d'une série ("vecteur") de notes (index etudid, valeur
|
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
|
||||||
numérique) en tenant compte des ex-aequos.
|
numérique) en tenant compte des ex-aequos.
|
||||||
|
|
||||||
Result: couple (tuple)
|
Result: couple (tuple)
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -273,7 +273,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
return s.index[s.notna()]
|
return s.index[s.notna()]
|
||||||
|
|
||||||
def etud_parcours_ues_ids(self, etudid: int) -> set[int]:
|
def etud_parcours_ues_ids(self, etudid: int) -> set[int]:
|
||||||
"""Ensemble des id des UEs que l'étudiant doit valider dans ce semestre compte tenu
|
"""Ensemble les id des UEs que l'étudiant doit valider dans ce semestre compte tenu
|
||||||
du parcours dans lequel il est inscrit.
|
du parcours dans lequel il est inscrit.
|
||||||
Se base sur le parcours dans ce semestre, et le référentiel de compétences.
|
Se base sur le parcours dans ce semestre, et le référentiel de compétences.
|
||||||
Note: il n'est pas nécessairement inscrit à toutes ces UEs.
|
Note: il n'est pas nécessairement inscrit à toutes ces UEs.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -234,7 +234,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
|
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
|
||||||
impossible à déterminer pour l'étudiant <a href="{
|
impossible à déterminer pour l'étudiant <a href="{
|
||||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||||
}" class="discretelink">{etud.nom_disp()}</a></p>
|
}" class="discretelink">{etud.nom_disp()}</a></p>
|
||||||
<p>Il faut <a href="{
|
<p>Il faut <a href="{
|
||||||
url_for("notes.formsemestre_edit_uecoefs", scodoc_dept=g.scodoc_dept,
|
url_for("notes.formsemestre_edit_uecoefs", scodoc_dept=g.scodoc_dept,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -9,13 +9,12 @@
|
|||||||
|
|
||||||
from collections import Counter, defaultdict
|
from collections import Counter, defaultdict
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
import datetime
|
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import sqlalchemy as sa
|
|
||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
@ -23,19 +22,14 @@ from app.comp import res_sem
|
|||||||
from app.comp.res_cache import ResultatsCache
|
from app.comp.res_cache import ResultatsCache
|
||||||
from app.comp.jury import ValidationsSemestre
|
from app.comp.jury import ValidationsSemestre
|
||||||
from app.comp.moy_mod import ModuleImplResults
|
from app.comp.moy_mod import ModuleImplResults
|
||||||
from app.models import (
|
from app.models import FormSemestre, FormSemestreUECoef
|
||||||
Evaluation,
|
from app.models import Identite
|
||||||
FormSemestre,
|
from app.models import ModuleImpl, ModuleImplInscription
|
||||||
FormSemestreUECoef,
|
from app.models import ScolarAutorisationInscription
|
||||||
Identite,
|
from app.models.ues import UniteEns
|
||||||
ModuleImpl,
|
|
||||||
ModuleImplInscription,
|
|
||||||
ScolarAutorisationInscription,
|
|
||||||
UniteEns,
|
|
||||||
)
|
|
||||||
from app.scodoc.sco_cache import ResultatsSemestreCache
|
from app.scodoc.sco_cache import ResultatsSemestreCache
|
||||||
from app.scodoc.codes_cursus import UE_SPORT
|
from app.scodoc.codes_cursus import UE_SPORT
|
||||||
from app.scodoc.sco_exceptions import ScoValueError, ScoTemporaryError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
@ -198,82 +192,16 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
*[mr.etudids_attente for mr in self.modimpls_results.values()]
|
*[mr.etudids_attente for mr in self.modimpls_results.values()]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Etat des évaluations
|
# # Etat des évaluations
|
||||||
def get_evaluation_etat(self, evaluation: Evaluation) -> dict:
|
# # (se substitue à do_evaluation_etat, sans les moyennes par groupes)
|
||||||
"""État d'une évaluation
|
# def get_evaluations_etats(evaluation_id: int) -> dict:
|
||||||
{
|
# """Renvoie dict avec les clés:
|
||||||
"coefficient" : float, # 0 si None
|
# last_modif
|
||||||
"description" : str, # de l'évaluation, "" si None
|
# nb_evals_completes
|
||||||
"etat" {
|
# nb_evals_en_cours
|
||||||
"evalcomplete" : bool,
|
# nb_evals_vides
|
||||||
"last_modif" : datetime.datetime | None, # saisie de note la plus récente
|
# attente
|
||||||
"nb_notes" : int, # nb notes d'étudiants inscrits
|
# """
|
||||||
},
|
|
||||||
"evaluatiuon_id" : int,
|
|
||||||
"jour" : datetime.datetime, # e.date_debut or datetime.datetime(1900, 1, 1)
|
|
||||||
"publish_incomplete" : bool,
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
mod_results = self.modimpls_results.get(evaluation.moduleimpl_id)
|
|
||||||
if mod_results is None:
|
|
||||||
raise ScoTemporaryError() # argh !
|
|
||||||
etat = mod_results.evaluations_etat.get(evaluation.id)
|
|
||||||
if etat is None:
|
|
||||||
raise ScoTemporaryError() # argh !
|
|
||||||
# Date de dernière saisie de note
|
|
||||||
cursor = db.session.execute(
|
|
||||||
sa.text(
|
|
||||||
"SELECT MAX(date) FROM notes_notes WHERE evaluation_id = :evaluation_id"
|
|
||||||
),
|
|
||||||
{"evaluation_id": evaluation.id},
|
|
||||||
)
|
|
||||||
date_modif = cursor.one_or_none()
|
|
||||||
last_modif = date_modif[0] if date_modif else None
|
|
||||||
return {
|
|
||||||
"coefficient": evaluation.coefficient or 0.0,
|
|
||||||
"description": evaluation.description or "",
|
|
||||||
"evaluation_id": evaluation.id,
|
|
||||||
"jour": evaluation.date_debut or datetime.datetime(1900, 1, 1),
|
|
||||||
"etat": {
|
|
||||||
"evalcomplete": etat.is_complete,
|
|
||||||
"nb_notes": etat.nb_notes,
|
|
||||||
"last_modif": last_modif,
|
|
||||||
},
|
|
||||||
"publish_incomplete": evaluation.publish_incomplete,
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_mod_evaluation_etat_list(self, modimpl: ModuleImpl) -> list[dict]:
|
|
||||||
"""Liste des états des évaluations de ce module
|
|
||||||
[ evaluation_etat, ... ] (voir get_evaluation_etat)
|
|
||||||
trié par (numero desc, date_debut desc)
|
|
||||||
"""
|
|
||||||
# nouvelle version 2024-02-02
|
|
||||||
return list(
|
|
||||||
reversed(
|
|
||||||
[
|
|
||||||
self.get_evaluation_etat(evaluation)
|
|
||||||
for evaluation in modimpl.evaluations
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# modernisation de get_mod_evaluation_etat_list
|
|
||||||
# utilisé par:
|
|
||||||
# sco_evaluations.do_evaluation_etat_in_mod
|
|
||||||
# e["etat"]["evalcomplete"]
|
|
||||||
# e["etat"]["nb_notes"]
|
|
||||||
# e["etat"]["last_modif"]
|
|
||||||
#
|
|
||||||
# sco_formsemestre_status.formsemestre_description_table
|
|
||||||
# "jour" (qui est e.date_debut or datetime.date(1900, 1, 1))
|
|
||||||
# "description"
|
|
||||||
# "coefficient"
|
|
||||||
# e["etat"]["evalcomplete"]
|
|
||||||
# publish_incomplete
|
|
||||||
#
|
|
||||||
# sco_formsemestre_status.formsemestre_tableau_modules
|
|
||||||
# e["etat"]["nb_notes"]
|
|
||||||
#
|
|
||||||
|
|
||||||
# --- JURY...
|
# --- JURY...
|
||||||
def get_formsemestre_validations(self) -> ValidationsSemestre:
|
def get_formsemestre_validations(self) -> ValidationsSemestre:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -408,7 +408,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
de ce module.
|
de ce module.
|
||||||
Évaluation "complete" ssi toutes notes saisies ou en attente.
|
Évaluation "complete" ssi toutes notes saisies ou en attente.
|
||||||
"""
|
"""
|
||||||
modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
|
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||||
modimpl_results = self.modimpls_results.get(moduleimpl_id)
|
modimpl_results = self.modimpls_results.get(moduleimpl_id)
|
||||||
if not modimpl_results:
|
if not modimpl_results:
|
||||||
return [] # safeguard
|
return [] # safeguard
|
||||||
@ -423,37 +423,30 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
)
|
)
|
||||||
return evaluations
|
return evaluations
|
||||||
|
|
||||||
def get_evaluations_etats(self) -> dict[int, dict]:
|
def get_evaluations_etats(self) -> list[dict]:
|
||||||
""" "état" de chaque évaluation du semestre
|
"""Liste de toutes les évaluations du semestre
|
||||||
{
|
[ {...evaluation et son etat...} ]"""
|
||||||
evaluation_id : {
|
# TODO: à moderniser (voir dans ResultatsSemestre)
|
||||||
"evalcomplete" : bool,
|
# utilisé par
|
||||||
"last_modif" : datetime | None
|
# do_evaluation_etat_in_sem
|
||||||
"nb_notes" : int,
|
|
||||||
}, ...
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
# utilisé par do_evaluation_etat_in_sem
|
|
||||||
evaluations_etats = {}
|
|
||||||
for modimpl in self.formsemestre.modimpls_sorted:
|
|
||||||
for evaluation in modimpl.evaluations:
|
|
||||||
evaluation_etat = self.get_evaluation_etat(evaluation)
|
|
||||||
evaluations_etats[evaluation.id] = evaluation_etat["etat"]
|
|
||||||
return evaluations_etats
|
|
||||||
|
|
||||||
# ancienne version < 2024-02-02
|
from app.scodoc import sco_evaluations
|
||||||
# def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
|
|
||||||
# """Liste des états des évaluations de ce module
|
if not hasattr(self, "_evaluations_etats"):
|
||||||
# ordonnée selon (numero desc, date_debut desc)
|
self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem(
|
||||||
# """
|
self.formsemestre.id
|
||||||
# # à moderniser: lent, recharge des données que l'on a déjà...
|
)
|
||||||
# # remplacemé par ResultatsSemestre.get_mod_evaluation_etat_list
|
|
||||||
# #
|
return self._evaluations_etats
|
||||||
# return [
|
|
||||||
# e
|
def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
|
||||||
# for e in self.get_evaluations_etats()
|
"""Liste des états des évaluations de ce module"""
|
||||||
# if e["moduleimpl_id"] == moduleimpl_id
|
# XXX TODO à moderniser: lent, recharge des données que l'on a déjà...
|
||||||
# ]
|
return [
|
||||||
|
e
|
||||||
|
for e in self.get_evaluations_etats()
|
||||||
|
if e["moduleimpl_id"] == moduleimpl_id
|
||||||
|
]
|
||||||
|
|
||||||
def get_moduleimpls_attente(self):
|
def get_moduleimpls_attente(self):
|
||||||
"""Liste des modimpls du semestre ayant des notes en attente"""
|
"""Liste des modimpls du semestre ayant des notes en attente"""
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ from flask import Blueprint
|
|||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.models import Departement
|
from app.models import Departement
|
||||||
import app.scodoc.sco_utils as scu
|
|
||||||
|
|
||||||
bp = Blueprint("entreprises", __name__)
|
bp = Blueprint("entreprises", __name__)
|
||||||
|
|
||||||
@ -16,12 +15,12 @@ SIRET_PROVISOIRE_START = "xx"
|
|||||||
|
|
||||||
@bp.app_template_filter()
|
@bp.app_template_filter()
|
||||||
def format_prenom(s):
|
def format_prenom(s):
|
||||||
return scu.format_prenom(s)
|
return sco_etud.format_prenom(s)
|
||||||
|
|
||||||
|
|
||||||
@bp.app_template_filter()
|
@bp.app_template_filter()
|
||||||
def format_nom(s):
|
def format_nom(s):
|
||||||
return scu.format_nom(s)
|
return sco_etud.format_nom(s)
|
||||||
|
|
||||||
|
|
||||||
@bp.app_template_filter()
|
@bp.app_template_filter()
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -1580,8 +1580,8 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif request.method == "GET":
|
elif request.method == "GET":
|
||||||
form.etudiant.data = f"""{scu.format_nom(etudiant.nom)} {
|
form.etudiant.data = f"""{sco_etud.format_nom(etudiant.nom)} {
|
||||||
scu.format_prenom(etudiant.prenom)}"""
|
sco_etud.format_prenom(etudiant.prenom)}"""
|
||||||
form.etudid.data = etudiant.id
|
form.etudid.data = etudiant.id
|
||||||
form.type_offre.data = stage_apprentissage.type_offre
|
form.type_offre.data = stage_apprentissage.type_offre
|
||||||
form.date_debut.data = stage_apprentissage.date_debut
|
form.date_debut.data = stage_apprentissage.date_debut
|
||||||
@ -1699,7 +1699,7 @@ def json_etudiants():
|
|||||||
list = []
|
list = []
|
||||||
for etudiant in etudiants:
|
for etudiant in etudiants:
|
||||||
content = {}
|
content = {}
|
||||||
value = f"{scu.format_nom(etudiant.nom)} {scu.format_prenom(etudiant.prenom)}"
|
value = f"{sco_etud.format_nom(etudiant.nom)} {sco_etud.format_prenom(etudiant.prenom)}"
|
||||||
if etudiant.inscription_courante() is not None:
|
if etudiant.inscription_courante() is not None:
|
||||||
content = {
|
content = {
|
||||||
"id": f"{etudiant.id}",
|
"id": f"{etudiant.id}",
|
||||||
|
@ -1,202 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# ScoDoc
|
|
||||||
#
|
|
||||||
# Copyright (c) 1999 - 2024 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
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""
|
|
||||||
Formulaire ajout d'une "assiduité" sur un étudiant
|
|
||||||
Formulaire ajout d'un justificatif sur un étudiant
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask_wtf import FlaskForm
|
|
||||||
from flask_wtf.file import MultipleFileField
|
|
||||||
from wtforms import (
|
|
||||||
BooleanField,
|
|
||||||
SelectField,
|
|
||||||
StringField,
|
|
||||||
SubmitField,
|
|
||||||
RadioField,
|
|
||||||
TextAreaField,
|
|
||||||
validators,
|
|
||||||
)
|
|
||||||
from wtforms.validators import DataRequired
|
|
||||||
from app.scodoc import sco_utils as scu
|
|
||||||
|
|
||||||
|
|
||||||
class AjoutAssiOrJustForm(FlaskForm):
|
|
||||||
"""Elements communs aux deux formulaires ajout
|
|
||||||
assiduité et justificatif
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
"Init form, adding a filed for our error messages"
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.ok = True
|
|
||||||
self.error_messages: list[str] = [] # used to report our errors
|
|
||||||
|
|
||||||
def set_error(self, err_msg, field=None):
|
|
||||||
"Set error message both in form and field"
|
|
||||||
self.ok = False
|
|
||||||
self.error_messages.append(err_msg)
|
|
||||||
if field:
|
|
||||||
field.errors.append(err_msg)
|
|
||||||
|
|
||||||
date_debut = StringField(
|
|
||||||
"Date de début",
|
|
||||||
validators=[validators.Length(max=10)],
|
|
||||||
render_kw={
|
|
||||||
"class": "datepicker",
|
|
||||||
"size": 10,
|
|
||||||
"id": "assi_date_debut",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
heure_debut = StringField(
|
|
||||||
"Heure début",
|
|
||||||
default="",
|
|
||||||
validators=[validators.Length(max=5)],
|
|
||||||
render_kw={
|
|
||||||
"class": "timepicker",
|
|
||||||
"size": 5,
|
|
||||||
"id": "assi_heure_debut",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
heure_fin = StringField(
|
|
||||||
"Heure fin",
|
|
||||||
default="",
|
|
||||||
validators=[validators.Length(max=5)],
|
|
||||||
render_kw={
|
|
||||||
"class": "timepicker",
|
|
||||||
"size": 5,
|
|
||||||
"id": "assi_heure_fin",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
date_fin = StringField(
|
|
||||||
"Date de fin (si plusieurs jours)",
|
|
||||||
validators=[validators.Length(max=10)],
|
|
||||||
render_kw={
|
|
||||||
"class": "datepicker",
|
|
||||||
"size": 10,
|
|
||||||
"id": "assi_date_fin",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
entry_date = StringField(
|
|
||||||
"Date de dépôt ou saisie",
|
|
||||||
validators=[validators.Length(max=10)],
|
|
||||||
render_kw={
|
|
||||||
"class": "datepicker",
|
|
||||||
"size": 10,
|
|
||||||
"id": "entry_date",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
entry_time = StringField(
|
|
||||||
"Heure dépôt",
|
|
||||||
default="",
|
|
||||||
validators=[validators.Length(max=5)],
|
|
||||||
render_kw={
|
|
||||||
"class": "timepicker",
|
|
||||||
"size": 5,
|
|
||||||
"id": "assi_heure_fin",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
submit = SubmitField("Enregistrer")
|
|
||||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
|
||||||
|
|
||||||
|
|
||||||
class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
|
|
||||||
"Formulaire de saisie d'une assiduité pour un étudiant"
|
|
||||||
description = TextAreaField(
|
|
||||||
"Description",
|
|
||||||
render_kw={
|
|
||||||
"id": "description",
|
|
||||||
"cols": 75,
|
|
||||||
"rows": 4,
|
|
||||||
"maxlength": 500,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assi_etat = RadioField(
|
|
||||||
"Signaler:",
|
|
||||||
choices=[("absent", "absence"), ("retard", "retard"), ("present", "présence")],
|
|
||||||
default="absent",
|
|
||||||
validators=[
|
|
||||||
validators.DataRequired("spécifiez le type d'évènement à signaler"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
modimpl = SelectField(
|
|
||||||
"Module",
|
|
||||||
choices={}, # will be populated dynamically
|
|
||||||
)
|
|
||||||
est_just = BooleanField("Justifiée")
|
|
||||||
|
|
||||||
|
|
||||||
class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
|
||||||
"Formulaire de saisie d'un justificatif pour un étudiant"
|
|
||||||
raison = TextAreaField(
|
|
||||||
"Raison",
|
|
||||||
render_kw={
|
|
||||||
"id": "raison",
|
|
||||||
"cols": 75,
|
|
||||||
"rows": 4,
|
|
||||||
"maxlength": 500,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
etat = SelectField(
|
|
||||||
"État du justificatif",
|
|
||||||
choices=[
|
|
||||||
("", "Choisir..."), # Placeholder
|
|
||||||
(scu.EtatJustificatif.ATTENTE.value, "En attente de validation"),
|
|
||||||
(scu.EtatJustificatif.NON_VALIDE.value, "Non valide"),
|
|
||||||
(scu.EtatJustificatif.MODIFIE.value, "Modifié"),
|
|
||||||
(scu.EtatJustificatif.VALIDE.value, "Valide"),
|
|
||||||
],
|
|
||||||
validators=[DataRequired(message="This field is required.")],
|
|
||||||
)
|
|
||||||
fichiers = MultipleFileField(label="Ajouter des fichiers")
|
|
||||||
|
|
||||||
|
|
||||||
class ChoixDateForm(FlaskForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
"Init form, adding a filed for our error messages"
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.ok = True
|
|
||||||
self.error_messages: list[str] = [] # used to report our errors
|
|
||||||
|
|
||||||
def set_error(self, err_msg, field=None):
|
|
||||||
"Set error message both in form and field"
|
|
||||||
self.ok = False
|
|
||||||
self.error_messages.append(err_msg)
|
|
||||||
if field:
|
|
||||||
field.errors.append(err_msg)
|
|
||||||
|
|
||||||
date = StringField(
|
|
||||||
"Date",
|
|
||||||
validators=[validators.Length(max=10)],
|
|
||||||
render_kw={
|
|
||||||
"class": "datepicker",
|
|
||||||
"size": 10,
|
|
||||||
"id": "date",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
submit = SubmitField("Enregistrer")
|
|
||||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
|
@ -17,7 +17,7 @@ def UEParcoursECTSForm(ue: UniteEns) -> FlaskForm:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
parcours: list[ApcParcours] = ue.formation.referentiel_competence.parcours
|
parcours: list[ApcParcours] = ue.formation.referentiel_competence.parcours
|
||||||
# Initialise un champ de saisie par parcours
|
# Initialise un champs de saisie par parcours
|
||||||
for parcour in parcours:
|
for parcour in parcours:
|
||||||
ects = ue.get_ects(parcour, only_parcours=True)
|
ects = ue.get_ects(parcour, only_parcours=True)
|
||||||
setattr(
|
setattr(
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -43,7 +43,6 @@ def gen_formsemestre_change_formation_form(
|
|||||||
formations: list[Formation],
|
formations: list[Formation],
|
||||||
) -> FormSemestreChangeFormationForm:
|
) -> FormSemestreChangeFormationForm:
|
||||||
"Create our dynamical form"
|
"Create our dynamical form"
|
||||||
|
|
||||||
# see https://wtforms.readthedocs.io/en/2.3.x/specific_problems/#dynamic-form-composition
|
# see https://wtforms.readthedocs.io/en/2.3.x/specific_problems/#dynamic-form-composition
|
||||||
class F(FormSemestreChangeFormationForm):
|
class F(FormSemestreChangeFormationForm):
|
||||||
pass
|
pass
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -34,11 +34,52 @@ import re
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import DecimalField, SubmitField, ValidationError
|
from wtforms import DecimalField, SubmitField, ValidationError
|
||||||
from wtforms.fields.simple import StringField
|
from wtforms.fields.simple import StringField
|
||||||
from wtforms.validators import Optional, Length
|
from wtforms.validators import Optional
|
||||||
|
|
||||||
from wtforms.widgets import TimeInput
|
from wtforms.widgets import TimeInput
|
||||||
|
|
||||||
|
|
||||||
|
class TimeField(StringField):
|
||||||
|
"""HTML5 time input.
|
||||||
|
tiré de : https://gist.github.com/tachyondecay/6016d32f65a996d0d94f
|
||||||
|
"""
|
||||||
|
|
||||||
|
widget = TimeInput()
|
||||||
|
|
||||||
|
def __init__(self, label=None, validators=None, fmt="%H:%M:%S", **kwargs):
|
||||||
|
super(TimeField, self).__init__(label, validators, **kwargs)
|
||||||
|
self.fmt = fmt
|
||||||
|
self.data = None
|
||||||
|
|
||||||
|
def _value(self):
|
||||||
|
if self.raw_data:
|
||||||
|
return " ".join(self.raw_data)
|
||||||
|
if self.data and isinstance(self.data, str):
|
||||||
|
self.data = datetime.time(*map(int, self.data.split(":")))
|
||||||
|
return self.data and self.data.strftime(self.fmt) or ""
|
||||||
|
|
||||||
|
def process_formdata(self, valuelist):
|
||||||
|
if valuelist:
|
||||||
|
time_str = " ".join(valuelist)
|
||||||
|
try:
|
||||||
|
components = time_str.split(":")
|
||||||
|
hour = 0
|
||||||
|
minutes = 0
|
||||||
|
seconds = 0
|
||||||
|
if len(components) in range(2, 4):
|
||||||
|
hour = int(components[0])
|
||||||
|
minutes = int(components[1])
|
||||||
|
|
||||||
|
if len(components) == 3:
|
||||||
|
seconds = int(components[2])
|
||||||
|
else:
|
||||||
|
raise ValueError
|
||||||
|
self.data = datetime.time(hour, minutes, seconds)
|
||||||
|
except ValueError as exc:
|
||||||
|
self.data = None
|
||||||
|
raise ValueError(self.gettext("Not a valid time string")) from exc
|
||||||
|
|
||||||
|
|
||||||
def check_tick_time(form, field):
|
def check_tick_time(form, field):
|
||||||
"""Le tick_time doit être entre 0 et 60 minutes"""
|
"""Le tick_time doit être entre 0 et 60 minutes"""
|
||||||
if field.data < 1 or field.data > 59:
|
if field.data < 1 or field.data > 59:
|
||||||
@ -77,38 +118,12 @@ def check_ics_regexp(form, field):
|
|||||||
|
|
||||||
class ConfigAssiduitesForm(FlaskForm):
|
class ConfigAssiduitesForm(FlaskForm):
|
||||||
"Formulaire paramétrage Module Assiduité"
|
"Formulaire paramétrage Module Assiduité"
|
||||||
assi_morning_time = StringField(
|
|
||||||
"Début de la journée",
|
|
||||||
default="",
|
|
||||||
validators=[Length(max=5)],
|
|
||||||
render_kw={
|
|
||||||
"class": "timepicker",
|
|
||||||
"size": 5,
|
|
||||||
"id": "assi_morning_time",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assi_lunch_time = StringField(
|
|
||||||
"Heure de midi (date pivot entre matin et après-midi)",
|
|
||||||
default="",
|
|
||||||
validators=[Length(max=5)],
|
|
||||||
render_kw={
|
|
||||||
"class": "timepicker",
|
|
||||||
"size": 5,
|
|
||||||
"id": "assi_lunch_time",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assi_afternoon_time = StringField(
|
|
||||||
"Fin de la journée",
|
|
||||||
validators=[Length(max=5)],
|
|
||||||
default="",
|
|
||||||
render_kw={
|
|
||||||
"class": "timepicker",
|
|
||||||
"size": 5,
|
|
||||||
"id": "assi_afternoon_time",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assi_tick_time = DecimalField(
|
morning_time = TimeField("Début de la journée")
|
||||||
|
lunch_time = TimeField("Heure de midi (date pivot entre Matin et Après Midi)")
|
||||||
|
afternoon_time = TimeField("Fin de la journée")
|
||||||
|
|
||||||
|
tick_time = DecimalField(
|
||||||
"Granularité de la timeline (temps en minutes)",
|
"Granularité de la timeline (temps en minutes)",
|
||||||
places=0,
|
places=0,
|
||||||
validators=[check_tick_time],
|
validators=[check_tick_time],
|
||||||
@ -122,19 +137,9 @@ class ConfigAssiduitesForm(FlaskForm):
|
|||||||
Si ce champ n'est pas renseigné, les emplois du temps ne seront pas utilisés.""",
|
Si ce champ n'est pas renseigné, les emplois du temps ne seront pas utilisés.""",
|
||||||
validators=[Optional(), check_ics_path],
|
validators=[Optional(), check_ics_path],
|
||||||
)
|
)
|
||||||
edt_ics_user_path = StringField(
|
|
||||||
label="Chemin vers les ics des utilisateurs (enseignants)",
|
|
||||||
description="""Optionnel. Chemin absolu unix sur le serveur vers le fichier ics donnant l'emploi
|
|
||||||
du temps d'un enseignant. La balise <tt>{edt_id}</tt> sera remplacée par l'edt_id du
|
|
||||||
de l'utilisateur.
|
|
||||||
Dans certains cas (XXX), ScoDoc peut générer ces fichiers et les écrira suivant
|
|
||||||
ce chemin (avec edt_id).
|
|
||||||
""",
|
|
||||||
validators=[Optional(), check_ics_path],
|
|
||||||
)
|
|
||||||
|
|
||||||
edt_ics_title_field = StringField(
|
edt_ics_title_field = StringField(
|
||||||
label="Champ contenant le titre",
|
label="Champs contenant le titre",
|
||||||
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
|
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
|
||||||
validators=[Optional(), check_ics_field],
|
validators=[Optional(), check_ics_field],
|
||||||
)
|
)
|
||||||
@ -147,7 +152,7 @@ class ConfigAssiduitesForm(FlaskForm):
|
|||||||
validators=[Optional(), check_ics_regexp],
|
validators=[Optional(), check_ics_regexp],
|
||||||
)
|
)
|
||||||
edt_ics_group_field = StringField(
|
edt_ics_group_field = StringField(
|
||||||
label="Champ contenant le groupe",
|
label="Champs contenant le groupe",
|
||||||
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
|
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
|
||||||
validators=[Optional(), check_ics_field],
|
validators=[Optional(), check_ics_field],
|
||||||
)
|
)
|
||||||
@ -160,7 +165,7 @@ class ConfigAssiduitesForm(FlaskForm):
|
|||||||
validators=[Optional(), check_ics_regexp],
|
validators=[Optional(), check_ics_regexp],
|
||||||
)
|
)
|
||||||
edt_ics_mod_field = StringField(
|
edt_ics_mod_field = StringField(
|
||||||
label="Champ contenant le module",
|
label="Champs contenant le module",
|
||||||
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
|
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
|
||||||
validators=[Optional(), check_ics_field],
|
validators=[Optional(), check_ics_field],
|
||||||
)
|
)
|
||||||
@ -172,19 +177,6 @@ class ConfigAssiduitesForm(FlaskForm):
|
|||||||
""",
|
""",
|
||||||
validators=[Optional(), check_ics_regexp],
|
validators=[Optional(), check_ics_regexp],
|
||||||
)
|
)
|
||||||
edt_ics_uid_field = StringField(
|
|
||||||
label="Champ contenant les enseignants",
|
|
||||||
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
|
|
||||||
validators=[Optional(), check_ics_field],
|
|
||||||
)
|
|
||||||
edt_ics_uid_regexp = StringField(
|
|
||||||
label="Extraction des enseignants",
|
|
||||||
description=r"""expression régulière python permettant d'extraire les
|
|
||||||
identifiants des enseignants associés à l'évènement.
|
|
||||||
(contrairement aux autres champs, il peut y avoir plusieurs enseignants par évènement.)
|
|
||||||
Exemple: <tt>[0-9]+</tt>
|
|
||||||
""",
|
|
||||||
validators=[Optional(), check_ics_regexp],
|
|
||||||
)
|
|
||||||
submit = SubmitField("Valider")
|
submit = SubmitField("Valider")
|
||||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -82,7 +82,7 @@ class ConfigCASForm(FlaskForm):
|
|||||||
|
|
||||||
cas_attribute_id = StringField(
|
cas_attribute_id = StringField(
|
||||||
label="Attribut CAS utilisé comme id (laissez vide pour prendre l'id par défaut)",
|
label="Attribut CAS utilisé comme id (laissez vide pour prendre l'id par défaut)",
|
||||||
description="""Le champ CAS qui sera considéré comme l'id unique des
|
description="""Le champs CAS qui sera considéré comme l'id unique des
|
||||||
comptes utilisateurs.""",
|
comptes utilisateurs.""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -77,12 +77,7 @@ class ScoDocConfigurationForm(FlaskForm):
|
|||||||
Attention: si ce champ peut aussi être défini dans chaque département.""",
|
Attention: si ce champ peut aussi être défini dans chaque département.""",
|
||||||
validators=[Optional(), Email()],
|
validators=[Optional(), Email()],
|
||||||
)
|
)
|
||||||
user_require_email_institutionnel = BooleanField(
|
disable_bul_pdf = BooleanField("empêcher les exports des bulletins en PDF")
|
||||||
"imposer la saisie du mail institutionnel dans le formulaire de création utilisateur"
|
|
||||||
)
|
|
||||||
disable_bul_pdf = BooleanField(
|
|
||||||
"interdire les exports des bulletins en PDF (déconseillé)"
|
|
||||||
)
|
|
||||||
submit_scodoc = SubmitField("Valider")
|
submit_scodoc = SubmitField("Valider")
|
||||||
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||||
|
|
||||||
@ -102,7 +97,6 @@ def configuration():
|
|||||||
"month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(),
|
"month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(),
|
||||||
"email_from_addr": ScoDocSiteConfig.get("email_from_addr"),
|
"email_from_addr": ScoDocSiteConfig.get("email_from_addr"),
|
||||||
"disable_bul_pdf": ScoDocSiteConfig.is_bul_pdf_disabled(),
|
"disable_bul_pdf": ScoDocSiteConfig.is_bul_pdf_disabled(),
|
||||||
"user_require_email_institutionnel": ScoDocSiteConfig.is_user_require_email_institutionnel_enabled(),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if request.method == "POST" and (
|
if request.method == "POST" and (
|
||||||
@ -155,18 +149,6 @@ def configuration():
|
|||||||
"Exports PDF "
|
"Exports PDF "
|
||||||
+ ("désactivés" if form_scodoc.data["disable_bul_pdf"] else "réactivés")
|
+ ("désactivés" if form_scodoc.data["disable_bul_pdf"] else "réactivés")
|
||||||
)
|
)
|
||||||
if ScoDocSiteConfig.set(
|
|
||||||
"user_require_email_institutionnel",
|
|
||||||
"on" if form_scodoc.data["user_require_email_institutionnel"] else "",
|
|
||||||
):
|
|
||||||
flash(
|
|
||||||
(
|
|
||||||
"impose"
|
|
||||||
if form_scodoc.data["user_require_email_institutionnel"]
|
|
||||||
else "n'impose pas"
|
|
||||||
)
|
|
||||||
+ " la saisie du mail institutionnel des utilisateurs"
|
|
||||||
)
|
|
||||||
return redirect(url_for("scodoc.index"))
|
return redirect(url_for("scodoc.index"))
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
# -*- mode: python -*-
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# ScoDoc
|
|
||||||
#
|
|
||||||
# Copyright (c) 1999 - 2024 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
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""
|
|
||||||
Formulaire configuration RGPD
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask_wtf import FlaskForm
|
|
||||||
from wtforms import SubmitField
|
|
||||||
from wtforms.fields.simple import TextAreaField
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigRGPDForm(FlaskForm):
|
|
||||||
"Formulaire paramétrage RGPD"
|
|
||||||
rgpd_coordonnees_dpo = TextAreaField(
|
|
||||||
label="Optionnel: coordonnées du DPO",
|
|
||||||
description="""Le délégué à la protection des données (DPO) est chargé de mettre en œuvre
|
|
||||||
la conformité au règlement européen sur la protection des données (RGPD) au sein de l’organisme.
|
|
||||||
Indiquer ici les coordonnées (format libre) qui seront affichées aux utilisateurs de ScoDoc.
|
|
||||||
""",
|
|
||||||
render_kw={"rows": 5, "cols": 72},
|
|
||||||
)
|
|
||||||
|
|
||||||
submit = SubmitField("Valider")
|
|
||||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -23,17 +23,8 @@ convention = {
|
|||||||
metadata_obj = sqlalchemy.MetaData(naming_convention=convention)
|
metadata_obj = sqlalchemy.MetaData(naming_convention=convention)
|
||||||
|
|
||||||
|
|
||||||
class ScoDocModel(db.Model):
|
class ScoDocModel:
|
||||||
"""Superclass for our models. Add some useful methods for editing, cloning, etc.
|
"Mixin class for our models. Add somme useful methods for editing, cloning, etc."
|
||||||
- clone() : clone object and add copy to session, do not commit.
|
|
||||||
- create_from_dict() : create instance from given dict, applying conversions.
|
|
||||||
- convert_dict_fields() : convert dict values, called before instance creation.
|
|
||||||
By default, do nothing.
|
|
||||||
- from_dict() : update object using data from dict. data is first converted.
|
|
||||||
- edit() : update from wtf form.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__abstract__ = True # declare an abstract class for SQLAlchemy
|
|
||||||
|
|
||||||
def clone(self, not_copying=()):
|
def clone(self, not_copying=()):
|
||||||
"""Clone, not copying the given attrs
|
"""Clone, not copying the given attrs
|
||||||
@ -49,28 +40,21 @@ class ScoDocModel(db.Model):
|
|||||||
return copy
|
return copy
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_from_dict(cls, data: dict) -> "ScoDocModel":
|
def create_from_dict(cls, data: dict):
|
||||||
"""Create a new instance of the model with attributes given in dict.
|
"""Create a new instance of the model with attributes given in dict.
|
||||||
The instance is added to the session (but not flushed nor committed).
|
The instance is added to the session (but not flushed nor committed).
|
||||||
Use only relevant attributes for the given model and ignore others.
|
Use only relevant arributes for the given model and ignore others.
|
||||||
"""
|
"""
|
||||||
if data:
|
|
||||||
args = cls.convert_dict_fields(cls.filter_model_attributes(data))
|
args = cls.convert_dict_fields(cls.filter_model_attributes(data))
|
||||||
if args:
|
|
||||||
obj = cls(**args)
|
obj = cls(**args)
|
||||||
else:
|
|
||||||
obj = cls()
|
|
||||||
else:
|
|
||||||
obj = cls()
|
|
||||||
db.session.add(obj)
|
db.session.add(obj)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
|
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
|
||||||
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded.
|
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded.
|
||||||
Add 'id' to excluded."""
|
By default, excluded == { 'id' }"""
|
||||||
excluded = excluded or set()
|
excluded = {"id"} if excluded is None else set()
|
||||||
excluded.add("id") # always exclude id
|
|
||||||
# Les attributs du modèle qui sont des variables: (élimine les __ et les alias comme adm_id)
|
# Les attributs du modèle qui sont des variables: (élimine les __ et les alias comme adm_id)
|
||||||
my_attributes = [
|
my_attributes = [
|
||||||
a
|
a
|
||||||
@ -86,7 +70,7 @@ class ScoDocModel(db.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def convert_dict_fields(cls, args: dict) -> dict:
|
def convert_dict_fields(cls, args: dict) -> dict:
|
||||||
"""Convert fields from the given dict to model's attributes values. No side effect.
|
"""Convert fields in the given dict. No side effect.
|
||||||
By default, do nothing, but is overloaded by some subclasses.
|
By default, do nothing, but is overloaded by some subclasses.
|
||||||
args: dict with args in application.
|
args: dict with args in application.
|
||||||
returns: dict to store in model's db.
|
returns: dict to store in model's db.
|
||||||
@ -94,27 +78,13 @@ class ScoDocModel(db.Model):
|
|||||||
# virtual, by default, do nothing
|
# virtual, by default, do nothing
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool:
|
def from_dict(self, args: dict):
|
||||||
"""Update object's fields given in dict. Add to session but don't commit.
|
"Update object's fields given in dict. Add to session but don't commit."
|
||||||
True if modification.
|
args_dict = self.convert_dict_fields(self.filter_model_attributes(args))
|
||||||
"""
|
|
||||||
args_dict = self.convert_dict_fields(
|
|
||||||
self.filter_model_attributes(args, excluded=excluded)
|
|
||||||
)
|
|
||||||
modified = False
|
|
||||||
for key, value in args_dict.items():
|
for key, value in args_dict.items():
|
||||||
if hasattr(self, key) and value != getattr(self, key):
|
if hasattr(self, key):
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
modified = True
|
|
||||||
db.session.add(self)
|
db.session.add(self)
|
||||||
return modified
|
|
||||||
|
|
||||||
def edit_from_form(self, form) -> bool:
|
|
||||||
"""Generic edit method for updating model instance.
|
|
||||||
True if modification.
|
|
||||||
"""
|
|
||||||
args = {field.name: field.data for field in form}
|
|
||||||
return self.from_dict(args)
|
|
||||||
|
|
||||||
|
|
||||||
from app.models.absences import Absence, AbsenceNotification, BilletAbsence
|
from app.models.absences import Absence, AbsenceNotification, BilletAbsence
|
||||||
@ -160,6 +130,7 @@ from app.models.notes import (
|
|||||||
NotesNotesLog,
|
NotesNotesLog,
|
||||||
)
|
)
|
||||||
from app.models.validations import (
|
from app.models.validations import (
|
||||||
|
ScolarEvent,
|
||||||
ScolarFormSemestreValidation,
|
ScolarFormSemestreValidation,
|
||||||
ScolarAutorisationInscription,
|
ScolarAutorisationInscription,
|
||||||
)
|
)
|
||||||
@ -178,4 +149,3 @@ from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
|||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
|
|
||||||
from app.models.assiduites import Assiduite, Justificatif
|
from app.models.assiduites import Assiduite, Justificatif
|
||||||
from app.models.scolar_event import ScolarEvent
|
|
||||||
|
@ -3,35 +3,23 @@
|
|||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask_login import current_user
|
from app import db, log
|
||||||
from flask_sqlalchemy.query import Query
|
from app.models import ModuleImpl, Scolog, FormSemestre, FormSemestreInscription
|
||||||
from sqlalchemy.exc import DataError
|
|
||||||
|
|
||||||
from app import db, log, g, set_sco_dept
|
|
||||||
from app.models import (
|
|
||||||
ModuleImpl,
|
|
||||||
Module,
|
|
||||||
Scolog,
|
|
||||||
FormSemestre,
|
|
||||||
FormSemestreInscription,
|
|
||||||
ScoDocModel,
|
|
||||||
)
|
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.scodoc import sco_abs_notification
|
from app.scodoc import sco_abs_notification
|
||||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_permissions import Permission
|
|
||||||
from app.scodoc.sco_utils import (
|
from app.scodoc.sco_utils import (
|
||||||
EtatAssiduite,
|
EtatAssiduite,
|
||||||
EtatJustificatif,
|
EtatJustificatif,
|
||||||
localize_datetime,
|
localize_datetime,
|
||||||
is_assiduites_module_forced,
|
is_assiduites_module_forced,
|
||||||
NonWorkDays,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from flask_sqlalchemy.query import Query
|
||||||
|
|
||||||
class Assiduite(ScoDocModel):
|
|
||||||
|
class Assiduite(db.Model):
|
||||||
"""
|
"""
|
||||||
Représente une assiduité:
|
Représente une assiduité:
|
||||||
- une plage horaire lié à un état et un étudiant
|
- une plage horaire lié à un état et un étudiant
|
||||||
@ -89,12 +77,10 @@ class Assiduite(ScoDocModel):
|
|||||||
lazy="select",
|
lazy="select",
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_dict(self, format_api=True, restrict: bool | None = None) -> dict:
|
def to_dict(self, format_api=True) -> dict:
|
||||||
"""Retourne la représentation json de l'assiduité
|
"""Retourne la représentation json de l'assiduité"""
|
||||||
restrict n'est pas utilisé ici.
|
|
||||||
"""
|
|
||||||
etat = self.etat
|
etat = self.etat
|
||||||
user: User | None = None
|
user: User = None
|
||||||
if format_api:
|
if format_api:
|
||||||
# format api utilise les noms "present,absent,retard" au lieu des int
|
# format api utilise les noms "present,absent,retard" au lieu des int
|
||||||
etat = EtatAssiduite.inverse().get(self.etat).name
|
etat = EtatAssiduite.inverse().get(self.etat).name
|
||||||
@ -149,50 +135,16 @@ class Assiduite(ScoDocModel):
|
|||||||
external_data: dict = None,
|
external_data: dict = None,
|
||||||
notify_mail=False,
|
notify_mail=False,
|
||||||
) -> "Assiduite":
|
) -> "Assiduite":
|
||||||
"""Créer une nouvelle assiduité pour l'étudiant.
|
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||||
Les datetime doivent être en timezone serveur.
|
|
||||||
Raises ScoValueError en cas de conflit ou erreur.
|
|
||||||
"""
|
|
||||||
if date_debut.tzinfo is None:
|
if date_debut.tzinfo is None:
|
||||||
log(
|
log(
|
||||||
f"Warning: create_assiduite: date_debut without timezone ({date_debut})"
|
f"Warning: create_assiduite: date_debut without timezone ({date_debut})"
|
||||||
)
|
)
|
||||||
if date_fin.tzinfo is None:
|
if date_fin.tzinfo is None:
|
||||||
log(f"Warning: create_assiduite: date_fin without timezone ({date_fin})")
|
log(f"Warning: create_assiduite: date_fin without timezone ({date_fin})")
|
||||||
|
|
||||||
# Vérification jours non travaillés
|
|
||||||
# -> vérifie si la date de début ou la date de fin est sur un jour non travaillé
|
|
||||||
# On récupère les formsemestres des dates de début et de fin
|
|
||||||
formsemestre_date_debut: FormSemestre = get_formsemestre_from_data(
|
|
||||||
{
|
|
||||||
"etudid": etud.id,
|
|
||||||
"date_debut": date_debut,
|
|
||||||
"date_fin": date_debut,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
formsemestre_date_fin: FormSemestre = get_formsemestre_from_data(
|
|
||||||
{
|
|
||||||
"etudid": etud.id,
|
|
||||||
"date_debut": date_fin,
|
|
||||||
"date_fin": date_fin,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if date_debut.weekday() in NonWorkDays.get_all_non_work_days(
|
|
||||||
formsemestre_id=formsemestre_date_debut
|
|
||||||
):
|
|
||||||
raise ScoValueError("La date de début n'est pas un jour travaillé")
|
|
||||||
if date_fin.weekday() in NonWorkDays.get_all_non_work_days(
|
|
||||||
formsemestre_id=formsemestre_date_fin
|
|
||||||
):
|
|
||||||
raise ScoValueError("La date de fin n'est pas un jour travaillé")
|
|
||||||
|
|
||||||
# Vérification de non duplication des périodes
|
# Vérification de non duplication des périodes
|
||||||
assiduites: Query = etud.assiduites
|
assiduites: Query = etud.assiduites
|
||||||
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):
|
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):
|
||||||
log(
|
|
||||||
f"""create_assiduite: period_conflicting etudid={etud.id} date_debut={
|
|
||||||
date_debut} date_fin={date_fin}"""
|
|
||||||
)
|
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
"Duplication: la période rentre en conflit avec une plage enregistrée"
|
"Duplication: la période rentre en conflit avec une plage enregistrée"
|
||||||
)
|
)
|
||||||
@ -242,8 +194,7 @@ class Assiduite(ScoDocModel):
|
|||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
)
|
)
|
||||||
db.session.add(nouv_assiduite)
|
db.session.add(nouv_assiduite)
|
||||||
db.session.flush()
|
log(f"create_assiduite: {etud.id} {nouv_assiduite}")
|
||||||
log(f"create_assiduite: {etud.id} id={nouv_assiduite.id} {nouv_assiduite}")
|
|
||||||
Scolog.logdb(
|
Scolog.logdb(
|
||||||
method="create_assiduite",
|
method="create_assiduite",
|
||||||
etudid=etud.id,
|
etudid=etud.id,
|
||||||
@ -253,139 +204,8 @@ class Assiduite(ScoDocModel):
|
|||||||
sco_abs_notification.abs_notify(etud.id, nouv_assiduite.date_debut)
|
sco_abs_notification.abs_notify(etud.id, nouv_assiduite.date_debut)
|
||||||
return nouv_assiduite
|
return nouv_assiduite
|
||||||
|
|
||||||
def set_moduleimpl(self, moduleimpl_id: int | str):
|
|
||||||
"""Mise à jour du moduleimpl_id
|
|
||||||
Les valeurs du champ "moduleimpl_id" possibles sont :
|
|
||||||
- <int> (un id classique)
|
|
||||||
- <str> ("autre" ou "<id>")
|
|
||||||
- "" (pas de moduleimpl_id)
|
|
||||||
Si la valeur est "autre" il faut:
|
|
||||||
- mettre à None assiduité.moduleimpl_id
|
|
||||||
- mettre à jour assiduite.external_data["module"] = "autre"
|
|
||||||
En fonction de la configuration du semestre (option force_module) la valeur "" peut-être
|
|
||||||
considérée comme invalide.
|
|
||||||
- Il faudra donc vérifier que ce n'est pas le cas avant de mettre à jour l'assiduité
|
|
||||||
"""
|
|
||||||
moduleimpl: ModuleImpl = None
|
|
||||||
if moduleimpl_id == "autre":
|
|
||||||
# Configuration de external_data pour Module Autre
|
|
||||||
# Si self.external_data None alors on créé un dictionnaire {"module": "autre"}
|
|
||||||
# Sinon on met à jour external_data["module"] à "autre"
|
|
||||||
|
|
||||||
if self.external_data is None:
|
class Justificatif(db.Model):
|
||||||
self.external_data = {"module": "autre"}
|
|
||||||
else:
|
|
||||||
self.external_data["module"] = "autre"
|
|
||||||
|
|
||||||
# Dans tous les cas une fois fait, assiduite.moduleimpl_id doit être None
|
|
||||||
self.moduleimpl_id = None
|
|
||||||
|
|
||||||
# Ici pas de vérification du force module car on l'a mis dans "external_data"
|
|
||||||
return
|
|
||||||
|
|
||||||
if moduleimpl_id != "":
|
|
||||||
try:
|
|
||||||
moduleimpl_id = int(moduleimpl_id)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise ScoValueError("Module non reconnu") from exc
|
|
||||||
moduleimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
|
|
||||||
|
|
||||||
# ici moduleimpl est None si non spécifié
|
|
||||||
|
|
||||||
# Vérification ModuleImpl not None (raise ScoValueError)
|
|
||||||
if moduleimpl is None:
|
|
||||||
self._check_force_module()
|
|
||||||
# Ici uniquement si on est autorisé à ne pas avoir de module
|
|
||||||
self.moduleimpl_id = None
|
|
||||||
return
|
|
||||||
|
|
||||||
# Vérification Inscription ModuleImpl (raise ScoValueError)
|
|
||||||
if moduleimpl.est_inscrit(self.etudiant):
|
|
||||||
self.moduleimpl_id = moduleimpl.id
|
|
||||||
else:
|
|
||||||
raise ScoValueError("L'étudiant n'est pas inscrit au module")
|
|
||||||
|
|
||||||
def supprime(self):
|
|
||||||
"Supprime l'assiduité. Log et commit."
|
|
||||||
from app.scodoc import sco_assiduites as scass
|
|
||||||
|
|
||||||
if g.scodoc_dept is None and self.etudiant.dept_id is not None:
|
|
||||||
# route sans département
|
|
||||||
set_sco_dept(self.etudiant.departement.acronym)
|
|
||||||
obj_dict: dict = self.to_dict()
|
|
||||||
# Suppression de l'objet et LOG
|
|
||||||
log(f"delete_assidutite: {self.etudiant.id} {self}")
|
|
||||||
Scolog.logdb(
|
|
||||||
method="delete_assiduite",
|
|
||||||
etudid=self.etudiant.id,
|
|
||||||
msg=f"Assiduité: {self}",
|
|
||||||
)
|
|
||||||
db.session.delete(self)
|
|
||||||
db.session.commit()
|
|
||||||
# Invalidation du cache
|
|
||||||
scass.simple_invalidate_cache(obj_dict)
|
|
||||||
|
|
||||||
def get_formsemestre(self) -> FormSemestre:
|
|
||||||
"""Le formsemestre associé.
|
|
||||||
Attention: en cas d'inscription multiple prend arbitrairement l'un des semestres.
|
|
||||||
A utiliser avec précaution !
|
|
||||||
"""
|
|
||||||
return get_formsemestre_from_data(self.to_dict())
|
|
||||||
|
|
||||||
def get_module(self, traduire: bool = False) -> int | str:
|
|
||||||
"TODO documenter"
|
|
||||||
if self.moduleimpl_id is not None:
|
|
||||||
if traduire:
|
|
||||||
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
|
|
||||||
mod: Module = Module.query.get(modimpl.module_id)
|
|
||||||
return f"{mod.code} {mod.titre}"
|
|
||||||
|
|
||||||
elif self.external_data is not None and "module" in self.external_data:
|
|
||||||
return (
|
|
||||||
"Tout module"
|
|
||||||
if self.external_data["module"] == "Autre"
|
|
||||||
else self.external_data["module"]
|
|
||||||
)
|
|
||||||
|
|
||||||
return "Non spécifié" if traduire else None
|
|
||||||
|
|
||||||
def get_saisie(self) -> str:
|
|
||||||
"""
|
|
||||||
retourne le texte "saisie le <date> par <User>"
|
|
||||||
"""
|
|
||||||
|
|
||||||
date: str = self.entry_date.strftime("%d/%m/%Y à %H:%M")
|
|
||||||
utilisateur: str = ""
|
|
||||||
if self.user != None:
|
|
||||||
self.user: User
|
|
||||||
utilisateur = f"par {self.user.get_prenomnom()}"
|
|
||||||
|
|
||||||
return f"saisie le {date} {utilisateur}"
|
|
||||||
|
|
||||||
def _check_force_module(self):
|
|
||||||
"""Vérification si module forcé:
|
|
||||||
Si le module est requis, raise ScoValueError
|
|
||||||
sinon ne fait rien.
|
|
||||||
"""
|
|
||||||
# cherche le formsemestre affecté pour utiliser ses préférences
|
|
||||||
formsemestre: FormSemestre = get_formsemestre_from_data(
|
|
||||||
{
|
|
||||||
"etudid": self.etudid,
|
|
||||||
"date_debut": self.date_debut,
|
|
||||||
"date_fin": self.date_fin,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
formsemestre_id = formsemestre.id if formsemestre else None
|
|
||||||
# si pas de formsemestre, utilisera les prefs globales du département
|
|
||||||
dept_id = self.etudiant.dept_id
|
|
||||||
force = is_assiduites_module_forced(
|
|
||||||
formsemestre_id=formsemestre_id, dept_id=dept_id
|
|
||||||
)
|
|
||||||
if force:
|
|
||||||
raise ScoValueError("Module non renseigné")
|
|
||||||
|
|
||||||
|
|
||||||
class Justificatif(ScoDocModel):
|
|
||||||
"""
|
"""
|
||||||
Représente un justificatif:
|
Représente un justificatif:
|
||||||
- une plage horaire lié à un état et un étudiant
|
- une plage horaire lié à un état et un étudiant
|
||||||
@ -417,8 +237,6 @@ class Justificatif(ScoDocModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
"date de création de l'élément: date de saisie"
|
|
||||||
# pourrait devenir date de dépôt au secrétariat, si différente
|
|
||||||
|
|
||||||
user_id = db.Column(
|
user_id = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
@ -437,35 +255,23 @@ class Justificatif(ScoDocModel):
|
|||||||
etudiant = db.relationship(
|
etudiant = db.relationship(
|
||||||
"Identite", back_populates="justificatifs", lazy="joined"
|
"Identite", back_populates="justificatifs", lazy="joined"
|
||||||
)
|
)
|
||||||
# En revanche, user est rarement accédé:
|
|
||||||
user = db.relationship(
|
|
||||||
"User",
|
|
||||||
backref=db.backref(
|
|
||||||
"justificatifs", lazy="select", order_by="Justificatif.entry_date"
|
|
||||||
),
|
|
||||||
lazy="select",
|
|
||||||
)
|
|
||||||
|
|
||||||
external_data = db.Column(db.JSON, nullable=True)
|
external_data = db.Column(db.JSON, nullable=True)
|
||||||
|
|
||||||
@classmethod
|
def to_dict(self, format_api: bool = False) -> dict:
|
||||||
def get_justificatif(cls, justif_id: int) -> "Justificatif":
|
"""transformation de l'objet en dictionnaire sérialisable"""
|
||||||
"""Justificatif ou 404, cherche uniquement dans le département courant"""
|
|
||||||
query = Justificatif.query.filter_by(id=justif_id)
|
|
||||||
if g.scodoc_dept:
|
|
||||||
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
|
||||||
return query.first_or_404()
|
|
||||||
|
|
||||||
def to_dict(self, format_api: bool = False, restrict: bool = False) -> dict:
|
|
||||||
"""L'objet en dictionnaire sérialisable.
|
|
||||||
Si restrict, ne donne par la raison et les fichiers et external_data
|
|
||||||
"""
|
|
||||||
|
|
||||||
etat = self.etat
|
etat = self.etat
|
||||||
user: User = self.user if self.user_id is not None else None
|
username = self.user_id
|
||||||
|
|
||||||
if format_api:
|
if format_api:
|
||||||
etat = EtatJustificatif.inverse().get(self.etat).name
|
etat = EtatJustificatif.inverse().get(self.etat).name
|
||||||
|
if self.user_id is not None:
|
||||||
|
user: User = db.session.get(User, self.user_id)
|
||||||
|
if user is None:
|
||||||
|
username = "Non renseigné"
|
||||||
|
else:
|
||||||
|
username = user.get_prenomnom()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"justif_id": self.justif_id,
|
"justif_id": self.justif_id,
|
||||||
@ -474,47 +280,30 @@ class Justificatif(ScoDocModel):
|
|||||||
"date_debut": self.date_debut,
|
"date_debut": self.date_debut,
|
||||||
"date_fin": self.date_fin,
|
"date_fin": self.date_fin,
|
||||||
"etat": etat,
|
"etat": etat,
|
||||||
"raison": None if restrict else self.raison,
|
"raison": self.raison,
|
||||||
"fichier": None if restrict else self.fichier,
|
"fichier": self.fichier,
|
||||||
"entry_date": self.entry_date,
|
"entry_date": self.entry_date,
|
||||||
"user_id": None if user is None else user.id, # l'uid
|
"user_id": username,
|
||||||
"user_name": None if user is None else user.user_name, # le login
|
"external_data": self.external_data,
|
||||||
"user_nom_complet": None if user is None else user.get_nomcomplet(),
|
|
||||||
"external_data": None if restrict else self.external_data,
|
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"chaine pour journaux et debug (lisible par humain français)"
|
"chaine pour journaux et debug (lisible par humain français)"
|
||||||
try:
|
try:
|
||||||
etat_str = EtatJustificatif(self.etat).name
|
etat_str = EtatJustificatif(self.etat).name
|
||||||
except ValueError:
|
except ValueError:
|
||||||
etat_str = "Invalide"
|
etat_str = "Invalide"
|
||||||
return f"""Justificatif id={self.id} {etat_str} de {
|
return f"""Justificatif {etat_str} de {
|
||||||
self.date_debut.strftime("%d/%m/%Y %Hh%M")
|
self.date_debut.strftime("%d/%m/%Y %Hh%M")
|
||||||
} à {
|
} à {
|
||||||
self.date_fin.strftime("%d/%m/%Y %Hh%M")
|
self.date_fin.strftime("%d/%m/%Y %Hh%M")
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def convert_dict_fields(cls, args: dict) -> dict:
|
|
||||||
"""Convert fields. Called by ScoDocModel's create_from_dict, edit and from_dict
|
|
||||||
Raises ScoValueError si paramètres incorrects.
|
|
||||||
"""
|
|
||||||
if not isinstance(args["date_debut"], datetime) or not isinstance(
|
|
||||||
args["date_fin"], datetime
|
|
||||||
):
|
|
||||||
raise ScoValueError("type date incorrect")
|
|
||||||
if args["date_fin"] <= args["date_debut"]:
|
|
||||||
raise ScoValueError("dates incompatibles")
|
|
||||||
if args["entry_date"] and not isinstance(args["entry_date"], datetime):
|
|
||||||
raise ScoValueError("type entry_date incorrect")
|
|
||||||
return args
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_justificatif(
|
def create_justificatif(
|
||||||
cls,
|
cls,
|
||||||
etudiant: Identite,
|
etud: Identite,
|
||||||
date_debut: datetime,
|
date_debut: datetime,
|
||||||
date_fin: datetime,
|
date_fin: datetime,
|
||||||
etat: EtatJustificatif,
|
etat: EtatJustificatif,
|
||||||
@ -523,75 +312,28 @@ class Justificatif(ScoDocModel):
|
|||||||
user_id: int = None,
|
user_id: int = None,
|
||||||
external_data: dict = None,
|
external_data: dict = None,
|
||||||
) -> "Justificatif":
|
) -> "Justificatif":
|
||||||
"""Créer un nouveau justificatif pour l'étudiant.
|
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||||
Raises ScoValueError si paramètres incorrects.
|
nouv_justificatif = Justificatif(
|
||||||
"""
|
date_debut=date_debut,
|
||||||
nouv_justificatif = cls.create_from_dict(locals())
|
date_fin=date_fin,
|
||||||
db.session.commit()
|
etat=etat,
|
||||||
log(f"create_justificatif: etudid={etudiant.id} {nouv_justificatif}")
|
etudiant=etud,
|
||||||
|
raison=raison,
|
||||||
|
entry_date=entry_date,
|
||||||
|
user_id=user_id,
|
||||||
|
external_data=external_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(nouv_justificatif)
|
||||||
|
|
||||||
|
log(f"create_justificatif: {etud.id} {nouv_justificatif}")
|
||||||
Scolog.logdb(
|
Scolog.logdb(
|
||||||
method="create_justificatif",
|
method="create_justificatif",
|
||||||
etudid=etudiant.id,
|
etudid=etud.id,
|
||||||
msg=f"justificatif: {nouv_justificatif}",
|
msg=f"justificatif: {nouv_justificatif}",
|
||||||
)
|
)
|
||||||
return nouv_justificatif
|
return nouv_justificatif
|
||||||
|
|
||||||
def supprime(self):
|
|
||||||
"Supprime le justificatif. Log et commit."
|
|
||||||
from app.scodoc import sco_assiduites as scass
|
|
||||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
|
||||||
|
|
||||||
# Récupération de l'archive du justificatif
|
|
||||||
archive_name: str = self.fichier
|
|
||||||
|
|
||||||
if archive_name is not None:
|
|
||||||
# Si elle existe : on essaye de la supprimer
|
|
||||||
archiver: JustificatifArchiver = JustificatifArchiver()
|
|
||||||
try:
|
|
||||||
archiver.delete_justificatif(self.etudiant, archive_name)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if g.scodoc_dept is None and self.etudiant.dept_id is not None:
|
|
||||||
# route sans département
|
|
||||||
set_sco_dept(self.etudiant.departement.acronym)
|
|
||||||
# On invalide le cache
|
|
||||||
scass.simple_invalidate_cache(self.to_dict())
|
|
||||||
# Suppression de l'objet et LOG
|
|
||||||
log(f"delete_justificatif: {self.etudiant.id} {self}")
|
|
||||||
Scolog.logdb(
|
|
||||||
method="delete_justificatif",
|
|
||||||
etudid=self.etudiant.id,
|
|
||||||
msg=f"Justificatif: {self}",
|
|
||||||
)
|
|
||||||
db.session.delete(self)
|
|
||||||
db.session.commit()
|
|
||||||
# On actualise les assiduités justifiées de l'étudiant concerné
|
|
||||||
compute_assiduites_justified(
|
|
||||||
self.etudid,
|
|
||||||
Justificatif.query.filter_by(etudid=self.etudid).all(),
|
|
||||||
True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_fichiers(self) -> tuple[list[str], int]:
|
|
||||||
"""Renvoie la liste des noms de fichiers justicatifs
|
|
||||||
accessibles par l'utilisateur courant et le nombre total
|
|
||||||
de fichiers.
|
|
||||||
(ces fichiers sont dans l'archive associée)
|
|
||||||
"""
|
|
||||||
if self.fichier is None:
|
|
||||||
return [], 0
|
|
||||||
archive_name: str = self.fichier
|
|
||||||
archiver: JustificatifArchiver = JustificatifArchiver()
|
|
||||||
filenames = archiver.list_justificatifs(archive_name, self.etudiant)
|
|
||||||
accessible_filenames = []
|
|
||||||
#
|
|
||||||
for filename in filenames:
|
|
||||||
if int(filename[1]) == current_user.id or current_user.has_permission(
|
|
||||||
Permission.AbsJustifView
|
|
||||||
):
|
|
||||||
accessible_filenames.append(filename[0])
|
|
||||||
return accessible_filenames, len(filenames)
|
|
||||||
|
|
||||||
|
|
||||||
def is_period_conflicting(
|
def is_period_conflicting(
|
||||||
date_debut: datetime,
|
date_debut: datetime,
|
||||||
@ -619,6 +361,8 @@ def compute_assiduites_justified(
|
|||||||
etudid: int, justificatifs: list[Justificatif] = None, reset: bool = False
|
etudid: int, justificatifs: list[Justificatif] = None, reset: bool = False
|
||||||
) -> list[int]:
|
) -> list[int]:
|
||||||
"""
|
"""
|
||||||
|
compute_assiduites_justified_faster
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
etudid (int): l'identifiant de l'étudiant
|
etudid (int): l'identifiant de l'étudiant
|
||||||
justificatifs (list[Justificatif]): La liste des justificatifs qui seront utilisés
|
justificatifs (list[Justificatif]): La liste des justificatifs qui seront utilisés
|
||||||
@ -627,12 +371,6 @@ def compute_assiduites_justified(
|
|||||||
Returns:
|
Returns:
|
||||||
list[int]: la liste des assiduités qui ont été justifiées.
|
list[int]: la liste des assiduités qui ont été justifiées.
|
||||||
"""
|
"""
|
||||||
# TODO à optimiser (car très long avec 40000 assiduités)
|
|
||||||
# On devrait :
|
|
||||||
# - récupérer uniquement les assiduités qui sont sur la période des justificatifs donnés
|
|
||||||
# - Pour chaque assiduité trouvée, il faut récupérer les justificatifs qui la justifie
|
|
||||||
# - Si au moins un justificatif valide couvre la période de l'assiduité alors on la justifie
|
|
||||||
|
|
||||||
# Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant
|
# Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant
|
||||||
if justificatifs is None:
|
if justificatifs is None:
|
||||||
justificatifs: list[Justificatif] = Justificatif.query.filter_by(
|
justificatifs: list[Justificatif] = Justificatif.query.filter_by(
|
||||||
@ -691,8 +429,7 @@ def get_assiduites_justif(assiduite_id: int, long: bool) -> list[int | dict]:
|
|||||||
des identifiants des justificatifs
|
des identifiants des justificatifs
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[int | dict]: La liste des justificatifs (par défaut uniquement
|
list[int | dict]: La liste des justificatifs (par défaut uniquement les identifiants, sinon les Dict si long est vrai)
|
||||||
les identifiants, sinon les dict si long est vrai)
|
|
||||||
"""
|
"""
|
||||||
assi: Assiduite = Assiduite.query.get_or_404(assiduite_id)
|
assi: Assiduite = Assiduite.query.get_or_404(assiduite_id)
|
||||||
return get_justifs_from_date(assi.etudid, assi.date_debut, assi.date_fin, long)
|
return get_justifs_from_date(assi.etudid, assi.date_debut, assi.date_fin, long)
|
||||||
@ -722,8 +459,7 @@ def get_justifs_from_date(
|
|||||||
Defaults to False.
|
Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[int | dict]: La liste des justificatifs (par défaut uniquement
|
list[int | dict]: La liste des justificatifs (par défaut uniquement les identifiants, sinon les Dict si long est vrai)
|
||||||
les identifiants, sinon les dict si long est vrai)
|
|
||||||
"""
|
"""
|
||||||
# On récupère les justificatifs d'un étudiant couvrant la période donnée
|
# On récupère les justificatifs d'un étudiant couvrant la période donnée
|
||||||
justifs: Query = Justificatif.query.filter(
|
justifs: Query = Justificatif.query.filter(
|
||||||
@ -736,20 +472,16 @@ def get_justifs_from_date(
|
|||||||
if valid:
|
if valid:
|
||||||
justifs = justifs.filter(Justificatif.etat == EtatJustificatif.VALIDE)
|
justifs = justifs.filter(Justificatif.etat == EtatJustificatif.VALIDE)
|
||||||
|
|
||||||
# On renvoie la liste des id des justificatifs si long est Faux,
|
# On renvoie la liste des id des justificatifs si long est Faux, sinon on renvoie les dicts des justificatifs
|
||||||
# sinon on renvoie les dicts des justificatifs
|
return [j.justif_id if not long else j.to_dict(True) for j in justifs]
|
||||||
if long:
|
|
||||||
return [j.to_dict(True) for j in justifs]
|
|
||||||
return [j.justif_id for j in justifs]
|
|
||||||
|
|
||||||
|
|
||||||
def get_formsemestre_from_data(data: dict[str, datetime | int]) -> FormSemestre:
|
def get_formsemestre_from_data(data: dict[str, datetime | int]) -> FormSemestre:
|
||||||
"""
|
"""
|
||||||
get_formsemestre_from_data récupère un formsemestre en fonction des données passées
|
get_formsemestre_from_data récupère un formsemestre en fonction des données passées
|
||||||
Si l'étudiant est inscrit à plusieurs formsemestre, prend le premier.
|
|
||||||
Args:
|
Args:
|
||||||
data (dict[str, datetime | int]): Une représentation simplifiée d'une
|
data (dict[str, datetime | int]): Une réprésentation simplifiée d'une assiduité ou d'un justificatif
|
||||||
assiduité ou d'un justificatif
|
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"etudid" : int,
|
"etudid" : int,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
||||||
|
@ -9,7 +9,6 @@ from app.models.but_refcomp import ApcNiveau
|
|||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.scodoc import sco_preferences
|
|
||||||
|
|
||||||
|
|
||||||
class ApcValidationRCUE(db.Model):
|
class ApcValidationRCUE(db.Model):
|
||||||
@ -77,12 +76,10 @@ class ApcValidationRCUE(db.Model):
|
|||||||
niveau = self.niveau()
|
niveau = self.niveau()
|
||||||
return niveau.annee if niveau else None
|
return niveau.annee if niveau else None
|
||||||
|
|
||||||
def niveau(self) -> ApcNiveau | None:
|
def niveau(self) -> ApcNiveau:
|
||||||
"""Le niveau de compétence associé à cet RCUE."""
|
"""Le niveau de compétence associé à cet RCUE."""
|
||||||
# Par convention, il est donné par la seconde UE
|
# Par convention, il est donné par la seconde UE
|
||||||
# à défaut (si l'UE a été désacciée entre temps), la première
|
return self.ue2.niveau_competence
|
||||||
# et à défaut, renvoie None
|
|
||||||
return self.ue2.niveau_competence or self.ue1.niveau_competence
|
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"as a dict"
|
"as a dict"
|
||||||
@ -221,7 +218,6 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
|||||||
decisions["descr_decisions_rcue"] = ""
|
decisions["descr_decisions_rcue"] = ""
|
||||||
decisions["descr_decisions_niveaux"] = ""
|
decisions["descr_decisions_niveaux"] = ""
|
||||||
# --- Année: prend la validation pour l'année scolaire et l'ordre de ce semestre
|
# --- Année: prend la validation pour l'année scolaire et l'ordre de ce semestre
|
||||||
if sco_preferences.get_preference("bul_but_code_annuel", formsemestre.id):
|
|
||||||
annee_but = (formsemestre.semestre_id + 1) // 2
|
annee_but = (formsemestre.semestre_id + 1) // 2
|
||||||
validation = ApcValidationAnnee.query.filter_by(
|
validation = ApcValidationAnnee.query.filter_by(
|
||||||
etudid=etud.id,
|
etudid=etud.id,
|
||||||
@ -233,6 +229,4 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
|||||||
decisions["decision_annee"] = validation.to_dict_bul()
|
decisions["decision_annee"] = validation.to_dict_bul()
|
||||||
else:
|
else:
|
||||||
decisions["decision_annee"] = None
|
decisions["decision_annee"] = None
|
||||||
else:
|
|
||||||
decisions["decision_annee"] = None
|
|
||||||
return decisions
|
return decisions
|
||||||
|
@ -95,7 +95,6 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
"month_debut_annee_scolaire": int,
|
"month_debut_annee_scolaire": int,
|
||||||
"month_debut_periode2": int,
|
"month_debut_periode2": int,
|
||||||
"disable_bul_pdf": bool,
|
"disable_bul_pdf": bool,
|
||||||
"user_require_email_institutionnel": bool,
|
|
||||||
# CAS
|
# CAS
|
||||||
"cas_enable": bool,
|
"cas_enable": bool,
|
||||||
"cas_server": str,
|
"cas_server": str,
|
||||||
@ -232,26 +231,12 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
cfg = ScoDocSiteConfig.query.filter_by(name="cas_enable").first()
|
cfg = ScoDocSiteConfig.query.filter_by(name="cas_enable").first()
|
||||||
return cfg is not None and cfg.value
|
return cfg is not None and cfg.value
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def is_cas_forced(cls) -> bool:
|
|
||||||
"""True si CAS forcé"""
|
|
||||||
cfg = ScoDocSiteConfig.query.filter_by(name="cas_force").first()
|
|
||||||
return cfg is not None and cfg.value
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_entreprises_enabled(cls) -> bool:
|
def is_entreprises_enabled(cls) -> bool:
|
||||||
"""True si on doit activer le module entreprise"""
|
"""True si on doit activer le module entreprise"""
|
||||||
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
||||||
return cfg is not None and cfg.value
|
return cfg is not None and cfg.value
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def is_user_require_email_institutionnel_enabled(cls) -> bool:
|
|
||||||
"""True si impose saisie email_institutionnel"""
|
|
||||||
cfg = ScoDocSiteConfig.query.filter_by(
|
|
||||||
name="user_require_email_institutionnel"
|
|
||||||
).first()
|
|
||||||
return cfg is not None and cfg.value
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_bul_pdf_disabled(cls) -> bool:
|
def is_bul_pdf_disabled(cls) -> bool:
|
||||||
"""True si on interdit les exports PDF des bulltins"""
|
"""True si on interdit les exports PDF des bulltins"""
|
||||||
@ -259,14 +244,36 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
return cfg is not None and cfg.value
|
return cfg is not None and cfg.value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def enable_entreprises(cls, enabled: bool = True) -> bool:
|
def enable_entreprises(cls, enabled=True) -> bool:
|
||||||
"""Active (ou déactive) le module entreprises. True si changement."""
|
"""Active (ou déactive) le module entreprises. True si changement."""
|
||||||
return cls.set("enable_entreprises", "on" if enabled else "")
|
if enabled != ScoDocSiteConfig.is_entreprises_enabled():
|
||||||
|
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
||||||
|
if cfg is None:
|
||||||
|
cfg = ScoDocSiteConfig(
|
||||||
|
name="enable_entreprises", value="on" if enabled else ""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cfg.value = "on" if enabled else ""
|
||||||
|
db.session.add(cfg)
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def disable_bul_pdf(cls, enabled=True) -> bool:
|
def disable_bul_pdf(cls, enabled=True) -> bool:
|
||||||
"""Interdit (ou autorise) les exports PDF. True si changement."""
|
"""Interedit (ou autorise) les exports PDF. True si changement."""
|
||||||
return cls.set("disable_bul_pdf", "on" if enabled else "")
|
if enabled != ScoDocSiteConfig.is_bul_pdf_disabled():
|
||||||
|
cfg = ScoDocSiteConfig.query.filter_by(name="disable_bul_pdf").first()
|
||||||
|
if cfg is None:
|
||||||
|
cfg = ScoDocSiteConfig(
|
||||||
|
name="disable_bul_pdf", value="on" if enabled else ""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cfg.value = "on" if enabled else ""
|
||||||
|
db.session.add(cfg)
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, name: str, default: str = "") -> str:
|
def get(cls, name: str, default: str = "") -> str:
|
||||||
@ -285,10 +292,9 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
if cfg is None:
|
if cfg is None:
|
||||||
cfg = ScoDocSiteConfig(name=name, value=value_str)
|
cfg = ScoDocSiteConfig(name=name, value=value_str)
|
||||||
else:
|
else:
|
||||||
cfg.value = value_str
|
cfg.value = str(value or "")
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f"""ScoDocSiteConfig: recording {cfg.name}='{cfg.value[:32]}{
|
f"""ScoDocSiteConfig: recording {cfg.name}='{cfg.value[:32]}...'"""
|
||||||
'...' if len(cfg.value)>32 else ''}'"""
|
|
||||||
)
|
)
|
||||||
db.session.add(cfg)
|
db.session.add(cfg)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -297,7 +303,7 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_int_field(cls, name: str, default=None) -> int:
|
def _get_int_field(cls, name: str, default=None) -> int:
|
||||||
"""Valeur d'un champ integer"""
|
"""Valeur d'un champs integer"""
|
||||||
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
||||||
if (cfg is None) or cfg.value is None:
|
if (cfg is None) or cfg.value is None:
|
||||||
return default
|
return default
|
||||||
@ -311,7 +317,7 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
default=None,
|
default=None,
|
||||||
range_values: tuple = (),
|
range_values: tuple = (),
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Set champ integer. True si changement."""
|
"""Set champs integer. True si changement."""
|
||||||
if value != cls._get_int_field(name, default=default):
|
if value != cls._get_int_field(name, default=default):
|
||||||
if not isinstance(value, int) or (
|
if not isinstance(value, int) or (
|
||||||
range_values and (value < range_values[0]) or (value > range_values[1])
|
range_values and (value < range_values[0]) or (value > range_values[1])
|
||||||
|
@ -15,15 +15,14 @@ from sqlalchemy import desc, text
|
|||||||
|
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app import models
|
from app import models
|
||||||
from app.models.departements import Departement
|
|
||||||
from app.models.scolar_event import ScolarEvent
|
|
||||||
from app.scodoc import notesdb as ndb
|
from app.scodoc import notesdb as ndb
|
||||||
from app.scodoc.sco_bac import Baccalaureat
|
from app.scodoc.sco_bac import Baccalaureat
|
||||||
from app.scodoc.sco_exceptions import ScoGenError, ScoInvalidParamError, ScoValueError
|
from app.scodoc.sco_exceptions import ScoInvalidParamError, ScoValueError
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class Identite(models.ScoDocModel):
|
class Identite(db.Model, models.ScoDocModel):
|
||||||
"""étudiant"""
|
"""étudiant"""
|
||||||
|
|
||||||
__tablename__ = "identite"
|
__tablename__ = "identite"
|
||||||
@ -101,12 +100,7 @@ class Identite(models.ScoDocModel):
|
|||||||
adresses = db.relationship(
|
adresses = db.relationship(
|
||||||
"Adresse", back_populates="etud", cascade="all,delete", lazy="dynamic"
|
"Adresse", back_populates="etud", cascade="all,delete", lazy="dynamic"
|
||||||
)
|
)
|
||||||
annotations = db.relationship(
|
|
||||||
"EtudAnnotation",
|
|
||||||
backref="etudiant",
|
|
||||||
cascade="all, delete-orphan",
|
|
||||||
lazy="dynamic",
|
|
||||||
)
|
|
||||||
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
||||||
#
|
#
|
||||||
dispense_ues = db.relationship(
|
dispense_ues = db.relationship(
|
||||||
@ -124,9 +118,6 @@ class Identite(models.ScoDocModel):
|
|||||||
"Justificatif", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
|
"Justificatif", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Champs "protégés" par ViewEtudData (RGPD)
|
|
||||||
protected_attrs = {"boursier"}
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
||||||
@ -179,13 +170,9 @@ class Identite(models.ScoDocModel):
|
|||||||
|
|
||||||
def html_link_fiche(self) -> str:
|
def html_link_fiche(self) -> str:
|
||||||
"lien vers la fiche"
|
"lien vers la fiche"
|
||||||
return f"""<a class="etudlink" href="{self.url_fiche()}">{self.nomprenom}</a>"""
|
return f"""<a class="stdlink" href="{
|
||||||
|
url_for("scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id)
|
||||||
def url_fiche(self) -> str:
|
}">{self.nomprenom}</a>"""
|
||||||
"url de la fiche étudiant"
|
|
||||||
return url_for(
|
|
||||||
"scolar.fiche_etud", scodoc_dept=self.departement.acronym, etudid=self.id
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_request(cls, etudid=None, code_nip=None) -> "Identite":
|
def from_request(cls, etudid=None, code_nip=None) -> "Identite":
|
||||||
@ -213,48 +200,19 @@ class Identite(models.ScoDocModel):
|
|||||||
return cls.create_from_dict(args)
|
return cls.create_from_dict(args)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_from_dict(cls, args) -> "Identite":
|
def create_from_dict(cls, data) -> "Identite":
|
||||||
"""Crée un étudiant à partir d'un dict, avec admission et adresse vides.
|
"""Crée un étudiant à partir d'un dict, avec admission et adresse vides.
|
||||||
If required dept_id or dept are not specified, set it to the current dept.
|
|
||||||
args: dict with args in application.
|
|
||||||
Les clés adresses et admission ne SONT PAS utilisées.
|
|
||||||
(added to session but not flushed nor commited)
|
(added to session but not flushed nor commited)
|
||||||
"""
|
"""
|
||||||
if not "dept_id" in args:
|
etud: Identite = super(cls, cls).create_from_dict(data)
|
||||||
if "dept" in args:
|
if (data.get("admission_id", None) is None) and (
|
||||||
departement = Departement.query.filter_by(acronym=args["dept"]).first()
|
data.get("admission", None) is None
|
||||||
if departement:
|
):
|
||||||
args["dept_id"] = departement.id
|
|
||||||
if not "dept_id" in args:
|
|
||||||
args["dept_id"] = g.scodoc_dept_id
|
|
||||||
etud: Identite = super().create_from_dict(args)
|
|
||||||
if args.get("admission_id", None) is None:
|
|
||||||
etud.admission = Admission()
|
etud.admission = Admission()
|
||||||
etud.adresses.append(Adresse(typeadresse="domicile"))
|
etud.adresses.append(Adresse(typeadresse="domicile"))
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
event = ScolarEvent(etud=etud, event_type="CREATION")
|
|
||||||
db.session.add(event)
|
|
||||||
log(f"Identite.create {etud}")
|
|
||||||
return etud
|
return etud
|
||||||
|
|
||||||
def from_dict(self, args, **kwargs) -> bool:
|
|
||||||
"""Check arguments, then modify.
|
|
||||||
Add to session but don't commit.
|
|
||||||
True if modification.
|
|
||||||
"""
|
|
||||||
check_etud_duplicate_code(args, "code_nip")
|
|
||||||
check_etud_duplicate_code(args, "code_ine")
|
|
||||||
return super().from_dict(args, **kwargs)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
|
|
||||||
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded."""
|
|
||||||
return super().filter_model_attributes(
|
|
||||||
data,
|
|
||||||
excluded=(excluded or set()) | {"adresses", "admission", "departement"},
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def civilite_str(self) -> str:
|
def civilite_str(self) -> str:
|
||||||
"""returns civilité usuelle: 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
"""returns civilité usuelle: 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
||||||
@ -301,13 +259,14 @@ class Identite(models.ScoDocModel):
|
|||||||
def nomprenom(self, reverse=False) -> str:
|
def nomprenom(self, reverse=False) -> str:
|
||||||
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
|
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
|
||||||
Si reverse, "Dupont Pierre", sans civilité.
|
Si reverse, "Dupont Pierre", sans civilité.
|
||||||
Prend l'identité courant et non celle de l'état civile si elles diffèrent.
|
|
||||||
"""
|
"""
|
||||||
nom = self.nom_usuel or self.nom
|
nom = self.nom_usuel or self.nom
|
||||||
prenom = self.prenom_str
|
prenom = self.prenom_str
|
||||||
if reverse:
|
if reverse:
|
||||||
return f"{nom} {prenom}".strip()
|
fields = (nom, prenom)
|
||||||
return f"{self.civilite_str} {prenom} {nom}".strip()
|
else:
|
||||||
|
fields = (self.civilite_str, prenom, nom)
|
||||||
|
return " ".join([x for x in fields if x])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def prenom_str(self):
|
def prenom_str(self):
|
||||||
@ -323,10 +282,12 @@ class Identite(models.ScoDocModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def etat_civil(self) -> str:
|
def etat_civil(self) -> str:
|
||||||
"M. PRÉNOM NOM, utilisant les données état civil si présentes, usuelles sinon."
|
"M. Prénom NOM, utilisant les données état civil si présentes, usuelles sinon."
|
||||||
return f"""{self.civilite_etat_civil_str} {
|
if self.prenom_etat_civil:
|
||||||
self.prenom_etat_civil or self.prenom or ''} {
|
civ = {"M": "M.", "F": "Mme", "X": ""}[self.civilite_etat_civil]
|
||||||
self.nom or ''}""".strip()
|
return f"{civ} {self.prenom_etat_civil} {self.nom}"
|
||||||
|
else:
|
||||||
|
return self.nomprenom
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def nom_short(self):
|
def nom_short(self):
|
||||||
@ -336,8 +297,6 @@ class Identite(models.ScoDocModel):
|
|||||||
@cached_property
|
@cached_property
|
||||||
def sort_key(self) -> tuple:
|
def sort_key(self) -> tuple:
|
||||||
"clé pour tris par ordre alphabétique"
|
"clé pour tris par ordre alphabétique"
|
||||||
# Note: scodoc7 utilisait sco_etud.etud_sort_key, à mettre à jour
|
|
||||||
# si on modifie cette méthode.
|
|
||||||
return (
|
return (
|
||||||
scu.sanitize_string(
|
scu.sanitize_string(
|
||||||
self.nom_usuel or self.nom or "", remove_spaces=False
|
self.nom_usuel or self.nom or "", remove_spaces=False
|
||||||
@ -359,45 +318,11 @@ class Identite(models.ScoDocModel):
|
|||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_modimpls_by_formsemestre(
|
|
||||||
self, annee_scolaire: int
|
|
||||||
) -> dict[int, list["ModuleImpl"]]:
|
|
||||||
"""Pour chaque semestre de l'année indiquée dans lequel l'étudiant
|
|
||||||
est inscrit à des moduleimpls, liste ceux ci.
|
|
||||||
{ formsemestre_id : [ modimpl, ... ] }
|
|
||||||
annee_scolaire est un nombre: eg 2023
|
|
||||||
"""
|
|
||||||
date_debut_annee = scu.date_debut_annee_scolaire(annee_scolaire)
|
|
||||||
date_fin_annee = scu.date_fin_annee_scolaire(annee_scolaire)
|
|
||||||
modimpls = (
|
|
||||||
ModuleImpl.query.join(ModuleImplInscription)
|
|
||||||
.join(FormSemestre)
|
|
||||||
.filter(
|
|
||||||
(FormSemestre.date_debut <= date_fin_annee)
|
|
||||||
& (FormSemestre.date_fin >= date_debut_annee)
|
|
||||||
)
|
|
||||||
.join(Identite)
|
|
||||||
.filter_by(id=self.id)
|
|
||||||
)
|
|
||||||
# Tri, par semestre puis par module, suivant le type de formation:
|
|
||||||
formsemestres = sorted(
|
|
||||||
{m.formsemestre for m in modimpls}, key=lambda s: s.sort_key()
|
|
||||||
)
|
|
||||||
modimpls_by_formsemestre = {}
|
|
||||||
for formsemestre in formsemestres:
|
|
||||||
modimpls_sem = [m for m in modimpls if m.formsemestre_id == formsemestre.id]
|
|
||||||
if formsemestre.formation.is_apc():
|
|
||||||
modimpls_sem.sort(key=lambda m: m.module.sort_key_apc())
|
|
||||||
else:
|
|
||||||
modimpls_sem.sort(
|
|
||||||
key=lambda m: (m.module.ue.numero or 0, m.module.numero or 0)
|
|
||||||
)
|
|
||||||
modimpls_by_formsemestre[formsemestre.id] = modimpls_sem
|
|
||||||
return modimpls_by_formsemestre
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def convert_dict_fields(cls, args: dict) -> dict:
|
def convert_dict_fields(cls, args: dict) -> dict:
|
||||||
"""Convert fields in the given dict. No other side effect.
|
"""Convert fields in the given dict. No other side effect.
|
||||||
|
If required dept_id is not specified, set it to the current dept.
|
||||||
|
args: dict with args in application.
|
||||||
returns: dict to store in model's db.
|
returns: dict to store in model's db.
|
||||||
"""
|
"""
|
||||||
# Les champs qui sont toujours stockés en majuscules:
|
# Les champs qui sont toujours stockés en majuscules:
|
||||||
@ -416,6 +341,8 @@ class Identite(models.ScoDocModel):
|
|||||||
"code_ine",
|
"code_ine",
|
||||||
}
|
}
|
||||||
args_dict = {}
|
args_dict = {}
|
||||||
|
if not "dept_id" in args:
|
||||||
|
args["dept_id"] = g.scodoc_dept_id
|
||||||
for key, value in args.items():
|
for key, value in args.items():
|
||||||
if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
|
if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
|
||||||
# compat scodoc7 (mauvaise idée de l'époque)
|
# compat scodoc7 (mauvaise idée de l'époque)
|
||||||
@ -428,14 +355,14 @@ class Identite(models.ScoDocModel):
|
|||||||
elif key == "civilite_etat_civil":
|
elif key == "civilite_etat_civil":
|
||||||
value = input_civilite_etat_civil(value)
|
value = input_civilite_etat_civil(value)
|
||||||
elif key == "boursier":
|
elif key == "boursier":
|
||||||
value = scu.to_bool(value)
|
value = bool(value)
|
||||||
elif key == "date_naissance":
|
elif key == "date_naissance":
|
||||||
value = ndb.DateDMYtoISO(value)
|
value = ndb.DateDMYtoISO(value)
|
||||||
args_dict[key] = value
|
args_dict[key] = value
|
||||||
return args_dict
|
return args_dict
|
||||||
|
|
||||||
def to_dict_short(self) -> dict:
|
def to_dict_short(self) -> dict:
|
||||||
"""Les champs essentiels (aucune donnée perso protégée)"""
|
"""Les champs essentiels"""
|
||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"civilite": self.civilite,
|
"civilite": self.civilite,
|
||||||
@ -450,11 +377,9 @@ class Identite(models.ScoDocModel):
|
|||||||
"prenom_etat_civil": self.prenom_etat_civil,
|
"prenom_etat_civil": self.prenom_etat_civil,
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_dict_scodoc7(self, restrict=False, with_inscriptions=False) -> dict:
|
def to_dict_scodoc7(self) -> dict:
|
||||||
"""Représentation dictionnaire,
|
"""Représentation dictionnaire,
|
||||||
compatible ScoDoc7 mais sans infos admission.
|
compatible ScoDoc7 mais sans infos admission
|
||||||
Si restrict, cache les infos "personnelles" si pas permission ViewEtudData
|
|
||||||
Si with_inscriptions, inclut les champs "inscription"
|
|
||||||
"""
|
"""
|
||||||
e_dict = self.__dict__.copy() # dict(self.__dict__)
|
e_dict = self.__dict__.copy() # dict(self.__dict__)
|
||||||
e_dict.pop("_sa_instance_state", None)
|
e_dict.pop("_sa_instance_state", None)
|
||||||
@ -465,9 +390,7 @@ class Identite(models.ScoDocModel):
|
|||||||
e_dict["nomprenom"] = self.nomprenom
|
e_dict["nomprenom"] = self.nomprenom
|
||||||
adresse = self.adresses.first()
|
adresse = self.adresses.first()
|
||||||
if adresse:
|
if adresse:
|
||||||
e_dict.update(adresse.to_dict(restrict=restrict))
|
e_dict.update(adresse.to_dict())
|
||||||
if with_inscriptions:
|
|
||||||
e_dict.update(self.inscription_descr())
|
|
||||||
return {k: v or "" for k, v in e_dict.items()} # convert_null_outputs_to_empty
|
return {k: v or "" for k, v in e_dict.items()} # convert_null_outputs_to_empty
|
||||||
|
|
||||||
def to_dict_bul(self, include_urls=True):
|
def to_dict_bul(self, include_urls=True):
|
||||||
@ -482,9 +405,9 @@ class Identite(models.ScoDocModel):
|
|||||||
"civilite": self.civilite,
|
"civilite": self.civilite,
|
||||||
"code_ine": self.code_ine or "",
|
"code_ine": self.code_ine or "",
|
||||||
"code_nip": self.code_nip or "",
|
"code_nip": self.code_nip or "",
|
||||||
"date_naissance": (
|
"date_naissance": self.date_naissance.strftime("%d/%m/%Y")
|
||||||
self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""
|
if self.date_naissance
|
||||||
),
|
else "",
|
||||||
"dept_acronym": self.departement.acronym,
|
"dept_acronym": self.departement.acronym,
|
||||||
"dept_id": self.dept_id,
|
"dept_id": self.dept_id,
|
||||||
"dept_naissance": self.dept_naissance or "",
|
"dept_naissance": self.dept_naissance or "",
|
||||||
@ -502,7 +425,7 @@ class Identite(models.ScoDocModel):
|
|||||||
if include_urls and has_request_context():
|
if include_urls and has_request_context():
|
||||||
# test request context so we can use this func in tests under the flask shell
|
# test request context so we can use this func in tests under the flask shell
|
||||||
d["fiche_url"] = url_for(
|
d["fiche_url"] = url_for(
|
||||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=self.id
|
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id
|
||||||
)
|
)
|
||||||
d["photo_url"] = sco_photos.get_etud_photo_url(self.id)
|
d["photo_url"] = sco_photos.get_etud_photo_url(self.id)
|
||||||
adresse = self.adresses.first()
|
adresse = self.adresses.first()
|
||||||
@ -511,33 +434,16 @@ class Identite(models.ScoDocModel):
|
|||||||
d["id"] = self.id # a été écrasé par l'id de adresse
|
d["id"] = self.id # a été écrasé par l'id de adresse
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def to_dict_api(self, restrict=False, with_annotations=False) -> dict:
|
def to_dict_api(self) -> dict:
|
||||||
"""Représentation dictionnaire pour export API, avec adresses et admission.
|
"""Représentation dictionnaire pour export API, avec adresses et admission."""
|
||||||
Si restrict, supprime les infos "personnelles" (boursier)
|
|
||||||
"""
|
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
e.pop("_sa_instance_state", None)
|
e.pop("_sa_instance_state", None)
|
||||||
admission = self.admission
|
admission = self.admission
|
||||||
e["admission"] = admission.to_dict() if admission is not None else None
|
e["admission"] = admission.to_dict() if admission is not None else None
|
||||||
e["adresses"] = [adr.to_dict(restrict=restrict) for adr in self.adresses]
|
e["adresses"] = [adr.to_dict() for adr in self.adresses]
|
||||||
e["dept_acronym"] = self.departement.acronym
|
e["dept_acronym"] = self.departement.acronym
|
||||||
e.pop("departement", None)
|
e.pop("departement", None)
|
||||||
e["sort_key"] = self.sort_key
|
e["sort_key"] = self.sort_key
|
||||||
if with_annotations:
|
|
||||||
e["annotations"] = (
|
|
||||||
[
|
|
||||||
annot.to_dict()
|
|
||||||
for annot in EtudAnnotation.query.filter_by(
|
|
||||||
etudid=self.id
|
|
||||||
).order_by(desc(EtudAnnotation.date))
|
|
||||||
]
|
|
||||||
if not restrict
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
if restrict:
|
|
||||||
# Met à None les attributs protégés:
|
|
||||||
for attr in self.protected_attrs:
|
|
||||||
e[attr] = None
|
|
||||||
return e
|
return e
|
||||||
|
|
||||||
def inscriptions(self) -> list["FormSemestreInscription"]:
|
def inscriptions(self) -> list["FormSemestreInscription"]:
|
||||||
@ -593,9 +499,7 @@ class Identite(models.ScoDocModel):
|
|||||||
return r[0] if r else None
|
return r[0] if r else None
|
||||||
|
|
||||||
def inscription_descr(self) -> dict:
|
def inscription_descr(self) -> dict:
|
||||||
"""Description de l'état d'inscription
|
"""Description de l'état d'inscription"""
|
||||||
avec champs compatibles templates ScoDoc7
|
|
||||||
"""
|
|
||||||
inscription_courante = self.inscription_courante()
|
inscription_courante = self.inscription_courante()
|
||||||
if inscription_courante:
|
if inscription_courante:
|
||||||
titre_sem = inscription_courante.formsemestre.titre_mois()
|
titre_sem = inscription_courante.formsemestre.titre_mois()
|
||||||
@ -606,7 +510,7 @@ class Identite(models.ScoDocModel):
|
|||||||
else:
|
else:
|
||||||
inscr_txt = "Inscrit en"
|
inscr_txt = "Inscrit en"
|
||||||
|
|
||||||
result = {
|
return {
|
||||||
"etat_in_cursem": inscription_courante.etat,
|
"etat_in_cursem": inscription_courante.etat,
|
||||||
"inscription_courante": inscription_courante,
|
"inscription_courante": inscription_courante,
|
||||||
"inscription": titre_sem,
|
"inscription": titre_sem,
|
||||||
@ -629,20 +533,15 @@ class Identite(models.ScoDocModel):
|
|||||||
inscription = "ancien"
|
inscription = "ancien"
|
||||||
situation = "ancien élève"
|
situation = "ancien élève"
|
||||||
else:
|
else:
|
||||||
inscription = "non inscrit"
|
inscription = ("non inscrit",)
|
||||||
situation = inscription
|
situation = inscription
|
||||||
result = {
|
return {
|
||||||
"etat_in_cursem": "?",
|
"etat_in_cursem": "?",
|
||||||
"inscription_courante": None,
|
"inscription_courante": None,
|
||||||
"inscription": inscription,
|
"inscription": inscription,
|
||||||
"inscription_str": inscription,
|
"inscription_str": inscription,
|
||||||
"situation": situation,
|
"situation": situation,
|
||||||
}
|
}
|
||||||
# aliases pour compat templates ScoDoc7
|
|
||||||
result["etatincursem"] = result["etat_in_cursem"]
|
|
||||||
result["inscriptionstr"] = result["inscription_str"]
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def inscription_etat(self, formsemestre_id: int) -> str:
|
def inscription_etat(self, formsemestre_id: int) -> str:
|
||||||
"""État de l'inscription de cet étudiant au semestre:
|
"""État de l'inscription de cet étudiant au semestre:
|
||||||
@ -763,58 +662,6 @@ class Identite(models.ScoDocModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_etud_duplicate_code(args, code_name, edit=True):
|
|
||||||
"""Vérifie que le code n'est pas dupliqué.
|
|
||||||
Raises ScoGenError si problème.
|
|
||||||
"""
|
|
||||||
etudid = args.get("etudid", None)
|
|
||||||
if not args.get(code_name, None):
|
|
||||||
return
|
|
||||||
etuds = Identite.query.filter_by(
|
|
||||||
**{code_name: str(args[code_name]), "dept_id": g.scodoc_dept_id}
|
|
||||||
).all()
|
|
||||||
duplicate = False
|
|
||||||
if edit:
|
|
||||||
duplicate = (len(etuds) > 1) or ((len(etuds) == 1) and etuds[0].id != etudid)
|
|
||||||
else:
|
|
||||||
duplicate = len(etuds) > 0
|
|
||||||
if duplicate:
|
|
||||||
listh = [] # liste des doubles
|
|
||||||
for etud in etuds:
|
|
||||||
listh.append(f"Autre étudiant: {etud.html_link_fiche()}")
|
|
||||||
if etudid:
|
|
||||||
submit_label = "retour à la fiche étudiant"
|
|
||||||
dest_endpoint = "scolar.fiche_etud"
|
|
||||||
parameters = {"etudid": etudid}
|
|
||||||
else:
|
|
||||||
if "tf_submitted" in args:
|
|
||||||
del args["tf_submitted"]
|
|
||||||
submit_label = "Continuer"
|
|
||||||
dest_endpoint = "scolar.etudident_create_form"
|
|
||||||
parameters = args
|
|
||||||
else:
|
|
||||||
submit_label = "Annuler"
|
|
||||||
dest_endpoint = "notes.index_html"
|
|
||||||
parameters = {}
|
|
||||||
|
|
||||||
err_page = f"""<h3><h3>Code étudiant ({code_name}) dupliqué !</h3>
|
|
||||||
<p class="help">Le {code_name} {args[code_name]} est déjà utilisé: un seul étudiant peut avoir
|
|
||||||
ce code. Vérifier votre valeur ou supprimer l'autre étudiant avec cette valeur.
|
|
||||||
</p>
|
|
||||||
<ul><li>
|
|
||||||
{ '</li><li>'.join(listh) }
|
|
||||||
</li></ul>
|
|
||||||
<p>
|
|
||||||
<a href="{ url_for(dest_endpoint, scodoc_dept=g.scodoc_dept, **parameters) }
|
|
||||||
">{submit_label}</a>
|
|
||||||
</p>
|
|
||||||
"""
|
|
||||||
|
|
||||||
log(f"*** error: code {code_name} duplique: {args[code_name]}")
|
|
||||||
|
|
||||||
raise ScoGenError(err_page)
|
|
||||||
|
|
||||||
|
|
||||||
def make_etud_args(
|
def make_etud_args(
|
||||||
etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True
|
etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True
|
||||||
) -> dict:
|
) -> dict:
|
||||||
@ -895,7 +742,7 @@ def pivot_year(y) -> int:
|
|||||||
return y
|
return y
|
||||||
|
|
||||||
|
|
||||||
class Adresse(models.ScoDocModel):
|
class Adresse(db.Model, models.ScoDocModel):
|
||||||
"""Adresse d'un étudiant
|
"""Adresse d'un étudiant
|
||||||
(le modèle permet plusieurs adresses, mais l'UI n'en gère qu'une seule)
|
(le modèle permet plusieurs adresses, mais l'UI n'en gère qu'une seule)
|
||||||
"""
|
"""
|
||||||
@ -922,29 +769,16 @@ class Adresse(models.ScoDocModel):
|
|||||||
)
|
)
|
||||||
description = db.Column(db.Text)
|
description = db.Column(db.Text)
|
||||||
|
|
||||||
# Champs "protégés" par ViewEtudData (RGPD)
|
def to_dict(self, convert_nulls_to_str=False):
|
||||||
protected_attrs = {
|
"""Représentation dictionnaire,"""
|
||||||
"emailperso",
|
|
||||||
"domicile",
|
|
||||||
"codepostaldomicile",
|
|
||||||
"villedomicile",
|
|
||||||
"telephone",
|
|
||||||
"telephonemobile",
|
|
||||||
"fax",
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_dict(self, convert_nulls_to_str=False, restrict=False):
|
|
||||||
"""Représentation dictionnaire. Si restrict, filtre les champs protégés (RGPD)."""
|
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
e.pop("_sa_instance_state", None)
|
e.pop("_sa_instance_state", None)
|
||||||
if convert_nulls_to_str:
|
if convert_nulls_to_str:
|
||||||
e = {k: v or "" for k, v in e.items()}
|
return {k: e[k] or "" for k in e}
|
||||||
if restrict:
|
|
||||||
e = {k: v for (k, v) in e.items() if k not in self.protected_attrs}
|
|
||||||
return e
|
return e
|
||||||
|
|
||||||
|
|
||||||
class Admission(models.ScoDocModel):
|
class Admission(db.Model, models.ScoDocModel):
|
||||||
"""Informations liées à l'admission d'un étudiant"""
|
"""Informations liées à l'admission d'un étudiant"""
|
||||||
|
|
||||||
__tablename__ = "admissions"
|
__tablename__ = "admissions"
|
||||||
@ -995,16 +829,12 @@ class Admission(models.ScoDocModel):
|
|||||||
# classement (1..Ngr) par le jury dans le groupe APB
|
# classement (1..Ngr) par le jury dans le groupe APB
|
||||||
apb_classement_gr = db.Column(db.Integer)
|
apb_classement_gr = db.Column(db.Integer)
|
||||||
|
|
||||||
# Tous les champs sont "protégés" par ViewEtudData (RGPD)
|
|
||||||
# sauf:
|
|
||||||
not_protected_attrs = {"bac", "specialite", "anne_bac"}
|
|
||||||
|
|
||||||
def get_bac(self) -> Baccalaureat:
|
def get_bac(self) -> Baccalaureat:
|
||||||
"Le bac. utiliser bac.abbrev() pour avoir une chaine de caractères."
|
"Le bac. utiliser bac.abbrev() pour avoir une chaine de caractères."
|
||||||
return Baccalaureat(self.bac, specialite=self.specialite)
|
return Baccalaureat(self.bac, specialite=self.specialite)
|
||||||
|
|
||||||
def to_dict(self, no_nulls=False, restrict=False):
|
def to_dict(self, no_nulls=False):
|
||||||
"""Représentation dictionnaire. Si restrict, filtre les champs protégés (RGPD)."""
|
"""Représentation dictionnaire,"""
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
if no_nulls:
|
if no_nulls:
|
||||||
@ -1019,8 +849,6 @@ class Admission(models.ScoDocModel):
|
|||||||
d[key] = 0
|
d[key] = 0
|
||||||
elif isinstance(col_type, sqlalchemy.Boolean):
|
elif isinstance(col_type, sqlalchemy.Boolean):
|
||||||
d[key] = False
|
d[key] = False
|
||||||
if restrict:
|
|
||||||
d = {k: v for (k, v) in d.items() if k in self.not_protected_attrs}
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -1088,17 +916,6 @@ class EtudAnnotation(db.Model):
|
|||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
etudid = db.Column(db.Integer, db.ForeignKey(Identite.id))
|
etudid = db.Column(db.Integer) # sans contrainte (compat ScoDoc 7))
|
||||||
author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user
|
author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user
|
||||||
comment = db.Column(db.Text)
|
comment = db.Column(db.Text)
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
"""Représentation dictionnaire."""
|
|
||||||
e = dict(self.__dict__)
|
|
||||||
e.pop("_sa_instance_state", None)
|
|
||||||
return e
|
|
||||||
|
|
||||||
|
|
||||||
from app.models.formsemestre import FormSemestre
|
|
||||||
from app.models.modules import Module
|
|
||||||
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
|
||||||
|
@ -5,14 +5,16 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from flask import abort, g, url_for
|
from flask import g, url_for
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.events import ScolarNews
|
from app.models.events import ScolarNews
|
||||||
|
from app.models.moduleimpls import ModuleImpl
|
||||||
from app.models.notes import NotesNotes
|
from app.models.notes import NotesNotes
|
||||||
|
from app.models.ues import UniteEns
|
||||||
|
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||||
@ -65,7 +67,7 @@ class Evaluation(db.Model):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def create(
|
def create(
|
||||||
cls,
|
cls,
|
||||||
moduleimpl: "ModuleImpl" = None,
|
moduleimpl: ModuleImpl = None,
|
||||||
date_debut: datetime.datetime = None,
|
date_debut: datetime.datetime = None,
|
||||||
date_fin: datetime.datetime = None,
|
date_fin: datetime.datetime = None,
|
||||||
description=None,
|
description=None,
|
||||||
@ -112,7 +114,7 @@ class Evaluation(db.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_new_numero(
|
def get_new_numero(
|
||||||
cls, moduleimpl: "ModuleImpl", date_debut: datetime.datetime
|
cls, moduleimpl: ModuleImpl, date_debut: datetime.datetime
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Get a new numero for an evaluation in this moduleimpl
|
"""Get a new numero for an evaluation in this moduleimpl
|
||||||
If necessary, renumber existing evals to make room for a new one.
|
If necessary, renumber existing evals to make room for a new one.
|
||||||
@ -143,7 +145,7 @@ class Evaluation(db.Model):
|
|||||||
"delete evaluation (commit) (check permission)"
|
"delete evaluation (commit) (check permission)"
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
|
|
||||||
modimpl: "ModuleImpl" = self.moduleimpl
|
modimpl: ModuleImpl = self.moduleimpl
|
||||||
if not modimpl.can_edit_evaluation(current_user):
|
if not modimpl.can_edit_evaluation(current_user):
|
||||||
raise AccessDenied(
|
raise AccessDenied(
|
||||||
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
||||||
@ -184,7 +186,7 @@ class Evaluation(db.Model):
|
|||||||
# ScoDoc7 output_formators
|
# ScoDoc7 output_formators
|
||||||
e_dict["evaluation_id"] = self.id
|
e_dict["evaluation_id"] = self.id
|
||||||
e_dict["date_debut"] = self.date_debut.isoformat() if self.date_debut else None
|
e_dict["date_debut"] = self.date_debut.isoformat() if self.date_debut else None
|
||||||
e_dict["date_fin"] = self.date_fin.isoformat() if self.date_fin else None
|
e_dict["date_fin"] = self.date_debut.isoformat() if self.date_fin else None
|
||||||
e_dict["numero"] = self.numero or 0
|
e_dict["numero"] = self.numero or 0
|
||||||
e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
||||||
|
|
||||||
@ -237,29 +239,10 @@ class Evaluation(db.Model):
|
|||||||
check_convert_evaluation_args(self.moduleimpl, data)
|
check_convert_evaluation_args(self.moduleimpl, data)
|
||||||
if data.get("numero") is None:
|
if data.get("numero") is None:
|
||||||
data["numero"] = Evaluation.get_max_numero(self.moduleimpl.id) + 1
|
data["numero"] = Evaluation.get_max_numero(self.moduleimpl.id) + 1
|
||||||
for k in self.__dict__:
|
for k in self.__dict__.keys():
|
||||||
if k != "_sa_instance_state" and k != "id" and k in data:
|
if k != "_sa_instance_state" and k != "id" and k in data:
|
||||||
setattr(self, k, data[k])
|
setattr(self, k, data[k])
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_evaluation(
|
|
||||||
cls, evaluation_id: int | str, dept_id: int = None
|
|
||||||
) -> "Evaluation":
|
|
||||||
"""Evaluation ou 404, cherche uniquement dans le département spécifié ou le courant."""
|
|
||||||
from app.models import FormSemestre, ModuleImpl
|
|
||||||
|
|
||||||
if not isinstance(evaluation_id, int):
|
|
||||||
try:
|
|
||||||
evaluation_id = int(evaluation_id)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
abort(404, "evaluation_id invalide")
|
|
||||||
if g.scodoc_dept:
|
|
||||||
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
|
||||||
query = cls.query.filter_by(id=evaluation_id)
|
|
||||||
if dept_id is not None:
|
|
||||||
query = query.join(ModuleImpl).join(FormSemestre).filter_by(dept_id=dept_id)
|
|
||||||
return query.first_or_404()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_max_numero(cls, moduleimpl_id: int) -> int:
|
def get_max_numero(cls, moduleimpl_id: int) -> int:
|
||||||
"""Return max numero among evaluations in this
|
"""Return max numero among evaluations in this
|
||||||
@ -274,7 +257,7 @@ class Evaluation(db.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def moduleimpl_evaluation_renumber(
|
def moduleimpl_evaluation_renumber(
|
||||||
cls, moduleimpl: "ModuleImpl", only_if_unumbered=False
|
cls, moduleimpl: ModuleImpl, only_if_unumbered=False
|
||||||
):
|
):
|
||||||
"""Renumber evaluations in this moduleimpl, according to their date. (numero=0: oldest one)
|
"""Renumber evaluations in this moduleimpl, according to their date. (numero=0: oldest one)
|
||||||
Needed because previous versions of ScoDoc did not have eval numeros
|
Needed because previous versions of ScoDoc did not have eval numeros
|
||||||
@ -284,9 +267,7 @@ class Evaluation(db.Model):
|
|||||||
evaluations = moduleimpl.evaluations.order_by(
|
evaluations = moduleimpl.evaluations.order_by(
|
||||||
Evaluation.date_debut, Evaluation.numero
|
Evaluation.date_debut, Evaluation.numero
|
||||||
).all()
|
).all()
|
||||||
numeros_distincts = {e.numero for e in evaluations if e.numero is not None}
|
all_numbered = all(e.numero is not None for e in evaluations)
|
||||||
# pas de None, pas de dupliqués
|
|
||||||
all_numbered = len(numeros_distincts) == len(evaluations)
|
|
||||||
if all_numbered and only_if_unumbered:
|
if all_numbered and only_if_unumbered:
|
||||||
return # all ok
|
return # all ok
|
||||||
|
|
||||||
@ -297,7 +278,6 @@ class Evaluation(db.Model):
|
|||||||
db.session.add(e)
|
db.session.add(e)
|
||||||
i += 1
|
i += 1
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
sco_cache.invalidate_formsemestre(moduleimpl.formsemestre_id)
|
|
||||||
|
|
||||||
def descr_heure(self) -> str:
|
def descr_heure(self) -> str:
|
||||||
"Description de la plage horaire pour affichages ('de 13h00 à 14h00')"
|
"Description de la plage horaire pour affichages ('de 13h00 à 14h00')"
|
||||||
@ -414,8 +394,6 @@ class Evaluation(db.Model):
|
|||||||
"""set poids vers les UE (remplace existants)
|
"""set poids vers les UE (remplace existants)
|
||||||
ue_poids_dict = { ue_id : poids }
|
ue_poids_dict = { ue_id : poids }
|
||||||
"""
|
"""
|
||||||
from app.models.ues import UniteEns
|
|
||||||
|
|
||||||
L = []
|
L = []
|
||||||
for ue_id, poids in ue_poids_dict.items():
|
for ue_id, poids in ue_poids_dict.items():
|
||||||
ue = db.session.get(UniteEns, ue_id)
|
ue = db.session.get(UniteEns, ue_id)
|
||||||
@ -449,8 +427,8 @@ class Evaluation(db.Model):
|
|||||||
|
|
||||||
def get_ue_poids_str(self) -> str:
|
def get_ue_poids_str(self) -> str:
|
||||||
"""string describing poids, for excel cells and pdfs
|
"""string describing poids, for excel cells and pdfs
|
||||||
Note: les poids nuls ou non initialisés (poids par défaut),
|
Note: si les poids ne sont pas initialisés (poids par défaut),
|
||||||
ne sont pas affichés.
|
ils ne sont pas affichés.
|
||||||
"""
|
"""
|
||||||
# restreint aux UE du semestre dans lequel est cette évaluation
|
# restreint aux UE du semestre dans lequel est cette évaluation
|
||||||
# au cas où le module ait changé de semestre et qu'il reste des poids
|
# au cas où le module ait changé de semestre et qu'il reste des poids
|
||||||
@ -461,7 +439,7 @@ class Evaluation(db.Model):
|
|||||||
for p in sorted(
|
for p in sorted(
|
||||||
self.ue_poids, key=lambda p: (p.ue.numero or 0, p.ue.acronyme)
|
self.ue_poids, key=lambda p: (p.ue.numero or 0, p.ue.acronyme)
|
||||||
)
|
)
|
||||||
if evaluation_semestre_idx == p.ue.semestre_idx and (p.poids or 0) > 0
|
if evaluation_semestre_idx == p.ue.semestre_idx
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -496,7 +474,7 @@ class EvaluationUEPoids(db.Model):
|
|||||||
backref=db.backref("ue_poids", cascade="all, delete-orphan"),
|
backref=db.backref("ue_poids", cascade="all, delete-orphan"),
|
||||||
)
|
)
|
||||||
ue = db.relationship(
|
ue = db.relationship(
|
||||||
"UniteEns",
|
UniteEns,
|
||||||
backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan"),
|
backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -528,7 +506,7 @@ def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
|
|||||||
return e_dict
|
return e_dict
|
||||||
|
|
||||||
|
|
||||||
def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
|
def check_convert_evaluation_args(moduleimpl: ModuleImpl, data: dict):
|
||||||
"""Check coefficient, dates and duration, raises exception if invalid.
|
"""Check coefficient, dates and duration, raises exception if invalid.
|
||||||
Convert date and time strings to date and time objects.
|
Convert date and time strings to date and time objects.
|
||||||
|
|
||||||
@ -605,10 +583,20 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
|
|||||||
if date_debut and date_fin:
|
if date_debut and date_fin:
|
||||||
duration = data["date_fin"] - data["date_debut"]
|
duration = data["date_fin"] - data["date_debut"]
|
||||||
if duration.total_seconds() < 0 or duration > MAX_EVALUATION_DURATION:
|
if duration.total_seconds() < 0 or duration > MAX_EVALUATION_DURATION:
|
||||||
raise ScoValueError(
|
raise ScoValueError("Heures de l'évaluation incohérentes !")
|
||||||
"Heures de l'évaluation incohérentes !",
|
# # --- heures
|
||||||
dest_url="javascript:history.back();",
|
# heure_debut = data.get("heure_debut", None)
|
||||||
)
|
# if heure_debut and not isinstance(heure_debut, datetime.time):
|
||||||
|
# if date_format == "dmy":
|
||||||
|
# data["heure_debut"] = heure_to_time(heure_debut)
|
||||||
|
# else: # ISO
|
||||||
|
# data["heure_debut"] = datetime.time.fromisoformat(heure_debut)
|
||||||
|
# heure_fin = data.get("heure_fin", None)
|
||||||
|
# if heure_fin and not isinstance(heure_fin, datetime.time):
|
||||||
|
# if date_format == "dmy":
|
||||||
|
# data["heure_fin"] = heure_to_time(heure_fin)
|
||||||
|
# else: # ISO
|
||||||
|
# data["heure_fin"] = datetime.time.fromisoformat(heure_fin)
|
||||||
|
|
||||||
|
|
||||||
def heure_to_time(heure: str) -> datetime.time:
|
def heure_to_time(heure: str) -> datetime.time:
|
||||||
@ -618,6 +606,19 @@ def heure_to_time(heure: str) -> datetime.time:
|
|||||||
return datetime.time(int(h), int(m))
|
return datetime.time(int(h), int(m))
|
||||||
|
|
||||||
|
|
||||||
|
def _time_duration_HhM(heure_debut: str, heure_fin: str) -> int:
|
||||||
|
"""duree (nb entier de minutes) entre deux heures a notre format
|
||||||
|
ie 12h23
|
||||||
|
"""
|
||||||
|
if heure_debut and heure_fin:
|
||||||
|
h0, m0 = [int(x) for x in heure_debut.split("h")]
|
||||||
|
h1, m1 = [int(x) for x in heure_fin.split("h")]
|
||||||
|
d = (h1 - h0) * 60 + (m1 - m0)
|
||||||
|
return d
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _moduleimpl_evaluation_insert_before(
|
def _moduleimpl_evaluation_insert_before(
|
||||||
evaluations: list[Evaluation], next_eval: Evaluation
|
evaluations: list[Evaluation], next_eval: Evaluation
|
||||||
) -> int:
|
) -> int:
|
||||||
|
@ -13,6 +13,7 @@ from app import email
|
|||||||
from app import log
|
from app import log
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
|
from app.models.moduleimpls import ModuleImpl
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
|
||||||
@ -132,7 +133,7 @@ class ScolarNews(db.Model):
|
|||||||
return query.order_by(cls.date.desc()).limit(n).all()
|
return query.order_by(cls.date.desc()).limit(n).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add(cls, typ, obj=None, text="", url=None, max_frequency=600, dept_id=None):
|
def add(cls, typ, obj=None, text="", url=None, max_frequency=600):
|
||||||
"""Enregistre une nouvelle
|
"""Enregistre une nouvelle
|
||||||
Si max_frequency, ne génère pas 2 nouvelles "identiques"
|
Si max_frequency, ne génère pas 2 nouvelles "identiques"
|
||||||
à moins de max_frequency secondes d'intervalle (10 minutes par défaut).
|
à moins de max_frequency secondes d'intervalle (10 minutes par défaut).
|
||||||
@ -140,11 +141,10 @@ class ScolarNews(db.Model):
|
|||||||
même (obj, typ, user).
|
même (obj, typ, user).
|
||||||
La nouvelle enregistrée est aussi envoyée par mail.
|
La nouvelle enregistrée est aussi envoyée par mail.
|
||||||
"""
|
"""
|
||||||
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
|
||||||
if max_frequency:
|
if max_frequency:
|
||||||
last_news = (
|
last_news = (
|
||||||
cls.query.filter_by(
|
cls.query.filter_by(
|
||||||
dept_id=dept_id,
|
dept_id=g.scodoc_dept_id,
|
||||||
authenticated_user=current_user.user_name,
|
authenticated_user=current_user.user_name,
|
||||||
type=typ,
|
type=typ,
|
||||||
object=obj,
|
object=obj,
|
||||||
@ -163,7 +163,7 @@ class ScolarNews(db.Model):
|
|||||||
return
|
return
|
||||||
|
|
||||||
news = ScolarNews(
|
news = ScolarNews(
|
||||||
dept_id=dept_id,
|
dept_id=g.scodoc_dept_id,
|
||||||
authenticated_user=current_user.user_name,
|
authenticated_user=current_user.user_name,
|
||||||
type=typ,
|
type=typ,
|
||||||
object=obj,
|
object=obj,
|
||||||
@ -180,7 +180,6 @@ class ScolarNews(db.Model):
|
|||||||
None si inexistant
|
None si inexistant
|
||||||
"""
|
"""
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.models.moduleimpls import ModuleImpl
|
|
||||||
|
|
||||||
formsemestre_id = None
|
formsemestre_id = None
|
||||||
if self.type == self.NEWS_INSCR:
|
if self.type == self.NEWS_INSCR:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -10,15 +10,13 @@
|
|||||||
|
|
||||||
"""ScoDoc models: formsemestre
|
"""ScoDoc models: formsemestre
|
||||||
"""
|
"""
|
||||||
from collections import defaultdict
|
|
||||||
import datetime
|
import datetime
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from itertools import chain
|
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from flask import abort, flash, g, url_for
|
from flask import flash, g, url_for
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
|
||||||
@ -32,16 +30,11 @@ from app.models.but_refcomp import (
|
|||||||
parcours_formsemestre,
|
parcours_formsemestre,
|
||||||
)
|
)
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
from app.models.departements import Departement
|
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.evaluations import Evaluation
|
from app.models.evaluations import Evaluation
|
||||||
from app.models.formations import Formation
|
from app.models.formations import Formation
|
||||||
from app.models.groups import GroupDescr, Partition
|
from app.models.groups import GroupDescr, Partition
|
||||||
from app.models.moduleimpls import (
|
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
||||||
ModuleImpl,
|
|
||||||
ModuleImplInscription,
|
|
||||||
notes_modules_enseignants,
|
|
||||||
)
|
|
||||||
from app.models.modules import Module
|
from app.models.modules import Module
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.models.validations import ScolarFormSemestreValidation
|
from app.models.validations import ScolarFormSemestreValidation
|
||||||
@ -51,6 +44,8 @@ from app.scodoc.sco_permissions import Permission
|
|||||||
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV, translate_assiduites_metric
|
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV, translate_assiduites_metric
|
||||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||||
|
|
||||||
|
from app.scodoc.sco_utils import translate_assiduites_metric
|
||||||
|
|
||||||
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
||||||
|
|
||||||
|
|
||||||
@ -68,7 +63,7 @@ class FormSemestre(db.Model):
|
|||||||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||||
titre = db.Column(db.Text(), nullable=False)
|
titre = db.Column(db.Text(), nullable=False)
|
||||||
date_debut = db.Column(db.Date(), nullable=False)
|
date_debut = db.Column(db.Date(), nullable=False)
|
||||||
date_fin = db.Column(db.Date(), nullable=False) # jour inclus
|
date_fin = db.Column(db.Date(), nullable=False)
|
||||||
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
|
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
|
||||||
"identifiant emplois du temps (unicité non imposée)"
|
"identifiant emplois du temps (unicité non imposée)"
|
||||||
etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true")
|
etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true")
|
||||||
@ -185,14 +180,9 @@ class FormSemestre(db.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_formsemestre(
|
def get_formsemestre(
|
||||||
cls, formsemestre_id: int | str, dept_id: int = None
|
cls, formsemestre_id: int, dept_id: int = None
|
||||||
) -> "FormSemestre":
|
) -> "FormSemestre":
|
||||||
"""FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant"""
|
""" "FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant"""
|
||||||
if not isinstance(formsemestre_id, int):
|
|
||||||
try:
|
|
||||||
formsemestre_id = int(formsemestre_id)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
abort(404, "formsemestre_id invalide")
|
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
||||||
if dept_id is not None:
|
if dept_id is not None:
|
||||||
@ -279,15 +269,10 @@ class FormSemestre(db.Model):
|
|||||||
return default_partition.groups.first()
|
return default_partition.groups.first()
|
||||||
raise ScoValueError("Le semestre n'a pas de groupe par défaut")
|
raise ScoValueError("Le semestre n'a pas de groupe par défaut")
|
||||||
|
|
||||||
def get_edt_ids(self) -> list[str]:
|
def get_edt_id(self) -> str:
|
||||||
"""Les ids pour l'emploi du temps: à défaut, les codes étape Apogée.
|
"l'id pour l'emploi du temps: à défaut, le 1er code étape Apogée"
|
||||||
Les edt_id de formsemestres ne sont pas normalisés afin de contrôler
|
|
||||||
précisément l'accès au fichier ics.
|
|
||||||
"""
|
|
||||||
return (
|
return (
|
||||||
scu.split_id(self.edt_id)
|
self.edt_id or "" or (self.etapes[0].etape_apo if len(self.etapes) else "")
|
||||||
or [e.etape_apo.strip() for e in self.etapes if e.etape_apo]
|
|
||||||
or []
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_infos_dict(self) -> dict:
|
def get_infos_dict(self) -> dict:
|
||||||
@ -392,80 +377,6 @@ class FormSemestre(db.Model):
|
|||||||
_cache[key] = ues
|
_cache[key] = ues
|
||||||
return ues
|
return ues
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_user_formsemestres_annee_by_dept(
|
|
||||||
cls, user: User
|
|
||||||
) -> tuple[
|
|
||||||
defaultdict[int, list["FormSemestre"]], defaultdict[int, list[ModuleImpl]]
|
|
||||||
]:
|
|
||||||
"""Liste des formsemestres de l'année scolaire
|
|
||||||
dans lesquels user intervient (comme resp., resp. de module ou enseignant),
|
|
||||||
ainsi que la liste des modimpls concernés dans chaque formsemestre
|
|
||||||
Attention: les semestres et modimpls peuvent être de différents départements !
|
|
||||||
Résultat:
|
|
||||||
{ dept_id : [ formsemestre, ... ] },
|
|
||||||
{ formsemestre_id : [ modimpl, ... ]}
|
|
||||||
"""
|
|
||||||
debut_annee_scolaire = scu.date_debut_annee_scolaire()
|
|
||||||
fin_annee_scolaire = scu.date_fin_annee_scolaire()
|
|
||||||
|
|
||||||
query = FormSemestre.query.filter(
|
|
||||||
FormSemestre.date_fin >= debut_annee_scolaire,
|
|
||||||
FormSemestre.date_debut < fin_annee_scolaire,
|
|
||||||
)
|
|
||||||
# responsable ?
|
|
||||||
formsemestres_resp = query.join(notes_formsemestre_responsables).filter_by(
|
|
||||||
responsable_id=user.id
|
|
||||||
)
|
|
||||||
# Responsable d'un modimpl ?
|
|
||||||
modimpls_resp = (
|
|
||||||
ModuleImpl.query.filter_by(responsable_id=user.id)
|
|
||||||
.join(FormSemestre)
|
|
||||||
.filter(
|
|
||||||
FormSemestre.date_fin >= debut_annee_scolaire,
|
|
||||||
FormSemestre.date_debut < fin_annee_scolaire,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# Enseignant dans un modimpl ?
|
|
||||||
modimpls_ens = (
|
|
||||||
ModuleImpl.query.join(notes_modules_enseignants)
|
|
||||||
.filter_by(ens_id=user.id)
|
|
||||||
.join(FormSemestre)
|
|
||||||
.filter(
|
|
||||||
FormSemestre.date_fin >= debut_annee_scolaire,
|
|
||||||
FormSemestre.date_debut < fin_annee_scolaire,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# Liste les modimpls, uniques
|
|
||||||
modimpls = modimpls_resp.all()
|
|
||||||
ids = {modimpl.id for modimpl in modimpls}
|
|
||||||
for modimpl in modimpls_ens:
|
|
||||||
if modimpl.id not in ids:
|
|
||||||
modimpls.append(modimpl)
|
|
||||||
ids.add(modimpl.id)
|
|
||||||
# Liste les formsemestres et modimpls associés
|
|
||||||
modimpls_by_formsemestre = defaultdict(lambda: [])
|
|
||||||
formsemestres = formsemestres_resp.all()
|
|
||||||
ids = {formsemestre.id for formsemestre in formsemestres}
|
|
||||||
for modimpl in chain(modimpls_resp, modimpls_ens):
|
|
||||||
if modimpl.formsemestre_id not in ids:
|
|
||||||
formsemestres.append(modimpl.formsemestre)
|
|
||||||
ids.add(modimpl.formsemestre_id)
|
|
||||||
modimpls_by_formsemestre[modimpl.formsemestre_id].append(modimpl)
|
|
||||||
# Tris et organisation par département
|
|
||||||
formsemestres_by_dept = defaultdict(lambda: [])
|
|
||||||
formsemestres.sort(key=lambda x: (x.departement.acronym,) + x.sort_key())
|
|
||||||
for formsemestre in formsemestres:
|
|
||||||
formsemestres_by_dept[formsemestre.dept_id].append(formsemestre)
|
|
||||||
modimpls = modimpls_by_formsemestre[formsemestre.id]
|
|
||||||
if formsemestre.formation.is_apc():
|
|
||||||
key = lambda x: x.module.sort_key_apc()
|
|
||||||
else:
|
|
||||||
key = lambda x: x.module.sort_key()
|
|
||||||
modimpls.sort(key=key)
|
|
||||||
|
|
||||||
return formsemestres_by_dept, modimpls_by_formsemestre
|
|
||||||
|
|
||||||
def get_evaluations(self) -> list[Evaluation]:
|
def get_evaluations(self) -> list[Evaluation]:
|
||||||
"Liste de toutes les évaluations du semestre, triées par module/numero"
|
"Liste de toutes les évaluations du semestre, triées par module/numero"
|
||||||
return (
|
return (
|
||||||
@ -476,7 +387,7 @@ class FormSemestre(db.Model):
|
|||||||
Module.numero,
|
Module.numero,
|
||||||
Module.code,
|
Module.code,
|
||||||
Evaluation.numero,
|
Evaluation.numero,
|
||||||
Evaluation.date_debut,
|
Evaluation.date_debut.desc(),
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
@ -486,7 +397,6 @@ class FormSemestre(db.Model):
|
|||||||
"""Liste des modimpls du semestre (y compris bonus)
|
"""Liste des modimpls du semestre (y compris bonus)
|
||||||
- triée par type/numéro/code en APC
|
- triée par type/numéro/code en APC
|
||||||
- triée par numéros d'UE/matières/modules pour les formations standard.
|
- triée par numéros d'UE/matières/modules pour les formations standard.
|
||||||
Hors APC, élimine les modules de type ressources et SAEs.
|
|
||||||
"""
|
"""
|
||||||
modimpls = self.modimpls.all()
|
modimpls = self.modimpls.all()
|
||||||
if self.formation.is_apc():
|
if self.formation.is_apc():
|
||||||
@ -498,14 +408,6 @@ class FormSemestre(db.Model):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
modimpls = [
|
|
||||||
mi
|
|
||||||
for mi in modimpls
|
|
||||||
if (
|
|
||||||
mi.module.module_type
|
|
||||||
not in (scu.ModuleType.RESSOURCE, scu.ModuleType.SAE)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
modimpls.sort(
|
modimpls.sort(
|
||||||
key=lambda m: (
|
key=lambda m: (
|
||||||
m.module.ue.numero or 0,
|
m.module.ue.numero or 0,
|
||||||
@ -617,7 +519,7 @@ class FormSemestre(db.Model):
|
|||||||
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
|
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
|
||||||
jour_pivot_annee=1,
|
jour_pivot_annee=1,
|
||||||
jour_pivot_periode=1,
|
jour_pivot_periode=1,
|
||||||
) -> tuple[int, int]:
|
):
|
||||||
"""Calcule la session associée à un formsemestre commençant en date_debut
|
"""Calcule la session associée à un formsemestre commençant en date_debut
|
||||||
sous la forme (année, période)
|
sous la forme (année, période)
|
||||||
année: première année de l'année scolaire
|
année: première année de l'année scolaire
|
||||||
@ -667,26 +569,6 @@ class FormSemestre(db.Model):
|
|||||||
mois_pivot_periode=ScoDocSiteConfig.get_month_debut_periode2(),
|
mois_pivot_periode=ScoDocSiteConfig.get_month_debut_periode2(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_dept_formsemestres_courants(
|
|
||||||
cls, dept: Departement, date_courante: datetime.datetime | None = None
|
|
||||||
) -> db.Query:
|
|
||||||
"""Liste (query) ordonnée des formsemestres courants, c'est
|
|
||||||
à dire contenant la date courant (si None, la date actuelle)"""
|
|
||||||
date_courante = date_courante or db.func.current_date()
|
|
||||||
# Les semestres en cours de ce département
|
|
||||||
formsemestres = FormSemestre.query.filter(
|
|
||||||
FormSemestre.dept_id == dept.id,
|
|
||||||
FormSemestre.date_debut <= date_courante,
|
|
||||||
FormSemestre.date_fin >= date_courante,
|
|
||||||
)
|
|
||||||
return formsemestres.order_by(
|
|
||||||
FormSemestre.date_debut.desc(),
|
|
||||||
FormSemestre.modalite,
|
|
||||||
FormSemestre.semestre_id,
|
|
||||||
FormSemestre.titre,
|
|
||||||
)
|
|
||||||
|
|
||||||
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
|
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
|
||||||
"Liste des vdis"
|
"Liste des vdis"
|
||||||
# was read_formsemestre_etapes
|
# was read_formsemestre_etapes
|
||||||
@ -1158,33 +1040,6 @@ class FormSemestre(db.Model):
|
|||||||
nb_recorded += 1
|
nb_recorded += 1
|
||||||
return nb_recorded
|
return nb_recorded
|
||||||
|
|
||||||
def change_formation(self, formation_dest: Formation):
|
|
||||||
"""Associe ce formsemestre à une autre formation.
|
|
||||||
Ce n'est possible que si la formation destination possède des modules de
|
|
||||||
même code que ceux utilisés dans la formation d'origine du formsemestre.
|
|
||||||
S'il manque un module, l'opération est annulée.
|
|
||||||
Commit (or rollback) session.
|
|
||||||
"""
|
|
||||||
ok = True
|
|
||||||
for mi in self.modimpls:
|
|
||||||
dest_modules = formation_dest.modules.filter_by(code=mi.module.code).all()
|
|
||||||
match len(dest_modules):
|
|
||||||
case 1:
|
|
||||||
mi.module = dest_modules[0]
|
|
||||||
db.session.add(mi)
|
|
||||||
case 0:
|
|
||||||
print(f"Argh ! no module found with code={mi.module.code}")
|
|
||||||
ok = False
|
|
||||||
case _:
|
|
||||||
print(f"Arg ! several modules found with code={mi.module.code}")
|
|
||||||
ok = False
|
|
||||||
|
|
||||||
if ok:
|
|
||||||
self.formation_id = formation_dest.id
|
|
||||||
db.session.commit()
|
|
||||||
else:
|
|
||||||
db.session.rollback()
|
|
||||||
|
|
||||||
|
|
||||||
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
|
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
|
||||||
notes_formsemestre_responsables = db.Table(
|
notes_formsemestre_responsables = db.Table(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -11,15 +11,14 @@ from operator import attrgetter
|
|||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app.models import ScoDocModel, GROUPNAME_STR_LEN, SHORT_STR_LEN
|
from app.models import Scolog, GROUPNAME_STR_LEN, SHORT_STR_LEN
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.events import Scolog
|
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||||
|
|
||||||
|
|
||||||
class Partition(ScoDocModel):
|
class Partition(db.Model):
|
||||||
"""Partition: découpage d'une promotion en groupes"""
|
"""Partition: découpage d'une promotion en groupes"""
|
||||||
|
|
||||||
__table_args__ = (db.UniqueConstraint("formsemestre_id", "partition_name"),)
|
__table_args__ = (db.UniqueConstraint("formsemestre_id", "partition_name"),)
|
||||||
@ -205,7 +204,7 @@ class Partition(ScoDocModel):
|
|||||||
return group
|
return group
|
||||||
|
|
||||||
|
|
||||||
class GroupDescr(ScoDocModel):
|
class GroupDescr(db.Model):
|
||||||
"""Description d'un groupe d'une partition"""
|
"""Description d'un groupe d'une partition"""
|
||||||
|
|
||||||
__tablename__ = "group_descr"
|
__tablename__ = "group_descr"
|
||||||
@ -242,20 +241,15 @@ class GroupDescr(ScoDocModel):
|
|||||||
|
|
||||||
def to_dict(self, with_partition=True) -> dict:
|
def to_dict(self, with_partition=True) -> dict:
|
||||||
"""as a dict, with or without partition"""
|
"""as a dict, with or without partition"""
|
||||||
if with_partition:
|
|
||||||
partition_dict = self.partition.to_dict(with_groups=False)
|
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
if with_partition:
|
if with_partition:
|
||||||
d["partition"] = partition_dict
|
d["partition"] = self.partition.to_dict(with_groups=False)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def get_edt_ids(self) -> list[str]:
|
def get_edt_id(self) -> str:
|
||||||
"les ids normalisés pour l'emploi du temps: à défaut, le nom scodoc du groupe"
|
"l'id pour l'emploi du temps: à défaut, le nom scodoc du groupe"
|
||||||
return [
|
return self.edt_id or self.group_name or ""
|
||||||
scu.normalize_edt_id(x)
|
|
||||||
for x in scu.split_id(self.edt_id) or [self.group_name] or []
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_nb_inscrits(self) -> int:
|
def get_nb_inscrits(self) -> int:
|
||||||
"""Nombre inscrits à ce group et au formsemestre.
|
"""Nombre inscrits à ce group et au formsemestre.
|
||||||
|
@ -2,23 +2,20 @@
|
|||||||
"""ScoDoc models: moduleimpls
|
"""ScoDoc models: moduleimpls
|
||||||
"""
|
"""
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from flask import abort, g
|
|
||||||
from flask_login import current_user
|
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.comp import df_cache
|
from app.comp import df_cache
|
||||||
from app.models import APO_CODE_STR_LEN, ScoDocModel
|
from app.models import APO_CODE_STR_LEN
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.evaluations import Evaluation
|
|
||||||
from app.models.modules import Module
|
from app.models.modules import Module
|
||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
|
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class ModuleImpl(ScoDocModel):
|
class ModuleImpl(db.Model):
|
||||||
"""Mise en oeuvre d'un module pour une annee/semestre"""
|
"""Mise en oeuvre d'un module pour une annee/semestre"""
|
||||||
|
|
||||||
__tablename__ = "notes_moduleimpl"
|
__tablename__ = "notes_moduleimpl"
|
||||||
@ -37,27 +34,18 @@ class ModuleImpl(ScoDocModel):
|
|||||||
index=True,
|
index=True,
|
||||||
nullable=False,
|
nullable=False,
|
||||||
)
|
)
|
||||||
responsable_id = db.Column(
|
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
|
||||||
"responsable_id", db.Integer, db.ForeignKey("user.id", ondelete="SET NULL")
|
|
||||||
)
|
|
||||||
responsable = db.relationship("User", back_populates="modimpls")
|
|
||||||
# formule de calcul moyenne:
|
# formule de calcul moyenne:
|
||||||
computation_expr = db.Column(db.Text())
|
computation_expr = db.Column(db.Text())
|
||||||
|
|
||||||
evaluations = db.relationship(
|
evaluations = db.relationship("Evaluation", lazy="dynamic", backref="moduleimpl")
|
||||||
"Evaluation",
|
|
||||||
lazy="dynamic",
|
|
||||||
backref="moduleimpl",
|
|
||||||
order_by=(Evaluation.numero, Evaluation.date_debut),
|
|
||||||
)
|
|
||||||
"évaluations, triées par numéro et dates croissants, donc la plus ancienne d'abord."
|
|
||||||
enseignants = db.relationship(
|
enseignants = db.relationship(
|
||||||
"User",
|
"User",
|
||||||
secondary="notes_modules_enseignants",
|
secondary="notes_modules_enseignants",
|
||||||
lazy="dynamic",
|
lazy="dynamic",
|
||||||
backref="moduleimpl",
|
backref="moduleimpl",
|
||||||
|
viewonly=True,
|
||||||
)
|
)
|
||||||
"enseignants du module (sans le responsable)"
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
||||||
@ -70,12 +58,13 @@ class ModuleImpl(ScoDocModel):
|
|||||||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||||
return self.module.get_codes_apogee()
|
return self.module.get_codes_apogee()
|
||||||
|
|
||||||
def get_edt_ids(self) -> list[str]:
|
def get_edt_id(self) -> str:
|
||||||
"les ids pour l'emploi du temps: à défaut, les codes Apogée"
|
"l'id pour l'emploi du temps: à défaut, le 1er code Apogée"
|
||||||
return [
|
return (
|
||||||
scu.normalize_edt_id(x)
|
self.edt_id
|
||||||
for x in scu.split_id(self.edt_id) or scu.split_id(self.code_apogee)
|
or (self.code_apogee.split(",")[0] if self.code_apogee else "")
|
||||||
] or self.module.get_edt_ids()
|
or self.module.get_edt_id()
|
||||||
|
)
|
||||||
|
|
||||||
def get_evaluations_poids(self) -> pd.DataFrame:
|
def get_evaluations_poids(self) -> pd.DataFrame:
|
||||||
"""Les poids des évaluations vers les UE (accès via cache)"""
|
"""Les poids des évaluations vers les UE (accès via cache)"""
|
||||||
@ -87,23 +76,6 @@ class ModuleImpl(ScoDocModel):
|
|||||||
df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids)
|
df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids)
|
||||||
return evaluations_poids
|
return evaluations_poids
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_modimpl(cls, moduleimpl_id: int | str, dept_id: int = None) -> "ModuleImpl":
|
|
||||||
"""ModuleImpl ou 404, cherche uniquement dans le département spécifié ou le courant."""
|
|
||||||
from app.models.formsemestre import FormSemestre
|
|
||||||
|
|
||||||
if not isinstance(moduleimpl_id, int):
|
|
||||||
try:
|
|
||||||
moduleimpl_id = int(moduleimpl_id)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
abort(404, "moduleimpl_id invalide")
|
|
||||||
if g.scodoc_dept:
|
|
||||||
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
|
||||||
query = cls.query.filter_by(id=moduleimpl_id)
|
|
||||||
if dept_id is not None:
|
|
||||||
query = query.join(FormSemestre).filter_by(dept_id=dept_id)
|
|
||||||
return query.first_or_404()
|
|
||||||
|
|
||||||
def invalidate_evaluations_poids(self):
|
def invalidate_evaluations_poids(self):
|
||||||
"""Invalide poids cachés"""
|
"""Invalide poids cachés"""
|
||||||
df_cache.EvaluationsPoidsCache.delete(self.id)
|
df_cache.EvaluationsPoidsCache.delete(self.id)
|
||||||
@ -191,7 +163,7 @@ class ModuleImpl(ScoDocModel):
|
|||||||
return allow_ens and user.id in (ens.id for ens in self.enseignants)
|
return allow_ens and user.id in (ens.id for ens in self.enseignants)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def can_change_responsable(self, user: User, raise_exc=False) -> bool:
|
def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
|
||||||
"""Check if user can modify module resp.
|
"""Check if user can modify module resp.
|
||||||
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
||||||
= Admin, et dir des etud. (si option l'y autorise)
|
= Admin, et dir des etud. (si option l'y autorise)
|
||||||
@ -212,32 +184,11 @@ class ModuleImpl(ScoDocModel):
|
|||||||
raise AccessDenied(f"Modification impossible pour {user}")
|
raise AccessDenied(f"Modification impossible pour {user}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def can_change_ens(self, user: User | None = None, raise_exc=True) -> bool:
|
|
||||||
"""check if user can modify ens list (raise exception if not)"
|
|
||||||
if user is None, current user.
|
|
||||||
"""
|
|
||||||
user = current_user if user is None else user
|
|
||||||
if not self.formsemestre.etat:
|
|
||||||
if raise_exc:
|
|
||||||
raise ScoLockedSemError("Modification impossible: semestre verrouille")
|
|
||||||
return False
|
|
||||||
# -- check access
|
|
||||||
# admin, resp. module ou resp. semestre
|
|
||||||
if (
|
|
||||||
user.id != self.responsable_id
|
|
||||||
and not user.has_permission(Permission.EditFormSemestre)
|
|
||||||
and user.id not in (u.id for u in self.formsemestre.responsables)
|
|
||||||
):
|
|
||||||
if raise_exc:
|
|
||||||
raise AccessDenied(f"Modification impossible pour {user}")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def est_inscrit(self, etud: Identite) -> bool:
|
def est_inscrit(self, etud: Identite) -> bool:
|
||||||
"""
|
"""
|
||||||
Vérifie si l'étudiant est bien inscrit au moduleimpl (même si DEM ou DEF au semestre).
|
Vérifie si l'étudiant est bien inscrit au moduleimpl
|
||||||
(lent, pas de cache: pour un accès rapide, utiliser nt.modimpl_inscr_df).
|
|
||||||
Retourne Vrai si inscrit au module, faux sinon.
|
Retourne Vrai si c'est le cas, faux sinon
|
||||||
"""
|
"""
|
||||||
|
|
||||||
is_module: int = (
|
is_module: int = (
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""ScoDoc 9 models : Modules
|
"""ScoDoc 9 models : Modules
|
||||||
"""
|
"""
|
||||||
from flask import current_app, g
|
from flask import current_app
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import APO_CODE_STR_LEN
|
from app.models import APO_CODE_STR_LEN
|
||||||
@ -22,7 +22,6 @@ class Module(db.Model):
|
|||||||
abbrev = db.Column(db.Text()) # nom court
|
abbrev = db.Column(db.Text()) # nom court
|
||||||
# certains départements ont des codes infiniment longs: donc Text !
|
# certains départements ont des codes infiniment longs: donc Text !
|
||||||
code = db.Column(db.Text(), nullable=False)
|
code = db.Column(db.Text(), nullable=False)
|
||||||
"code module, chaine non nullable"
|
|
||||||
heures_cours = db.Column(db.Float)
|
heures_cours = db.Column(db.Float)
|
||||||
heures_td = db.Column(db.Float)
|
heures_td = db.Column(db.Float)
|
||||||
heures_tp = db.Column(db.Float)
|
heures_tp = db.Column(db.Float)
|
||||||
@ -160,10 +159,6 @@ class Module(db.Model):
|
|||||||
"Identifiant du module à afficher : abbrev ou titre ou code"
|
"Identifiant du module à afficher : abbrev ou titre ou code"
|
||||||
return self.abbrev or self.titre or self.code
|
return self.abbrev or self.titre or self.code
|
||||||
|
|
||||||
def sort_key(self) -> tuple:
|
|
||||||
"""Clé de tri pour formations classiques"""
|
|
||||||
return self.numero or 0, self.code
|
|
||||||
|
|
||||||
def sort_key_apc(self) -> tuple:
|
def sort_key_apc(self) -> tuple:
|
||||||
"""Clé de tri pour avoir
|
"""Clé de tri pour avoir
|
||||||
présentation par type (res, sae), parcours, type, numéro
|
présentation par type (res, sae), parcours, type, numéro
|
||||||
@ -290,12 +285,13 @@ class Module(db.Model):
|
|||||||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
def get_edt_ids(self) -> list[str]:
|
def get_edt_id(self) -> str:
|
||||||
"les ids pour l'emploi du temps: à défaut, le 1er code Apogée"
|
"l'id pour l'emploi du temps: à défaut, le 1er code Apogée"
|
||||||
return [
|
return (
|
||||||
scu.normalize_edt_id(x)
|
self.edt_id
|
||||||
for x in scu.split_id(self.edt_id) or scu.split_id(self.code_apogee) or []
|
or (self.code_apogee.split(",")[0] if self.code_apogee else "")
|
||||||
]
|
or ""
|
||||||
|
)
|
||||||
|
|
||||||
def get_parcours(self) -> list[ApcParcours]:
|
def get_parcours(self) -> list[ApcParcours]:
|
||||||
"""Les parcours utilisant ce module.
|
"""Les parcours utilisant ce module.
|
||||||
@ -310,14 +306,6 @@ class Module(db.Model):
|
|||||||
return []
|
return []
|
||||||
return self.parcours
|
return self.parcours
|
||||||
|
|
||||||
def add_tag(self, tag: "NotesTag"):
|
|
||||||
"""Add tag to module. Check if already has it."""
|
|
||||||
if tag.id in {t.id for t in self.tags}:
|
|
||||||
return
|
|
||||||
self.tags.append(tag)
|
|
||||||
db.session.add(self)
|
|
||||||
db.session.flush()
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleUECoef(db.Model):
|
class ModuleUECoef(db.Model):
|
||||||
"""Coefficients des modules vers les UE (APC, BUT)
|
"""Coefficients des modules vers les UE (APC, BUT)
|
||||||
@ -380,19 +368,6 @@ class NotesTag(db.Model):
|
|||||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||||
title = db.Column(db.Text(), nullable=False)
|
title = db.Column(db.Text(), nullable=False)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_or_create(cls, title: str, dept_id: int | None = None) -> "NotesTag":
|
|
||||||
"""Get tag, or create it if it doesn't yet exists.
|
|
||||||
If dept_id unspecified, use current dept.
|
|
||||||
"""
|
|
||||||
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
|
||||||
tag = NotesTag.query.filter_by(dept_id=dept_id, title=title).first()
|
|
||||||
if tag is None:
|
|
||||||
tag = NotesTag(dept_id=dept_id, title=title)
|
|
||||||
db.session.add(tag)
|
|
||||||
db.session.flush()
|
|
||||||
return tag
|
|
||||||
|
|
||||||
|
|
||||||
# Association tag <-> module
|
# Association tag <-> module
|
||||||
notes_modules_tags = db.Table(
|
notes_modules_tags = db.Table(
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
"""évènements scolaires dans la vie d'un étudiant(inscription, ...)
|
|
||||||
"""
|
|
||||||
from app import db
|
|
||||||
from app.models import SHORT_STR_LEN
|
|
||||||
|
|
||||||
|
|
||||||
class ScolarEvent(db.Model):
|
|
||||||
"""Evenement dans le parcours scolaire d'un étudiant"""
|
|
||||||
|
|
||||||
__tablename__ = "scolar_events"
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
event_id = db.synonym("id")
|
|
||||||
etudid = db.Column(
|
|
||||||
db.Integer,
|
|
||||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
|
||||||
)
|
|
||||||
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
|
||||||
formsemestre_id = db.Column(
|
|
||||||
db.Integer,
|
|
||||||
db.ForeignKey("notes_formsemestre.id", ondelete="SET NULL"),
|
|
||||||
)
|
|
||||||
ue_id = db.Column(
|
|
||||||
db.Integer,
|
|
||||||
db.ForeignKey("notes_ue.id", ondelete="SET NULL"),
|
|
||||||
)
|
|
||||||
# 'CREATION', 'INSCRIPTION', 'DEMISSION',
|
|
||||||
# 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
|
|
||||||
# 'ECHEC_SEM'
|
|
||||||
# 'UTIL_COMPENSATION'
|
|
||||||
event_type = db.Column(db.String(SHORT_STR_LEN))
|
|
||||||
# Semestre compensé par formsemestre_id:
|
|
||||||
comp_formsemestre_id = db.Column(
|
|
||||||
db.Integer,
|
|
||||||
db.ForeignKey("notes_formsemestre.id"),
|
|
||||||
)
|
|
||||||
etud = db.relationship("Identite", lazy="select", backref="events", uselist=False)
|
|
||||||
formsemestre = db.relationship(
|
|
||||||
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
|
|
||||||
)
|
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
|
||||||
"as a dict"
|
|
||||||
d = dict(self.__dict__)
|
|
||||||
d.pop("_sa_instance_state", None)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self.__class__.__name__}({self.event_type}, {self.event_date.isoformat()}, {self.formsemestre})"
|
|
@ -5,7 +5,6 @@ from flask import g
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app import models
|
|
||||||
from app.models import APO_CODE_STR_LEN
|
from app.models import APO_CODE_STR_LEN
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
from app.models.but_refcomp import ApcNiveau, ApcParcours
|
from app.models.but_refcomp import ApcNiveau, ApcParcours
|
||||||
@ -13,7 +12,7 @@ from app.models.modules import Module
|
|||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class UniteEns(models.ScoDocModel):
|
class UniteEns(db.Model):
|
||||||
"""Unité d'Enseignement (UE)"""
|
"""Unité d'Enseignement (UE)"""
|
||||||
|
|
||||||
__tablename__ = "notes_ue"
|
__tablename__ = "notes_ue"
|
||||||
@ -82,7 +81,7 @@ class UniteEns(models.ScoDocModel):
|
|||||||
'EXTERNE' if self.is_external else ''})>"""
|
'EXTERNE' if self.is_external else ''})>"""
|
||||||
|
|
||||||
def clone(self):
|
def clone(self):
|
||||||
"""Create a new copy of this ue, add to session.
|
"""Create a new copy of this ue.
|
||||||
Ne copie pas le code, ni le code Apogée, ni les liens au réf. de comp.
|
Ne copie pas le code, ni le code Apogée, ni les liens au réf. de comp.
|
||||||
(parcours et niveau).
|
(parcours et niveau).
|
||||||
"""
|
"""
|
||||||
@ -101,26 +100,8 @@ class UniteEns(models.ScoDocModel):
|
|||||||
coef_rcue=self.coef_rcue,
|
coef_rcue=self.coef_rcue,
|
||||||
color=self.color,
|
color=self.color,
|
||||||
)
|
)
|
||||||
db.session.add(ue)
|
|
||||||
return ue
|
return ue
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def convert_dict_fields(cls, args: dict) -> dict:
|
|
||||||
"""Convert fields from the given dict to model's attributes values. No side effect.
|
|
||||||
|
|
||||||
args: dict with args in application.
|
|
||||||
returns: dict to store in model's db.
|
|
||||||
"""
|
|
||||||
args = args.copy()
|
|
||||||
if "type" in args:
|
|
||||||
args["type"] = int(args["type"] or 0)
|
|
||||||
if "is_external" in args:
|
|
||||||
args["is_external"] = scu.to_bool(args["is_external"])
|
|
||||||
if "ects" in args:
|
|
||||||
args["ects"] = float(args["ects"])
|
|
||||||
|
|
||||||
return args
|
|
||||||
|
|
||||||
def to_dict(self, convert_objects=False, with_module_ue_coefs=True):
|
def to_dict(self, convert_objects=False, with_module_ue_coefs=True):
|
||||||
"""as a dict, with the same conversions as in ScoDoc7.
|
"""as a dict, with the same conversions as in ScoDoc7.
|
||||||
If convert_objects, convert all attributes to native types
|
If convert_objects, convert all attributes to native types
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
|
|
||||||
"""Notes, décisions de jury
|
"""Notes, décisions de jury, évènements scolaires
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
@ -218,3 +218,47 @@ class ScolarAutorisationInscription(db.Model):
|
|||||||
msg=f"Passage vers S{autorisation.semestre_id}: effacé",
|
msg=f"Passage vers S{autorisation.semestre_id}: effacé",
|
||||||
)
|
)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
class ScolarEvent(db.Model):
|
||||||
|
"""Evenement dans le parcours scolaire d'un étudiant"""
|
||||||
|
|
||||||
|
__tablename__ = "scolar_events"
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
event_id = db.synonym("id")
|
||||||
|
etudid = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
|
)
|
||||||
|
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
|
formsemestre_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("notes_formsemestre.id", ondelete="SET NULL"),
|
||||||
|
)
|
||||||
|
ue_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("notes_ue.id", ondelete="SET NULL"),
|
||||||
|
)
|
||||||
|
# 'CREATION', 'INSCRIPTION', 'DEMISSION',
|
||||||
|
# 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
|
||||||
|
# 'ECHEC_SEM'
|
||||||
|
# 'UTIL_COMPENSATION'
|
||||||
|
event_type = db.Column(db.String(SHORT_STR_LEN))
|
||||||
|
# Semestre compensé par formsemestre_id:
|
||||||
|
comp_formsemestre_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("notes_formsemestre.id"),
|
||||||
|
)
|
||||||
|
etud = db.relationship("Identite", lazy="select", backref="events", uselist=False)
|
||||||
|
formsemestre = db.relationship(
|
||||||
|
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"as a dict"
|
||||||
|
d = dict(self.__dict__)
|
||||||
|
d.pop("_sa_instance_state", None)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.__class__.__name__}({self.event_type}, {self.event_date.isoformat()}, {self.formsemestre})"
|
||||||
|
@ -3,3 +3,6 @@
|
|||||||
Conçu et développé sur ScoDoc 7 par Cléo Baras (IUT de Grenoble) pour le DUT.
|
Conçu et développé sur ScoDoc 7 par Cléo Baras (IUT de Grenoble) pour le DUT.
|
||||||
|
|
||||||
Actuellement non opérationnel dans ScoDoc 9.
|
Actuellement non opérationnel dans ScoDoc 9.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
##############################################################################
|
|
||||||
# Module "Avis de poursuite d'étude"
|
|
||||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""Affichages, debug
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask import g
|
|
||||||
from app import log
|
|
||||||
|
|
||||||
PE_DEBUG = False
|
|
||||||
|
|
||||||
|
|
||||||
# On stocke les logs PE dans g.scodoc_pe_log
|
|
||||||
# pour ne pas modifier les nombreux appels à pe_print.
|
|
||||||
def pe_start_log() -> list[str]:
|
|
||||||
"Initialize log"
|
|
||||||
g.scodoc_pe_log = []
|
|
||||||
return g.scodoc_pe_log
|
|
||||||
|
|
||||||
|
|
||||||
def pe_print(*a):
|
|
||||||
"Log (or print in PE_DEBUG mode) and store in g"
|
|
||||||
lines = getattr(g, "scodoc_pe_log")
|
|
||||||
if lines is None:
|
|
||||||
lines = pe_start_log()
|
|
||||||
msg = " ".join(a)
|
|
||||||
lines.append(msg)
|
|
||||||
if PE_DEBUG:
|
|
||||||
print(msg)
|
|
||||||
else:
|
|
||||||
log(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def pe_get_log() -> str:
|
|
||||||
"Renvoie une chaîne avec tous les messages loggués"
|
|
||||||
return "\n".join(getattr(g, "scodoc_pe_log", []))
|
|
||||||
|
|
||||||
|
|
||||||
# Affichage dans le tableur pe en cas d'absence de notes
|
|
||||||
SANS_NOTE = "-"
|
|
||||||
NOM_STAT_GROUPE = "statistiques du groupe"
|
|
||||||
NOM_STAT_PROMO = "statistiques de la promo"
|
|
517
app/pe/pe_avislatex.py
Normal file
517
app/pe/pe_avislatex.py
Normal file
@ -0,0 +1,517 @@
|
|||||||
|
# -*- mode: python -*-
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gestion scolarite IUT
|
||||||
|
#
|
||||||
|
# Copyright (c) 1999 - 2023 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
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Module "Avis de poursuite d'étude"
|
||||||
|
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
import os
|
||||||
|
import codecs
|
||||||
|
import re
|
||||||
|
from app.pe import pe_tagtable
|
||||||
|
from app.pe import pe_jurype
|
||||||
|
from app.pe import pe_tools
|
||||||
|
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
import app.scodoc.notesdb as ndb
|
||||||
|
from app import log
|
||||||
|
from app.scodoc.gen_tables import GenTable, SeqGenTable
|
||||||
|
from app.scodoc import sco_preferences
|
||||||
|
from app.scodoc import sco_etud
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG = False # Pour debug et repérage des prints à changer en Log
|
||||||
|
|
||||||
|
DONNEE_MANQUANTE = (
|
||||||
|
"" # Caractère de remplacement des données manquantes dans un avis PE
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------
|
||||||
|
def get_code_latex_from_modele(fichier):
|
||||||
|
"""Lit le code latex à partir d'un modèle. Renvoie une chaine unicode.
|
||||||
|
|
||||||
|
Le fichier doit contenir le chemin relatif
|
||||||
|
vers le modele : attention pas de vérification du format d'encodage
|
||||||
|
Le fichier doit donc etre enregistré avec le même codage que ScoDoc (utf-8)
|
||||||
|
"""
|
||||||
|
fid_latex = codecs.open(fichier, "r", encoding=scu.SCO_ENCODING)
|
||||||
|
un_avis_latex = fid_latex.read()
|
||||||
|
fid_latex.close()
|
||||||
|
return un_avis_latex
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------
|
||||||
|
def get_code_latex_from_scodoc_preference(formsemestre_id, champ="pe_avis_latex_tmpl"):
|
||||||
|
"""
|
||||||
|
Extrait le template (ou le tag d'annotation au regard du champ fourni) des préférences LaTeX
|
||||||
|
et s'assure qu'il est renvoyé au format unicode
|
||||||
|
"""
|
||||||
|
template_latex = sco_preferences.get_preference(champ, formsemestre_id)
|
||||||
|
|
||||||
|
return template_latex or ""
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------
|
||||||
|
def get_tags_latex(code_latex):
|
||||||
|
"""Recherche tous les tags présents dans un code latex (ce code étant obtenu
|
||||||
|
à la lecture d'un modèle d'avis pe).
|
||||||
|
Ces tags sont répérés par les balises **, débutant et finissant le tag
|
||||||
|
et sont renvoyés sous la forme d'une liste.
|
||||||
|
|
||||||
|
result: liste de chaines unicode
|
||||||
|
"""
|
||||||
|
if code_latex:
|
||||||
|
# changé par EV: était r"([\*]{2}[a-zA-Z0-9:éèàâêëïôöù]+[\*]{2})"
|
||||||
|
res = re.findall(r"([\*]{2}[^\t\n\r\f\v\*]+[\*]{2})", code_latex)
|
||||||
|
return [tag[2:-2] for tag in res]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def comp_latex_parcourstimeline(etudiant, promo, taille=17):
|
||||||
|
"""Interprète un tag dans un avis latex **parcourstimeline**
|
||||||
|
et génère le code latex permettant de retracer le parcours d'un étudiant
|
||||||
|
sous la forme d'une frise temporelle.
|
||||||
|
Nota: modeles/parcourstimeline.tex doit avoir été inclu dans le préambule
|
||||||
|
|
||||||
|
result: chaine unicode (EV:)
|
||||||
|
"""
|
||||||
|
codelatexDebut = (
|
||||||
|
""""
|
||||||
|
\\begin{parcourstimeline}{**debut**}{**fin**}{**nbreSemestres**}{%d}
|
||||||
|
"""
|
||||||
|
% taille
|
||||||
|
)
|
||||||
|
|
||||||
|
modeleEvent = """
|
||||||
|
\\parcoursevent{**nosem**}{**nomsem**}{**descr**}
|
||||||
|
"""
|
||||||
|
|
||||||
|
codelatexFin = """
|
||||||
|
\\end{parcourstimeline}
|
||||||
|
"""
|
||||||
|
reslatex = codelatexDebut
|
||||||
|
reslatex = reslatex.replace("**debut**", etudiant["entree"])
|
||||||
|
reslatex = reslatex.replace("**fin**", str(etudiant["promo"]))
|
||||||
|
reslatex = reslatex.replace("**nbreSemestres**", str(etudiant["nbSemestres"]))
|
||||||
|
# Tri du parcours par ordre croissant : de la forme descr, nom sem date-date
|
||||||
|
parcours = etudiant["parcours"][::-1] # EV: XXX je ne comprend pas ce commentaire ?
|
||||||
|
|
||||||
|
for no_sem in range(etudiant["nbSemestres"]):
|
||||||
|
descr = modeleEvent
|
||||||
|
nom_semestre_dans_parcours = parcours[no_sem]["nom_semestre_dans_parcours"]
|
||||||
|
descr = descr.replace("**nosem**", str(no_sem + 1))
|
||||||
|
if no_sem % 2 == 0:
|
||||||
|
descr = descr.replace("**nomsem**", nom_semestre_dans_parcours)
|
||||||
|
descr = descr.replace("**descr**", "")
|
||||||
|
else:
|
||||||
|
descr = descr.replace("**nomsem**", "")
|
||||||
|
descr = descr.replace("**descr**", nom_semestre_dans_parcours)
|
||||||
|
reslatex += descr
|
||||||
|
reslatex += codelatexFin
|
||||||
|
return reslatex
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------
|
||||||
|
def interprete_tag_latex(tag):
|
||||||
|
"""Découpe les tags latex de la forme S1:groupe:dut:min et renvoie si possible
|
||||||
|
le résultat sous la forme d'un quadruplet.
|
||||||
|
"""
|
||||||
|
infotag = tag.split(":")
|
||||||
|
if len(infotag) == 4:
|
||||||
|
return (
|
||||||
|
infotag[0].upper(),
|
||||||
|
infotag[1].lower(),
|
||||||
|
infotag[2].lower(),
|
||||||
|
infotag[3].lower(),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (None, None, None, None)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------
|
||||||
|
def get_code_latex_avis_etudiant(
|
||||||
|
donnees_etudiant, un_avis_latex, annotationPE, footer_latex, prefs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Renvoie le code latex permettant de générer l'avis d'un étudiant en utilisant ses
|
||||||
|
donnees_etudiant contenu dans le dictionnaire de synthèse du jury PE et en suivant un
|
||||||
|
fichier modele donné
|
||||||
|
|
||||||
|
result: chaine unicode
|
||||||
|
"""
|
||||||
|
if not donnees_etudiant or not un_avis_latex: # Cas d'un template vide
|
||||||
|
return annotationPE if annotationPE else ""
|
||||||
|
|
||||||
|
# Le template latex (corps + footer)
|
||||||
|
code = un_avis_latex + "\n\n" + footer_latex
|
||||||
|
|
||||||
|
# Recherche des tags dans le fichier
|
||||||
|
tags_latex = get_tags_latex(code)
|
||||||
|
if DEBUG:
|
||||||
|
log("Les tags" + str(tags_latex))
|
||||||
|
|
||||||
|
# Interprète et remplace chaque tags latex par les données numériques de l'étudiant (y compris les
|
||||||
|
# tags "macros" tels que parcourstimeline
|
||||||
|
for tag_latex in tags_latex:
|
||||||
|
# les tags numériques
|
||||||
|
valeur = DONNEE_MANQUANTE
|
||||||
|
|
||||||
|
if ":" in tag_latex:
|
||||||
|
(aggregat, groupe, tag_scodoc, champ) = interprete_tag_latex(tag_latex)
|
||||||
|
valeur = str_from_syntheseJury(
|
||||||
|
donnees_etudiant, aggregat, groupe, tag_scodoc, champ
|
||||||
|
)
|
||||||
|
|
||||||
|
# La macro parcourstimeline
|
||||||
|
elif tag_latex == "parcourstimeline":
|
||||||
|
valeur = comp_latex_parcourstimeline(
|
||||||
|
donnees_etudiant, donnees_etudiant["promo"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Le tag annotationPE
|
||||||
|
elif tag_latex == "annotation":
|
||||||
|
valeur = annotationPE
|
||||||
|
|
||||||
|
# Le tag bilanParTag
|
||||||
|
elif tag_latex == "bilanParTag":
|
||||||
|
valeur = get_bilanParTag(donnees_etudiant)
|
||||||
|
|
||||||
|
# Les tags "simples": par ex. nom, prenom, civilite, ...
|
||||||
|
else:
|
||||||
|
if tag_latex in donnees_etudiant:
|
||||||
|
valeur = donnees_etudiant[tag_latex]
|
||||||
|
elif tag_latex in prefs: # les champs **NomResponsablePE**, ...
|
||||||
|
valeur = pe_tools.escape_for_latex(prefs[tag_latex])
|
||||||
|
|
||||||
|
# Vérification des pb d'encodage (debug)
|
||||||
|
# assert isinstance(tag_latex, unicode)
|
||||||
|
# assert isinstance(valeur, unicode)
|
||||||
|
|
||||||
|
# Substitution
|
||||||
|
code = code.replace("**" + tag_latex + "**", valeur)
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------
|
||||||
|
def get_annotation_PE(etudid, tag_annotation_pe):
|
||||||
|
"""Renvoie l'annotation PE dans la liste de ces annotations ;
|
||||||
|
Cette annotation est reconnue par la présence d'un tag **PE**
|
||||||
|
(cf. .get_preferences -> pe_tag_annotation_avis_latex).
|
||||||
|
|
||||||
|
Result: chaine unicode
|
||||||
|
"""
|
||||||
|
if tag_annotation_pe:
|
||||||
|
cnx = ndb.GetDBConnexion()
|
||||||
|
annotations = sco_etud.etud_annotations_list(
|
||||||
|
cnx, args={"etudid": etudid}
|
||||||
|
) # Les annotations de l'étudiant
|
||||||
|
annotationsPE = []
|
||||||
|
|
||||||
|
exp = re.compile(r"^" + tag_annotation_pe)
|
||||||
|
|
||||||
|
for a in annotations:
|
||||||
|
commentaire = scu.unescape_html(a["comment"])
|
||||||
|
if exp.match(commentaire): # tag en début de commentaire ?
|
||||||
|
a["comment_u"] = commentaire # unicode, HTML non quoté
|
||||||
|
annotationsPE.append(
|
||||||
|
a
|
||||||
|
) # sauvegarde l'annotation si elle contient le tag
|
||||||
|
|
||||||
|
if annotationsPE: # Si des annotations existent, prend la plus récente
|
||||||
|
annotationPE = sorted(annotationsPE, key=lambda a: a["date"], reverse=True)[
|
||||||
|
0
|
||||||
|
]["comment_u"]
|
||||||
|
|
||||||
|
annotationPE = exp.sub(
|
||||||
|
"", annotationPE
|
||||||
|
) # Suppression du tag d'annotation PE
|
||||||
|
annotationPE = annotationPE.replace("\r", "") # Suppression des \r
|
||||||
|
annotationPE = annotationPE.replace(
|
||||||
|
"<br>", "\n\n"
|
||||||
|
) # Interprète les retours chariots html
|
||||||
|
return annotationPE
|
||||||
|
return "" # pas d'annotations
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------
|
||||||
|
def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ):
|
||||||
|
"""Extrait du dictionnaire de synthèse du juryPE pour un étudiant donnée,
|
||||||
|
une valeur indiquée par un champ ;
|
||||||
|
si champ est une liste, renvoie la liste des valeurs extraites.
|
||||||
|
|
||||||
|
Result: chaine unicode ou liste de chaines unicode
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(champ, list):
|
||||||
|
return [
|
||||||
|
str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, chp)
|
||||||
|
for chp in champ
|
||||||
|
]
|
||||||
|
else: # champ = str à priori
|
||||||
|
valeur = DONNEE_MANQUANTE
|
||||||
|
if (
|
||||||
|
(aggregat in donnees_etudiant)
|
||||||
|
and (groupe in donnees_etudiant[aggregat])
|
||||||
|
and (tag_scodoc in donnees_etudiant[aggregat][groupe])
|
||||||
|
):
|
||||||
|
donnees_numeriques = donnees_etudiant[aggregat][groupe][tag_scodoc]
|
||||||
|
if champ == "rang":
|
||||||
|
valeur = "%s/%d" % (
|
||||||
|
donnees_numeriques[
|
||||||
|
pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index("rang")
|
||||||
|
],
|
||||||
|
donnees_numeriques[
|
||||||
|
pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index(
|
||||||
|
"nbinscrits"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
elif champ in pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS:
|
||||||
|
indice_champ = pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index(
|
||||||
|
champ
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
len(donnees_numeriques) > indice_champ
|
||||||
|
and donnees_numeriques[indice_champ] != None
|
||||||
|
):
|
||||||
|
if isinstance(
|
||||||
|
donnees_numeriques[indice_champ], float
|
||||||
|
): # valeur numérique avec formattage unicode
|
||||||
|
valeur = "%2.2f" % donnees_numeriques[indice_champ]
|
||||||
|
else:
|
||||||
|
valeur = "%s" % donnees_numeriques[indice_champ]
|
||||||
|
|
||||||
|
return valeur
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------
|
||||||
|
def get_bilanParTag(donnees_etudiant, groupe="groupe"):
|
||||||
|
"""Renvoie le code latex d'un tableau récapitulant, pour tous les tags trouvés dans
|
||||||
|
les données étudiants, ses résultats.
|
||||||
|
result: chaine unicode
|
||||||
|
"""
|
||||||
|
|
||||||
|
entete = [
|
||||||
|
(
|
||||||
|
agg,
|
||||||
|
pe_jurype.JuryPE.PARCOURS[agg]["affichage_court"],
|
||||||
|
pe_jurype.JuryPE.PARCOURS[agg]["ordre"],
|
||||||
|
)
|
||||||
|
for agg in pe_jurype.JuryPE.PARCOURS
|
||||||
|
]
|
||||||
|
entete = sorted(entete, key=lambda t: t[2])
|
||||||
|
|
||||||
|
lignes = []
|
||||||
|
valeurs = {"note": [], "rang": []}
|
||||||
|
for (indice_aggregat, (aggregat, intitule, _)) in enumerate(entete):
|
||||||
|
# print("> " + aggregat)
|
||||||
|
# listeTags = jury.get_allTagForAggregat(aggregat) # les tags de l'aggrégat
|
||||||
|
listeTags = [
|
||||||
|
tag for tag in donnees_etudiant[aggregat][groupe].keys() if tag != "dut"
|
||||||
|
] #
|
||||||
|
for tag in listeTags:
|
||||||
|
|
||||||
|
if tag not in lignes:
|
||||||
|
lignes.append(tag)
|
||||||
|
valeurs["note"].append(
|
||||||
|
[""] * len(entete)
|
||||||
|
) # Ajout d'une ligne de données
|
||||||
|
valeurs["rang"].append(
|
||||||
|
[""] * len(entete)
|
||||||
|
) # Ajout d'une ligne de données
|
||||||
|
indice_tag = lignes.index(tag) # l'indice de ligne du tag
|
||||||
|
|
||||||
|
# print(" --- " + tag + "(" + str(indice_tag) + "," + str(indice_aggregat) + ")")
|
||||||
|
[note, rang] = str_from_syntheseJury(
|
||||||
|
donnees_etudiant, aggregat, groupe, tag, ["note", "rang"]
|
||||||
|
)
|
||||||
|
valeurs["note"][indice_tag][indice_aggregat] = "" + note + ""
|
||||||
|
valeurs["rang"][indice_tag][indice_aggregat] = (
|
||||||
|
("\\textit{" + rang + "}") if note else ""
|
||||||
|
) # rang masqué si pas de notes
|
||||||
|
|
||||||
|
code_latex = "\\begin{tabular}{|c|" + "|c" * (len(entete)) + "|}\n"
|
||||||
|
code_latex += "\\hline \n"
|
||||||
|
code_latex += (
|
||||||
|
" & "
|
||||||
|
+ " & ".join(["\\textbf{" + intitule + "}" for (agg, intitule, _) in entete])
|
||||||
|
+ " \\\\ \n"
|
||||||
|
)
|
||||||
|
code_latex += "\\hline"
|
||||||
|
code_latex += "\\hline \n"
|
||||||
|
for (i, ligne_val) in enumerate(valeurs["note"]):
|
||||||
|
titre = lignes[i] # règle le pb d'encodage
|
||||||
|
code_latex += "\\textbf{" + titre + "} & " + " & ".join(ligne_val) + "\\\\ \n"
|
||||||
|
code_latex += (
|
||||||
|
" & "
|
||||||
|
+ " & ".join(
|
||||||
|
["{\\scriptsize " + clsmt + "}" for clsmt in valeurs["rang"][i]]
|
||||||
|
)
|
||||||
|
+ "\\\\ \n"
|
||||||
|
)
|
||||||
|
code_latex += "\\hline \n"
|
||||||
|
code_latex += "\\end{tabular}"
|
||||||
|
|
||||||
|
return code_latex
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------
|
||||||
|
def get_avis_poursuite_par_etudiant(
|
||||||
|
jury, etudid, template_latex, tag_annotation_pe, footer_latex, prefs
|
||||||
|
):
|
||||||
|
"""Renvoie un nom de fichier et le contenu de l'avis latex d'un étudiant dont l'etudid est fourni.
|
||||||
|
result: [ chaine unicode, chaine unicode ]
|
||||||
|
"""
|
||||||
|
if pe_tools.PE_DEBUG:
|
||||||
|
pe_tools.pe_print(jury.syntheseJury[etudid]["nom"] + " " + str(etudid))
|
||||||
|
|
||||||
|
civilite_str = jury.syntheseJury[etudid]["civilite_str"]
|
||||||
|
nom = jury.syntheseJury[etudid]["nom"].replace(" ", "-")
|
||||||
|
prenom = jury.syntheseJury[etudid]["prenom"].replace(" ", "-")
|
||||||
|
|
||||||
|
nom_fichier = scu.sanitize_filename(
|
||||||
|
"avis_poursuite_%s_%s_%s" % (nom, prenom, etudid)
|
||||||
|
)
|
||||||
|
if pe_tools.PE_DEBUG:
|
||||||
|
pe_tools.pe_print("fichier latex =" + nom_fichier, type(nom_fichier))
|
||||||
|
|
||||||
|
# Entete (commentaire)
|
||||||
|
contenu_latex = (
|
||||||
|
"%% ---- Etudiant: " + civilite_str + " " + nom + " " + prenom + "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# les annnotations
|
||||||
|
annotationPE = get_annotation_PE(etudid, tag_annotation_pe=tag_annotation_pe)
|
||||||
|
if pe_tools.PE_DEBUG:
|
||||||
|
pe_tools.pe_print(annotationPE, type(annotationPE))
|
||||||
|
|
||||||
|
# le LaTeX
|
||||||
|
avis = get_code_latex_avis_etudiant(
|
||||||
|
jury.syntheseJury[etudid], template_latex, annotationPE, footer_latex, prefs
|
||||||
|
)
|
||||||
|
# if pe_tools.PE_DEBUG: pe_tools.pe_print(avis, type(avis))
|
||||||
|
contenu_latex += avis + "\n"
|
||||||
|
|
||||||
|
return [nom_fichier, contenu_latex]
|
||||||
|
|
||||||
|
|
||||||
|
def get_templates_from_distrib(template="avis"):
|
||||||
|
"""Récupère le template (soit un_avis.tex soit le footer.tex) à partir des fichiers mémorisés dans la distrib des avis pe (distrib local
|
||||||
|
ou par défaut et le renvoie"""
|
||||||
|
if template == "avis":
|
||||||
|
pe_local_tmpl = pe_tools.PE_LOCAL_AVIS_LATEX_TMPL
|
||||||
|
pe_default_tmpl = pe_tools.PE_DEFAULT_AVIS_LATEX_TMPL
|
||||||
|
elif template == "footer":
|
||||||
|
pe_local_tmpl = pe_tools.PE_LOCAL_FOOTER_TMPL
|
||||||
|
pe_default_tmpl = pe_tools.PE_DEFAULT_FOOTER_TMPL
|
||||||
|
|
||||||
|
if template in ["avis", "footer"]:
|
||||||
|
# pas de preference pour le template: utilise fichier du serveur
|
||||||
|
if os.path.exists(pe_local_tmpl):
|
||||||
|
template_latex = get_code_latex_from_modele(pe_local_tmpl)
|
||||||
|
else:
|
||||||
|
if os.path.exists(pe_default_tmpl):
|
||||||
|
template_latex = get_code_latex_from_modele(pe_default_tmpl)
|
||||||
|
else:
|
||||||
|
template_latex = "" # fallback: avis vides
|
||||||
|
return template_latex
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------
|
||||||
|
def table_syntheseAnnotationPE(syntheseJury, tag_annotation_pe):
|
||||||
|
"""Génère un fichier excel synthétisant les annotations PE telles qu'inscrites dans les fiches de chaque étudiant"""
|
||||||
|
sT = SeqGenTable() # le fichier excel à générer
|
||||||
|
|
||||||
|
# Les etudids des étudiants à afficher, triés par ordre alphabétiques de nom+prénom
|
||||||
|
donnees_tries = sorted(
|
||||||
|
[
|
||||||
|
(etudid, syntheseJury[etudid]["nom"] + " " + syntheseJury[etudid]["prenom"])
|
||||||
|
for etudid in syntheseJury.keys()
|
||||||
|
],
|
||||||
|
key=lambda c: c[1],
|
||||||
|
)
|
||||||
|
etudids = [e[0] for e in donnees_tries]
|
||||||
|
if not etudids: # Si pas d'étudiants
|
||||||
|
T = GenTable(
|
||||||
|
columns_ids=["pas d'étudiants"],
|
||||||
|
rows=[],
|
||||||
|
titles={"pas d'étudiants": "pas d'étudiants"},
|
||||||
|
html_sortable=True,
|
||||||
|
xls_sheet_name="dut",
|
||||||
|
)
|
||||||
|
sT.add_genTable("Annotation PE", T)
|
||||||
|
return sT
|
||||||
|
|
||||||
|
# Si des étudiants
|
||||||
|
maxParcours = max(
|
||||||
|
[syntheseJury[etudid]["nbSemestres"] for etudid in etudids]
|
||||||
|
) # le nombre de semestre le + grand
|
||||||
|
|
||||||
|
infos = ["civilite", "nom", "prenom", "age", "nbSemestres"]
|
||||||
|
entete = ["etudid"]
|
||||||
|
entete.extend(infos)
|
||||||
|
entete.extend(["P%d" % i for i in range(1, maxParcours + 1)]) # ajout du parcours
|
||||||
|
entete.append("Annotation PE")
|
||||||
|
columns_ids = entete # les id et les titres de colonnes sont ici identiques
|
||||||
|
titles = {i: i for i in columns_ids}
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for (
|
||||||
|
etudid
|
||||||
|
) in etudids: # parcours des étudiants par ordre alphabétique des nom+prénom
|
||||||
|
e = syntheseJury[etudid]
|
||||||
|
# Les info générales:
|
||||||
|
row = {
|
||||||
|
"etudid": etudid,
|
||||||
|
"civilite": e["civilite"],
|
||||||
|
"nom": e["nom"],
|
||||||
|
"prenom": e["prenom"],
|
||||||
|
"age": e["age"],
|
||||||
|
"nbSemestres": e["nbSemestres"],
|
||||||
|
}
|
||||||
|
# Les parcours: P1, P2, ...
|
||||||
|
n = 1
|
||||||
|
for p in e["parcours"]:
|
||||||
|
row["P%d" % n] = p["titreannee"]
|
||||||
|
n += 1
|
||||||
|
|
||||||
|
# L'annotation PE
|
||||||
|
annotationPE = get_annotation_PE(etudid, tag_annotation_pe=tag_annotation_pe)
|
||||||
|
row["Annotation PE"] = annotationPE if annotationPE else ""
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
T = GenTable(
|
||||||
|
columns_ids=columns_ids,
|
||||||
|
rows=rows,
|
||||||
|
titles=titles,
|
||||||
|
html_sortable=True,
|
||||||
|
xls_sheet_name="Annotation PE",
|
||||||
|
)
|
||||||
|
sT.add_genTable("Annotation PE", T)
|
||||||
|
return sT
|
@ -1,286 +0,0 @@
|
|||||||
# -*- mode: python -*-
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gestion scolarite IUT
|
|
||||||
#
|
|
||||||
# Copyright (c) 1999 - 2024 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
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
# Module "Avis de poursuite d'étude"
|
|
||||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""
|
|
||||||
Created on Thu Sep 8 09:36:33 2016
|
|
||||||
|
|
||||||
@author: barasc
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import datetime
|
|
||||||
import re
|
|
||||||
import unicodedata
|
|
||||||
|
|
||||||
|
|
||||||
from flask import g
|
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
|
||||||
|
|
||||||
from app.models import FormSemestre
|
|
||||||
from app.pe.pe_rcs import TYPES_RCS
|
|
||||||
from app.scodoc import sco_formsemestre
|
|
||||||
from app.scodoc.sco_logos import find_logo
|
|
||||||
|
|
||||||
|
|
||||||
# Generated LaTeX files are encoded as:
|
|
||||||
PE_LATEX_ENCODING = "utf-8"
|
|
||||||
|
|
||||||
# /opt/scodoc/tools/doc_poursuites_etudes
|
|
||||||
REP_DEFAULT_AVIS = os.path.join(scu.SCO_TOOLS_DIR, "doc_poursuites_etudes/")
|
|
||||||
REP_LOCAL_AVIS = os.path.join(scu.SCODOC_CFG_DIR, "doc_poursuites_etudes/")
|
|
||||||
|
|
||||||
PE_DEFAULT_AVIS_LATEX_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_avis.tex"
|
|
||||||
PE_LOCAL_AVIS_LATEX_TMPL = REP_LOCAL_AVIS + "local/modeles/un_avis.tex"
|
|
||||||
PE_DEFAULT_FOOTER_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_footer.tex"
|
|
||||||
PE_LOCAL_FOOTER_TMPL = REP_LOCAL_AVIS + "local/modeles/un_footer.tex"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
"""
|
|
||||||
Descriptif d'un parcours classique BUT
|
|
||||||
|
|
||||||
TODO:: A améliorer si BUT en moins de 6 semestres
|
|
||||||
"""
|
|
||||||
|
|
||||||
NBRE_SEMESTRES_DIPLOMANT = 6
|
|
||||||
AGGREGAT_DIPLOMANT = (
|
|
||||||
"6S" # aggrégat correspondant à la totalité des notes pour le diplôme
|
|
||||||
)
|
|
||||||
TOUS_LES_SEMESTRES = TYPES_RCS[AGGREGAT_DIPLOMANT]["aggregat"]
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------------------
|
|
||||||
def calcul_age(born: datetime.date) -> int:
|
|
||||||
"""Calcule l'age connaissant la date de naissance ``born``. (L'age est calculé
|
|
||||||
à partir de l'horloge système).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
born: La date de naissance
|
|
||||||
|
|
||||||
Return:
|
|
||||||
L'age (au regard de la date actuelle)
|
|
||||||
"""
|
|
||||||
if not born or not isinstance(born, datetime.date):
|
|
||||||
return None
|
|
||||||
|
|
||||||
today = datetime.date.today()
|
|
||||||
return today.year - born.year - ((today.month, today.day) < (born.month, born.day))
|
|
||||||
|
|
||||||
|
|
||||||
# Nota: scu.suppress_accents fait la même chose mais renvoie un str et non un bytes
|
|
||||||
def remove_accents(input_unicode_str: str) -> bytes:
|
|
||||||
"""Supprime les accents d'une chaine unicode"""
|
|
||||||
nfkd_form = unicodedata.normalize("NFKD", input_unicode_str)
|
|
||||||
only_ascii = nfkd_form.encode("ASCII", "ignore")
|
|
||||||
return only_ascii
|
|
||||||
|
|
||||||
|
|
||||||
def escape_for_latex(s):
|
|
||||||
"""Protège les caractères pour inclusion dans du source LaTeX"""
|
|
||||||
if not s:
|
|
||||||
return ""
|
|
||||||
conv = {
|
|
||||||
"&": r"\&",
|
|
||||||
"%": r"\%",
|
|
||||||
"$": r"\$",
|
|
||||||
"#": r"\#",
|
|
||||||
"_": r"\_",
|
|
||||||
"{": r"\{",
|
|
||||||
"}": r"\}",
|
|
||||||
"~": r"\textasciitilde{}",
|
|
||||||
"^": r"\^{}",
|
|
||||||
"\\": r"\textbackslash{}",
|
|
||||||
"<": r"\textless ",
|
|
||||||
">": r"\textgreater ",
|
|
||||||
}
|
|
||||||
exp = re.compile(
|
|
||||||
"|".join(
|
|
||||||
re.escape(key)
|
|
||||||
for key in sorted(list(conv.keys()), key=lambda item: -len(item))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return exp.sub(lambda match: conv[match.group()], s)
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------------------
|
|
||||||
def list_directory_filenames(path: str) -> list[str]:
|
|
||||||
"""List of regular filenames (paths) in a directory (recursive)
|
|
||||||
Excludes files and directories begining with .
|
|
||||||
"""
|
|
||||||
paths = []
|
|
||||||
for root, dirs, files in os.walk(path, topdown=True):
|
|
||||||
dirs[:] = [d for d in dirs if d[0] != "."]
|
|
||||||
paths += [os.path.join(root, fn) for fn in files if fn[0] != "."]
|
|
||||||
return paths
|
|
||||||
|
|
||||||
|
|
||||||
def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip):
|
|
||||||
"""Read pathname server file and add content to zip under path_in_zip"""
|
|
||||||
rooted_path_in_zip = os.path.join(ziproot, path_in_zip)
|
|
||||||
zipfile.write(filename=pathname, arcname=rooted_path_in_zip)
|
|
||||||
# data = open(pathname).read()
|
|
||||||
# zipfile.writestr(rooted_path_in_zip, data)
|
|
||||||
|
|
||||||
|
|
||||||
def add_refs_to_register(register, directory):
|
|
||||||
"""Ajoute les fichiers trouvés dans directory au registre (dictionaire) sous la forme
|
|
||||||
filename => pathname
|
|
||||||
"""
|
|
||||||
length = len(directory)
|
|
||||||
for pathname in list_directory_filenames(directory):
|
|
||||||
filename = pathname[length + 1 :]
|
|
||||||
register[filename] = pathname
|
|
||||||
|
|
||||||
|
|
||||||
def add_pe_stuff_to_zip(zipfile, ziproot):
|
|
||||||
"""Add auxiliary files to (already opened) zip
|
|
||||||
Put all local files found under config/doc_poursuites_etudes/local
|
|
||||||
and config/doc_poursuites_etudes/distrib
|
|
||||||
If a file is present in both subtrees, take the one in local.
|
|
||||||
|
|
||||||
Also copy logos
|
|
||||||
"""
|
|
||||||
register = {}
|
|
||||||
# first add standard (distrib references)
|
|
||||||
distrib_dir = os.path.join(REP_DEFAULT_AVIS, "distrib")
|
|
||||||
add_refs_to_register(register=register, directory=distrib_dir)
|
|
||||||
# then add local references (some oh them may overwrite distrib refs)
|
|
||||||
local_dir = os.path.join(REP_LOCAL_AVIS, "local")
|
|
||||||
add_refs_to_register(register=register, directory=local_dir)
|
|
||||||
# at this point register contains all refs (filename, pathname) to be saved
|
|
||||||
for filename, pathname in register.items():
|
|
||||||
add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename)
|
|
||||||
|
|
||||||
# Logos: (add to logos/ directory in zip)
|
|
||||||
logos_names = ["header", "footer"]
|
|
||||||
for name in logos_names:
|
|
||||||
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
|
|
||||||
if logo is not None:
|
|
||||||
add_local_file_to_zip(
|
|
||||||
zipfile, ziproot, logo.filepath, "avis/logos/" + logo.filename
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------------------
|
|
||||||
def get_annee_diplome_semestre(
|
|
||||||
sem_base: FormSemestre | dict, nbre_sem_formation: int = 6
|
|
||||||
) -> int:
|
|
||||||
"""Pour un semestre ``sem_base`` donné (supposé être un semestre d'une formation BUT
|
|
||||||
à 6 semestres) et connaissant le numéro du semestre, ses dates de début et de fin du
|
|
||||||
semestre, prédit l'année à laquelle sera remis le diplôme BUT des étudiants qui y
|
|
||||||
sont scolarisés (en supposant qu'il n'y ait pas de redoublement à venir).
|
|
||||||
|
|
||||||
**Remarque sur le calcul** : Les semestres de 1ère partie d'année (S1, S3, S5 ou S4,
|
|
||||||
S6 pour des semestres décalés)
|
|
||||||
s'étalent sur deux années civiles ; contrairement au semestre de seconde partie
|
|
||||||
d'année universitaire.
|
|
||||||
|
|
||||||
Par exemple :
|
|
||||||
|
|
||||||
* S5 débutant en 2025 finissant en 2026 : diplome en 2026
|
|
||||||
* S3 debutant en 2025 et finissant en 2026 : diplome en 2027
|
|
||||||
|
|
||||||
La fonction est adaptée au cas des semestres décalés.
|
|
||||||
|
|
||||||
Par exemple :
|
|
||||||
|
|
||||||
* S5 décalé débutant en 2025 et finissant en 2025 : diplome en 2026
|
|
||||||
* S3 décalé débutant en 2025 et finissant en 2025 : diplome en 2027
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sem_base: Le semestre à partir duquel est prédit l'année de diplomation, soit :
|
|
||||||
|
|
||||||
* un ``FormSemestre`` (Scodoc9)
|
|
||||||
* un dict (format compatible avec Scodoc7)
|
|
||||||
|
|
||||||
nbre_sem_formation: Le nombre de semestre prévu dans la formation (par défaut 6 pour un BUT)
|
|
||||||
"""
|
|
||||||
|
|
||||||
if isinstance(sem_base, FormSemestre):
|
|
||||||
sem_id = sem_base.semestre_id
|
|
||||||
annee_fin = sem_base.date_fin.year
|
|
||||||
annee_debut = sem_base.date_debut.year
|
|
||||||
else: # sem_base est un dictionnaire (Scodoc 7)
|
|
||||||
sem_id = sem_base["semestre_id"]
|
|
||||||
annee_fin = int(sem_base["annee_fin"])
|
|
||||||
annee_debut = int(sem_base["annee_debut"])
|
|
||||||
if (
|
|
||||||
1 <= sem_id <= nbre_sem_formation
|
|
||||||
): # Si le semestre est un semestre BUT => problème si formation BUT en 1 an ??
|
|
||||||
nb_sem_restants = (
|
|
||||||
nbre_sem_formation - sem_id
|
|
||||||
) # nombre de semestres restant avant diplome
|
|
||||||
nb_annees_restantes = (
|
|
||||||
nb_sem_restants // 2
|
|
||||||
) # nombre d'annees restant avant diplome
|
|
||||||
# Flag permettant d'activer ou désactiver un increment
|
|
||||||
# à prendre en compte en cas de semestre décalé
|
|
||||||
# avec 1 - delta = 0 si semestre de 1ere partie d'année / 1 sinon
|
|
||||||
delta = annee_fin - annee_debut
|
|
||||||
decalage = nb_sem_restants % 2 # 0 si S4, 1 si S3, 0 si S2, 1 si S1
|
|
||||||
increment = decalage * (1 - delta)
|
|
||||||
return annee_fin + nb_annees_restantes + increment
|
|
||||||
|
|
||||||
|
|
||||||
def get_cosemestres_diplomants(annee_diplome: int) -> dict[int, FormSemestre]:
|
|
||||||
"""Ensemble des cosemestres donnant lieu à diplomation à l'``annee_diplome``.
|
|
||||||
|
|
||||||
**Définition** : Un co-semestre est un semestre :
|
|
||||||
|
|
||||||
* dont l'année de diplômation prédite (sans redoublement) est la même
|
|
||||||
* dont la formation est la même (optionnel)
|
|
||||||
* qui a des étudiants inscrits
|
|
||||||
|
|
||||||
Args:
|
|
||||||
annee_diplome: L'année de diplomation
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Un dictionnaire {fid: FormSemestre(fid)} contenant les cosemestres
|
|
||||||
"""
|
|
||||||
tous_les_sems = (
|
|
||||||
sco_formsemestre.do_formsemestre_list()
|
|
||||||
) # tous les semestres memorisés dans scodoc
|
|
||||||
|
|
||||||
cosemestres_fids = {
|
|
||||||
sem["id"]
|
|
||||||
for sem in tous_les_sems
|
|
||||||
if get_annee_diplome_semestre(sem) == annee_diplome
|
|
||||||
}
|
|
||||||
|
|
||||||
cosemestres = {}
|
|
||||||
for fid in cosemestres_fids:
|
|
||||||
cosem = FormSemestre.get_formsemestre(fid)
|
|
||||||
if len(cosem.etuds_inscriptions) > 0:
|
|
||||||
cosemestres[fid] = cosem
|
|
||||||
|
|
||||||
return cosemestres
|
|
@ -1,618 +0,0 @@
|
|||||||
# -*- mode: python -*-
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gestion scolarite IUT
|
|
||||||
#
|
|
||||||
# Copyright (c) 1999 - 2024 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
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
# Module "Avis de poursuite d'étude"
|
|
||||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""
|
|
||||||
Created on 17/01/2024
|
|
||||||
|
|
||||||
@author: barasc
|
|
||||||
"""
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from app.models import FormSemestre, Identite, Formation
|
|
||||||
from app.pe import pe_comp, pe_affichage
|
|
||||||
from app.scodoc import codes_cursus
|
|
||||||
from app.scodoc import sco_utils as scu
|
|
||||||
from app.comp.res_sem import load_formsemestre_results
|
|
||||||
|
|
||||||
|
|
||||||
class EtudiantsJuryPE:
|
|
||||||
"""Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE"""
|
|
||||||
|
|
||||||
def __init__(self, annee_diplome: int):
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
annee_diplome: L'année de diplomation
|
|
||||||
"""
|
|
||||||
self.annee_diplome = annee_diplome
|
|
||||||
"""L'année du diplôme"""
|
|
||||||
|
|
||||||
self.identites: dict[int, Identite] = {} # ex. ETUDINFO_DICT
|
|
||||||
"Les identités des étudiants traités pour le jury"
|
|
||||||
|
|
||||||
self.cursus: dict[int, dict] = {}
|
|
||||||
"Les cursus (semestres suivis, abandons) des étudiants"
|
|
||||||
|
|
||||||
self.trajectoires = {}
|
|
||||||
"""Les trajectoires/chemins de semestres suivis par les étudiants
|
|
||||||
pour atteindre un aggrégat donné
|
|
||||||
(par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements)"""
|
|
||||||
|
|
||||||
self.etudiants_diplomes = {}
|
|
||||||
"""Les identités des étudiants à considérer au jury (ceux qui seront effectivement
|
|
||||||
diplômés)"""
|
|
||||||
|
|
||||||
self.diplomes_ids = {}
|
|
||||||
"""Les etudids des étudiants diplômés"""
|
|
||||||
|
|
||||||
self.etudiants_ids = {}
|
|
||||||
"""Les etudids des étudiants dont il faut calculer les moyennes/classements
|
|
||||||
(même si d'éventuels abandons).
|
|
||||||
Il s'agit des étudiants inscrits dans les co-semestres (ceux du jury mais aussi
|
|
||||||
d'autres ayant été réorientés ou ayant abandonnés)"""
|
|
||||||
|
|
||||||
self.cosemestres: dict[int, FormSemestre] = None
|
|
||||||
"Les cosemestres donnant lieu à même année de diplome"
|
|
||||||
|
|
||||||
self.abandons = {}
|
|
||||||
"""Les étudiants qui ne seront pas diplômés à ce jury (redoublants/réorientés)"""
|
|
||||||
self.abandons_ids = {}
|
|
||||||
"""Les etudids des étudiants redoublants/réorientés"""
|
|
||||||
|
|
||||||
def find_etudiants(self):
|
|
||||||
"""Liste des étudiants à prendre en compte dans le jury PE, en les recherchant
|
|
||||||
de manière automatique par rapport à leur année de diplomation ``annee_diplome``.
|
|
||||||
|
|
||||||
Les données obtenues sont stockées dans les attributs de EtudiantsJuryPE.
|
|
||||||
|
|
||||||
*Remarque* : ex: JuryPE.get_etudiants_in_jury()
|
|
||||||
"""
|
|
||||||
cosemestres = pe_comp.get_cosemestres_diplomants(self.annee_diplome)
|
|
||||||
self.cosemestres = cosemestres
|
|
||||||
|
|
||||||
pe_affichage.pe_print(
|
|
||||||
f"1) Recherche des coSemestres -> {len(cosemestres)} trouvés"
|
|
||||||
)
|
|
||||||
|
|
||||||
pe_affichage.pe_print("2) Liste des étudiants dans les différents co-semestres")
|
|
||||||
self.etudiants_ids = get_etudiants_dans_semestres(cosemestres)
|
|
||||||
pe_affichage.pe_print(
|
|
||||||
f" => {len(self.etudiants_ids)} étudiants trouvés dans les cosemestres"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Analyse des parcours étudiants pour déterminer leur année effective de diplome
|
|
||||||
# avec prise en compte des redoublements, des abandons, ....
|
|
||||||
pe_affichage.pe_print("3) Analyse des parcours individuels des étudiants")
|
|
||||||
|
|
||||||
for etudid in self.etudiants_ids:
|
|
||||||
self.identites[etudid] = Identite.get_etud(etudid)
|
|
||||||
|
|
||||||
# Analyse son cursus
|
|
||||||
self.analyse_etat_etudiant(etudid, cosemestres)
|
|
||||||
|
|
||||||
# Analyse son parcours pour atteindre chaque semestre de la formation
|
|
||||||
self.structure_cursus_etudiant(etudid)
|
|
||||||
|
|
||||||
# Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris
|
|
||||||
self.etudiants_diplomes = self.get_etudiants_diplomes()
|
|
||||||
self.diplomes_ids = set(self.etudiants_diplomes.keys())
|
|
||||||
self.etudiants_ids = set(self.identites.keys())
|
|
||||||
|
|
||||||
# Les abandons (pour debug)
|
|
||||||
self.abandons = self.get_etudiants_redoublants_ou_reorientes()
|
|
||||||
# Les identités des étudiants ayant redoublés ou ayant abandonnés
|
|
||||||
|
|
||||||
self.abandons_ids = set(self.abandons)
|
|
||||||
# Les identifiants des étudiants ayant redoublés ou ayant abandonnés
|
|
||||||
|
|
||||||
# Synthèse
|
|
||||||
pe_affichage.pe_print(
|
|
||||||
f" => {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}"
|
|
||||||
)
|
|
||||||
nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes)
|
|
||||||
assert nbre_abandons == len(self.abandons_ids)
|
|
||||||
|
|
||||||
pe_affichage.pe_print(
|
|
||||||
f" => {nbre_abandons} étudiants non considérés (redoublement, réorientation, abandon"
|
|
||||||
)
|
|
||||||
# pe_affichage.pe_print(
|
|
||||||
# " => quelques étudiants futurs diplômés : "
|
|
||||||
# + ", ".join([str(etudid) for etudid in list(self.etudiants_diplomes)[:10]])
|
|
||||||
# )
|
|
||||||
# pe_affichage.pe_print(
|
|
||||||
# " => semestres dont il faut calculer les moyennes : "
|
|
||||||
# + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)])
|
|
||||||
# )
|
|
||||||
|
|
||||||
def get_etudiants_diplomes(self) -> dict[int, Identite]:
|
|
||||||
"""Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}`
|
|
||||||
qui vont être à traiter au jury PE pour
|
|
||||||
l'année de diplômation donnée et n'ayant ni été réorienté, ni abandonné.
|
|
||||||
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Un dictionnaire `{etudid: Identite(etudid)}`
|
|
||||||
"""
|
|
||||||
etudids = [
|
|
||||||
etudid
|
|
||||||
for etudid, cursus_etud in self.cursus.items()
|
|
||||||
if cursus_etud["diplome"] == self.annee_diplome
|
|
||||||
and cursus_etud["abandon"] is False
|
|
||||||
]
|
|
||||||
etudiants = {etudid: self.identites[etudid] for etudid in etudids}
|
|
||||||
return etudiants
|
|
||||||
|
|
||||||
def get_etudiants_redoublants_ou_reorientes(self) -> dict[int, Identite]:
|
|
||||||
"""Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}`
|
|
||||||
dont les notes seront prises en compte (pour les classements) mais qui n'apparaitront
|
|
||||||
pas dans le jury car diplômé une autre année (redoublants) ou réorienté ou démissionnaire.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Un dictionnaire `{etudid: Identite(etudid)}`
|
|
||||||
"""
|
|
||||||
etudids = [
|
|
||||||
etudid
|
|
||||||
for etudid, cursus_etud in self.cursus.items()
|
|
||||||
if cursus_etud["diplome"] != self.annee_diplome
|
|
||||||
or cursus_etud["abandon"] is True
|
|
||||||
]
|
|
||||||
etudiants = {etudid: self.identites[etudid] for etudid in etudids}
|
|
||||||
return etudiants
|
|
||||||
|
|
||||||
def analyse_etat_etudiant(self, etudid: int, cosemestres: dict[int, FormSemestre]):
|
|
||||||
"""Analyse le cursus d'un étudiant pouvant être :
|
|
||||||
|
|
||||||
* l'un de ceux sur lesquels le jury va statuer (année de diplômation du jury considéré)
|
|
||||||
* un étudiant qui ne sera pas considéré dans le jury mais qui a participé dans sa scolarité
|
|
||||||
à un (ou plusieurs) semestres communs aux étudiants du jury (et impactera les classements)
|
|
||||||
|
|
||||||
L'analyse consiste :
|
|
||||||
|
|
||||||
* à insérer une entrée dans ``self.cursus`` pour mémoriser son identité,
|
|
||||||
avec son nom, prénom, etc...
|
|
||||||
* à analyser son parcours, pour déterminer s'il n'a (ou non) abandonné l'IUT en cours de
|
|
||||||
route (cf. clé abandon)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
etudid: L'etudid d'un étudiant, à ajouter à ceux traiter par le jury
|
|
||||||
cosemestres: Dictionnaire {fid: Formsemestre(fid)} donnant accès aux cosemestres
|
|
||||||
de même année de diplomation
|
|
||||||
"""
|
|
||||||
identite = Identite.get_etud(etudid)
|
|
||||||
|
|
||||||
# Le cursus global de l'étudiant (restreint aux semestres APC)
|
|
||||||
formsemestres = identite.get_formsemestres()
|
|
||||||
|
|
||||||
semestres_etudiant = {
|
|
||||||
formsemestre.formsemestre_id: formsemestre
|
|
||||||
for formsemestre in formsemestres
|
|
||||||
if formsemestre.formation.is_apc()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.cursus[etudid] = {
|
|
||||||
"etudid": etudid, # les infos sur l'étudiant
|
|
||||||
"etat_civil": identite.etat_civil, # Ajout à la table jury
|
|
||||||
"nom": identite.nom,
|
|
||||||
"entree": formsemestres[-1].date_debut.year, # La date d'entrée à l'IUT
|
|
||||||
"diplome": get_annee_diplome(
|
|
||||||
identite
|
|
||||||
), # Le date prévisionnelle de son diplôme
|
|
||||||
"formsemestres": semestres_etudiant, # les semestres de l'étudiant
|
|
||||||
"nb_semestres": len(
|
|
||||||
semestres_etudiant
|
|
||||||
), # le nombre de semestres de l'étudiant
|
|
||||||
"abandon": False, # va être traité en dessous
|
|
||||||
}
|
|
||||||
|
|
||||||
# Est-il démissionnaire : charge son dernier semestre pour connaitre son état ?
|
|
||||||
dernier_semes_etudiant = formsemestres[0]
|
|
||||||
res = load_formsemestre_results(dernier_semes_etudiant)
|
|
||||||
etud_etat = res.get_etud_etat(etudid)
|
|
||||||
if etud_etat == scu.DEMISSION:
|
|
||||||
self.cursus[etudid]["abandon"] |= True
|
|
||||||
else:
|
|
||||||
# Est-il réorienté ou a-t-il arrêté volontairement sa formation ?
|
|
||||||
self.cursus[etudid]["abandon"] |= arret_de_formation(identite, cosemestres)
|
|
||||||
|
|
||||||
def get_semestres_significatifs(self, etudid: int):
|
|
||||||
"""Ensemble des semestres d'un étudiant, qui l'auraient amené à être diplomé
|
|
||||||
l'année visée (supprime les semestres qui conduisent à une diplomation
|
|
||||||
postérieure à celle du jury visé)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
etudid: L'identifiant d'un étudiant
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Un dictionnaire ``{fid: FormSemestre(fid)`` dans lequel les semestres
|
|
||||||
amènent à une diplomation avant l'annee de diplomation du jury
|
|
||||||
"""
|
|
||||||
semestres_etudiant = self.cursus[etudid]["formsemestres"]
|
|
||||||
semestres_significatifs = {}
|
|
||||||
for fid in semestres_etudiant:
|
|
||||||
semestre = semestres_etudiant[fid]
|
|
||||||
if pe_comp.get_annee_diplome_semestre(semestre) <= self.annee_diplome:
|
|
||||||
semestres_significatifs[fid] = semestre
|
|
||||||
return semestres_significatifs
|
|
||||||
|
|
||||||
def structure_cursus_etudiant(self, etudid: int):
|
|
||||||
"""Structure les informations sur les semestres suivis par un
|
|
||||||
étudiant, pour identifier les semestres qui seront pris en compte lors de ses calculs
|
|
||||||
de moyennes PE.
|
|
||||||
|
|
||||||
Cette structuration s'appuie sur les numéros de semestre: pour chaque Si, stocke :
|
|
||||||
le dernier semestre (en date) de numéro i qu'il a suivi (1 ou 0 si pas encore suivi).
|
|
||||||
Ce semestre influera les interclassement par semestre dans la promo.
|
|
||||||
"""
|
|
||||||
semestres_significatifs = self.get_semestres_significatifs(etudid)
|
|
||||||
|
|
||||||
# Tri des semestres par numéro de semestre
|
|
||||||
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1):
|
|
||||||
# les semestres de n°i de l'étudiant:
|
|
||||||
semestres_i = {
|
|
||||||
fid: sem_sig
|
|
||||||
for fid, sem_sig in semestres_significatifs.items()
|
|
||||||
if sem_sig.semestre_id == i
|
|
||||||
}
|
|
||||||
self.cursus[etudid][f"S{i}"] = semestres_i
|
|
||||||
|
|
||||||
def get_formsemestres_terminaux_aggregat(
|
|
||||||
self, aggregat: str
|
|
||||||
) -> dict[int, FormSemestre]:
|
|
||||||
"""Pour un aggrégat donné, ensemble des formsemestres terminaux possibles pour l'aggrégat
|
|
||||||
(pour l'aggrégat '3S' incluant S1+S2+S3, a pour semestre terminal S3).
|
|
||||||
Ces formsemestres traduisent :
|
|
||||||
|
|
||||||
* les différents parcours des étudiants liés par exemple au choix de modalité
|
|
||||||
(par ex: S1 FI + S2 FI + S3 FI ou S1 FI + S2 FI + S3 UFA), en renvoyant les
|
|
||||||
formsemestre_id du S3 FI et du S3 UFA.
|
|
||||||
* les éventuelles situations de redoublement (par ex pour 1 étudiant ayant
|
|
||||||
redoublé sa 2ème année :
|
|
||||||
S1 + S2 + S3 (1ère session) et S1 + S2 + S3 + S4 + S3 (2ème session), en
|
|
||||||
renvoyant les formsemestre_id du S3 (1ère session) et du S3 (2ème session)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
aggregat: L'aggrégat
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Un dictionnaire ``{fid: FormSemestre(fid)}``
|
|
||||||
"""
|
|
||||||
formsemestres_terminaux = {}
|
|
||||||
for trajectoire_aggr in self.trajectoires.values():
|
|
||||||
trajectoire = trajectoire_aggr[aggregat]
|
|
||||||
if trajectoire:
|
|
||||||
# Le semestre terminal de l'étudiant de l'aggrégat
|
|
||||||
fid = trajectoire.formsemestre_final.formsemestre_id
|
|
||||||
formsemestres_terminaux[fid] = trajectoire.formsemestre_final
|
|
||||||
return formsemestres_terminaux
|
|
||||||
|
|
||||||
def nbre_etapes_max_diplomes(self, etudids: list[int]) -> int:
|
|
||||||
"""Partant d'un ensemble d'étudiants,
|
|
||||||
nombre de semestres (étapes) maximum suivis par les étudiants du jury.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
etudids: Liste d'étudid d'étudiants
|
|
||||||
"""
|
|
||||||
nbres_semestres = []
|
|
||||||
for etudid in etudids:
|
|
||||||
nbres_semestres.append(self.cursus[etudid]["nb_semestres"])
|
|
||||||
if not nbres_semestres:
|
|
||||||
return 0
|
|
||||||
return max(nbres_semestres)
|
|
||||||
|
|
||||||
def df_administratif(self, etudids: list[int]) -> pd.DataFrame:
|
|
||||||
"""Synthétise toutes les données administratives d'un groupe
|
|
||||||
d'étudiants fournis par les etudid dans un dataFrame
|
|
||||||
|
|
||||||
Args:
|
|
||||||
etudids: La liste des étudiants à prendre en compte
|
|
||||||
"""
|
|
||||||
|
|
||||||
etudids = list(etudids)
|
|
||||||
|
|
||||||
# Récupération des données des étudiants
|
|
||||||
administratif = {}
|
|
||||||
nbre_semestres_max = self.nbre_etapes_max_diplomes(etudids)
|
|
||||||
|
|
||||||
for etudid in etudids:
|
|
||||||
etudiant = self.identites[etudid]
|
|
||||||
cursus = self.cursus[etudid]
|
|
||||||
formsemestres = cursus["formsemestres"]
|
|
||||||
|
|
||||||
if cursus["diplome"]:
|
|
||||||
diplome = cursus["diplome"]
|
|
||||||
else:
|
|
||||||
diplome = "indéterminé"
|
|
||||||
|
|
||||||
administratif[etudid] = {
|
|
||||||
"etudid": etudiant.id,
|
|
||||||
"INE": etudiant.code_ine or "",
|
|
||||||
"NIP": etudiant.code_nip or "",
|
|
||||||
"Nom": etudiant.nom,
|
|
||||||
"Prenom": etudiant.prenom,
|
|
||||||
"Civilite": etudiant.civilite_str,
|
|
||||||
"Age": pe_comp.calcul_age(etudiant.date_naissance),
|
|
||||||
"Date entree": cursus["entree"],
|
|
||||||
"Date diplome": diplome,
|
|
||||||
"Nb semestres": len(formsemestres),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Ajout des noms de semestres parcourus
|
|
||||||
etapes = etapes_du_cursus(formsemestres, nbre_semestres_max)
|
|
||||||
administratif[etudid] |= etapes
|
|
||||||
|
|
||||||
# Construction du dataframe
|
|
||||||
df = pd.DataFrame.from_dict(administratif, orient="index")
|
|
||||||
|
|
||||||
# Tri par nom/prénom
|
|
||||||
df.sort_values(by=["Nom", "Prenom"], inplace=True)
|
|
||||||
return df
|
|
||||||
|
|
||||||
|
|
||||||
def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set:
|
|
||||||
"""Ensemble d'identifiants des étudiants (identifiés via leur ``etudid``)
|
|
||||||
inscrits à l'un des semestres de la liste de ``semestres``.
|
|
||||||
|
|
||||||
Remarque : Les ``cosemestres`` sont généralement obtenus avec
|
|
||||||
``sco_formsemestre.do_formsemestre_list()``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
semestres: Un dictionnaire ``{fid: Formsemestre(fid)}`` donnant un
|
|
||||||
ensemble d'identifiant de semestres
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Un ensemble d``etudid``
|
|
||||||
"""
|
|
||||||
|
|
||||||
etudiants_ids = set()
|
|
||||||
for sem in semestres.values(): # pour chacun des semestres de la liste
|
|
||||||
etudiants_du_sem = {ins.etudid for ins in sem.inscriptions}
|
|
||||||
|
|
||||||
pe_affichage.pe_print(f" --> {sem} : {len(etudiants_du_sem)} etudiants")
|
|
||||||
etudiants_ids = (
|
|
||||||
etudiants_ids | etudiants_du_sem
|
|
||||||
) # incluant la suppression des doublons
|
|
||||||
|
|
||||||
return etudiants_ids
|
|
||||||
|
|
||||||
|
|
||||||
def get_annee_diplome(etud: Identite) -> int | None:
|
|
||||||
"""L'année de diplôme prévue d'un étudiant en fonction de ses semestres
|
|
||||||
d'inscription (pour un BUT).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
identite: L'identité d'un étudiant
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
L'année prévue de sa diplômation, ou None si aucun semestre
|
|
||||||
"""
|
|
||||||
formsemestres_apc = get_semestres_apc(etud)
|
|
||||||
|
|
||||||
if formsemestres_apc:
|
|
||||||
dates_possibles_diplome = []
|
|
||||||
# Années de diplômation prédites en fonction des semestres
|
|
||||||
# (d'une formation APC) d'un étudiant
|
|
||||||
for sem_base in formsemestres_apc:
|
|
||||||
annee = pe_comp.get_annee_diplome_semestre(sem_base)
|
|
||||||
if annee:
|
|
||||||
dates_possibles_diplome.append(annee)
|
|
||||||
if dates_possibles_diplome:
|
|
||||||
return max(dates_possibles_diplome)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_semestres_apc(identite: Identite) -> list:
|
|
||||||
"""Liste des semestres d'un étudiant qui corresponde à une formation APC.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
identite: L'identité d'un étudiant
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Liste de ``FormSemestre`` correspondant à une formation APC
|
|
||||||
"""
|
|
||||||
semestres = identite.get_formsemestres()
|
|
||||||
semestres_apc = []
|
|
||||||
for sem in semestres:
|
|
||||||
if sem.formation.is_apc():
|
|
||||||
semestres_apc.append(sem)
|
|
||||||
return semestres_apc
|
|
||||||
|
|
||||||
|
|
||||||
def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool:
|
|
||||||
"""Détermine si un étudiant a arrêté sa formation. Il peut s'agir :
|
|
||||||
|
|
||||||
* d'une réorientation à l'initiative du jury de semestre ou d'une démission
|
|
||||||
(on pourrait utiliser les code NAR pour réorienté & DEM pour démissionnaire
|
|
||||||
des résultats du jury renseigné dans la BDD, mais pas nécessaire ici)
|
|
||||||
|
|
||||||
* d'un arrêt volontaire : l'étudiant disparait des listes d'inscrits (sans pour
|
|
||||||
autant avoir été indiqué NAR ou DEM).
|
|
||||||
|
|
||||||
Dans les cas, on considérera que l'étudiant a arrêté sa formation s'il n'est pas
|
|
||||||
dans l'un des "derniers" cosemestres (semestres conduisant à la même année de diplômation)
|
|
||||||
connu dans Scodoc.
|
|
||||||
|
|
||||||
Par ex: au moment du jury PE en fin de S5 (pas de S6 renseigné dans Scodoc),
|
|
||||||
l'étudiant doit appartenir à une instance des S5 qui conduisent à la diplomation dans
|
|
||||||
l'année visée. S'il n'est que dans un S4, il a sans doute arrêté. A moins qu'il ne soit
|
|
||||||
parti à l'étranger et là, pas de notes.
|
|
||||||
TODO:: Cas de l'étranger, à coder/tester
|
|
||||||
|
|
||||||
**Attention** : Cela suppose que toutes les instances d'un semestre donné
|
|
||||||
(par ex: toutes les instances de S6 accueillant un étudiant soient créées ; sinon les
|
|
||||||
étudiants non inscrits dans un S6 seront considérés comme ayant abandonnés)
|
|
||||||
TODO:: Peut-être à mettre en regard avec les propositions d'inscriptions d'étudiants dans un nouveau semestre
|
|
||||||
|
|
||||||
Pour chaque étudiant, recherche son dernier semestre en date (validé ou non) et
|
|
||||||
regarde s'il n'existe pas parmi les semestres existants dans Scodoc un semestre :
|
|
||||||
* dont les dates sont postérieures (en terme de date de début)
|
|
||||||
* de n° au moins égal à celui de son dernier semestre valide (S5 -> S5 ou S5 -> S6)
|
|
||||||
dans lequel il aurait pu s'inscrire mais ne l'a pas fait.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
etud: L'identité d'un étudiant
|
|
||||||
cosemestres: Les semestres donnant lieu à diplômation (sans redoublement) en date du jury
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Est-il réorienté, démissionnaire ou a-t-il arrêté de son propre chef sa formation ?
|
|
||||||
|
|
||||||
TODO:: A reprendre pour le cas des étudiants à l'étranger
|
|
||||||
TODO:: A reprendre si BUT avec semestres décalés
|
|
||||||
"""
|
|
||||||
# Les semestres APC de l'étudiant
|
|
||||||
semestres = get_semestres_apc(etud)
|
|
||||||
semestres_apc = {sem.semestre_id: sem for sem in semestres}
|
|
||||||
if not semestres_apc:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Son dernier semestre APC en date
|
|
||||||
dernier_formsemestre = get_dernier_semestre_en_date(semestres_apc)
|
|
||||||
numero_dernier_formsemestre = dernier_formsemestre.semestre_id
|
|
||||||
|
|
||||||
# Les numéro de semestres possible dans lesquels il pourrait s'incrire
|
|
||||||
# semestre impair => passage de droit en semestre pair suivant (effet de l'annualisation)
|
|
||||||
if numero_dernier_formsemestre % 2 == 1:
|
|
||||||
numeros_possibles = list(
|
|
||||||
range(numero_dernier_formsemestre + 1, pe_comp.NBRE_SEMESTRES_DIPLOMANT)
|
|
||||||
)
|
|
||||||
# semestre pair => passage en année supérieure ou redoublement
|
|
||||||
else: #
|
|
||||||
numeros_possibles = list(
|
|
||||||
range(
|
|
||||||
max(numero_dernier_formsemestre - 1, 1),
|
|
||||||
pe_comp.NBRE_SEMESTRES_DIPLOMANT,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Y-a-t-il des cosemestres dans lesquels il aurait pu s'incrire ?
|
|
||||||
formsestres_superieurs_possibles = []
|
|
||||||
for fid, sem in cosemestres.items(): # Les semestres ayant des inscrits
|
|
||||||
if (
|
|
||||||
fid != dernier_formsemestre.formsemestre_id
|
|
||||||
and sem.semestre_id in numeros_possibles
|
|
||||||
and sem.date_debut.year >= dernier_formsemestre.date_debut.year
|
|
||||||
):
|
|
||||||
# date de debut des semestres possibles postérieur au dernier semestre de l'étudiant
|
|
||||||
# et de niveau plus élevé que le dernier semestre valide de l'étudiant
|
|
||||||
formsestres_superieurs_possibles.append(fid)
|
|
||||||
|
|
||||||
if len(formsestres_superieurs_possibles) > 0:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSemestre:
|
|
||||||
"""Renvoie le dernier semestre en **date de fin** d'un dictionnaire
|
|
||||||
de semestres (potentiellement non trié) de la forme ``{fid: FormSemestre(fid)}``.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
semestres: Un dictionnaire de semestres
|
|
||||||
|
|
||||||
Return:
|
|
||||||
Le FormSemestre du semestre le plus récent
|
|
||||||
"""
|
|
||||||
if semestres:
|
|
||||||
fid_dernier_semestre = list(semestres.keys())[0]
|
|
||||||
dernier_semestre: FormSemestre = semestres[fid_dernier_semestre]
|
|
||||||
for fid in semestres:
|
|
||||||
if semestres[fid].date_fin > dernier_semestre.date_fin:
|
|
||||||
dernier_semestre = semestres[fid]
|
|
||||||
return dernier_semestre
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def etapes_du_cursus(
|
|
||||||
semestres: dict[int, FormSemestre], nbre_etapes_max: int
|
|
||||||
) -> list[str]:
|
|
||||||
"""Partant d'un dictionnaire de semestres (qui retrace
|
|
||||||
la scolarité d'un étudiant), liste les noms des
|
|
||||||
semestres (en version abbrégée)
|
|
||||||
qu'un étudiant a suivi au cours de sa scolarité à l'IUT.
|
|
||||||
Les noms des semestres sont renvoyés dans un dictionnaire
|
|
||||||
``{"etape i": nom_semestre_a_etape_i}``
|
|
||||||
avec i variant jusqu'à nbre_semestres_max. (S'il n'y a pas de semestre à l'étape i,
|
|
||||||
le nom affiché est vide.
|
|
||||||
|
|
||||||
La fonction suppose la liste des semestres triées par ordre
|
|
||||||
décroissant de date.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
semestres: une liste de ``FormSemestre``
|
|
||||||
nbre_etapes_max: le nombre d'étapes max prise en compte
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Une liste de nom de semestre (dans le même ordre que les ``semestres``)
|
|
||||||
|
|
||||||
See also:
|
|
||||||
app.pe.pe_affichage.nom_semestre_etape
|
|
||||||
"""
|
|
||||||
assert len(semestres) <= nbre_etapes_max
|
|
||||||
|
|
||||||
noms = [nom_semestre_etape(sem, avec_fid=False) for (fid, sem) in semestres.items()]
|
|
||||||
noms = noms[::-1] # trie par ordre croissant
|
|
||||||
|
|
||||||
dico = {f"Etape {i+1}": "" for i in range(nbre_etapes_max)}
|
|
||||||
for i, nom in enumerate(noms): # Charge les noms de semestres
|
|
||||||
dico[f"Etape {i+1}"] = nom
|
|
||||||
return dico
|
|
||||||
|
|
||||||
|
|
||||||
def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str:
|
|
||||||
"""Nom d'un semestre à afficher dans le descriptif des étapes de la scolarité
|
|
||||||
d'un étudiant.
|
|
||||||
|
|
||||||
Par ex: Pour un S2, affiche ``"Semestre 2 FI S014-2015 (129)"`` avec :
|
|
||||||
|
|
||||||
* 2 le numéro du semestre,
|
|
||||||
* FI la modalité,
|
|
||||||
* 2014-2015 les dates
|
|
||||||
|
|
||||||
Args:
|
|
||||||
semestre: Un ``FormSemestre``
|
|
||||||
avec_fid: Ajoute le n° du semestre à la description
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
La chaine de caractères décrivant succintement le semestre
|
|
||||||
"""
|
|
||||||
formation: Formation = semestre.formation
|
|
||||||
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
|
|
||||||
|
|
||||||
description = [
|
|
||||||
parcours.SESSION_NAME.capitalize(),
|
|
||||||
str(semestre.semestre_id),
|
|
||||||
semestre.modalite, # eg FI ou FC
|
|
||||||
f"{semestre.date_debut.year}-{semestre.date_fin.year}",
|
|
||||||
]
|
|
||||||
if avec_fid:
|
|
||||||
description.append(f"({semestre.formsemestre_id})")
|
|
||||||
|
|
||||||
return " ".join(description)
|
|
@ -1,160 +0,0 @@
|
|||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gestion scolarite IUT
|
|
||||||
#
|
|
||||||
# Copyright (c) 1999 - 2024 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
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
# Module "Avis de poursuite d'étude"
|
|
||||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""
|
|
||||||
Created on Thu Sep 8 09:36:33 2016
|
|
||||||
|
|
||||||
@author: barasc
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from app.pe.pe_tabletags import TableTag, MoyenneTag
|
|
||||||
from app.pe.pe_etudiant import EtudiantsJuryPE
|
|
||||||
from app.pe.pe_rcs import RCS, RCSsJuryPE
|
|
||||||
from app.pe.pe_rcstag import RCSTag
|
|
||||||
|
|
||||||
|
|
||||||
class RCSInterclasseTag(TableTag):
|
|
||||||
"""
|
|
||||||
Interclasse l'ensemble des étudiants diplômés à une année
|
|
||||||
donnée (celle du jury), pour un RCS donné (par ex: 'S2', '3S')
|
|
||||||
en reportant :
|
|
||||||
|
|
||||||
* les moyennes obtenues sur la trajectoire qu'il ont suivi pour atteindre
|
|
||||||
le numéro de semestre de fin de l'aggrégat (indépendamment de son
|
|
||||||
formsemestre)
|
|
||||||
* calculant le classement sur les étudiants diplômes
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
nom_rcs: str,
|
|
||||||
etudiants: EtudiantsJuryPE,
|
|
||||||
rcss_jury_pe: RCSsJuryPE,
|
|
||||||
rcss_tags: dict[tuple, RCSTag],
|
|
||||||
):
|
|
||||||
TableTag.__init__(self)
|
|
||||||
|
|
||||||
self.nom_rcs = nom_rcs
|
|
||||||
"""Le nom du RCS interclassé"""
|
|
||||||
|
|
||||||
self.nom = self.get_repr()
|
|
||||||
|
|
||||||
"""Les étudiants diplômés et leurs rcss""" # TODO
|
|
||||||
self.diplomes_ids = etudiants.etudiants_diplomes
|
|
||||||
self.etudiants_diplomes = {etudid for etudid in self.diplomes_ids}
|
|
||||||
# pour les exports sous forme de dataFrame
|
|
||||||
self.etudiants = {
|
|
||||||
etudid: etudiants.identites[etudid].etat_civil
|
|
||||||
for etudid in self.diplomes_ids
|
|
||||||
}
|
|
||||||
|
|
||||||
# Les trajectoires (et leur version tagguées), en ne gardant que
|
|
||||||
# celles associées à l'aggrégat
|
|
||||||
self.rcss: dict[int, RCS] = {}
|
|
||||||
"""Ensemble des trajectoires associées à l'aggrégat"""
|
|
||||||
for trajectoire_id in rcss_jury_pe.rcss:
|
|
||||||
trajectoire = rcss_jury_pe.rcss[trajectoire_id]
|
|
||||||
if trajectoire_id[0] == nom_rcs:
|
|
||||||
self.rcss[trajectoire_id] = trajectoire
|
|
||||||
|
|
||||||
self.trajectoires_taggues: dict[int, RCS] = {}
|
|
||||||
"""Ensemble des trajectoires tagguées associées à l'aggrégat"""
|
|
||||||
for trajectoire_id in self.rcss:
|
|
||||||
self.trajectoires_taggues[trajectoire_id] = rcss_tags[trajectoire_id]
|
|
||||||
|
|
||||||
# Les trajectoires suivies par les étudiants du jury, en ne gardant que
|
|
||||||
# celles associées aux diplomés
|
|
||||||
self.suivi: dict[int, RCS] = {}
|
|
||||||
"""Association entre chaque étudiant et la trajectoire tagguée à prendre en
|
|
||||||
compte pour l'aggrégat"""
|
|
||||||
for etudid in self.diplomes_ids:
|
|
||||||
self.suivi[etudid] = rcss_jury_pe.suivi[etudid][nom_rcs]
|
|
||||||
|
|
||||||
self.tags_sorted = self.do_taglist()
|
|
||||||
"""Liste des tags (triés par ordre alphabétique)"""
|
|
||||||
|
|
||||||
# Construit la matrice de notes
|
|
||||||
self.notes = self.compute_notes_matrice()
|
|
||||||
"""Matrice des notes de l'aggrégat"""
|
|
||||||
|
|
||||||
# Synthétise les moyennes/classements par tag
|
|
||||||
self.moyennes_tags: dict[str, MoyenneTag] = {}
|
|
||||||
for tag in self.tags_sorted:
|
|
||||||
moy_gen_tag = self.notes[tag]
|
|
||||||
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag)
|
|
||||||
|
|
||||||
# Est significatif ? (aka a-t-il des tags et des notes)
|
|
||||||
self.significatif = len(self.tags_sorted) > 0
|
|
||||||
|
|
||||||
def get_repr(self) -> str:
|
|
||||||
"""Une représentation textuelle"""
|
|
||||||
return f"Aggrégat {self.nom_rcs}"
|
|
||||||
|
|
||||||
def do_taglist(self):
|
|
||||||
"""Synthétise les tags à partir des trajectoires_tagguées
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Une liste de tags triés par ordre alphabétique
|
|
||||||
"""
|
|
||||||
tags = []
|
|
||||||
for trajectoire in self.trajectoires_taggues.values():
|
|
||||||
tags.extend(trajectoire.tags_sorted)
|
|
||||||
return sorted(set(tags))
|
|
||||||
|
|
||||||
def compute_notes_matrice(self):
|
|
||||||
"""Construit la matrice de notes (etudid x tags)
|
|
||||||
retraçant les moyennes obtenues par les étudiants dans les semestres associés à
|
|
||||||
l'aggrégat (une trajectoire ayant pour numéro de semestre final, celui de l'aggrégat).
|
|
||||||
"""
|
|
||||||
# nb_tags = len(self.tags_sorted) unused ?
|
|
||||||
# nb_etudiants = len(self.diplomes_ids)
|
|
||||||
|
|
||||||
# Index de la matrice (etudids -> dim 0, tags -> dim 1)
|
|
||||||
etudids = list(self.diplomes_ids)
|
|
||||||
tags = self.tags_sorted
|
|
||||||
|
|
||||||
# Partant d'un dataframe vierge
|
|
||||||
df = pd.DataFrame(np.nan, index=etudids, columns=tags)
|
|
||||||
|
|
||||||
for trajectoire in self.trajectoires_taggues.values():
|
|
||||||
# Charge les moyennes par tag de la trajectoire tagguée
|
|
||||||
notes = trajectoire.notes
|
|
||||||
# Etudiants/Tags communs entre la trajectoire_tagguée et les données interclassées
|
|
||||||
etudids_communs = df.index.intersection(notes.index)
|
|
||||||
tags_communs = df.columns.intersection(notes.columns)
|
|
||||||
|
|
||||||
# Injecte les notes par tag
|
|
||||||
df.loc[etudids_communs, tags_communs] = notes.loc[
|
|
||||||
etudids_communs, tags_communs
|
|
||||||
]
|
|
||||||
|
|
||||||
return df
|
|
@ -1,734 +0,0 @@
|
|||||||
# -*- mode: python -*-
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gestion scolarite IUT
|
|
||||||
#
|
|
||||||
# Copyright (c) 1999 - 2024 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
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
# Module "Avis de poursuite d'étude"
|
|
||||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""
|
|
||||||
Created on Fri Sep 9 09:15:05 2016
|
|
||||||
|
|
||||||
@author: barasc
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
# Ensemble des fonctions et des classes
|
|
||||||
# permettant les calculs preliminaires (hors affichage)
|
|
||||||
# a l'edition d'un jury de poursuites d'etudes
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
|
|
||||||
import io
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
from zipfile import ZipFile
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE
|
|
||||||
import app.pe.pe_affichage as pe_affichage
|
|
||||||
from app.pe.pe_etudiant import * # TODO A éviter -> pe_etudiant.
|
|
||||||
from app.pe.pe_rcs import * # TODO A éviter
|
|
||||||
from app.pe.pe_rcstag import RCSTag
|
|
||||||
from app.pe.pe_semtag import SemestreTag
|
|
||||||
from app.pe.pe_interclasstag import RCSInterclasseTag
|
|
||||||
|
|
||||||
|
|
||||||
class JuryPE(object):
|
|
||||||
"""
|
|
||||||
Classe mémorisant toutes les informations nécessaires pour établir un jury de PE, sur la base
|
|
||||||
d'une année de diplôme. De ce semestre est déduit :
|
|
||||||
1. l'année d'obtention du DUT,
|
|
||||||
2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, diplome):
|
|
||||||
pe_affichage.pe_start_log()
|
|
||||||
self.diplome = diplome
|
|
||||||
"L'année du diplome"
|
|
||||||
|
|
||||||
self.nom_export_zip = f"Jury_PE_{self.diplome}"
|
|
||||||
"Nom du zip où ranger les fichiers générés"
|
|
||||||
|
|
||||||
pe_affichage.pe_print(
|
|
||||||
f"Données de poursuite d'étude générées le {time.strftime('%d/%m/%Y à %H:%M')}\n"
|
|
||||||
)
|
|
||||||
# Chargement des étudiants à prendre en compte dans le jury
|
|
||||||
pe_affichage.pe_print(
|
|
||||||
f"""*** Recherche et chargement des étudiants diplômés en {
|
|
||||||
self.diplome}"""
|
|
||||||
)
|
|
||||||
self.etudiants = EtudiantsJuryPE(self.diplome) # Les infos sur les étudiants
|
|
||||||
self.etudiants.find_etudiants()
|
|
||||||
self.diplomes_ids = self.etudiants.diplomes_ids
|
|
||||||
|
|
||||||
self.zipdata = io.BytesIO()
|
|
||||||
with ZipFile(self.zipdata, "w") as zipfile:
|
|
||||||
if not self.diplomes_ids:
|
|
||||||
pe_affichage.pe_print("*** Aucun étudiant diplômé")
|
|
||||||
else:
|
|
||||||
self._gen_xls_diplomes(zipfile)
|
|
||||||
self._gen_xls_semestre_taggues(zipfile)
|
|
||||||
self._gen_xls_rcss_tags(zipfile)
|
|
||||||
self._gen_xls_interclassements_rcss(zipfile)
|
|
||||||
self._gen_xls_synthese_jury_par_tag(zipfile)
|
|
||||||
self._gen_xls_synthese_par_etudiant(zipfile)
|
|
||||||
# et le log
|
|
||||||
self._add_log_to_zip(zipfile)
|
|
||||||
|
|
||||||
# Fin !!!! Tada :)
|
|
||||||
|
|
||||||
def _gen_xls_diplomes(self, zipfile: ZipFile):
|
|
||||||
"Intègre le bilan des semestres taggués au zip"
|
|
||||||
output = io.BytesIO()
|
|
||||||
with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated
|
|
||||||
output, engine="openpyxl"
|
|
||||||
) as writer:
|
|
||||||
if self.diplomes_ids:
|
|
||||||
onglet = "diplômés"
|
|
||||||
df_diplome = self.etudiants.df_administratif(self.diplomes_ids)
|
|
||||||
df_diplome.to_excel(writer, onglet, index=True, header=True)
|
|
||||||
if self.etudiants.abandons_ids:
|
|
||||||
onglet = "redoublants-réorientés"
|
|
||||||
df_abandon = self.etudiants.df_administratif(
|
|
||||||
self.etudiants.abandons_ids
|
|
||||||
)
|
|
||||||
df_abandon.to_excel(writer, onglet, index=True, header=True)
|
|
||||||
output.seek(0)
|
|
||||||
|
|
||||||
self.add_file_to_zip(
|
|
||||||
zipfile,
|
|
||||||
f"etudiants_{self.diplome}.xlsx",
|
|
||||||
output.read(),
|
|
||||||
path="details",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _gen_xls_semestre_taggues(self, zipfile: ZipFile):
|
|
||||||
"Génère les semestres taggués (avec le calcul des moyennes) pour le jury PE"
|
|
||||||
pe_affichage.pe_print("*** Génère les semestres taggués")
|
|
||||||
self.sems_tags = compute_semestres_tag(self.etudiants)
|
|
||||||
|
|
||||||
# Intègre le bilan des semestres taggués au zip final
|
|
||||||
output = io.BytesIO()
|
|
||||||
with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated
|
|
||||||
output, engine="openpyxl"
|
|
||||||
) as writer:
|
|
||||||
for formsemestretag in self.sems_tags.values():
|
|
||||||
onglet = formsemestretag.nom
|
|
||||||
df = formsemestretag.df_moyennes_et_classements()
|
|
||||||
# écriture dans l'onglet
|
|
||||||
df.to_excel(writer, onglet, index=True, header=True)
|
|
||||||
output.seek(0)
|
|
||||||
|
|
||||||
self.add_file_to_zip(
|
|
||||||
zipfile,
|
|
||||||
f"semestres_taggues_{self.diplome}.xlsx",
|
|
||||||
output.read(),
|
|
||||||
path="details",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _gen_xls_rcss_tags(self, zipfile: ZipFile):
|
|
||||||
"""Génère les RCS (combinaisons de semestres suivis
|
|
||||||
par un étudiant)
|
|
||||||
"""
|
|
||||||
pe_affichage.pe_print(
|
|
||||||
"*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants"
|
|
||||||
)
|
|
||||||
self.rcss = RCSsJuryPE(self.diplome)
|
|
||||||
self.rcss.cree_rcss(self.etudiants)
|
|
||||||
|
|
||||||
# Génère les moyennes par tags des trajectoires
|
|
||||||
pe_affichage.pe_print("*** Calcule les moyennes par tag des RCS possibles")
|
|
||||||
self.rcss_tags = compute_trajectoires_tag(
|
|
||||||
self.rcss, self.etudiants, self.sems_tags
|
|
||||||
)
|
|
||||||
|
|
||||||
# Intègre le bilan des trajectoires tagguées au zip final
|
|
||||||
output = io.BytesIO()
|
|
||||||
with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated
|
|
||||||
output, engine="openpyxl"
|
|
||||||
) as writer:
|
|
||||||
for rcs_tag in self.rcss_tags.values():
|
|
||||||
onglet = rcs_tag.get_repr()
|
|
||||||
df = rcs_tag.df_moyennes_et_classements()
|
|
||||||
# écriture dans l'onglet
|
|
||||||
df.to_excel(writer, onglet, index=True, header=True)
|
|
||||||
output.seek(0)
|
|
||||||
|
|
||||||
self.add_file_to_zip(
|
|
||||||
zipfile,
|
|
||||||
f"RCS_taggues_{self.diplome}.xlsx",
|
|
||||||
output.read(),
|
|
||||||
path="details",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _gen_xls_interclassements_rcss(self, zipfile: ZipFile):
|
|
||||||
"""Intègre le bilan des RCS (interclassé par promo) au zip"""
|
|
||||||
# Génère les interclassements (par promo et) par (nom d') aggrégat
|
|
||||||
pe_affichage.pe_print("*** Génère les interclassements par aggrégat")
|
|
||||||
self.interclassements_taggues = compute_interclassements(
|
|
||||||
self.etudiants, self.rcss, self.rcss_tags
|
|
||||||
)
|
|
||||||
|
|
||||||
# Intègre le bilan des aggrégats (interclassé par promo) au zip final
|
|
||||||
output = io.BytesIO()
|
|
||||||
with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated
|
|
||||||
output, engine="openpyxl"
|
|
||||||
) as writer:
|
|
||||||
for interclass_tag in self.interclassements_taggues.values():
|
|
||||||
if interclass_tag.significatif: # Avec des notes
|
|
||||||
onglet = interclass_tag.get_repr()
|
|
||||||
df = interclass_tag.df_moyennes_et_classements()
|
|
||||||
# écriture dans l'onglet
|
|
||||||
df.to_excel(writer, onglet, index=True, header=True)
|
|
||||||
output.seek(0)
|
|
||||||
|
|
||||||
self.add_file_to_zip(
|
|
||||||
zipfile,
|
|
||||||
f"interclassements_taggues_{self.diplome}.xlsx",
|
|
||||||
output.read(),
|
|
||||||
path="details",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _gen_xls_synthese_jury_par_tag(self, zipfile: ZipFile):
|
|
||||||
"""Synthèse des éléments du jury PE tag par tag"""
|
|
||||||
# Synthèse des éléments du jury PE
|
|
||||||
self.synthese = self.synthetise_jury_par_tags()
|
|
||||||
|
|
||||||
# Export des données => mode 1 seule feuille -> supprimé
|
|
||||||
pe_affichage.pe_print("*** Export du jury de synthese par tags")
|
|
||||||
output = io.BytesIO()
|
|
||||||
with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated
|
|
||||||
output, engine="openpyxl"
|
|
||||||
) as writer:
|
|
||||||
for onglet, df in self.synthese.items():
|
|
||||||
# écriture dans l'onglet:
|
|
||||||
df.to_excel(writer, onglet, index=True, header=True)
|
|
||||||
output.seek(0)
|
|
||||||
|
|
||||||
self.add_file_to_zip(
|
|
||||||
zipfile, f"synthese_jury_{self.diplome}_par_tag.xlsx", output.read()
|
|
||||||
)
|
|
||||||
|
|
||||||
def _gen_xls_synthese_par_etudiant(self, zipfile: ZipFile):
|
|
||||||
"""Synthèse des éléments du jury PE, étudiant par étudiant"""
|
|
||||||
# Synthèse des éléments du jury PE
|
|
||||||
synthese = self.synthetise_jury_par_etudiants()
|
|
||||||
|
|
||||||
# Export des données => mode 1 seule feuille -> supprimé
|
|
||||||
pe_affichage.pe_print("*** Export du jury de synthese par étudiants")
|
|
||||||
output = io.BytesIO()
|
|
||||||
with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated
|
|
||||||
output, engine="openpyxl"
|
|
||||||
) as writer:
|
|
||||||
for onglet, df in synthese.items():
|
|
||||||
# écriture dans l'onglet:
|
|
||||||
df.to_excel(writer, onglet, index=True, header=True)
|
|
||||||
output.seek(0)
|
|
||||||
|
|
||||||
self.add_file_to_zip(
|
|
||||||
zipfile, f"synthese_jury_{self.diplome}_par_etudiant.xlsx", output.read()
|
|
||||||
)
|
|
||||||
|
|
||||||
def _add_log_to_zip(self, zipfile):
|
|
||||||
"""Add a text file with the log messages"""
|
|
||||||
log_data = pe_affichage.pe_get_log()
|
|
||||||
self.add_file_to_zip(zipfile, "pe_log.txt", log_data)
|
|
||||||
|
|
||||||
def add_file_to_zip(self, zipfile: ZipFile, filename: str, data, path=""):
|
|
||||||
"""Add a file to given zip
|
|
||||||
All files under NOM_EXPORT_ZIP/
|
|
||||||
path may specify a subdirectory
|
|
||||||
|
|
||||||
Args:
|
|
||||||
zipfile: ZipFile
|
|
||||||
filename: Le nom du fichier à intégrer au zip
|
|
||||||
data: Les données du fichier
|
|
||||||
path: Un dossier dans l'arborescence du zip
|
|
||||||
"""
|
|
||||||
path_in_zip = os.path.join(path, filename) # self.nom_export_zip,
|
|
||||||
zipfile.writestr(path_in_zip, data)
|
|
||||||
|
|
||||||
def get_zipped_data(self) -> io.BytesIO | None:
|
|
||||||
"""returns file-like data with a zip of all generated (CSV) files.
|
|
||||||
Warning: reset stream to the begining.
|
|
||||||
"""
|
|
||||||
self.zipdata.seek(0)
|
|
||||||
return self.zipdata
|
|
||||||
|
|
||||||
def do_tags_list(self, interclassements: dict[str, RCSInterclasseTag]):
|
|
||||||
"""La liste des tags extraites des interclassements"""
|
|
||||||
tags = []
|
|
||||||
for aggregat in interclassements:
|
|
||||||
interclass = interclassements[aggregat]
|
|
||||||
if interclass.tags_sorted:
|
|
||||||
tags.extend(interclass.tags_sorted)
|
|
||||||
tags = sorted(set(tags))
|
|
||||||
return tags
|
|
||||||
|
|
||||||
# **************************************************************************************************************** #
|
|
||||||
# Méthodes pour la synthèse du juryPE
|
|
||||||
# *****************************************************************************************************************
|
|
||||||
|
|
||||||
def synthetise_jury_par_tags(self) -> dict[pd.DataFrame]:
|
|
||||||
"""Synthétise tous les résultats du jury PE dans des dataframes,
|
|
||||||
dont les onglets sont les tags"""
|
|
||||||
|
|
||||||
pe_affichage.pe_print("*** Synthèse finale des moyennes par tag***")
|
|
||||||
|
|
||||||
synthese = {}
|
|
||||||
pe_affichage.pe_print(" -> Synthèse des données administratives")
|
|
||||||
synthese["administratif"] = self.etudiants.df_administratif(self.diplomes_ids)
|
|
||||||
|
|
||||||
tags = self.do_tags_list(self.interclassements_taggues)
|
|
||||||
for tag in tags:
|
|
||||||
pe_affichage.pe_print(f" -> Synthèse du tag {tag}")
|
|
||||||
synthese[tag] = self.df_tag(tag)
|
|
||||||
return synthese
|
|
||||||
|
|
||||||
def df_tag(self, tag):
|
|
||||||
"""Génère le DataFrame synthétisant les moyennes/classements (groupe,
|
|
||||||
interclassement promo) pour tous les aggrégats prévus,
|
|
||||||
tels que fourni dans l'excel final.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tag: Un des tags (a minima `but`)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
"""
|
|
||||||
|
|
||||||
etudids = list(self.diplomes_ids)
|
|
||||||
|
|
||||||
# Les données des étudiants
|
|
||||||
donnees_etudiants = {}
|
|
||||||
for etudid in etudids:
|
|
||||||
etudiant = self.etudiants.identites[etudid]
|
|
||||||
donnees_etudiants[etudid] = {
|
|
||||||
("Identité", "", "Civilite"): etudiant.civilite_str,
|
|
||||||
("Identité", "", "Nom"): etudiant.nom,
|
|
||||||
("Identité", "", "Prenom"): etudiant.prenom,
|
|
||||||
}
|
|
||||||
df_synthese = pd.DataFrame.from_dict(donnees_etudiants, orient="index")
|
|
||||||
|
|
||||||
# Ajout des aggrégats
|
|
||||||
for aggregat in TOUS_LES_RCS:
|
|
||||||
descr = TYPES_RCS[aggregat]["descr"]
|
|
||||||
|
|
||||||
# Les trajectoires (tagguées) suivies par les étudiants pour l'aggrégat et le tag
|
|
||||||
# considéré
|
|
||||||
trajectoires_tagguees = []
|
|
||||||
for etudid in etudids:
|
|
||||||
trajectoire = self.rcss.suivi[etudid][aggregat]
|
|
||||||
if trajectoire:
|
|
||||||
tid = trajectoire.rcs_id
|
|
||||||
trajectoire_tagguee = self.rcss_tags[tid]
|
|
||||||
if (
|
|
||||||
tag in trajectoire_tagguee.moyennes_tags
|
|
||||||
and trajectoire_tagguee not in trajectoires_tagguees
|
|
||||||
):
|
|
||||||
trajectoires_tagguees.append(trajectoire_tagguee)
|
|
||||||
|
|
||||||
# Combien de notes vont être injectées ?
|
|
||||||
nbre_notes_injectees = 0
|
|
||||||
for traj in trajectoires_tagguees:
|
|
||||||
moy_traj = traj.moyennes_tags[tag]
|
|
||||||
inscrits_traj = moy_traj.inscrits_ids
|
|
||||||
etudids_communs = set(etudids) & set(inscrits_traj)
|
|
||||||
nbre_notes_injectees += len(etudids_communs)
|
|
||||||
|
|
||||||
# Si l'aggrégat est significatif (aka il y a des notes)
|
|
||||||
if nbre_notes_injectees > 0:
|
|
||||||
# Ajout des données classements & statistiques
|
|
||||||
nom_stat_promo = f"{NOM_STAT_PROMO} {self.diplome}"
|
|
||||||
donnees = pd.DataFrame(
|
|
||||||
index=etudids,
|
|
||||||
columns=[
|
|
||||||
[descr] * (1 + 4 * 2),
|
|
||||||
[""] + [NOM_STAT_GROUPE] * 4 + [nom_stat_promo] * 4,
|
|
||||||
["note"] + ["class.", "min", "moy", "max"] * 2,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
for traj in trajectoires_tagguees:
|
|
||||||
# Les données des trajectoires_tagguees
|
|
||||||
moy_traj = traj.moyennes_tags[tag]
|
|
||||||
|
|
||||||
# Les étudiants communs entre tableur de synthèse et trajectoires
|
|
||||||
inscrits_traj = moy_traj.inscrits_ids
|
|
||||||
etudids_communs = list(set(etudids) & set(inscrits_traj))
|
|
||||||
|
|
||||||
# Les notes
|
|
||||||
champ = (descr, "", "note")
|
|
||||||
notes_traj = moy_traj.get_notes()
|
|
||||||
donnees.loc[etudids_communs, champ] = notes_traj.loc[
|
|
||||||
etudids_communs
|
|
||||||
]
|
|
||||||
|
|
||||||
# Les rangs
|
|
||||||
champ = (descr, NOM_STAT_GROUPE, "class.")
|
|
||||||
rangs = moy_traj.get_rangs_inscrits()
|
|
||||||
donnees.loc[etudids_communs, champ] = rangs.loc[etudids_communs]
|
|
||||||
|
|
||||||
# Les mins
|
|
||||||
champ = (descr, NOM_STAT_GROUPE, "min")
|
|
||||||
mins = moy_traj.get_min()
|
|
||||||
donnees.loc[etudids_communs, champ] = mins.loc[etudids_communs]
|
|
||||||
|
|
||||||
# Les max
|
|
||||||
champ = (descr, NOM_STAT_GROUPE, "max")
|
|
||||||
maxs = moy_traj.get_max()
|
|
||||||
donnees.loc[etudids_communs, champ] = maxs.loc[etudids_communs]
|
|
||||||
|
|
||||||
# Les moys
|
|
||||||
champ = (descr, NOM_STAT_GROUPE, "moy")
|
|
||||||
moys = moy_traj.get_moy()
|
|
||||||
donnees.loc[etudids_communs, champ] = moys.loc[etudids_communs]
|
|
||||||
|
|
||||||
# Ajoute les données d'interclassement
|
|
||||||
interclass = self.interclassements_taggues[aggregat]
|
|
||||||
moy_interclass = interclass.moyennes_tags[tag]
|
|
||||||
|
|
||||||
# Les étudiants communs entre tableur de synthèse et l'interclassement
|
|
||||||
inscrits_interclass = moy_interclass.inscrits_ids
|
|
||||||
etudids_communs = list(set(etudids) & set(inscrits_interclass))
|
|
||||||
|
|
||||||
# Les classements d'interclassement
|
|
||||||
champ = (descr, nom_stat_promo, "class.")
|
|
||||||
rangs = moy_interclass.get_rangs_inscrits()
|
|
||||||
donnees.loc[etudids_communs, champ] = rangs.loc[etudids_communs]
|
|
||||||
|
|
||||||
# Les mins
|
|
||||||
champ = (descr, nom_stat_promo, "min")
|
|
||||||
mins = moy_interclass.get_min()
|
|
||||||
donnees.loc[etudids_communs, champ] = mins.loc[etudids_communs]
|
|
||||||
|
|
||||||
# Les max
|
|
||||||
champ = (descr, nom_stat_promo, "max")
|
|
||||||
maxs = moy_interclass.get_max()
|
|
||||||
donnees.loc[etudids_communs, champ] = maxs.loc[etudids_communs]
|
|
||||||
|
|
||||||
# Les moys
|
|
||||||
champ = (descr, nom_stat_promo, "moy")
|
|
||||||
moys = moy_interclass.get_moy()
|
|
||||||
donnees.loc[etudids_communs, champ] = moys.loc[etudids_communs]
|
|
||||||
|
|
||||||
df_synthese = df_synthese.join(donnees)
|
|
||||||
# Fin de l'aggrégat
|
|
||||||
|
|
||||||
# Tri par nom/prénom
|
|
||||||
df_synthese.sort_values(
|
|
||||||
by=[("Identité", "", "Nom"), ("Identité", "", "Prenom")], inplace=True
|
|
||||||
)
|
|
||||||
return df_synthese
|
|
||||||
|
|
||||||
def synthetise_jury_par_etudiants(self) -> dict[pd.DataFrame]:
|
|
||||||
"""Synthétise tous les résultats du jury PE dans des dataframes,
|
|
||||||
dont les onglets sont les étudiants"""
|
|
||||||
pe_affichage.pe_print("*** Synthèse finale des moyennes par étudiants***")
|
|
||||||
|
|
||||||
synthese = {}
|
|
||||||
pe_affichage.pe_print(" -> Synthèse des données administratives")
|
|
||||||
synthese["administratif"] = self.etudiants.df_administratif(self.diplomes_ids)
|
|
||||||
|
|
||||||
etudids = list(self.diplomes_ids)
|
|
||||||
|
|
||||||
for etudid in etudids:
|
|
||||||
etudiant = self.etudiants.identites[etudid]
|
|
||||||
nom = etudiant.nom
|
|
||||||
prenom = etudiant.prenom[0] # initial du prénom
|
|
||||||
|
|
||||||
onglet = f"{nom} {prenom}. ({etudid})"
|
|
||||||
if len(onglet) > 32: # limite sur la taille des onglets
|
|
||||||
fin_onglet = f"{prenom}. ({etudid})"
|
|
||||||
onglet = f"{nom[:32-len(fin_onglet)-2]}." + fin_onglet
|
|
||||||
|
|
||||||
pe_affichage.pe_print(f" -> Synthèse de l'étudiant {etudid}")
|
|
||||||
synthese[onglet] = self.df_synthese_etudiant(etudid)
|
|
||||||
return synthese
|
|
||||||
|
|
||||||
def df_synthese_etudiant(self, etudid: int) -> pd.DataFrame:
|
|
||||||
"""Créé un DataFrame pour un étudiant donné par son etudid, retraçant
|
|
||||||
toutes ses moyennes aux différents tag et aggrégats"""
|
|
||||||
tags = self.do_tags_list(self.interclassements_taggues)
|
|
||||||
|
|
||||||
donnees = {}
|
|
||||||
|
|
||||||
for tag in tags:
|
|
||||||
# Une ligne pour le tag
|
|
||||||
donnees[tag] = {("", "", "tag"): tag}
|
|
||||||
|
|
||||||
for aggregat in TOUS_LES_RCS:
|
|
||||||
# Le dictionnaire par défaut des moyennes
|
|
||||||
donnees[tag] |= get_defaut_dict_synthese_aggregat(
|
|
||||||
aggregat, self.diplome
|
|
||||||
)
|
|
||||||
|
|
||||||
# La trajectoire de l'étudiant sur l'aggrégat
|
|
||||||
trajectoire = self.rcss.suivi[etudid][aggregat]
|
|
||||||
if trajectoire:
|
|
||||||
trajectoire_tagguee = self.rcss_tags[trajectoire.rcs_id]
|
|
||||||
if tag in trajectoire_tagguee.moyennes_tags:
|
|
||||||
# L'interclassement
|
|
||||||
interclass = self.interclassements_taggues[aggregat]
|
|
||||||
|
|
||||||
# Injection des données dans un dictionnaire
|
|
||||||
donnees[tag] |= get_dict_synthese_aggregat(
|
|
||||||
aggregat,
|
|
||||||
trajectoire_tagguee,
|
|
||||||
interclass,
|
|
||||||
etudid,
|
|
||||||
tag,
|
|
||||||
self.diplome,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Fin de l'aggrégat
|
|
||||||
# Construction du dataFrame
|
|
||||||
df = pd.DataFrame.from_dict(donnees, orient="index")
|
|
||||||
|
|
||||||
# Tri par nom/prénom
|
|
||||||
df.sort_values(by=[("", "", "tag")], inplace=True)
|
|
||||||
return df
|
|
||||||
|
|
||||||
|
|
||||||
def get_formsemestres_etudiants(etudiants: EtudiantsJuryPE) -> dict:
|
|
||||||
"""Ayant connaissance des étudiants dont il faut calculer les moyennes pour
|
|
||||||
le jury PE (attribut `self.etudiant_ids) et de leur cursus (semestres
|
|
||||||
parcourus),
|
|
||||||
renvoie un dictionnaire ``{fid: FormSemestre(fid)}``
|
|
||||||
contenant l'ensemble des formsemestres de leurs cursus, dont il faudra calculer
|
|
||||||
la moyenne.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
etudiants: Les étudiants du jury PE
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Un dictionnaire de la forme `{fid: FormSemestre(fid)}`
|
|
||||||
|
|
||||||
"""
|
|
||||||
semestres = {}
|
|
||||||
for etudid in etudiants.etudiants_ids:
|
|
||||||
for cle in etudiants.cursus[etudid]:
|
|
||||||
if cle.startswith("S"):
|
|
||||||
semestres = semestres | etudiants.cursus[etudid][cle]
|
|
||||||
return semestres
|
|
||||||
|
|
||||||
|
|
||||||
def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
|
|
||||||
"""Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés.
|
|
||||||
Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire
|
|
||||||
des étudiants (cf. attribut etudiants.cursus).
|
|
||||||
En crééant le semestre taggué, sont calculées les moyennes/classements par tag associé.
|
|
||||||
.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
etudiants: Un groupe d'étudiants participant au jury
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Un dictionnaire {fid: SemestreTag(fid)}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Création des semestres taggués, de type 'S1', 'S2', ...
|
|
||||||
pe_affichage.pe_print("*** Création des semestres taggués")
|
|
||||||
|
|
||||||
formsemestres = get_formsemestres_etudiants(etudiants)
|
|
||||||
|
|
||||||
semestres_tags = {}
|
|
||||||
for frmsem_id, formsemestre in formsemestres.items():
|
|
||||||
# Crée le semestre_tag et exécute les calculs de moyennes
|
|
||||||
formsemestretag = SemestreTag(frmsem_id)
|
|
||||||
pe_affichage.pe_print(
|
|
||||||
f" --> Semestre taggué {formsemestretag.nom} sur la base de {formsemestre}"
|
|
||||||
)
|
|
||||||
# Stocke le semestre taggué
|
|
||||||
semestres_tags[frmsem_id] = formsemestretag
|
|
||||||
|
|
||||||
return semestres_tags
|
|
||||||
|
|
||||||
|
|
||||||
def compute_trajectoires_tag(
|
|
||||||
trajectoires: RCSsJuryPE,
|
|
||||||
etudiants: EtudiantsJuryPE,
|
|
||||||
semestres_taggues: dict[int, SemestreTag],
|
|
||||||
):
|
|
||||||
"""Créée les trajectoires tagguées (combinaison aggrégeant plusieurs semestres au sens
|
|
||||||
d'un aggrégat (par ex: '3S')),
|
|
||||||
en calculant les moyennes et les classements par tag pour chacune.
|
|
||||||
|
|
||||||
Pour rappel : Chaque trajectoire est identifiée un nom d'aggrégat et par un formsemestre terminal.
|
|
||||||
|
|
||||||
Par exemple :
|
|
||||||
|
|
||||||
* combinaisons '3S' : S1+S2+S3 en prenant en compte tous les S3 qu'ont fréquenté les
|
|
||||||
étudiants du jury PE. Ces S3 marquent les formsemestre terminal de chaque combinaison.
|
|
||||||
|
|
||||||
* combinaisons 'S2' : 1 seul S2 pour des étudiants n'ayant pas redoublé, 2 pour des redoublants (dont les
|
|
||||||
notes seront moyennées sur leur 2 semestres S2). Ces combinaisons ont pour formsemestre le dernier S2 en
|
|
||||||
date (le S2 redoublé par les redoublants est forcément antérieur)
|
|
||||||
|
|
||||||
|
|
||||||
Args:
|
|
||||||
etudiants: Les données des étudiants
|
|
||||||
semestres_tag: Les semestres tag (pour lesquels des moyennes par tag ont été calculés)
|
|
||||||
|
|
||||||
Return:
|
|
||||||
Un dictionnaire de la forme ``{nom_aggregat: {fid_terminal: SetTag(fid_terminal)} }``
|
|
||||||
"""
|
|
||||||
trajectoires_tagguees = {}
|
|
||||||
|
|
||||||
for trajectoire_id, trajectoire in trajectoires.rcss.items():
|
|
||||||
nom = trajectoire.get_repr()
|
|
||||||
pe_affichage.pe_print(f" --> Aggrégat {nom}")
|
|
||||||
# Trajectoire_tagguee associée
|
|
||||||
trajectoire_tagguee = RCSTag(trajectoire, semestres_taggues)
|
|
||||||
# Mémorise le résultat
|
|
||||||
trajectoires_tagguees[trajectoire_id] = trajectoire_tagguee
|
|
||||||
|
|
||||||
return trajectoires_tagguees
|
|
||||||
|
|
||||||
|
|
||||||
def compute_interclassements(
|
|
||||||
etudiants: EtudiantsJuryPE,
|
|
||||||
trajectoires_jury_pe: RCSsJuryPE,
|
|
||||||
trajectoires_tagguees: dict[tuple, RCS],
|
|
||||||
):
|
|
||||||
"""Interclasse les étudiants, (nom d') aggrégat par aggrégat,
|
|
||||||
pour fournir un classement sur la promo. Le classement est établi au regard du nombre
|
|
||||||
d'étudiants ayant participé au même aggrégat.
|
|
||||||
"""
|
|
||||||
aggregats_interclasses_taggues = {}
|
|
||||||
for nom_aggregat in TOUS_LES_RCS:
|
|
||||||
pe_affichage.pe_print(f" --> Interclassement {nom_aggregat}")
|
|
||||||
interclass = RCSInterclasseTag(
|
|
||||||
nom_aggregat, etudiants, trajectoires_jury_pe, trajectoires_tagguees
|
|
||||||
)
|
|
||||||
aggregats_interclasses_taggues[nom_aggregat] = interclass
|
|
||||||
return aggregats_interclasses_taggues
|
|
||||||
|
|
||||||
|
|
||||||
def get_defaut_dict_synthese_aggregat(nom_rcs: str, diplome: int) -> dict:
|
|
||||||
"""Renvoie le dictionnaire de synthèse (à intégrer dans
|
|
||||||
un tableur excel) pour décrire les résultats d'un aggrégat
|
|
||||||
|
|
||||||
Args:
|
|
||||||
nom_rcs : Le nom du RCS visé
|
|
||||||
diplôme : l'année du diplôme
|
|
||||||
"""
|
|
||||||
# L'affichage de l'aggrégat dans le tableur excel
|
|
||||||
descr = get_descr_rcs(nom_rcs)
|
|
||||||
|
|
||||||
nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}"
|
|
||||||
donnees = {
|
|
||||||
(descr, "", "note"): SANS_NOTE,
|
|
||||||
# Les stat du groupe
|
|
||||||
(descr, NOM_STAT_GROUPE, "class."): SANS_NOTE,
|
|
||||||
(descr, NOM_STAT_GROUPE, "min"): SANS_NOTE,
|
|
||||||
(descr, NOM_STAT_GROUPE, "moy"): SANS_NOTE,
|
|
||||||
(descr, NOM_STAT_GROUPE, "max"): SANS_NOTE,
|
|
||||||
# Les stats de l'interclassement dans la promo
|
|
||||||
(descr, nom_stat_promo, "class."): SANS_NOTE,
|
|
||||||
(
|
|
||||||
descr,
|
|
||||||
nom_stat_promo,
|
|
||||||
"min",
|
|
||||||
): SANS_NOTE,
|
|
||||||
(
|
|
||||||
descr,
|
|
||||||
nom_stat_promo,
|
|
||||||
"moy",
|
|
||||||
): SANS_NOTE,
|
|
||||||
(
|
|
||||||
descr,
|
|
||||||
nom_stat_promo,
|
|
||||||
"max",
|
|
||||||
): SANS_NOTE,
|
|
||||||
}
|
|
||||||
return donnees
|
|
||||||
|
|
||||||
|
|
||||||
def get_dict_synthese_aggregat(
|
|
||||||
aggregat: str,
|
|
||||||
trajectoire_tagguee: RCSTag,
|
|
||||||
interclassement_taggue: RCSInterclasseTag,
|
|
||||||
etudid: int,
|
|
||||||
tag: str,
|
|
||||||
diplome: int,
|
|
||||||
):
|
|
||||||
"""Renvoie le dictionnaire (à intégrer au tableur excel de synthese)
|
|
||||||
traduisant les résultats (moy/class) d'un étudiant à une trajectoire tagguée associée
|
|
||||||
à l'aggrégat donné et pour un tag donné"""
|
|
||||||
donnees = {}
|
|
||||||
# L'affichage de l'aggrégat dans le tableur excel
|
|
||||||
descr = get_descr_rcs(aggregat)
|
|
||||||
|
|
||||||
# La note de l'étudiant (chargement à venir)
|
|
||||||
note = np.nan
|
|
||||||
|
|
||||||
# Les données de la trajectoire tagguée pour le tag considéré
|
|
||||||
moy_tag = trajectoire_tagguee.moyennes_tags[tag]
|
|
||||||
|
|
||||||
# Les données de l'étudiant
|
|
||||||
note = moy_tag.get_note_for_df(etudid)
|
|
||||||
|
|
||||||
classement = moy_tag.get_class_for_df(etudid)
|
|
||||||
nmin = moy_tag.get_min_for_df()
|
|
||||||
nmax = moy_tag.get_max_for_df()
|
|
||||||
nmoy = moy_tag.get_moy_for_df()
|
|
||||||
|
|
||||||
# Statistiques sur le groupe
|
|
||||||
if not pd.isna(note) and note != np.nan:
|
|
||||||
# Les moyennes de cette trajectoire
|
|
||||||
donnees |= {
|
|
||||||
(descr, "", "note"): note,
|
|
||||||
(descr, NOM_STAT_GROUPE, "class."): classement,
|
|
||||||
(descr, NOM_STAT_GROUPE, "min"): nmin,
|
|
||||||
(descr, NOM_STAT_GROUPE, "moy"): nmoy,
|
|
||||||
(descr, NOM_STAT_GROUPE, "max"): nmax,
|
|
||||||
}
|
|
||||||
|
|
||||||
# L'interclassement
|
|
||||||
moy_tag = interclassement_taggue.moyennes_tags[tag]
|
|
||||||
|
|
||||||
classement = moy_tag.get_class_for_df(etudid)
|
|
||||||
nmin = moy_tag.get_min_for_df()
|
|
||||||
nmax = moy_tag.get_max_for_df()
|
|
||||||
nmoy = moy_tag.get_moy_for_df()
|
|
||||||
|
|
||||||
if not pd.isna(note) and note != np.nan:
|
|
||||||
nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}"
|
|
||||||
|
|
||||||
donnees |= {
|
|
||||||
(descr, nom_stat_promo, "class."): classement,
|
|
||||||
(descr, nom_stat_promo, "min"): nmin,
|
|
||||||
(descr, nom_stat_promo, "moy"): nmoy,
|
|
||||||
(descr, nom_stat_promo, "max"): nmax,
|
|
||||||
}
|
|
||||||
|
|
||||||
return donnees
|
|
1271
app/pe/pe_jurype.py
Normal file
1271
app/pe/pe_jurype.py
Normal file
File diff suppressed because it is too large
Load Diff
269
app/pe/pe_rcs.py
269
app/pe/pe_rcs.py
@ -1,269 +0,0 @@
|
|||||||
##############################################################################
|
|
||||||
# Module "Avis de poursuite d'étude"
|
|
||||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""
|
|
||||||
Created on 01-2024
|
|
||||||
|
|
||||||
@author: barasc
|
|
||||||
"""
|
|
||||||
|
|
||||||
import app.pe.pe_comp as pe_comp
|
|
||||||
|
|
||||||
from app.models import FormSemestre
|
|
||||||
from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date
|
|
||||||
|
|
||||||
|
|
||||||
TYPES_RCS = {
|
|
||||||
"S1": {
|
|
||||||
"aggregat": ["S1"],
|
|
||||||
"descr": "Semestre 1 (S1)",
|
|
||||||
},
|
|
||||||
"S2": {
|
|
||||||
"aggregat": ["S2"],
|
|
||||||
"descr": "Semestre 2 (S2)",
|
|
||||||
},
|
|
||||||
"1A": {
|
|
||||||
"aggregat": ["S1", "S2"],
|
|
||||||
"descr": "BUT1 (S1+S2)",
|
|
||||||
},
|
|
||||||
"S3": {
|
|
||||||
"aggregat": ["S3"],
|
|
||||||
"descr": "Semestre 3 (S3)",
|
|
||||||
},
|
|
||||||
"S4": {
|
|
||||||
"aggregat": ["S4"],
|
|
||||||
"descr": "Semestre 4 (S4)",
|
|
||||||
},
|
|
||||||
"2A": {
|
|
||||||
"aggregat": ["S3", "S4"],
|
|
||||||
"descr": "BUT2 (S3+S4)",
|
|
||||||
},
|
|
||||||
"3S": {
|
|
||||||
"aggregat": ["S1", "S2", "S3"],
|
|
||||||
"descr": "Moyenne du semestre 1 au semestre 3 (S1+S2+S3)",
|
|
||||||
},
|
|
||||||
"4S": {
|
|
||||||
"aggregat": ["S1", "S2", "S3", "S4"],
|
|
||||||
"descr": "Moyenne du semestre 1 au semestre 4 (S1+S2+S3+S4)",
|
|
||||||
},
|
|
||||||
"S5": {
|
|
||||||
"aggregat": ["S5"],
|
|
||||||
"descr": "Semestre 5 (S5)",
|
|
||||||
},
|
|
||||||
"S6": {
|
|
||||||
"aggregat": ["S6"],
|
|
||||||
"descr": "Semestre 6 (S6)",
|
|
||||||
},
|
|
||||||
"3A": {
|
|
||||||
"aggregat": ["S5", "S6"],
|
|
||||||
"descr": "3ème année (S5+S6)",
|
|
||||||
},
|
|
||||||
"5S": {
|
|
||||||
"aggregat": ["S1", "S2", "S3", "S4", "S5"],
|
|
||||||
"descr": "Moyenne du semestre 1 au semestre 5 (S1+S2+S3+S4+S5)",
|
|
||||||
},
|
|
||||||
"6S": {
|
|
||||||
"aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"],
|
|
||||||
"descr": "Moyenne globale (S1+S2+S3+S4+S5+S6)",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
"""Dictionnaire détaillant les différents regroupements cohérents
|
|
||||||
de semestres (RCS), en leur attribuant un nom et en détaillant
|
|
||||||
le nom des semestres qu'ils regroupent et l'affichage qui en sera fait
|
|
||||||
dans les tableurs de synthèse.
|
|
||||||
"""
|
|
||||||
|
|
||||||
TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS if not cle.startswith("S")]
|
|
||||||
TOUS_LES_RCS = list(TYPES_RCS.keys())
|
|
||||||
TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")]
|
|
||||||
|
|
||||||
|
|
||||||
class RCS:
|
|
||||||
"""Modélise un ensemble de semestres d'étudiants
|
|
||||||
associé à un type de regroupement cohérent de semestres
|
|
||||||
donné (par ex: 'S2', '3S', '2A').
|
|
||||||
|
|
||||||
Si le RCS est un semestre de type Si, stocke le (ou les)
|
|
||||||
formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si
|
|
||||||
(en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants)
|
|
||||||
|
|
||||||
Pour le RCS de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie
|
|
||||||
les semestres que les étudiants ont suivis pour les amener jusqu'au semestre
|
|
||||||
terminal de la trajectoire (par ex: ici un S3).
|
|
||||||
|
|
||||||
Ces semestres peuvent être :
|
|
||||||
|
|
||||||
* des S1+S2+S1+S2+S3 si redoublement de la 1ère année
|
|
||||||
* des S1+S2+(année de césure)+S3 si césure, ...
|
|
||||||
|
|
||||||
Args:
|
|
||||||
nom_rcs: Un nom du RCS (par ex: '5S')
|
|
||||||
semestre_final: Le semestre final du RCS
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, nom_rcs: str, semestre_final: FormSemestre):
|
|
||||||
self.nom = nom_rcs
|
|
||||||
"""Nom du RCS"""
|
|
||||||
|
|
||||||
self.formsemestre_final = semestre_final
|
|
||||||
"""FormSemestre terminal du RCS"""
|
|
||||||
|
|
||||||
self.rcs_id = (nom_rcs, semestre_final.formsemestre_id)
|
|
||||||
"""Identifiant du RCS sous forme (nom_rcs, id du semestre_terminal)"""
|
|
||||||
|
|
||||||
self.semestres_aggreges = {}
|
|
||||||
"""Semestres regroupés dans le RCS"""
|
|
||||||
|
|
||||||
def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]):
|
|
||||||
"""Ajout de semestres aux semestres à regrouper
|
|
||||||
|
|
||||||
Args:
|
|
||||||
semestres: Dictionnaire ``{fid: FormSemestre(fid)}`` à ajouter
|
|
||||||
"""
|
|
||||||
self.semestres_aggreges = self.semestres_aggreges | semestres
|
|
||||||
|
|
||||||
def get_repr(self, verbose=True) -> str:
|
|
||||||
"""Représentation textuelle d'un RCS
|
|
||||||
basé sur ses semestres aggrégés"""
|
|
||||||
|
|
||||||
noms = []
|
|
||||||
for fid in self.semestres_aggreges:
|
|
||||||
semestre = self.semestres_aggreges[fid]
|
|
||||||
noms.append(f"S{semestre.semestre_id}({fid})")
|
|
||||||
noms = sorted(noms)
|
|
||||||
title = f"""{self.nom} ({
|
|
||||||
self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}"""
|
|
||||||
if verbose and noms:
|
|
||||||
title += " - " + "+".join(noms)
|
|
||||||
return title
|
|
||||||
|
|
||||||
|
|
||||||
class RCSsJuryPE:
|
|
||||||
"""Classe centralisant toutes les regroupements cohérents de
|
|
||||||
semestres (RCS) des étudiants à prendre en compte dans un jury PE
|
|
||||||
|
|
||||||
Args:
|
|
||||||
annee_diplome: L'année de diplomation
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, annee_diplome: int):
|
|
||||||
self.annee_diplome = annee_diplome
|
|
||||||
"""Année de diplômation"""
|
|
||||||
|
|
||||||
self.rcss: dict[tuple:RCS] = {}
|
|
||||||
"""Ensemble des RCS recensés : {(nom_RCS, fid_terminal): RCS}"""
|
|
||||||
|
|
||||||
self.suivi: dict[int:str] = {}
|
|
||||||
"""Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS,
|
|
||||||
son RCS : {etudid: {nom_RCS: RCS}}"""
|
|
||||||
|
|
||||||
def cree_rcss(self, etudiants: EtudiantsJuryPE):
|
|
||||||
"""Créé tous les RCS, au regard du cursus des étudiants
|
|
||||||
analysés + les mémorise dans les données de l'étudiant
|
|
||||||
|
|
||||||
Args:
|
|
||||||
etudiants: Les étudiants à prendre en compte dans le Jury PE
|
|
||||||
"""
|
|
||||||
|
|
||||||
for nom_rcs in pe_comp.TOUS_LES_SEMESTRES + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM:
|
|
||||||
# L'aggrégat considéré (par ex: 3S=S1+S2+S3), son nom de son semestre
|
|
||||||
# terminal (par ex: S3) et son numéro (par ex: 3)
|
|
||||||
noms_semestre_de_aggregat = TYPES_RCS[nom_rcs]["aggregat"]
|
|
||||||
nom_semestre_terminal = noms_semestre_de_aggregat[-1]
|
|
||||||
|
|
||||||
for etudid in etudiants.cursus:
|
|
||||||
if etudid not in self.suivi:
|
|
||||||
self.suivi[etudid] = {
|
|
||||||
aggregat: None
|
|
||||||
for aggregat in pe_comp.TOUS_LES_SEMESTRES
|
|
||||||
+ TOUS_LES_RCS_AVEC_PLUSIEURS_SEM
|
|
||||||
}
|
|
||||||
|
|
||||||
# Le formsemestre terminal (dernier en date) associé au
|
|
||||||
# semestre marquant la fin de l'aggrégat
|
|
||||||
# (par ex: son dernier S3 en date)
|
|
||||||
semestres = etudiants.cursus[etudid][nom_semestre_terminal]
|
|
||||||
if semestres:
|
|
||||||
formsemestre_final = get_dernier_semestre_en_date(semestres)
|
|
||||||
|
|
||||||
# Ajout ou récupération de la trajectoire
|
|
||||||
trajectoire_id = (nom_rcs, formsemestre_final.formsemestre_id)
|
|
||||||
if trajectoire_id not in self.rcss:
|
|
||||||
trajectoire = RCS(nom_rcs, formsemestre_final)
|
|
||||||
self.rcss[trajectoire_id] = trajectoire
|
|
||||||
else:
|
|
||||||
trajectoire = self.rcss[trajectoire_id]
|
|
||||||
|
|
||||||
# La liste des semestres de l'étudiant à prendre en compte
|
|
||||||
# pour cette trajectoire
|
|
||||||
semestres_a_aggreger = get_rcs_etudiant(
|
|
||||||
etudiants.cursus[etudid], formsemestre_final, nom_rcs
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ajout des semestres à la trajectoire
|
|
||||||
trajectoire.add_semestres_a_aggreger(semestres_a_aggreger)
|
|
||||||
|
|
||||||
# Mémoire la trajectoire suivie par l'étudiant
|
|
||||||
self.suivi[etudid][nom_rcs] = trajectoire
|
|
||||||
|
|
||||||
|
|
||||||
def get_rcs_etudiant(
|
|
||||||
semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str
|
|
||||||
) -> dict[int, FormSemestre]:
|
|
||||||
"""Ensemble des semestres parcourus par un étudiant, connaissant
|
|
||||||
les semestres de son cursus,
|
|
||||||
dans le cadre du RCS visé et ayant pour semestre terminal `formsemestre_final`.
|
|
||||||
|
|
||||||
Si le RCS est de type "Si", limite les semestres à ceux de numéro i.
|
|
||||||
Par ex: si formsemestre_terminal est un S3 et nom_agrregat "S3", ne prend en compte que les
|
|
||||||
semestres 3.
|
|
||||||
|
|
||||||
Si le RCS est de type "iA" ou "iS" (incluant plusieurs numéros de semestres), prend en
|
|
||||||
compte les dit numéros de semestres.
|
|
||||||
|
|
||||||
Par ex: si formsemestre_terminal est un S3, ensemble des S1,
|
|
||||||
S2, S3 suivi pour l'amener au S3 (il peut y avoir plusieurs S1,
|
|
||||||
ou S2, ou S3 s'il a redoublé).
|
|
||||||
|
|
||||||
Les semestres parcourus sont antérieurs (en terme de date de fin)
|
|
||||||
au formsemestre_terminal.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cursus: Dictionnaire {fid: FormSemestre(fid)} donnant l'ensemble des semestres
|
|
||||||
dans lesquels l'étudiant a été inscrit
|
|
||||||
formsemestre_final: le semestre final visé
|
|
||||||
nom_rcs: Nom du RCS visé
|
|
||||||
"""
|
|
||||||
numero_semestre_terminal = formsemestre_final.semestre_id
|
|
||||||
# semestres_significatifs = self.get_semestres_significatifs(etudid)
|
|
||||||
semestres_significatifs = {}
|
|
||||||
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1):
|
|
||||||
semestres_significatifs = semestres_significatifs | semestres[f"S{i}"]
|
|
||||||
|
|
||||||
if nom_rcs.startswith("S"): # les semestres
|
|
||||||
numero_semestres_possibles = [numero_semestre_terminal]
|
|
||||||
elif nom_rcs.endswith("A"): # les années
|
|
||||||
numero_semestres_possibles = [
|
|
||||||
int(sem[-1]) for sem in TYPES_RCS[nom_rcs]["aggregat"]
|
|
||||||
]
|
|
||||||
assert numero_semestre_terminal in numero_semestres_possibles
|
|
||||||
else: # les xS = tous les semestres jusqu'à Sx (eg S1, S2, S3 pour un S3 terminal)
|
|
||||||
numero_semestres_possibles = list(range(1, numero_semestre_terminal + 1))
|
|
||||||
|
|
||||||
semestres_aggreges = {}
|
|
||||||
for fid, semestre in semestres_significatifs.items():
|
|
||||||
# Semestres parmi ceux de n° possibles & qui lui sont antérieurs
|
|
||||||
if (
|
|
||||||
semestre.semestre_id in numero_semestres_possibles
|
|
||||||
and semestre.date_fin <= formsemestre_final.date_fin
|
|
||||||
):
|
|
||||||
semestres_aggreges[fid] = semestre
|
|
||||||
return semestres_aggreges
|
|
||||||
|
|
||||||
|
|
||||||
def get_descr_rcs(nom_rcs: str) -> str:
|
|
||||||
"""Renvoie la description pour les tableurs de synthèse
|
|
||||||
Excel d'un nom de RCS"""
|
|
||||||
return TYPES_RCS[nom_rcs]["descr"]
|
|
@ -1,217 +0,0 @@
|
|||||||
# -*- mode: python -*-
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gestion scolarite IUT
|
|
||||||
#
|
|
||||||
# Copyright (c) 1999 - 2024 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
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
# Module "Avis de poursuite d'étude"
|
|
||||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""
|
|
||||||
Created on Fri Sep 9 09:15:05 2016
|
|
||||||
|
|
||||||
@author: barasc
|
|
||||||
"""
|
|
||||||
|
|
||||||
from app.comp.res_sem import load_formsemestre_results
|
|
||||||
from app.pe.pe_semtag import SemestreTag
|
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
from app.pe.pe_rcs import RCS
|
|
||||||
|
|
||||||
from app.pe.pe_tabletags import TableTag, MoyenneTag
|
|
||||||
|
|
||||||
|
|
||||||
class RCSTag(TableTag):
|
|
||||||
def __init__(
|
|
||||||
self, rcs: RCS, semestres_taggues: dict[int, SemestreTag]
|
|
||||||
):
|
|
||||||
"""Calcule les moyennes par tag d'une combinaison de semestres
|
|
||||||
(RCS), pour extraire les classements par tag pour un
|
|
||||||
groupe d'étudiants donnés. Le groupe d'étudiants est formé par ceux ayant tous
|
|
||||||
participé au semestre terminal.
|
|
||||||
|
|
||||||
|
|
||||||
Args:
|
|
||||||
rcs: Un RCS (identifié par un nom et l'id de son semestre terminal)
|
|
||||||
semestres_taggues: Les données sur les semestres taggués
|
|
||||||
"""
|
|
||||||
TableTag.__init__(self)
|
|
||||||
|
|
||||||
|
|
||||||
self.rcs_id = rcs.rcs_id
|
|
||||||
"""Identifiant du RCS taggué (identique au RCS sur lequel il s'appuie)"""
|
|
||||||
|
|
||||||
self.rcs = rcs
|
|
||||||
"""RCS associé au RCS taggué"""
|
|
||||||
|
|
||||||
self.nom = self.get_repr()
|
|
||||||
"""Représentation textuelle du RCS taggué"""
|
|
||||||
|
|
||||||
self.formsemestre_terminal = rcs.formsemestre_final
|
|
||||||
"""Le formsemestre terminal"""
|
|
||||||
|
|
||||||
# Les résultats du formsemestre terminal
|
|
||||||
nt = load_formsemestre_results(self.formsemestre_terminal)
|
|
||||||
|
|
||||||
self.semestres_aggreges = rcs.semestres_aggreges
|
|
||||||
"""Les semestres aggrégés"""
|
|
||||||
|
|
||||||
self.semestres_tags_aggreges = {}
|
|
||||||
"""Les semestres tags associés aux semestres aggrégés"""
|
|
||||||
for frmsem_id in self.semestres_aggreges:
|
|
||||||
try:
|
|
||||||
self.semestres_tags_aggreges[frmsem_id] = semestres_taggues[frmsem_id]
|
|
||||||
except:
|
|
||||||
raise ValueError("Semestres taggués manquants")
|
|
||||||
|
|
||||||
"""Les étudiants (état civil + cursus connu)"""
|
|
||||||
self.etuds = nt.etuds
|
|
||||||
|
|
||||||
# assert self.etuds == trajectoire.suivi # manque-t-il des étudiants ?
|
|
||||||
self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds}
|
|
||||||
|
|
||||||
self.tags_sorted = self.do_taglist()
|
|
||||||
"""Tags extraits de tous les semestres"""
|
|
||||||
|
|
||||||
self.notes_cube = self.compute_notes_cube()
|
|
||||||
"""Cube de notes"""
|
|
||||||
|
|
||||||
etudids = list(self.etudiants.keys())
|
|
||||||
self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted)
|
|
||||||
"""Calcul les moyennes par tag sous forme d'un dataframe"""
|
|
||||||
|
|
||||||
self.moyennes_tags: dict[str, MoyenneTag] = {}
|
|
||||||
"""Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)"""
|
|
||||||
for tag in self.tags_sorted:
|
|
||||||
moy_gen_tag = self.notes[tag]
|
|
||||||
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
"""Egalité de 2 RCS taggués sur la base de leur identifiant"""
|
|
||||||
return self.rcs_id == other.rcs_id
|
|
||||||
|
|
||||||
def get_repr(self, verbose=False) -> str:
|
|
||||||
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
|
|
||||||
est basée)"""
|
|
||||||
return self.rcs.get_repr(verbose=verbose)
|
|
||||||
|
|
||||||
def compute_notes_cube(self):
|
|
||||||
"""Construit le cube de notes (etudid x tags x semestre_aggregé)
|
|
||||||
nécessaire au calcul des moyennes de l'aggrégat
|
|
||||||
"""
|
|
||||||
# nb_tags = len(self.tags_sorted)
|
|
||||||
# nb_etudiants = len(self.etuds)
|
|
||||||
# nb_semestres = len(self.semestres_tags_aggreges)
|
|
||||||
|
|
||||||
# Index du cube (etudids -> dim 0, tags -> dim 1)
|
|
||||||
etudids = [etud.etudid for etud in self.etuds]
|
|
||||||
tags = self.tags_sorted
|
|
||||||
semestres_id = list(self.semestres_tags_aggreges.keys())
|
|
||||||
|
|
||||||
dfs = {}
|
|
||||||
|
|
||||||
for frmsem_id in semestres_id:
|
|
||||||
# Partant d'un dataframe vierge
|
|
||||||
df = pd.DataFrame(np.nan, index=etudids, columns=tags)
|
|
||||||
|
|
||||||
# Charge les notes du semestre tag
|
|
||||||
notes = self.semestres_tags_aggreges[frmsem_id].notes
|
|
||||||
|
|
||||||
# Les étudiants & les tags commun au dataframe final et aux notes du semestre)
|
|
||||||
etudids_communs = df.index.intersection(notes.index)
|
|
||||||
tags_communs = df.columns.intersection(notes.columns)
|
|
||||||
|
|
||||||
# Injecte les notes par tag
|
|
||||||
df.loc[etudids_communs, tags_communs] = notes.loc[
|
|
||||||
etudids_communs, tags_communs
|
|
||||||
]
|
|
||||||
|
|
||||||
# Supprime tout ce qui n'est pas numérique
|
|
||||||
for col in df.columns:
|
|
||||||
df[col] = pd.to_numeric(df[col], errors="coerce")
|
|
||||||
|
|
||||||
# Stocke le df
|
|
||||||
dfs[frmsem_id] = df
|
|
||||||
|
|
||||||
"""Réunit les notes sous forme d'un cube etdids x tags x semestres"""
|
|
||||||
semestres_x_etudids_x_tags = [dfs[fid].values for fid in dfs]
|
|
||||||
etudids_x_tags_x_semestres = np.stack(semestres_x_etudids_x_tags, axis=-1)
|
|
||||||
|
|
||||||
return etudids_x_tags_x_semestres
|
|
||||||
|
|
||||||
def do_taglist(self):
|
|
||||||
"""Synthétise les tags à partir des semestres (taggués) aggrégés
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Une liste de tags triés par ordre alphabétique
|
|
||||||
"""
|
|
||||||
tags = []
|
|
||||||
for frmsem_id in self.semestres_tags_aggreges:
|
|
||||||
tags.extend(self.semestres_tags_aggreges[frmsem_id].tags_sorted)
|
|
||||||
return sorted(set(tags))
|
|
||||||
|
|
||||||
|
|
||||||
def compute_tag_moy(set_cube: np.array, etudids: list, tags: list):
|
|
||||||
"""Calcul de la moyenne par tag sur plusieurs semestres.
|
|
||||||
La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles
|
|
||||||
|
|
||||||
*Remarque* : Adaptation de moy_ue.compute_ue_moys_apc au cas des moyennes de tag
|
|
||||||
par aggrégat de plusieurs semestres.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
set_cube: notes moyennes aux modules ndarray
|
|
||||||
(etuds x modimpls x UEs), des floats avec des NaN
|
|
||||||
etudids: liste des étudiants (dim. 0 du cube)
|
|
||||||
tags: liste des tags (dim. 1 du cube)
|
|
||||||
Returns:
|
|
||||||
Un DataFrame avec pour columns les moyennes par tags,
|
|
||||||
et pour rows les etudid
|
|
||||||
"""
|
|
||||||
nb_etuds, nb_tags, nb_semestres = set_cube.shape
|
|
||||||
assert nb_etuds == len(etudids)
|
|
||||||
assert nb_tags == len(tags)
|
|
||||||
|
|
||||||
# Quelles entrées du cube contiennent des notes ?
|
|
||||||
mask = ~np.isnan(set_cube)
|
|
||||||
|
|
||||||
# Enlève les NaN du cube pour les entrées manquantes
|
|
||||||
set_cube_no_nan = np.nan_to_num(set_cube, nan=0.0)
|
|
||||||
|
|
||||||
# Les moyennes par tag
|
|
||||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
|
||||||
etud_moy_tag = np.sum(set_cube_no_nan, axis=2) / np.sum(mask, axis=2)
|
|
||||||
|
|
||||||
# Le dataFrame
|
|
||||||
etud_moy_tag_df = pd.DataFrame(
|
|
||||||
etud_moy_tag,
|
|
||||||
index=etudids, # les etudids
|
|
||||||
columns=tags, # les tags
|
|
||||||
)
|
|
||||||
|
|
||||||
etud_moy_tag_df.fillna(np.nan)
|
|
||||||
|
|
||||||
return etud_moy_tag_df
|
|
511
app/pe/pe_semestretag.py
Normal file
511
app/pe/pe_semestretag.py
Normal file
@ -0,0 +1,511 @@
|
|||||||
|
# -*- mode: python -*-
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gestion scolarite IUT
|
||||||
|
#
|
||||||
|
# Copyright (c) 1999 - 2023 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
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Module "Avis de poursuite d'étude"
|
||||||
|
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
Created on Fri Sep 9 09:15:05 2016
|
||||||
|
|
||||||
|
@author: barasc
|
||||||
|
"""
|
||||||
|
|
||||||
|
from app import db, log
|
||||||
|
from app.comp import res_sem
|
||||||
|
from app.comp.res_compat import NotesTableCompat
|
||||||
|
from app.models import FormSemestre
|
||||||
|
from app.models.moduleimpls import ModuleImpl
|
||||||
|
from app.pe import pe_tagtable
|
||||||
|
|
||||||
|
from app.scodoc import codes_cursus
|
||||||
|
from app.scodoc import sco_tag_module
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
|
class SemestreTag(pe_tagtable.TableTag):
|
||||||
|
"""Un SemestreTag représente un tableau de notes (basé sur notesTable)
|
||||||
|
modélisant les résultats des étudiants sous forme de moyennes par tag.
|
||||||
|
|
||||||
|
Attributs récupérés via des NotesTables :
|
||||||
|
- nt: le tableau de notes du semestre considéré
|
||||||
|
- nt.inscrlist: étudiants inscrits à ce semestre, par ordre alphabétique (avec demissions)
|
||||||
|
- nt.identdict: { etudid : ident }
|
||||||
|
- liste des moduleimpl { ... 'module_id', ...}
|
||||||
|
|
||||||
|
Attributs supplémentaires :
|
||||||
|
- inscrlist/identdict: étudiants inscrits hors démissionnaires ou défaillants
|
||||||
|
- _tagdict : Dictionnaire résumant les tags et les modules du semestre auxquels ils sont liés
|
||||||
|
|
||||||
|
|
||||||
|
Attributs hérités de TableTag :
|
||||||
|
- nom :
|
||||||
|
- resultats: {tag: { etudid: (note_moy, somme_coff), ...} , ...}
|
||||||
|
- rang
|
||||||
|
- statistiques
|
||||||
|
|
||||||
|
Redéfinition :
|
||||||
|
- get_etudids() : les etudids des étudiants non défaillants ni démissionnaires
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Fonctions d'initialisation
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def __init__(self, notetable, sem): # Initialisation sur la base d'une notetable
|
||||||
|
"""Instantiation d'un objet SemestreTag à partir d'un tableau de note
|
||||||
|
et des informations sur le semestre pour le dater
|
||||||
|
"""
|
||||||
|
pe_tagtable.TableTag.__init__(
|
||||||
|
self,
|
||||||
|
nom="S%d %s %s-%s"
|
||||||
|
% (
|
||||||
|
sem["semestre_id"],
|
||||||
|
"ENEPS"
|
||||||
|
if "ENEPS" in sem["titre"]
|
||||||
|
else "UFA"
|
||||||
|
if "UFA" in sem["titre"]
|
||||||
|
else "FI",
|
||||||
|
sem["annee_debut"],
|
||||||
|
sem["annee_fin"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Les attributs spécifiques
|
||||||
|
self.nt = notetable
|
||||||
|
|
||||||
|
# Les attributs hérités : la liste des étudiants
|
||||||
|
self.inscrlist = [
|
||||||
|
etud
|
||||||
|
for etud in self.nt.inscrlist
|
||||||
|
if self.nt.get_etud_etat(etud["etudid"]) == scu.INSCRIT
|
||||||
|
]
|
||||||
|
self.identdict = {
|
||||||
|
etudid: ident
|
||||||
|
for (etudid, ident) in self.nt.identdict.items()
|
||||||
|
if etudid in self.get_etudids()
|
||||||
|
} # Liste des étudiants non démissionnaires et non défaillants
|
||||||
|
|
||||||
|
# Les modules pris en compte dans le calcul des moyennes par tag => ceux des UE standards
|
||||||
|
self.modimpls = [
|
||||||
|
modimpl
|
||||||
|
for modimpl in self.nt.formsemestre.modimpls_sorted
|
||||||
|
if modimpl.module.ue.type == codes_cursus.UE_STANDARD
|
||||||
|
] # la liste des modules (objet modimpl)
|
||||||
|
self.somme_coeffs = sum(
|
||||||
|
[
|
||||||
|
modimpl.module.coefficient
|
||||||
|
for modimpl in self.modimpls
|
||||||
|
if modimpl.module.coefficient is not None
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def comp_data_semtag(self):
|
||||||
|
"""Calcule tous les données numériques associées au semtag"""
|
||||||
|
# Attributs relatifs aux tag pour les modules pris en compte
|
||||||
|
self.tagdict = (
|
||||||
|
self.do_tagdict()
|
||||||
|
) # Dictionnaire résumant les tags et les données (normalisées) des modules du semestre auxquels ils sont liés
|
||||||
|
|
||||||
|
# Calcul des moyennes de chaque étudiant puis ajoute la moyenne au sens "DUT"
|
||||||
|
for tag in self.tagdict:
|
||||||
|
self.add_moyennesTag(tag, self.comp_MoyennesTag(tag, force=True))
|
||||||
|
self.add_moyennesTag("dut", self.get_moyennes_DUT())
|
||||||
|
self.taglist = sorted(
|
||||||
|
list(self.tagdict.keys()) + ["dut"]
|
||||||
|
) # actualise la liste des tags
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def get_etudids(self):
|
||||||
|
"""Renvoie la liste des etud_id des étudiants inscrits au semestre"""
|
||||||
|
return [etud["etudid"] for etud in self.inscrlist]
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def do_tagdict(self):
|
||||||
|
"""Parcourt les modimpl du semestre (instance des modules d'un programme) et synthétise leurs données sous la
|
||||||
|
forme d'un dictionnaire reliant les tags saisis dans le programme aux
|
||||||
|
données des modules qui les concernent, à savoir les modimpl_id, les module_id, le code du module, le coeff,
|
||||||
|
la pondération fournie avec le tag (par défaut 1 si non indiquée).
|
||||||
|
{ tagname1 : { modimpl_id1 : { 'module_id' : ..., 'coeff' : ..., 'coeff_norm' : ..., 'ponderation' : ..., 'module_code' : ..., 'ue_xxx' : ...},
|
||||||
|
modimpl_id2 : ....
|
||||||
|
},
|
||||||
|
tagname2 : ...
|
||||||
|
}
|
||||||
|
Renvoie le dictionnaire ainsi construit.
|
||||||
|
|
||||||
|
Rq: choix fait de repérer les modules par rapport à leur modimpl_id (valable uniquement pour un semestre), car
|
||||||
|
correspond à la majorité des calculs de moyennes pour les étudiants
|
||||||
|
(seuls ceux qui ont capitalisé des ue auront un régime de calcul différent).
|
||||||
|
"""
|
||||||
|
tagdict = {}
|
||||||
|
|
||||||
|
for modimpl in self.modimpls:
|
||||||
|
modimpl_id = modimpl.id
|
||||||
|
# liste des tags pour le modimpl concerné:
|
||||||
|
tags = sco_tag_module.module_tag_list(modimpl.module.id)
|
||||||
|
|
||||||
|
for (
|
||||||
|
tag
|
||||||
|
) in tags: # tag de la forme "mathématiques", "théorie", "pe:0", "maths:2"
|
||||||
|
[tagname, ponderation] = sco_tag_module.split_tagname_coeff(
|
||||||
|
tag
|
||||||
|
) # extrait un tagname et un éventuel coefficient de pondération (par defaut: 1)
|
||||||
|
# tagname = tagname
|
||||||
|
if tagname not in tagdict: # Ajout d'une clé pour le tag
|
||||||
|
tagdict[tagname] = {}
|
||||||
|
|
||||||
|
# Ajout du modimpl au tagname considéré
|
||||||
|
tagdict[tagname][modimpl_id] = {
|
||||||
|
"module_id": modimpl.module.id, # les données sur le module
|
||||||
|
"coeff": modimpl.module.coefficient, # le coeff du module dans le semestre
|
||||||
|
"ponderation": ponderation, # la pondération demandée pour le tag sur le module
|
||||||
|
"module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee
|
||||||
|
"ue_id": modimpl.module.ue.id, # les données sur l'ue
|
||||||
|
"ue_code": modimpl.module.ue.ue_code,
|
||||||
|
"ue_acronyme": modimpl.module.ue.acronyme,
|
||||||
|
}
|
||||||
|
return tagdict
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def comp_MoyennesTag(self, tag, force=False) -> list:
|
||||||
|
"""Calcule et renvoie les "moyennes" de tous les étudiants du SemTag
|
||||||
|
(non défaillants) à un tag donné, en prenant en compte
|
||||||
|
tous les modimpl_id concerné par le tag, leur coeff et leur pondération.
|
||||||
|
Force ou non le calcul de la moyenne lorsque des notes sont manquantes.
|
||||||
|
|
||||||
|
Renvoie les informations sous la forme d'une liste
|
||||||
|
[ (moy, somme_coeff_normalise, etudid), ...]
|
||||||
|
"""
|
||||||
|
lesMoyennes = []
|
||||||
|
for etudid in self.get_etudids():
|
||||||
|
(
|
||||||
|
notes,
|
||||||
|
coeffs_norm,
|
||||||
|
ponderations,
|
||||||
|
) = self.get_listesNotesEtCoeffsTagEtudiant(
|
||||||
|
tag, etudid
|
||||||
|
) # les notes associées au tag
|
||||||
|
coeffs = comp_coeff_pond(
|
||||||
|
coeffs_norm, ponderations
|
||||||
|
) # les coeff pondérés par les tags
|
||||||
|
(moyenne, somme_coeffs) = pe_tagtable.moyenne_ponderee_terme_a_terme(
|
||||||
|
notes, coeffs, force=force
|
||||||
|
)
|
||||||
|
lesMoyennes += [
|
||||||
|
(moyenne, somme_coeffs, etudid)
|
||||||
|
] # Un tuple (pour classement résumant les données)
|
||||||
|
return lesMoyennes
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def get_moyennes_DUT(self):
|
||||||
|
"""Lit les moyennes DUT du semestre pour tous les étudiants
|
||||||
|
et les renvoie au même format que comp_MoyennesTag"""
|
||||||
|
return [
|
||||||
|
(self.nt.etud_moy_gen[etudid], 1.0, etudid) for etudid in self.get_etudids()
|
||||||
|
]
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def get_noteEtCoeff_modimpl(self, modimpl_id, etudid, profondeur=2):
|
||||||
|
"""Renvoie un couple donnant la note et le coeff normalisé d'un étudiant à un module d'id modimpl_id.
|
||||||
|
La note et le coeff sont extraits :
|
||||||
|
1) soit des données du semestre en normalisant le coefficient par rapport à la somme des coefficients des modules du semestre,
|
||||||
|
2) soit des données des UE précédemment capitalisées, en recherchant un module de même CODE que le modimpl_id proposé,
|
||||||
|
le coefficient normalisé l'étant alors par rapport au total des coefficients du semestre auquel appartient l'ue capitalisée
|
||||||
|
"""
|
||||||
|
(note, coeff_norm) = (None, None)
|
||||||
|
|
||||||
|
modimpl = get_moduleimpl(modimpl_id) # Le module considéré
|
||||||
|
if modimpl == None or profondeur < 0:
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
# Y-a-t-il eu capitalisation d'UE ?
|
||||||
|
ue_capitalisees = self.get_ue_capitalisees(
|
||||||
|
etudid
|
||||||
|
) # les ue capitalisées des étudiants
|
||||||
|
ue_capitalisees_id = {
|
||||||
|
ue_cap["ue_id"] for ue_cap in ue_capitalisees
|
||||||
|
} # les id des ue capitalisées
|
||||||
|
|
||||||
|
# Si le module ne fait pas partie des UE capitalisées
|
||||||
|
if modimpl.module.ue.id not in ue_capitalisees_id:
|
||||||
|
note = self.nt.get_etud_mod_moy(modimpl_id, etudid) # lecture de la note
|
||||||
|
coeff = modimpl.module.coefficient or 0.0 # le coeff (! non compatible BUT)
|
||||||
|
coeff_norm = (
|
||||||
|
coeff / self.somme_coeffs if self.somme_coeffs != 0 else 0
|
||||||
|
) # le coeff normalisé
|
||||||
|
|
||||||
|
# Si le module fait partie d'une UE capitalisée
|
||||||
|
elif len(ue_capitalisees) > 0:
|
||||||
|
moy_ue_actuelle = get_moy_ue_from_nt(
|
||||||
|
self.nt, etudid, modimpl_id
|
||||||
|
) # la moyenne actuelle
|
||||||
|
# A quel semestre correspond l'ue capitalisée et quelles sont ses notes ?
|
||||||
|
fids_prec = [
|
||||||
|
ue_cap["formsemestre_id"]
|
||||||
|
for ue_cap in ue_capitalisees
|
||||||
|
if ue_cap["ue_code"] == modimpl.module.ue.ue_code
|
||||||
|
] # and ue['semestre_id'] == semestre_id]
|
||||||
|
if len(fids_prec) > 0:
|
||||||
|
# => le formsemestre_id du semestre dont vient la capitalisation
|
||||||
|
fid_prec = fids_prec[0]
|
||||||
|
# Lecture des notes de ce semestre
|
||||||
|
# le tableau de note du semestre considéré:
|
||||||
|
formsemestre_prec = FormSemestre.get_formsemestre(fid_prec)
|
||||||
|
nt_prec: NotesTableCompat = res_sem.load_formsemestre_results(
|
||||||
|
formsemestre_prec
|
||||||
|
)
|
||||||
|
|
||||||
|
# Y-a-t-il un module équivalent c'est à dire correspondant au même code (le module_id n'étant pas significatif en cas de changement de PPN)
|
||||||
|
|
||||||
|
modimpl_prec = [
|
||||||
|
modi
|
||||||
|
for modi in nt_prec.formsemestre.modimpls_sorted
|
||||||
|
if modi.module.code == modimpl.module.code
|
||||||
|
]
|
||||||
|
if len(modimpl_prec) > 0: # si une correspondance est trouvée
|
||||||
|
modprec_id = modimpl_prec[0].id
|
||||||
|
moy_ue_capitalisee = get_moy_ue_from_nt(nt_prec, etudid, modprec_id)
|
||||||
|
if (
|
||||||
|
moy_ue_capitalisee is None
|
||||||
|
) or moy_ue_actuelle >= moy_ue_capitalisee: # on prend la meilleure ue
|
||||||
|
note = self.nt.get_etud_mod_moy(
|
||||||
|
modimpl_id, etudid
|
||||||
|
) # lecture de la note
|
||||||
|
coeff = modimpl.module.coefficient # le coeff
|
||||||
|
# nota: self.somme_coeffs peut être None
|
||||||
|
coeff_norm = (
|
||||||
|
coeff / self.somme_coeffs if self.somme_coeffs else 0
|
||||||
|
) # le coeff normalisé
|
||||||
|
else:
|
||||||
|
semtag_prec = SemestreTag(nt_prec, nt_prec.sem)
|
||||||
|
(note, coeff_norm) = semtag_prec.get_noteEtCoeff_modimpl(
|
||||||
|
modprec_id, etudid, profondeur=profondeur - 1
|
||||||
|
) # lecture de la note via le semtag associé au modimpl capitalisé
|
||||||
|
|
||||||
|
# Sinon - pas de notes à prendre en compte
|
||||||
|
return (note, coeff_norm)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def get_ue_capitalisees(self, etudid) -> list[dict]:
|
||||||
|
"""Renvoie la liste des capitalisation effectivement capitalisées par un étudiant"""
|
||||||
|
if etudid in self.nt.validations.ue_capitalisees.index:
|
||||||
|
return self.nt.validations.ue_capitalisees.loc[[etudid]].to_dict("records")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def get_listesNotesEtCoeffsTagEtudiant(self, tag, etudid):
|
||||||
|
"""Renvoie un triplet (notes, coeffs_norm, ponderations) où notes, coeff_norm et ponderation désignent trois listes
|
||||||
|
donnant -pour un tag donné- les note, coeff et ponderation de chaque modimpl à prendre en compte dans
|
||||||
|
le calcul de la moyenne du tag.
|
||||||
|
Les notes et coeff_norm sont extraits grâce à SemestreTag.get_noteEtCoeff_modimpl (donc dans semestre courant ou UE capitalisée).
|
||||||
|
Les pondérations sont celles déclarées avec le tag (cf. _tagdict)."""
|
||||||
|
|
||||||
|
notes = []
|
||||||
|
coeffs_norm = []
|
||||||
|
ponderations = []
|
||||||
|
for moduleimpl_id, modimpl in self.tagdict[
|
||||||
|
tag
|
||||||
|
].items(): # pour chaque module du semestre relatif au tag
|
||||||
|
(note, coeff_norm) = self.get_noteEtCoeff_modimpl(moduleimpl_id, etudid)
|
||||||
|
if note != None:
|
||||||
|
notes.append(note)
|
||||||
|
coeffs_norm.append(coeff_norm)
|
||||||
|
ponderations.append(modimpl["ponderation"])
|
||||||
|
return (notes, coeffs_norm, ponderations)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Fonctions d'affichage (et d'export csv) des données du semestre en mode debug
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def str_detail_resultat_d_un_tag(self, tag, etudid=None, delim=";"):
|
||||||
|
"""Renvoie une chaine de caractère décrivant les résultats d'étudiants à un tag :
|
||||||
|
rappelle les notes obtenues dans les modules à prendre en compte, les moyennes et les rangs calculés.
|
||||||
|
Si etudid=None, tous les étudiants inscrits dans le semestre sont pris en compte. Sinon seuls les étudiants indiqués sont affichés.
|
||||||
|
"""
|
||||||
|
# Entete
|
||||||
|
chaine = delim.join(["%15s" % "nom", "etudid"]) + delim
|
||||||
|
taglist = self.get_all_tags()
|
||||||
|
if tag in taglist:
|
||||||
|
for mod in self.tagdict[tag].values():
|
||||||
|
chaine += mod["module_code"] + delim
|
||||||
|
chaine += ("%1.1f" % mod["ponderation"]) + delim
|
||||||
|
chaine += "coeff" + delim
|
||||||
|
chaine += delim.join(
|
||||||
|
["moyenne", "rang", "nbinscrit", "somme_coeff", "somme_coeff"]
|
||||||
|
) # ligne 1
|
||||||
|
chaine += "\n"
|
||||||
|
|
||||||
|
# Différents cas de boucles sur les étudiants (de 1 à plusieurs)
|
||||||
|
if etudid == None:
|
||||||
|
lesEtuds = self.get_etudids()
|
||||||
|
elif isinstance(etudid, str) and etudid in self.get_etudids():
|
||||||
|
lesEtuds = [etudid]
|
||||||
|
elif isinstance(etudid, list):
|
||||||
|
lesEtuds = [eid for eid in self.get_etudids() if eid in etudid]
|
||||||
|
else:
|
||||||
|
lesEtuds = []
|
||||||
|
|
||||||
|
for etudid in lesEtuds:
|
||||||
|
descr = (
|
||||||
|
"%15s" % self.nt.get_nom_short(etudid)[:15]
|
||||||
|
+ delim
|
||||||
|
+ str(etudid)
|
||||||
|
+ delim
|
||||||
|
)
|
||||||
|
if tag in taglist:
|
||||||
|
for modimpl_id in self.tagdict[tag]:
|
||||||
|
(note, coeff) = self.get_noteEtCoeff_modimpl(modimpl_id, etudid)
|
||||||
|
descr += (
|
||||||
|
(
|
||||||
|
"%2.2f" % note
|
||||||
|
if note != None and isinstance(note, float)
|
||||||
|
else str(note)
|
||||||
|
)
|
||||||
|
+ delim
|
||||||
|
+ (
|
||||||
|
"%1.5f" % coeff
|
||||||
|
if coeff != None and isinstance(coeff, float)
|
||||||
|
else str(coeff)
|
||||||
|
)
|
||||||
|
+ delim
|
||||||
|
+ (
|
||||||
|
"%1.5f" % (coeff * self.somme_coeffs)
|
||||||
|
if coeff != None and isinstance(coeff, float)
|
||||||
|
else "???" # str(coeff * self._sum_coeff_semestre) # voir avec Cléo
|
||||||
|
)
|
||||||
|
+ delim
|
||||||
|
)
|
||||||
|
moy = self.get_moy_from_resultats(tag, etudid)
|
||||||
|
rang = self.get_rang_from_resultats(tag, etudid)
|
||||||
|
coeff = self.get_coeff_from_resultats(tag, etudid)
|
||||||
|
tot = (
|
||||||
|
coeff * self.somme_coeffs
|
||||||
|
if coeff != None
|
||||||
|
and self.somme_coeffs != None
|
||||||
|
and isinstance(coeff, float)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
descr += (
|
||||||
|
pe_tagtable.TableTag.str_moytag(
|
||||||
|
moy, rang, len(self.get_etudids()), delim=delim
|
||||||
|
)
|
||||||
|
+ delim
|
||||||
|
)
|
||||||
|
descr += (
|
||||||
|
(
|
||||||
|
"%1.5f" % coeff
|
||||||
|
if coeff != None and isinstance(coeff, float)
|
||||||
|
else str(coeff)
|
||||||
|
)
|
||||||
|
+ delim
|
||||||
|
+ (
|
||||||
|
"%.2f" % (tot)
|
||||||
|
if tot != None
|
||||||
|
else str(coeff) + "*" + str(self.somme_coeffs)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
chaine += descr
|
||||||
|
chaine += "\n"
|
||||||
|
return chaine
|
||||||
|
|
||||||
|
def str_tagsModulesEtCoeffs(self):
|
||||||
|
"""Renvoie une chaine affichant la liste des tags associés au semestre, les modules qui les concernent et les coeffs de pondération.
|
||||||
|
Plus concrêtement permet d'afficher le contenu de self._tagdict"""
|
||||||
|
chaine = "Semestre %s d'id %d" % (self.nom, id(self)) + "\n"
|
||||||
|
chaine += " -> somme de coeffs: " + str(self.somme_coeffs) + "\n"
|
||||||
|
taglist = self.get_all_tags()
|
||||||
|
for tag in taglist:
|
||||||
|
chaine += " > " + tag + ": "
|
||||||
|
for modid, mod in self.tagdict[tag].items():
|
||||||
|
chaine += (
|
||||||
|
mod["module_code"]
|
||||||
|
+ " ("
|
||||||
|
+ str(mod["coeff"])
|
||||||
|
+ "*"
|
||||||
|
+ str(mod["ponderation"])
|
||||||
|
+ ") "
|
||||||
|
+ str(modid)
|
||||||
|
+ ", "
|
||||||
|
)
|
||||||
|
chaine += "\n"
|
||||||
|
return chaine
|
||||||
|
|
||||||
|
|
||||||
|
# ************************************************************************
|
||||||
|
# Fonctions diverses
|
||||||
|
# ************************************************************************
|
||||||
|
|
||||||
|
|
||||||
|
# *********************************************
|
||||||
|
def comp_coeff_pond(coeffs, ponderations):
|
||||||
|
"""
|
||||||
|
Applique une ponderation (indiquée dans la liste ponderations) à une liste de coefficients :
|
||||||
|
ex: coeff = [2, 3, 1, None], ponderation = [1, 2, 0, 1] => [2*1, 3*2, 1*0, None]
|
||||||
|
Les coeff peuvent éventuellement être None auquel cas None est conservé ;
|
||||||
|
Les pondérations sont des floattants
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
coeffs == None
|
||||||
|
or ponderations == None
|
||||||
|
or not isinstance(coeffs, list)
|
||||||
|
or not isinstance(ponderations, list)
|
||||||
|
or len(coeffs) != len(ponderations)
|
||||||
|
):
|
||||||
|
raise ValueError("Erreur de paramètres dans comp_coeff_pond")
|
||||||
|
return [
|
||||||
|
(None if coeffs[i] == None else coeffs[i] * ponderations[i])
|
||||||
|
for i in range(len(coeffs))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def get_moduleimpl(modimpl_id) -> dict:
|
||||||
|
"""Renvoie l'objet modimpl dont l'id est modimpl_id"""
|
||||||
|
modimpl = db.session.get(ModuleImpl, modimpl_id)
|
||||||
|
if modimpl:
|
||||||
|
return modimpl
|
||||||
|
if SemestreTag.DEBUG:
|
||||||
|
log(
|
||||||
|
"SemestreTag.get_moduleimpl( %s ) : le modimpl recherche n'existe pas"
|
||||||
|
% (modimpl_id)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# **********************************************
|
||||||
|
def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float:
|
||||||
|
"""Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve
|
||||||
|
le module de modimpl_id
|
||||||
|
"""
|
||||||
|
# ré-écrit
|
||||||
|
modimpl = get_moduleimpl(modimpl_id) # le module
|
||||||
|
ue_status = nt.get_etud_ue_status(etudid, modimpl.module.ue.id)
|
||||||
|
if ue_status is None:
|
||||||
|
return None
|
||||||
|
return ue_status["moy"]
|
@ -1,310 +0,0 @@
|
|||||||
# -*- mode: python -*-
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gestion scolarite IUT
|
|
||||||
#
|
|
||||||
# Copyright (c) 1999 - 2024 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
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
# Module "Avis de poursuite d'étude"
|
|
||||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""
|
|
||||||
Created on Fri Sep 9 09:15:05 2016
|
|
||||||
|
|
||||||
@author: barasc
|
|
||||||
"""
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
import app.pe.pe_etudiant
|
|
||||||
from app import db, ScoValueError
|
|
||||||
from app import comp
|
|
||||||
from app.comp.res_sem import load_formsemestre_results
|
|
||||||
from app.models import FormSemestre
|
|
||||||
from app.models.moduleimpls import ModuleImpl
|
|
||||||
import app.pe.pe_affichage as pe_affichage
|
|
||||||
from app.pe.pe_tabletags import TableTag, MoyenneTag
|
|
||||||
from app.scodoc import sco_tag_module
|
|
||||||
from app.scodoc.codes_cursus import UE_SPORT
|
|
||||||
|
|
||||||
|
|
||||||
class SemestreTag(TableTag):
|
|
||||||
"""
|
|
||||||
Un SemestreTag représente les résultats des étudiants à un semestre, en donnant
|
|
||||||
accès aux moyennes par tag.
|
|
||||||
Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, formsemestre_id: int):
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
formsemestre_id: Identifiant du ``FormSemestre`` sur lequel il se base
|
|
||||||
"""
|
|
||||||
TableTag.__init__(self)
|
|
||||||
|
|
||||||
# Le semestre
|
|
||||||
self.formsemestre_id = formsemestre_id
|
|
||||||
self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
||||||
|
|
||||||
# Le nom du semestre taggué
|
|
||||||
self.nom = self.get_repr()
|
|
||||||
|
|
||||||
# Les résultats du semestre
|
|
||||||
self.nt = load_formsemestre_results(self.formsemestre)
|
|
||||||
|
|
||||||
# Les étudiants
|
|
||||||
self.etuds = self.nt.etuds
|
|
||||||
self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds}
|
|
||||||
|
|
||||||
# Les notes, les modules implémentés triés, les étudiants, les coeffs,
|
|
||||||
# récupérés notamment de py:mod:`res_but`
|
|
||||||
self.sem_cube = self.nt.sem_cube
|
|
||||||
self.modimpls_sorted = self.nt.formsemestre.modimpls_sorted
|
|
||||||
self.modimpl_coefs_df = self.nt.modimpl_coefs_df
|
|
||||||
|
|
||||||
# Les inscriptions au module et les dispenses d'UE
|
|
||||||
self.modimpl_inscr_df = self.nt.modimpl_inscr_df
|
|
||||||
self.ues = self.nt.ues
|
|
||||||
self.ues_inscr_parcours_df = self.nt.load_ues_inscr_parcours()
|
|
||||||
self.dispense_ues = self.nt.dispense_ues
|
|
||||||
|
|
||||||
# Les tags :
|
|
||||||
## Saisis par l'utilisateur
|
|
||||||
tags_personnalises = get_synthese_tags_personnalises_semestre(
|
|
||||||
self.nt.formsemestre
|
|
||||||
)
|
|
||||||
noms_tags_perso = list(set(tags_personnalises.keys()))
|
|
||||||
|
|
||||||
## Déduit des compétences
|
|
||||||
dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre)
|
|
||||||
noms_tags_comp = list(set(dict_ues_competences.values()))
|
|
||||||
noms_tags_auto = ["but"] + noms_tags_comp
|
|
||||||
self.tags = noms_tags_perso + noms_tags_auto
|
|
||||||
"""Tags du semestre taggué"""
|
|
||||||
|
|
||||||
## Vérifie l'unicité des tags
|
|
||||||
if len(set(self.tags)) != len(self.tags):
|
|
||||||
intersection = list(set(noms_tags_perso) & set(noms_tags_auto))
|
|
||||||
liste_intersection = "\n".join(
|
|
||||||
[f"<li><code>{tag}</code></li>" for tag in intersection]
|
|
||||||
)
|
|
||||||
s = "s" if len(intersection) > 0 else ""
|
|
||||||
message = f"""Erreur dans le module PE : Un des tags saisis dans votre
|
|
||||||
programme de formation fait parti des tags réservés. En particulier,
|
|
||||||
votre semestre <em>{self.formsemestre.titre_annee()}</em>
|
|
||||||
contient le{s} tag{s} réservé{s} suivant :
|
|
||||||
<ul>
|
|
||||||
{liste_intersection}
|
|
||||||
</ul>
|
|
||||||
Modifiez votre programme de formation pour le{s} supprimer.
|
|
||||||
Il{s} ser{'ont' if s else 'a'} automatiquement à vos documents de poursuites d'études.
|
|
||||||
"""
|
|
||||||
raise ScoValueError(message)
|
|
||||||
|
|
||||||
# Calcul des moyennes & les classements de chaque étudiant à chaque tag
|
|
||||||
self.moyennes_tags = {}
|
|
||||||
|
|
||||||
for tag in tags_personnalises:
|
|
||||||
# pe_affichage.pe_print(f" -> Traitement du tag {tag}")
|
|
||||||
moy_gen_tag = self.compute_moyenne_tag(tag, tags_personnalises)
|
|
||||||
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag)
|
|
||||||
|
|
||||||
# Ajoute les moyennes générales de BUT pour le semestre considéré
|
|
||||||
moy_gen_but = self.nt.etud_moy_gen
|
|
||||||
self.moyennes_tags["but"] = MoyenneTag("but", moy_gen_but)
|
|
||||||
|
|
||||||
# Ajoute les moyennes par compétence
|
|
||||||
for ue_id, competence in dict_ues_competences.items():
|
|
||||||
if competence not in self.moyennes_tags:
|
|
||||||
moy_ue = self.nt.etud_moy_ue[ue_id]
|
|
||||||
self.moyennes_tags[competence] = MoyenneTag(competence, moy_ue)
|
|
||||||
|
|
||||||
self.tags_sorted = self.get_all_tags()
|
|
||||||
"""Tags (personnalisés+compétences) par ordre alphabétique"""
|
|
||||||
|
|
||||||
# Synthétise l'ensemble des moyennes dans un dataframe
|
|
||||||
|
|
||||||
self.notes = self.df_notes()
|
|
||||||
"""Dataframe synthétique des notes par tag"""
|
|
||||||
|
|
||||||
pe_affichage.pe_print(
|
|
||||||
f" => Traitement des tags {', '.join(self.tags_sorted)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_repr(self):
|
|
||||||
"""Nom affiché pour le semestre taggué"""
|
|
||||||
return app.pe.pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True)
|
|
||||||
|
|
||||||
def compute_moyenne_tag(self, tag: str, tags_infos: dict) -> pd.Series:
|
|
||||||
"""Calcule la moyenne des étudiants pour le tag indiqué,
|
|
||||||
pour ce SemestreTag, en ayant connaissance des informations sur
|
|
||||||
les tags (dictionnaire donnant les coeff de repondération)
|
|
||||||
|
|
||||||
Sont pris en compte les modules implémentés associés au tag,
|
|
||||||
avec leur éventuel coefficient de **repondération**, en utilisant les notes
|
|
||||||
chargées pour ce SemestreTag.
|
|
||||||
|
|
||||||
Force ou non le calcul de la moyenne lorsque des notes sont manquantes.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
La série des moyennes
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Adaptation du mask de calcul des moyennes au tag visé
|
|
||||||
modimpls_mask = [
|
|
||||||
modimpl.module.ue.type != UE_SPORT
|
|
||||||
for modimpl in self.formsemestre.modimpls_sorted
|
|
||||||
]
|
|
||||||
|
|
||||||
# Désactive tous les modules qui ne sont pas pris en compte pour ce tag
|
|
||||||
for i, modimpl in enumerate(self.formsemestre.modimpls_sorted):
|
|
||||||
if modimpl.moduleimpl_id not in tags_infos[tag]:
|
|
||||||
modimpls_mask[i] = False
|
|
||||||
|
|
||||||
# Applique la pondération des coefficients
|
|
||||||
modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy()
|
|
||||||
for modimpl_id in tags_infos[tag]:
|
|
||||||
ponderation = tags_infos[tag][modimpl_id]["ponderation"]
|
|
||||||
modimpl_coefs_ponderes_df[modimpl_id] *= ponderation
|
|
||||||
|
|
||||||
# Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)#
|
|
||||||
moyennes_ues_tag = comp.moy_ue.compute_ue_moys_apc(
|
|
||||||
self.sem_cube,
|
|
||||||
self.etuds,
|
|
||||||
self.formsemestre.modimpls_sorted,
|
|
||||||
self.modimpl_inscr_df,
|
|
||||||
modimpl_coefs_ponderes_df,
|
|
||||||
modimpls_mask,
|
|
||||||
self.dispense_ues,
|
|
||||||
block=self.formsemestre.block_moyennes,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Les ects
|
|
||||||
ects = self.ues_inscr_parcours_df.fillna(0.0) * [
|
|
||||||
ue.ects for ue in self.ues if ue.type != UE_SPORT
|
|
||||||
]
|
|
||||||
|
|
||||||
# Calcule la moyenne générale dans le semestre (pondérée par le ECTS)
|
|
||||||
moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects(
|
|
||||||
moyennes_ues_tag,
|
|
||||||
ects,
|
|
||||||
formation_id=self.formsemestre.formation_id,
|
|
||||||
skip_empty_ues=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
return moy_gen_tag
|
|
||||||
|
|
||||||
|
|
||||||
def get_moduleimpl(modimpl_id) -> dict:
|
|
||||||
"""Renvoie l'objet modimpl dont l'id est modimpl_id"""
|
|
||||||
modimpl = db.session.get(ModuleImpl, modimpl_id)
|
|
||||||
if modimpl:
|
|
||||||
return modimpl
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float:
|
|
||||||
"""Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve
|
|
||||||
le module de modimpl_id
|
|
||||||
"""
|
|
||||||
# ré-écrit
|
|
||||||
modimpl = get_moduleimpl(modimpl_id) # le module
|
|
||||||
ue_status = nt.get_etud_ue_status(etudid, modimpl.module.ue.id)
|
|
||||||
if ue_status is None:
|
|
||||||
return None
|
|
||||||
return ue_status["moy"]
|
|
||||||
|
|
||||||
|
|
||||||
def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre):
|
|
||||||
"""Etant données les implémentations des modules du semestre (modimpls),
|
|
||||||
synthétise les tags renseignés dans le programme pédagogique &
|
|
||||||
associés aux modules du semestre,
|
|
||||||
en les associant aux modimpls qui les concernent (modimpl_id) et
|
|
||||||
aucoeff et pondération fournie avec le tag (par défaut 1 si non indiquée)).
|
|
||||||
|
|
||||||
|
|
||||||
Args:
|
|
||||||
formsemestre: Le formsemestre à la base de la recherche des tags
|
|
||||||
|
|
||||||
Return:
|
|
||||||
Un dictionnaire de tags
|
|
||||||
"""
|
|
||||||
synthese_tags = {}
|
|
||||||
|
|
||||||
# Instance des modules du semestre
|
|
||||||
modimpls = formsemestre.modimpls_sorted
|
|
||||||
|
|
||||||
for modimpl in modimpls:
|
|
||||||
modimpl_id = modimpl.id
|
|
||||||
|
|
||||||
# Liste des tags pour le module concerné
|
|
||||||
tags = sco_tag_module.module_tag_list(modimpl.module.id)
|
|
||||||
|
|
||||||
# Traitement des tags recensés, chacun pouvant étant de la forme
|
|
||||||
# "mathématiques", "théorie", "pe:0", "maths:2"
|
|
||||||
for tag in tags:
|
|
||||||
# Extraction du nom du tag et du coeff de pondération
|
|
||||||
(tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag)
|
|
||||||
|
|
||||||
# Ajout d'une clé pour le tag
|
|
||||||
if tagname not in synthese_tags:
|
|
||||||
synthese_tags[tagname] = {}
|
|
||||||
|
|
||||||
# Ajout du module (modimpl) au tagname considéré
|
|
||||||
synthese_tags[tagname][modimpl_id] = {
|
|
||||||
"modimpl": modimpl, # les données sur le module
|
|
||||||
# "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre
|
|
||||||
"ponderation": ponderation, # la pondération demandée pour le tag sur le module
|
|
||||||
# "module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee
|
|
||||||
# "ue_id": modimpl.module.ue.id, # les données sur l'ue
|
|
||||||
# "ue_code": modimpl.module.ue.ue_code,
|
|
||||||
# "ue_acronyme": modimpl.module.ue.acronyme,
|
|
||||||
}
|
|
||||||
|
|
||||||
return synthese_tags
|
|
||||||
|
|
||||||
|
|
||||||
def get_noms_competences_from_ues(formsemestre: FormSemestre) -> dict[int, str]:
|
|
||||||
"""Partant d'un formsemestre, extrait le nom des compétences associés
|
|
||||||
à (ou aux) parcours des étudiants du formsemestre.
|
|
||||||
|
|
||||||
Ignore les UEs non associées à un niveau de compétence.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
formsemestre: Un FormSemestre
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionnaire {ue_id: nom_competence} lisant tous les noms des compétences
|
|
||||||
en les raccrochant à leur ue
|
|
||||||
"""
|
|
||||||
# Les résultats du semestre
|
|
||||||
nt = load_formsemestre_results(formsemestre)
|
|
||||||
|
|
||||||
noms_competences = {}
|
|
||||||
for ue in nt.ues:
|
|
||||||
if ue.niveau_competence and ue.type != UE_SPORT:
|
|
||||||
# ?? inutilisé ordre = ue.niveau_competence.ordre
|
|
||||||
nom = ue.niveau_competence.competence.titre
|
|
||||||
noms_competences[ue.ue_id] = f"comp. {nom}"
|
|
||||||
return noms_competences
|
|
324
app/pe/pe_settag.py
Normal file
324
app/pe/pe_settag.py
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
# -*- mode: python -*-
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gestion scolarite IUT
|
||||||
|
#
|
||||||
|
# Copyright (c) 1999 - 2023 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
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Module "Avis de poursuite d'étude"
|
||||||
|
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
Created on Fri Sep 9 09:15:05 2016
|
||||||
|
|
||||||
|
@author: barasc
|
||||||
|
"""
|
||||||
|
|
||||||
|
from app.pe.pe_tools import pe_print, PE_DEBUG
|
||||||
|
from app.pe import pe_tagtable
|
||||||
|
|
||||||
|
|
||||||
|
class SetTag(pe_tagtable.TableTag):
|
||||||
|
"""Agrège plusieurs semestres (ou settag) taggués (SemestreTag/Settag de 1 à 4) pour extraire des moyennes
|
||||||
|
et des classements par tag pour un groupe d'étudiants donnés.
|
||||||
|
par. exemple fusion d'un parcours ['S1', 'S2', 'S3'] donnant un nom_combinaison = '3S'
|
||||||
|
Le settag est identifié sur la base du dernier semestre (ici le 'S3') ;
|
||||||
|
les étudiants considérés sont donc ceux inscrits dans ce S3
|
||||||
|
à condition qu'ils disposent d'un parcours sur tous les semestres fusionnés valides (par. ex
|
||||||
|
un etudiant non inscrit dans un S1 mais dans un S2 et un S3 n'est pas pris en compte).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------------------
|
||||||
|
def __init__(self, nom_combinaison, parcours):
|
||||||
|
|
||||||
|
pe_tagtable.TableTag.__init__(self, nom=nom_combinaison)
|
||||||
|
self.combinaison = nom_combinaison
|
||||||
|
self.parcours = parcours # Le groupe de semestres/parcours à aggréger
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------
|
||||||
|
def set_Etudiants(self, etudiants, juryPEDict, etudInfoDict, nom_sem_final=None):
|
||||||
|
"""Détermine la liste des étudiants à prendre en compte, en partant de
|
||||||
|
la liste en paramètre et en vérifiant qu'ils ont tous un parcours valide."""
|
||||||
|
if nom_sem_final:
|
||||||
|
self.nom += "_" + nom_sem_final
|
||||||
|
for etudid in etudiants:
|
||||||
|
parcours_incomplet = (
|
||||||
|
sum([juryPEDict[etudid][nom_sem] == None for nom_sem in self.parcours])
|
||||||
|
> 0
|
||||||
|
) # manque-t-il des formsemestre_id validant aka l'étudiant n'a pas été inscrit dans tous les semestres de l'aggrégat
|
||||||
|
if not parcours_incomplet:
|
||||||
|
self.inscrlist.append(etudInfoDict[etudid])
|
||||||
|
self.identdict[etudid] = etudInfoDict[etudid]
|
||||||
|
|
||||||
|
delta = len(etudiants) - len(self.inscrlist)
|
||||||
|
if delta > 0:
|
||||||
|
pe_print(self.nom + " -> " + str(delta) + " étudiants supprimés")
|
||||||
|
|
||||||
|
# Le sous-ensemble des parcours
|
||||||
|
self.parcoursDict = {etudid: juryPEDict[etudid] for etudid in self.identdict}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------
|
||||||
|
def get_Fids_in_settag(self):
|
||||||
|
"""Renvoie la liste des semestres (leur formsemestre_id) à prendre en compte
|
||||||
|
pour le calcul des moyennes, en considérant tous les étudiants inscrits et
|
||||||
|
tous les semestres de leur parcours"""
|
||||||
|
return list(
|
||||||
|
{
|
||||||
|
self.parcoursDict[etudid][nom_sem]
|
||||||
|
for etudid in self.identdict
|
||||||
|
for nom_sem in self.parcours
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
def set_SemTagDict(self, SemTagDict):
|
||||||
|
"""Mémorise les semtag nécessaires au jury."""
|
||||||
|
self.SemTagDict = {fid: SemTagDict[fid] for fid in self.get_Fids_in_settag()}
|
||||||
|
if PE_DEBUG >= 1:
|
||||||
|
pe_print(" => %d semestres fusionnés" % len(self.SemTagDict))
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------------------
|
||||||
|
def comp_data_settag(self):
|
||||||
|
"""Calcule tous les données numériques relatives au settag"""
|
||||||
|
# Attributs relatifs aux tag pour les modules pris en compte
|
||||||
|
self.taglist = self.do_taglist() # la liste des tags
|
||||||
|
self.do_tagdict() # le dico descriptif des tags
|
||||||
|
# if PE_DEBUG >= 1: pe_print(" => Tags = " + ", ".join( self.taglist ))
|
||||||
|
|
||||||
|
# Calcul des moyennes de chaque étudiant par tag
|
||||||
|
reussiteAjoutTag = {"OK": [], "KO": []}
|
||||||
|
for tag in self.taglist:
|
||||||
|
moyennes = self.comp_MoyennesSetTag(tag, force=False)
|
||||||
|
res = self.add_moyennesTag(tag, moyennes) # pas de notes => pas de moyenne
|
||||||
|
reussiteAjoutTag["OK" if res else "KO"].append(tag)
|
||||||
|
if len(reussiteAjoutTag["OK"]) > 0 and PE_DEBUG:
|
||||||
|
pe_print(
|
||||||
|
" => Fusion de %d tags : " % (len(reussiteAjoutTag["OK"]))
|
||||||
|
+ ", ".join(reussiteAjoutTag["OK"])
|
||||||
|
)
|
||||||
|
if len(reussiteAjoutTag["KO"]) > 0 and PE_DEBUG:
|
||||||
|
pe_print(
|
||||||
|
" => %d tags manquants : " % (len(reussiteAjoutTag["KO"]))
|
||||||
|
+ ", ".join(reussiteAjoutTag["KO"])
|
||||||
|
)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------------------
|
||||||
|
def get_etudids(self):
|
||||||
|
return list(self.identdict.keys())
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------------------
|
||||||
|
def do_taglist(self):
|
||||||
|
"""Parcourt les tags des semestres taggués et les synthétise sous la forme
|
||||||
|
d'une liste en supprimant les doublons
|
||||||
|
"""
|
||||||
|
ensemble = []
|
||||||
|
for semtag in self.SemTagDict.values():
|
||||||
|
ensemble.extend(semtag.get_all_tags())
|
||||||
|
return sorted(list(set(ensemble)))
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------------------
|
||||||
|
def do_tagdict(self):
|
||||||
|
"""Synthétise la liste des modules pris en compte dans le calcul d'un tag (pour analyse des résultats)"""
|
||||||
|
self.tagdict = {}
|
||||||
|
for semtag in self.SemTagDict.values():
|
||||||
|
for tag in semtag.get_all_tags():
|
||||||
|
if tag != "dut":
|
||||||
|
if tag not in self.tagdict:
|
||||||
|
self.tagdict[tag] = {}
|
||||||
|
for mod in semtag.tagdict[tag]:
|
||||||
|
self.tagdict[tag][mod] = semtag.tagdict[tag][mod]
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------------------
|
||||||
|
def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid):
|
||||||
|
"""Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs)
|
||||||
|
avec notes et coeffs deux listes"""
|
||||||
|
lesSemsDeLEtudiant = [
|
||||||
|
self.parcoursDict[etudid][nom_sem] for nom_sem in self.parcours
|
||||||
|
] # peuvent être None
|
||||||
|
|
||||||
|
notes = [
|
||||||
|
self.SemTagDict[fid].get_moy_from_resultats(tag, etudid)
|
||||||
|
for fid in lesSemsDeLEtudiant
|
||||||
|
if tag in self.SemTagDict[fid].taglist
|
||||||
|
] # eventuellement None
|
||||||
|
coeffs = [
|
||||||
|
self.SemTagDict[fid].get_coeff_from_resultats(tag, etudid)
|
||||||
|
for fid in lesSemsDeLEtudiant
|
||||||
|
if tag in self.SemTagDict[fid].taglist
|
||||||
|
]
|
||||||
|
return (notes, coeffs)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------------------
|
||||||
|
def comp_MoyennesSetTag(self, tag, force=False):
|
||||||
|
"""Calcule et renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les semestres taggués
|
||||||
|
de l'aggrégat, et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération
|
||||||
|
appliqué dans cette moyenne.
|
||||||
|
|
||||||
|
Force ou non le calcul de la moyenne lorsque des notes sont manquantes.
|
||||||
|
|
||||||
|
Renvoie les informations sous la forme d'une liste [etudid: (moy, somme_coeff_normalisée, rang), ...}
|
||||||
|
"""
|
||||||
|
# if tag not in self.get_all_tags() : return None
|
||||||
|
|
||||||
|
# Calcule les moyennes
|
||||||
|
lesMoyennes = []
|
||||||
|
for (
|
||||||
|
etudid
|
||||||
|
) in (
|
||||||
|
self.get_etudids()
|
||||||
|
): # Pour tous les étudiants non défaillants du semestre inscrits dans des modules relatifs au tag
|
||||||
|
(notes, coeffs_norm) = self.get_NotesEtCoeffsSetTagEtudiant(
|
||||||
|
tag, etudid
|
||||||
|
) # lecture des notes associées au tag
|
||||||
|
(moyenne, somme_coeffs) = pe_tagtable.moyenne_ponderee_terme_a_terme(
|
||||||
|
notes, coeffs_norm, force=force
|
||||||
|
)
|
||||||
|
lesMoyennes += [
|
||||||
|
(moyenne, somme_coeffs, etudid)
|
||||||
|
] # Un tuple (pour classement résumant les données)
|
||||||
|
return lesMoyennes
|
||||||
|
|
||||||
|
|
||||||
|
class SetTagInterClasse(pe_tagtable.TableTag):
|
||||||
|
"""Récupère les moyennes de SetTag aggrégant un même parcours (par ex un ['S1', 'S2'] n'ayant pas fini au même S2
|
||||||
|
pour fournir un interclassement sur un groupe d'étudiant => seul compte alors la promo
|
||||||
|
nom_combinaison = 'S1' ou '1A'
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------------------
|
||||||
|
def __init__(self, nom_combinaison, diplome):
|
||||||
|
|
||||||
|
pe_tagtable.TableTag.__init__(self, nom=f"{nom_combinaison}_{diplome or ''}")
|
||||||
|
self.combinaison = nom_combinaison
|
||||||
|
self.parcoursDict = {}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------
|
||||||
|
def set_Etudiants(self, etudiants, juryPEDict, etudInfoDict, nom_sem_final=None):
|
||||||
|
"""Détermine la liste des étudiants à prendre en compte, en partant de
|
||||||
|
la liste fournie en paramètre et en vérifiant que l'étudiant dispose bien d'un parcours valide pour la combinaison demandée.
|
||||||
|
Renvoie le nombre d'étudiants effectivement inscrits."""
|
||||||
|
if nom_sem_final:
|
||||||
|
self.nom += "_" + nom_sem_final
|
||||||
|
for etudid in etudiants:
|
||||||
|
if juryPEDict[etudid][self.combinaison] != None:
|
||||||
|
self.inscrlist.append(etudInfoDict[etudid])
|
||||||
|
self.identdict[etudid] = etudInfoDict[etudid]
|
||||||
|
self.parcoursDict[etudid] = juryPEDict[etudid]
|
||||||
|
return len(self.inscrlist)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------
|
||||||
|
def get_Fids_in_settag(self):
|
||||||
|
"""Renvoie la liste des semestres (les formsemestre_id finissant la combinaison par ex. '3S' dont les fid des S3) à prendre en compte
|
||||||
|
pour les moyennes, en considérant tous les étudiants inscrits"""
|
||||||
|
return list(
|
||||||
|
{self.parcoursDict[etudid][self.combinaison] for etudid in self.identdict}
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------------
|
||||||
|
def set_SetTagDict(self, SetTagDict):
|
||||||
|
"""Mémorise les settag nécessaires au jury."""
|
||||||
|
self.SetTagDict = {
|
||||||
|
fid: SetTagDict[fid] for fid in self.get_Fids_in_settag() if fid != None
|
||||||
|
}
|
||||||
|
if PE_DEBUG >= 1:
|
||||||
|
pe_print(" => %d semestres utilisés" % len(self.SetTagDict))
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------------------
|
||||||
|
def comp_data_settag(self):
|
||||||
|
"""Calcule tous les données numériques relatives au settag"""
|
||||||
|
# Attributs relatifs aux tag pour les modules pris en compte
|
||||||
|
self.taglist = self.do_taglist()
|
||||||
|
|
||||||
|
# if PE_DEBUG >= 1: pe_print(" => Tags = " + ", ".join( self.taglist ))
|
||||||
|
|
||||||
|
# Calcul des moyennes de chaque étudiant par tag
|
||||||
|
reussiteAjoutTag = {"OK": [], "KO": []}
|
||||||
|
for tag in self.taglist:
|
||||||
|
moyennes = self.get_MoyennesSetTag(tag, force=False)
|
||||||
|
res = self.add_moyennesTag(tag, moyennes) # pas de notes => pas de moyenne
|
||||||
|
reussiteAjoutTag["OK" if res else "KO"].append(tag)
|
||||||
|
if len(reussiteAjoutTag["OK"]) > 0 and PE_DEBUG:
|
||||||
|
pe_print(
|
||||||
|
" => Interclassement de %d tags : " % (len(reussiteAjoutTag["OK"]))
|
||||||
|
+ ", ".join(reussiteAjoutTag["OK"])
|
||||||
|
)
|
||||||
|
if len(reussiteAjoutTag["KO"]) > 0 and PE_DEBUG:
|
||||||
|
pe_print(
|
||||||
|
" => %d tags manquants : " % (len(reussiteAjoutTag["KO"]))
|
||||||
|
+ ", ".join(reussiteAjoutTag["KO"])
|
||||||
|
)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------------------
|
||||||
|
def get_etudids(self):
|
||||||
|
return list(self.identdict.keys())
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------------------
|
||||||
|
def do_taglist(self):
|
||||||
|
"""Parcourt les tags des semestres taggués et les synthétise sous la forme
|
||||||
|
d'une liste en supprimant les doublons
|
||||||
|
"""
|
||||||
|
ensemble = []
|
||||||
|
for settag in self.SetTagDict.values():
|
||||||
|
ensemble.extend(settag.get_all_tags())
|
||||||
|
return sorted(list(set(ensemble)))
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------------------
|
||||||
|
def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid):
|
||||||
|
"""Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs)
|
||||||
|
avec notes et coeffs deux listes"""
|
||||||
|
leSetTagDeLetudiant = self.parcoursDict[etudid][self.combinaison]
|
||||||
|
|
||||||
|
note = self.SetTagDict[leSetTagDeLetudiant].get_moy_from_resultats(tag, etudid)
|
||||||
|
coeff = self.SetTagDict[leSetTagDeLetudiant].get_coeff_from_resultats(
|
||||||
|
tag, etudid
|
||||||
|
)
|
||||||
|
return (note, coeff)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------------------
|
||||||
|
def get_MoyennesSetTag(self, tag, force=False):
|
||||||
|
"""Renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les settag de l'aggrégat,
|
||||||
|
et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération
|
||||||
|
appliqué dans cette moyenne.
|
||||||
|
|
||||||
|
Force ou non le calcul de la moyenne lorsque des notes sont manquantes.
|
||||||
|
|
||||||
|
Renvoie les informations sous la forme d'une liste [etudid: (moy, somme_coeff_normalisée, rang), ...}
|
||||||
|
"""
|
||||||
|
# if tag not in self.get_all_tags() : return None
|
||||||
|
|
||||||
|
# Calcule les moyennes
|
||||||
|
lesMoyennes = []
|
||||||
|
for (
|
||||||
|
etudid
|
||||||
|
) in (
|
||||||
|
self.get_etudids()
|
||||||
|
): # Pour tous les étudiants non défaillants du semestre inscrits dans des modules relatifs au tag
|
||||||
|
(moyenne, somme_coeffs) = self.get_NotesEtCoeffsSetTagEtudiant(
|
||||||
|
tag, etudid
|
||||||
|
) # lecture des notes associées au tag
|
||||||
|
lesMoyennes += [
|
||||||
|
(moyenne, somme_coeffs, etudid)
|
||||||
|
] # Un tuple (pour classement résumant les données)
|
||||||
|
return lesMoyennes
|
@ -1,263 +0,0 @@
|
|||||||
# -*- mode: python -*-
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gestion scolarite IUT
|
|
||||||
#
|
|
||||||
# Copyright (c) 1999 - 2024 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
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
# Module "Avis de poursuite d'étude"
|
|
||||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Created on Thu Sep 8 09:36:33 2016
|
|
||||||
|
|
||||||
@author: barasc
|
|
||||||
"""
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from app import ScoValueError
|
|
||||||
from app.comp.moy_sem import comp_ranks_series
|
|
||||||
from app.pe import pe_affichage
|
|
||||||
from app.pe.pe_affichage import SANS_NOTE
|
|
||||||
from app.scodoc import sco_utils as scu
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
|
|
||||||
TAGS_RESERVES = ["but"]
|
|
||||||
|
|
||||||
|
|
||||||
class MoyenneTag:
|
|
||||||
def __init__(self, tag: str, notes: pd.Series):
|
|
||||||
"""Classe centralisant la synthèse des moyennes/classements d'une série
|
|
||||||
d'étudiants à un tag donné, en stockant un dictionnaire :
|
|
||||||
|
|
||||||
``
|
|
||||||
{
|
|
||||||
"notes": la Serie pandas des notes (float),
|
|
||||||
"classements": la Serie pandas des classements (float),
|
|
||||||
"min": la note minimum,
|
|
||||||
"max": la note maximum,
|
|
||||||
"moy": la moyenne,
|
|
||||||
"nb_inscrits": le nombre d'étudiants ayant une note,
|
|
||||||
}
|
|
||||||
``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tag: Un tag
|
|
||||||
note: Une série de notes (moyenne) sous forme d'un pd.Series()
|
|
||||||
"""
|
|
||||||
self.tag = tag
|
|
||||||
"""Le tag associé à la moyenne"""
|
|
||||||
self.etudids = list(notes.index) # calcul à venir
|
|
||||||
"""Les id des étudiants"""
|
|
||||||
self.inscrits_ids = notes[notes.notnull()].index.to_list()
|
|
||||||
"""Les id des étudiants dont la moyenne est non nulle"""
|
|
||||||
self.df: pd.DataFrame = self.comp_moy_et_stat(notes)
|
|
||||||
"""Le dataframe retraçant les moyennes/classements/statistiques"""
|
|
||||||
self.synthese = self.to_dict()
|
|
||||||
"""La synthèse (dictionnaire) des notes/classements/statistiques"""
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
"""Egalité de deux MoyenneTag lorsque leur tag sont identiques"""
|
|
||||||
return self.tag == other.tag
|
|
||||||
|
|
||||||
def comp_moy_et_stat(self, notes: pd.Series) -> dict:
|
|
||||||
"""Calcule et structure les données nécessaires au PE pour une série
|
|
||||||
de notes (souvent une moyenne par tag) dans un dictionnaire spécifique.
|
|
||||||
|
|
||||||
Partant des notes, sont calculés les classements (en ne tenant compte
|
|
||||||
que des notes non nulles).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
notes: Une série de notes (avec des éventuels NaN)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Un dictionnaire stockant les notes, les classements, le min,
|
|
||||||
le max, la moyenne, le nb de notes (donc d'inscrits)
|
|
||||||
"""
|
|
||||||
df = pd.DataFrame(
|
|
||||||
np.nan,
|
|
||||||
index=self.etudids,
|
|
||||||
columns=[
|
|
||||||
"note",
|
|
||||||
"classement",
|
|
||||||
"rang",
|
|
||||||
"min",
|
|
||||||
"max",
|
|
||||||
"moy",
|
|
||||||
"nb_etuds",
|
|
||||||
"nb_inscrits",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Supprime d'éventuelles chaines de caractères dans les notes
|
|
||||||
notes = pd.to_numeric(notes, errors="coerce")
|
|
||||||
df["note"] = notes
|
|
||||||
|
|
||||||
# Les nb d'étudiants & nb d'inscrits
|
|
||||||
df["nb_etuds"] = len(self.etudids)
|
|
||||||
df.loc[self.inscrits_ids, "nb_inscrits"] = len(self.inscrits_ids)
|
|
||||||
|
|
||||||
# Le classement des inscrits
|
|
||||||
notes_non_nulles = notes[self.inscrits_ids]
|
|
||||||
(class_str, class_int) = comp_ranks_series(notes_non_nulles)
|
|
||||||
df.loc[self.inscrits_ids, "classement"] = class_int
|
|
||||||
|
|
||||||
# Le rang (classement/nb_inscrit)
|
|
||||||
df["rang"] = df["rang"].astype(str)
|
|
||||||
df.loc[self.inscrits_ids, "rang"] = (
|
|
||||||
df.loc[self.inscrits_ids, "classement"].astype(int).astype(str)
|
|
||||||
+ "/"
|
|
||||||
+ df.loc[self.inscrits_ids, "nb_inscrits"].astype(int).astype(str)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Les stat (des inscrits)
|
|
||||||
df.loc[self.inscrits_ids, "min"] = notes.min()
|
|
||||||
df.loc[self.inscrits_ids, "max"] = notes.max()
|
|
||||||
df.loc[self.inscrits_ids, "moy"] = notes.mean()
|
|
||||||
|
|
||||||
return df
|
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
|
||||||
"""Renvoie un dictionnaire de synthèse des moyennes/classements/statistiques"""
|
|
||||||
synthese = {
|
|
||||||
"notes": self.df["note"],
|
|
||||||
"classements": self.df["classement"],
|
|
||||||
"min": self.df["min"].mean(),
|
|
||||||
"max": self.df["max"].mean(),
|
|
||||||
"moy": self.df["moy"].mean(),
|
|
||||||
"nb_inscrits": self.df["nb_inscrits"].mean(),
|
|
||||||
}
|
|
||||||
return synthese
|
|
||||||
|
|
||||||
def get_notes(self):
|
|
||||||
"""Série des notes, arrondies à 2 chiffres après la virgule"""
|
|
||||||
return self.df["note"].round(2)
|
|
||||||
|
|
||||||
def get_rangs_inscrits(self) -> pd.Series:
|
|
||||||
"""Série des rangs classement/nbre_inscrit"""
|
|
||||||
return self.df["rang"]
|
|
||||||
|
|
||||||
def get_min(self) -> pd.Series:
|
|
||||||
"""Série des min"""
|
|
||||||
return self.df["min"].round(2)
|
|
||||||
|
|
||||||
def get_max(self) -> pd.Series:
|
|
||||||
"""Série des max"""
|
|
||||||
return self.df["max"].round(2)
|
|
||||||
|
|
||||||
def get_moy(self) -> pd.Series:
|
|
||||||
"""Série des moy"""
|
|
||||||
return self.df["moy"].round(2)
|
|
||||||
|
|
||||||
|
|
||||||
def get_note_for_df(self, etudid: int):
|
|
||||||
"""Note d'un étudiant donné par son etudid"""
|
|
||||||
return round(self.df["note"].loc[etudid], 2)
|
|
||||||
|
|
||||||
def get_min_for_df(self) -> float:
|
|
||||||
"""Min renseigné pour affichage dans un df"""
|
|
||||||
return round(self.synthese["min"], 2)
|
|
||||||
|
|
||||||
def get_max_for_df(self) -> float:
|
|
||||||
"""Max renseigné pour affichage dans un df"""
|
|
||||||
return round(self.synthese["max"], 2)
|
|
||||||
|
|
||||||
def get_moy_for_df(self) -> float:
|
|
||||||
"""Moyenne renseignée pour affichage dans un df"""
|
|
||||||
return round(self.synthese["moy"], 2)
|
|
||||||
|
|
||||||
def get_class_for_df(self, etudid: int) -> str:
|
|
||||||
"""Classement ramené au nombre d'inscrits,
|
|
||||||
pour un étudiant donné par son etudid"""
|
|
||||||
classement = self.df["rang"].loc[etudid]
|
|
||||||
if not pd.isna(classement):
|
|
||||||
return classement
|
|
||||||
else:
|
|
||||||
return pe_affichage.SANS_NOTE
|
|
||||||
|
|
||||||
def is_significatif(self) -> bool:
|
|
||||||
"""Indique si la moyenne est significative (c'est-à-dire à des notes)"""
|
|
||||||
return self.synthese["nb_inscrits"] > 0
|
|
||||||
|
|
||||||
|
|
||||||
class TableTag(object):
|
|
||||||
def __init__(self):
|
|
||||||
"""Classe centralisant différentes méthodes communes aux
|
|
||||||
SemestreTag, TrajectoireTag, AggregatInterclassTag
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------
|
|
||||||
def get_all_tags(self):
|
|
||||||
"""Liste des tags de la table, triée par ordre alphabétique,
|
|
||||||
extraite des clés du dictionnaire ``moyennes_tags`` connues (tags en doublon
|
|
||||||
possible).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Liste de tags triés par ordre alphabétique
|
|
||||||
"""
|
|
||||||
return sorted(list(self.moyennes_tags.keys()))
|
|
||||||
|
|
||||||
def df_moyennes_et_classements(self) -> pd.DataFrame:
|
|
||||||
"""Renvoie un dataframe listant toutes les moyennes,
|
|
||||||
et les classements des étudiants pour tous les tags.
|
|
||||||
|
|
||||||
Est utilisé pour afficher le détail d'un tableau taggué
|
|
||||||
(semestres, trajectoires ou aggrégat)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Le dataframe des notes et des classements
|
|
||||||
"""
|
|
||||||
|
|
||||||
etudiants = self.etudiants
|
|
||||||
df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"])
|
|
||||||
|
|
||||||
tags_tries = self.get_all_tags()
|
|
||||||
for tag in tags_tries:
|
|
||||||
moy_tag = self.moyennes_tags[tag]
|
|
||||||
df = df.join(moy_tag.synthese["notes"].rename(f"Moy {tag}"))
|
|
||||||
df = df.join(moy_tag.synthese["classements"].rename(f"Class {tag}"))
|
|
||||||
|
|
||||||
return df
|
|
||||||
|
|
||||||
def df_notes(self) -> pd.DataFrame | None:
|
|
||||||
"""Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Un dataframe etudids x tag (avec tag par ordre alphabétique)
|
|
||||||
"""
|
|
||||||
tags_tries = self.get_all_tags()
|
|
||||||
if tags_tries:
|
|
||||||
dict_series = {}
|
|
||||||
for tag in tags_tries:
|
|
||||||
# Les moyennes associés au tag
|
|
||||||
moy_tag = self.moyennes_tags[tag]
|
|
||||||
dict_series[tag] = moy_tag.synthese["notes"]
|
|
||||||
df = pd.DataFrame(dict_series)
|
|
||||||
return df
|
|
348
app/pe/pe_tagtable.py
Normal file
348
app/pe/pe_tagtable.py
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
# -*- mode: python -*-
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gestion scolarite IUT
|
||||||
|
#
|
||||||
|
# Copyright (c) 1999 - 2023 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
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Module "Avis de poursuite d'étude"
|
||||||
|
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Created on Thu Sep 8 09:36:33 2016
|
||||||
|
|
||||||
|
@author: barasc
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
|
class TableTag(object):
|
||||||
|
"""
|
||||||
|
Classe mémorisant les moyennes des étudiants à différents tag et permettant de calculer les rangs et les statistiques :
|
||||||
|
- nom : Nom représentatif des données de la Table
|
||||||
|
- inscrlist : Les étudiants inscrits dans le TagTag avec leur information de la forme :
|
||||||
|
{ etudid : dictionnaire d'info extrait de Scodoc, ...}
|
||||||
|
- taglist : Liste triée des noms des tags
|
||||||
|
- resultats : Dictionnaire donnant les notes-moyennes de chaque étudiant par tag et la somme commulée
|
||||||
|
des coeff utilisées dans le calcul de la moyenne pondérée, sous la forme :
|
||||||
|
{ tag : { etudid: (note_moy, somme_coeff_norm),
|
||||||
|
...}
|
||||||
|
- rangs : Dictionnaire donnant les rang par tag de chaque étudiant de la forme :
|
||||||
|
{ tag : {etudid: rang, ...} }
|
||||||
|
- nbinscrits : Nombre d'inscrits dans le semestre (pas de distinction entre les tags)
|
||||||
|
- statistiques : Dictionnaire donnant les stastitiques (moyenne, min, max) des résultats par tag de la forme :
|
||||||
|
{ tag : (moy, min, max), ...}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, nom=""):
|
||||||
|
self.nom = nom
|
||||||
|
self.inscrlist = []
|
||||||
|
self.identdict = {}
|
||||||
|
self.taglist = []
|
||||||
|
|
||||||
|
self.resultats = {}
|
||||||
|
self.rangs = {}
|
||||||
|
self.statistiques = {}
|
||||||
|
|
||||||
|
# *****************************************************************************************************************
|
||||||
|
# Accesseurs
|
||||||
|
# *****************************************************************************************************************
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------------------------------------
|
||||||
|
def get_moy_from_resultats(self, tag, etudid):
|
||||||
|
"""Renvoie la moyenne obtenue par un étudiant à un tag donné au regard du format de self.resultats"""
|
||||||
|
return (
|
||||||
|
self.resultats[tag][etudid][0]
|
||||||
|
if tag in self.resultats and etudid in self.resultats[tag]
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------------------------------------
|
||||||
|
def get_rang_from_resultats(self, tag, etudid):
|
||||||
|
"""Renvoie le rang à un tag d'un étudiant au regard du format de self.resultats"""
|
||||||
|
return (
|
||||||
|
self.rangs[tag][etudid]
|
||||||
|
if tag in self.resultats and etudid in self.resultats[tag]
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------------------------------------
|
||||||
|
def get_coeff_from_resultats(self, tag, etudid):
|
||||||
|
"""Renvoie la somme des coeffs de pondération normalisée utilisés dans le calcul de la moyenne à un tag d'un étudiant
|
||||||
|
au regard du format de self.resultats.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
self.resultats[tag][etudid][1]
|
||||||
|
if tag in self.resultats and etudid in self.resultats[tag]
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------------------------------------
|
||||||
|
def get_all_tags(self):
|
||||||
|
"""Renvoie la liste des tags du semestre triée par ordre alphabétique"""
|
||||||
|
# return self.taglist
|
||||||
|
return sorted(self.resultats.keys())
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------------------------------------
|
||||||
|
def get_nbinscrits(self):
|
||||||
|
"""Renvoie le nombre d'inscrits"""
|
||||||
|
return len(self.inscrlist)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------------------------------------
|
||||||
|
def get_moy_from_stats(self, tag):
|
||||||
|
"""Renvoie la moyenne des notes calculées pour d'un tag donné"""
|
||||||
|
return self.statistiques[tag][0] if tag in self.statistiques else None
|
||||||
|
|
||||||
|
def get_min_from_stats(self, tag):
|
||||||
|
"""Renvoie la plus basse des notes calculées pour d'un tag donné"""
|
||||||
|
return self.statistiques[tag][1] if tag in self.statistiques else None
|
||||||
|
|
||||||
|
def get_max_from_stats(self, tag):
|
||||||
|
"""Renvoie la plus haute des notes calculées pour d'un tag donné"""
|
||||||
|
return self.statistiques[tag][2] if tag in self.statistiques else None
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------------------------------------
|
||||||
|
# La structure des données mémorisées pour chaque tag dans le dictionnaire de synthèse
|
||||||
|
# d'un jury PE
|
||||||
|
FORMAT_DONNEES_ETUDIANTS = (
|
||||||
|
"note",
|
||||||
|
"coeff",
|
||||||
|
"rang",
|
||||||
|
"nbinscrits",
|
||||||
|
"moy",
|
||||||
|
"max",
|
||||||
|
"min",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_resultatsEtud(self, tag, etudid):
|
||||||
|
"""Renvoie un tuple (note, coeff, rang, nb_inscrit, moy, min, max) synthétisant les résultats d'un étudiant
|
||||||
|
à un tag donné. None sinon"""
|
||||||
|
return (
|
||||||
|
self.get_moy_from_resultats(tag, etudid),
|
||||||
|
self.get_coeff_from_resultats(tag, etudid),
|
||||||
|
self.get_rang_from_resultats(tag, etudid),
|
||||||
|
self.get_nbinscrits(),
|
||||||
|
self.get_moy_from_stats(tag),
|
||||||
|
self.get_min_from_stats(tag),
|
||||||
|
self.get_max_from_stats(tag),
|
||||||
|
)
|
||||||
|
|
||||||
|
# return self.tag_stats[tag]
|
||||||
|
# else :
|
||||||
|
# return self.pe_stats
|
||||||
|
|
||||||
|
# *****************************************************************************************************************
|
||||||
|
# Ajout des notes
|
||||||
|
# *****************************************************************************************************************
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------------------------------------
|
||||||
|
def add_moyennesTag(self, tag, listMoyEtCoeff) -> bool:
|
||||||
|
"""
|
||||||
|
Mémorise les moyennes, les coeffs de pondération et les etudid dans resultats
|
||||||
|
avec calcul du rang
|
||||||
|
:param tag: Un tag
|
||||||
|
:param listMoyEtCoeff: Une liste donnant [ (moy, coeff, etudid) ]
|
||||||
|
"""
|
||||||
|
# ajout des moyennes au dictionnaire résultat
|
||||||
|
if listMoyEtCoeff:
|
||||||
|
self.resultats[tag] = {
|
||||||
|
etudid: (moyenne, somme_coeffs)
|
||||||
|
for (moyenne, somme_coeffs, etudid) in listMoyEtCoeff
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calcule les rangs
|
||||||
|
lesMoyennesTriees = sorted(
|
||||||
|
listMoyEtCoeff,
|
||||||
|
reverse=True,
|
||||||
|
key=lambda col: col[0]
|
||||||
|
if isinstance(col[0], float)
|
||||||
|
else 0, # remplace les None et autres chaines par des zéros
|
||||||
|
) # triées
|
||||||
|
self.rangs[tag] = scu.comp_ranks(lesMoyennesTriees) # les rangs
|
||||||
|
|
||||||
|
# calcul des stats
|
||||||
|
self.comp_stats_d_un_tag(tag)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# *****************************************************************************************************************
|
||||||
|
# Méthodes dévolues aux calculs de statistiques (min, max, moy) sur chaque moyenne taguée
|
||||||
|
# *****************************************************************************************************************
|
||||||
|
|
||||||
|
def comp_stats_d_un_tag(self, tag):
|
||||||
|
"""
|
||||||
|
Calcule la moyenne generale, le min, le max pour un tag donné,
|
||||||
|
en ne prenant en compte que les moyennes significatives. Mémorise le resultat dans
|
||||||
|
self.statistiques
|
||||||
|
"""
|
||||||
|
stats = ("-NA-", "-", "-")
|
||||||
|
if tag not in self.resultats:
|
||||||
|
return stats
|
||||||
|
|
||||||
|
notes = [
|
||||||
|
self.get_moy_from_resultats(tag, etudid) for etudid in self.resultats[tag]
|
||||||
|
] # les notes du tag
|
||||||
|
notes_valides = [
|
||||||
|
note for note in notes if isinstance(note, float) and note != None
|
||||||
|
]
|
||||||
|
nb_notes_valides = len(notes_valides)
|
||||||
|
if nb_notes_valides > 0:
|
||||||
|
(moy, _) = moyenne_ponderee_terme_a_terme(notes_valides, force=True)
|
||||||
|
self.statistiques[tag] = (moy, max(notes_valides), min(notes_valides))
|
||||||
|
|
||||||
|
# ************************************************************************
|
||||||
|
# Méthodes dévolues aux affichages -> a revoir
|
||||||
|
# ************************************************************************
|
||||||
|
def str_resTag_d_un_etudiant(self, tag, etudid, delim=";"):
|
||||||
|
"""Renvoie une chaine de caractères (valable pour un csv)
|
||||||
|
décrivant la moyenne et le rang d'un étudiant, pour un tag donné ;
|
||||||
|
"""
|
||||||
|
if tag not in self.get_all_tags() or etudid not in self.resultats[tag]:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
moystr = TableTag.str_moytag(
|
||||||
|
self.get_moy_from_resultats(tag, etudid),
|
||||||
|
self.get_rang_from_resultats(tag, etudid),
|
||||||
|
self.get_nbinscrits(),
|
||||||
|
delim=delim,
|
||||||
|
)
|
||||||
|
return moystr
|
||||||
|
|
||||||
|
def str_res_d_un_etudiant(self, etudid, delim=";"):
|
||||||
|
"""Renvoie sur une ligne les résultats d'un étudiant à tous les tags (par ordre alphabétique)."""
|
||||||
|
return delim.join(
|
||||||
|
[self.str_resTag_d_un_etudiant(tag, etudid) for tag in self.get_all_tags()]
|
||||||
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
def str_moytag(cls, moyenne, rang, nbinscrit, delim=";"):
|
||||||
|
"""Renvoie une chaine de caractères représentant une moyenne (float ou string) et un rang
|
||||||
|
pour différents formats d'affichage : HTML, debug ligne de commande, csv"""
|
||||||
|
moystr = (
|
||||||
|
"%2.2f%s%s%s%d" % (moyenne, delim, rang, delim, nbinscrit)
|
||||||
|
if isinstance(moyenne, float)
|
||||||
|
else str(moyenne) + delim + str(rang) + delim + str(nbinscrit)
|
||||||
|
)
|
||||||
|
return moystr
|
||||||
|
|
||||||
|
str_moytag = classmethod(str_moytag)
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
def str_tagtable(self, delim=";", decimal_sep=","):
|
||||||
|
"""Renvoie une chaine de caractère listant toutes les moyennes, les rangs des étudiants pour tous les tags."""
|
||||||
|
entete = ["etudid", "nom", "prenom"]
|
||||||
|
for tag in self.get_all_tags():
|
||||||
|
entete += [titre + "_" + tag for titre in ["note", "rang", "nb_inscrit"]]
|
||||||
|
chaine = delim.join(entete) + "\n"
|
||||||
|
|
||||||
|
for etudid in self.identdict:
|
||||||
|
descr = delim.join(
|
||||||
|
[
|
||||||
|
etudid,
|
||||||
|
self.identdict[etudid]["nom"],
|
||||||
|
self.identdict[etudid]["prenom"],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
descr += delim + self.str_res_d_un_etudiant(etudid, delim)
|
||||||
|
chaine += descr + "\n"
|
||||||
|
|
||||||
|
# Ajout des stats ... à faire
|
||||||
|
|
||||||
|
if decimal_sep != ".":
|
||||||
|
return chaine.replace(".", decimal_sep)
|
||||||
|
else:
|
||||||
|
return chaine
|
||||||
|
|
||||||
|
|
||||||
|
# ************************************************************************
|
||||||
|
# Fonctions diverses
|
||||||
|
# ************************************************************************
|
||||||
|
|
||||||
|
|
||||||
|
# *********************************************
|
||||||
|
def moyenne_ponderee_terme_a_terme(notes, coefs=None, force=False):
|
||||||
|
"""
|
||||||
|
Calcule la moyenne pondérée d'une liste de notes avec d'éventuels coeffs de pondération.
|
||||||
|
Renvoie le résultat sous forme d'un tuple (moy, somme_coeff)
|
||||||
|
|
||||||
|
La liste de notes contient soit :
|
||||||
|
1) des valeurs numériques
|
||||||
|
2) des strings "-NA-" (pas de notes) ou "-NI-" (pas inscrit) ou "-c-" ue capitalisée,
|
||||||
|
3) None.
|
||||||
|
|
||||||
|
Le paramètre force indique si le calcul de la moyenne doit être forcée ou non, c'est à
|
||||||
|
dire s'il y a ou non omission des notes non numériques (auquel cas la moyenne est
|
||||||
|
calculée sur les notes disponibles) ; sinon renvoie (None, None).
|
||||||
|
"""
|
||||||
|
# Vérification des paramètres d'entrée
|
||||||
|
if not isinstance(notes, list) or (
|
||||||
|
coefs != None and not isinstance(coefs, list) and len(coefs) != len(notes)
|
||||||
|
):
|
||||||
|
raise ValueError("Erreur de paramètres dans moyenne_ponderee_terme_a_terme")
|
||||||
|
|
||||||
|
# Récupération des valeurs des paramètres d'entrée
|
||||||
|
coefs = [1] * len(notes) if coefs is None else coefs
|
||||||
|
|
||||||
|
# S'il n'y a pas de notes
|
||||||
|
if not notes: # Si notes = []
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
# Liste indiquant les notes valides
|
||||||
|
notes_valides = [
|
||||||
|
(isinstance(note, float) and not np.isnan(note)) or isinstance(note, int)
|
||||||
|
for note in notes
|
||||||
|
]
|
||||||
|
# Si on force le calcul de la moyenne ou qu'on ne le force pas
|
||||||
|
# et qu'on a le bon nombre de notes
|
||||||
|
if force or sum(notes_valides) == len(notes):
|
||||||
|
moyenne, ponderation = 0.0, 0.0
|
||||||
|
for i in range(len(notes)):
|
||||||
|
if notes_valides[i]:
|
||||||
|
moyenne += coefs[i] * notes[i]
|
||||||
|
ponderation += coefs[i]
|
||||||
|
return (
|
||||||
|
(moyenne / (ponderation * 1.0), ponderation)
|
||||||
|
if ponderation != 0
|
||||||
|
else (None, 0)
|
||||||
|
)
|
||||||
|
# Si on ne force pas le calcul de la moyenne
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------
|
||||||
|
def conversionDate_StrToDate(date_fin):
|
||||||
|
"""Conversion d'une date fournie sous la forme d'une chaine de caractère de
|
||||||
|
type 'jj/mm/aaaa' en un objet date du package datetime.
|
||||||
|
Fonction servant au tri des semestres par date
|
||||||
|
"""
|
||||||
|
(d, m, y) = [int(x) for x in date_fin.split("/")]
|
||||||
|
date_fin_dst = datetime.date(y, m, d)
|
||||||
|
return date_fin_dst
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user