Compare commits
210 Commits
Author | SHA1 | Date | |
---|---|---|---|
a200be586a | |||
607604f91e | |||
8eedac0f03 | |||
aea2204d9e | |||
9c15cbe647 | |||
6761f5a620 | |||
69a53adb55 | |||
b30ea5f5fd | |||
052fb3c7b9 | |||
dbd0124c2c | |||
e989a4ffa8 | |||
6ae2b0eb5f | |||
d7f3376103 | |||
677415fbfc | |||
6cbeeedb1c | |||
39e7ad3ad6 | |||
177d38428e | |||
f4c1d00046 | |||
86c12dee08 | |||
8cf85f78a8 | |||
9ec0ef27ba | |||
c8ac796347 | |||
2212990788 | |||
719d14673d | |||
98eb7699a0 | |||
7b22d26095 | |||
371d7eff64 | |||
0adcbb7c0b | |||
f10d46c230 | |||
4f41ef7050 | |||
ef4c2fa64b | |||
be39245e25 | |||
196dbab298 | |||
0594a659fa | |||
072d013590 | |||
9c4e2627ba | |||
bbdf5da2e8 | |||
5828d4aaaf | |||
e6a544906e | |||
bacd734ab5 | |||
e611fa4bfc | |||
128b282186 | |||
57d36927ac | |||
d5fdd5b8b8 | |||
7162d83f39 | |||
7805a6cab9 | |||
2c840b7803 | |||
0645db8ab0 | |||
9e13b51669 | |||
034800ab9a | |||
4b2e88c678 | |||
c9af2345fb | |||
0bf0311f2f | |||
5bbdc567f3 | |||
027f11e494 | |||
ef171364a6 | |||
9b9d7b611b | |||
02bfb626cb | |||
597a28f86d | |||
2915f4e981 | |||
af659d5f09 | |||
838ae7cf7e | |||
e4c8637c41 | |||
0bf3c22cd0 | |||
6700687e96 | |||
b8e20b6be8 | |||
78eeb9c67f | |||
66fbb0afbc | |||
387af40b65 | |||
952132695f | |||
0b6a4b5c7e | |||
556725b3ef | |||
90bf31fc03 | |||
f7e41dc7fe | |||
eefbe70944 | |||
5446ac0ed2 | |||
1f6f3620a2 | |||
04d1fbe272 | |||
c270c24c5b | |||
8b751608e1 | |||
0fb45fc9ca | |||
8652ef2e7b | |||
9be77e4f37 | |||
a00e2da461 | |||
3481f7c1c2 | |||
64d7e1ed42 | |||
d310304e9e | |||
fce23aa066 | |||
9c6d988fc3 | |||
cb5df2fffd | |||
3550e4290a | |||
787e514dca | |||
e25f7d4fc9 | |||
f87902d1ac | |||
39b3cd9e05 | |||
d1074a8227 | |||
1b18034adb | |||
871f5c1d61 | |||
79f07deac0 | |||
431dd20911 | |||
4985182b9a | |||
4681294cb8 | |||
f6051f930f | |||
cf415763b3 | |||
769f6c0ea0 | |||
33f2afb04b | |||
02bccb58aa | |||
4f7da8bfa4 | |||
a439c4c985 | |||
d991eb007c | |||
4f10d017be | |||
be7bb588cf | |||
83c6ec44c8 | |||
3a3d47ebe4 | |||
54be507e35 | |||
efd735542e | |||
776b0fb228 | |||
cd8d73b41f | |||
decc28b896 | |||
a4b25eb47b | |||
790ba910ee | |||
82713752c2 | |||
fc35974951 | |||
283daae4d9 | |||
ece689eb10 | |||
242771c619 | |||
aa45680ed8 | |||
086b8ee191 | |||
8477dc96ca | |||
e3cde87a0f | |||
8b3efe9dad | |||
|
dfbe0dc3ed | ||
02976c9996 | |||
2a239ab92f | |||
9989f419cb | |||
74b8b90a65 | |||
2ad77428a5 | |||
505f5e5f1c | |||
a7848f0a4e | |||
2660801dd5 | |||
e415a5255e | |||
dae04658b7 | |||
f842fa0b4f | |||
45c685d725 | |||
ef63e27aed | |||
|
a04278f301 | ||
a12505e8df | |||
|
32a4ada483 | ||
555e8af818 | |||
f09b2028e2 | |||
7d2d5a3ea9 | |||
a65c1d3c4a | |||
b8eb8bb77f | |||
4917034b6d | |||
81915b1522 | |||
c6910fc76e | |||
90c2516d01 | |||
aee4f14b81 | |||
340aa749b2 | |||
7a0b560d54 | |||
9e925aa500 | |||
ff63a32bbe | |||
ab116ee9e7 | |||
b91609950c | |||
f2f229df4a | |||
6908b0b8d2 | |||
706b21ede7 | |||
238fbe887c | |||
3e55391f7e | |||
9c1c316f14 | |||
c9336dd01c | |||
|
87e98b5478 | ||
|
7659bcb488 | ||
|
44de81857a | ||
|
78d97d2c2d | ||
|
4b304c559b | ||
21eeff90aa | |||
6d3f276cc0 | |||
e2ca673239 | |||
f55f3fe82f | |||
9104a8986e | |||
|
3e1f563ecd | ||
df20372abb | |||
0cafc0b184 | |||
2c42a1547c | |||
7ce57d28cb | |||
e3fc13f215 | |||
|
3a3f94b7cf | ||
|
023e3a4c04 | ||
76bedfb303 | |||
0634dbd0aa | |||
01d0f7d651 | |||
6d16927db9 | |||
2f81ce8ac2 | |||
898270d2f0 | |||
e28bfa34be | |||
86e8803c87 | |||
4babffd022 | |||
6461d14883 | |||
524df5cbc8 | |||
|
4d19d385f1 | ||
|
c639778b78 | ||
|
7d441b1c4d | ||
|
0fa35708f9 | ||
|
bcb01089ca | ||
|
a05801b78a | ||
a90fd6dcd0 | |||
ac9b5722cf | |||
e61ec5e04e | |||
|
5bb4e4e0eb |
@ -3,9 +3,11 @@
|
||||
from flask_json import as_json
|
||||
from flask import Blueprint
|
||||
from flask import request, g
|
||||
from flask_login import current_user
|
||||
from app import db
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoException
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
api_bp = Blueprint("api", __name__)
|
||||
api_web_bp = Blueprint("apiweb", __name__)
|
||||
@ -48,13 +50,21 @@ def requested_format(default_format="json", allowed_formats=None):
|
||||
|
||||
|
||||
@as_json
|
||||
def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model = None):
|
||||
def get_model_api_object(
|
||||
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]"
|
||||
|
||||
Filtrage du département en fonction d'une classe de jointure (eg: Identite, Formsemestre) -> join_cls
|
||||
|
||||
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)
|
||||
if g.scodoc_dept and join_cls is not None:
|
||||
@ -66,8 +76,9 @@ def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model
|
||||
404,
|
||||
message=f"{model_cls.__name__} inexistant(e)",
|
||||
)
|
||||
|
||||
return unique.to_dict(format_api=True)
|
||||
if restrict is None:
|
||||
return unique.to_dict(format_api=True)
|
||||
return unique.to_dict(format_api=True, restrict=restrict)
|
||||
|
||||
|
||||
from app.api import tokens
|
||||
|
@ -337,7 +337,7 @@ def assiduites_group(with_query: bool = False):
|
||||
try:
|
||||
etuds = [int(etu) for etu in etuds]
|
||||
except ValueError:
|
||||
return json_error(404, "Le champs etudids n'est pas correctement formé")
|
||||
return json_error(404, "Le champ etudids n'est pas correctement formé")
|
||||
|
||||
# Vérification que tous les étudiants sont du même département
|
||||
query = Identite.query.filter(Identite.id.in_(etuds))
|
||||
|
@ -295,7 +295,7 @@ def dept_formsemestres_courants_by_id(dept_id: int):
|
||||
if date_courante:
|
||||
test_date = datetime.fromisoformat(date_courante)
|
||||
else:
|
||||
test_date = app.db.func.now()
|
||||
test_date = db.func.current_date()
|
||||
# Les semestres en cours de ce département
|
||||
formsemestres = FormSemestre.query.filter(
|
||||
FormSemestre.dept_id == dept.id,
|
||||
|
@ -10,7 +10,7 @@
|
||||
from datetime import datetime
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import g, request
|
||||
from flask import g, request, Response
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user
|
||||
from flask_login import login_required
|
||||
@ -18,7 +18,7 @@ from sqlalchemy import desc, func, or_
|
||||
from sqlalchemy.dialects.postgresql import VARCHAR
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import tools
|
||||
from app.but import bulletin_but_court
|
||||
@ -26,6 +26,7 @@ from app.decorators import scodoc, permission_required
|
||||
from app.models import (
|
||||
Admission,
|
||||
Departement,
|
||||
EtudAnnotation,
|
||||
FormSemestreInscription,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
@ -54,6 +55,32 @@ 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/long", defaults={"long": True})
|
||||
@api_web_bp.route("/etudiants/courants", defaults={"long": False})
|
||||
@ -104,7 +131,10 @@ def etudiants_courants(long=False):
|
||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||
)
|
||||
if long:
|
||||
data = [etud.to_dict_api() for etud in etuds]
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
data = [
|
||||
etud.to_dict_api(restrict=restrict, with_annotations=True) for etud in etuds
|
||||
]
|
||||
else:
|
||||
data = [etud.to_dict_short() for etud in etuds]
|
||||
return data
|
||||
@ -138,8 +168,8 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
|
||||
return etud.to_dict_api()
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
return etud.to_dict_api(restrict=restrict, with_annotations=True)
|
||||
|
||||
|
||||
@bp.route("/etudiant/etudid/<int:etudid>/photo")
|
||||
@ -251,7 +281,10 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
||||
query = query.join(Departement).filter(
|
||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||
)
|
||||
return [etud.to_dict_api() for etud in query]
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
return [
|
||||
etud.to_dict_api(restrict=restrict, with_annotations=True) for etud in query
|
||||
]
|
||||
|
||||
|
||||
@bp.route("/etudiants/name/<string:start>")
|
||||
@ -278,7 +311,11 @@ def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
||||
)
|
||||
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:
|
||||
return [etud.to_dict_api() for etud in sorted(etuds, key=attrgetter("sort_key"))]
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
return [
|
||||
etud.to_dict_api(restrict=restrict)
|
||||
for etud in sorted(etuds, key=attrgetter("sort_key"))
|
||||
]
|
||||
|
||||
|
||||
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
||||
@ -379,28 +416,15 @@ def bulletin(
|
||||
pdf = True
|
||||
if version not in scu.BULLETINS_VERSIONS_BUT:
|
||||
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()
|
||||
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
||||
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
||||
return json_error(404, "formsemestre inexistant")
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
if code_type == "nip":
|
||||
query = Identite.query.filter_by(code_nip=code, dept_id=dept.id)
|
||||
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")
|
||||
ok, etud = _get_etud_by_code(code_type, code, dept)
|
||||
if not ok:
|
||||
return etud # json error
|
||||
|
||||
if version == "butcourt":
|
||||
if pdf:
|
||||
@ -543,7 +567,8 @@ def etudiant_create(force=False):
|
||||
# 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()
|
||||
|
||||
r = etud.to_dict_api(restrict=False) # pas de restriction, on vient de le créer
|
||||
return r
|
||||
|
||||
|
||||
@ -551,26 +576,15 @@ def etudiant_create(force=False):
|
||||
@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)"""
|
||||
if code_type == "nip":
|
||||
query = Identite.query.filter_by(code_nip=code)
|
||||
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)
|
||||
else:
|
||||
return json_error(404, "invalid code_type")
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
etud: Identite = query.first()
|
||||
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)
|
||||
@ -590,5 +604,70 @@ def etudiant_edit(
|
||||
# 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 = 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"
|
||||
|
@ -67,7 +67,7 @@ def get_evaluation(evaluation_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def evaluations(moduleimpl_id: int):
|
||||
def moduleimpl_evaluations(moduleimpl_id: int):
|
||||
"""
|
||||
Retourne la liste des évaluations d'un moduleimpl
|
||||
|
||||
@ -75,14 +75,8 @@ def evaluations(moduleimpl_id: int):
|
||||
|
||||
Exemple de résultat : voir /evaluation
|
||||
"""
|
||||
query = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id)
|
||||
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]
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
return [evaluation.to_dict_api() for evaluation in modimpl.evaluations]
|
||||
|
||||
|
||||
@bp.route("/evaluation/<int:evaluation_id>/notes")
|
||||
|
@ -11,7 +11,7 @@ from operator import attrgetter, itemgetter
|
||||
|
||||
from flask import g, make_response, request
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
import app
|
||||
from app import db
|
||||
@ -360,7 +360,8 @@ def formsemestre_etudiants(
|
||||
inscriptions = formsemestre.inscriptions
|
||||
|
||||
if long:
|
||||
etuds = [ins.etud.to_dict_api() for ins in inscriptions]
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
etuds = [ins.etud.to_dict_api(restrict=restrict) for ins in inscriptions]
|
||||
else:
|
||||
etuds = [ins.etud.to_dict_short() for ins in inscriptions]
|
||||
# Ajout des groupes de chaque étudiants
|
||||
@ -425,7 +426,7 @@ def etat_evals(formsemestre_id: int):
|
||||
for modimpl_id in nt.modimpls_results:
|
||||
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id]
|
||||
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id)
|
||||
modimpl_dict = modimpl.to_dict(convert_objects=True)
|
||||
modimpl_dict = modimpl.to_dict(convert_objects=True, with_module=False)
|
||||
|
||||
list_eval = []
|
||||
for evaluation_id in modimpl_results.evaluations_etat:
|
||||
|
@ -66,7 +66,7 @@ def _news_delete_jury_etud(etud: Identite):
|
||||
"génère news sur effacement décision"
|
||||
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
|
||||
url = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=etud.departement.acronym, etudid=etud.id
|
||||
"scolar.fiche_etud", scodoc_dept=etud.departement.acronym, etudid=etud.id
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_JURY,
|
||||
|
@ -15,7 +15,7 @@ from werkzeug.exceptions import NotFound
|
||||
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import db
|
||||
from app import db, set_sco_dept
|
||||
from app.api import api_bp as bp
|
||||
from app.api import api_web_bp
|
||||
from app.api import get_model_api_object, tools
|
||||
@ -53,14 +53,19 @@ def justificatif(justif_id: int = None):
|
||||
"date_fin": "2022-10-31T10:00+01:00",
|
||||
"etat": "valide",
|
||||
"fichier": "archive_id",
|
||||
"raison": "une raison",
|
||||
"raison": "une raison", // VIDE si pas le droit
|
||||
"entry_date": "2022-10-31T08:00+01:00",
|
||||
"user_id": 1 or null,
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
return get_model_api_object(Justificatif, justif_id, Identite)
|
||||
return get_model_api_object(
|
||||
Justificatif,
|
||||
justif_id,
|
||||
Identite,
|
||||
restrict=not current_user.has_permission(Permission.AbsJustifView),
|
||||
)
|
||||
|
||||
|
||||
# etudid
|
||||
@ -133,8 +138,9 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
|
||||
|
||||
# Mise en forme des données puis retour en JSON
|
||||
data_set: list[dict] = []
|
||||
restrict = not current_user.has_permission(Permission.AbsJustifView)
|
||||
for just in justificatifs_query.all():
|
||||
data = just.to_dict(format_api=True)
|
||||
data = just.to_dict(format_api=True, restrict=restrict)
|
||||
data_set.append(data)
|
||||
|
||||
return data_set
|
||||
@ -151,7 +157,10 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def justificatifs_dept(dept_id: int = None, with_query: bool = False):
|
||||
"""XXX TODO missing doc"""
|
||||
"""
|
||||
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
|
||||
dept: Departement = Departement.query.get(dept_id)
|
||||
@ -169,14 +178,15 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
|
||||
justificatifs_query: Query = _filter_manager(request, justificatifs_query)
|
||||
|
||||
# Mise en forme des données et retour JSON
|
||||
restrict = not current_user.has_permission(Permission.AbsJustifView)
|
||||
data_set: list[dict] = []
|
||||
for just in justificatifs_query:
|
||||
data_set.append(_set_sems(just))
|
||||
data_set.append(_set_sems(just, restrict=restrict))
|
||||
|
||||
return data_set
|
||||
|
||||
|
||||
def _set_sems(justi: Justificatif) -> dict:
|
||||
def _set_sems(justi: Justificatif, restrict: bool) -> dict:
|
||||
"""
|
||||
_set_sems Ajoute le formsemestre associé au justificatif s'il existe
|
||||
|
||||
@ -189,7 +199,7 @@ def _set_sems(justi: Justificatif) -> dict:
|
||||
dict: La représentation de l'assiduité en dictionnaire
|
||||
"""
|
||||
# Conversion du justificatif en dictionnaire
|
||||
data = justi.to_dict(format_api=True)
|
||||
data = justi.to_dict(format_api=True, restrict=restrict)
|
||||
|
||||
# Récupération du formsemestre de l'assiduité
|
||||
formsemestre: FormSemestre = get_formsemestre_from_data(justi.to_dict())
|
||||
@ -243,9 +253,10 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
justificatifs_query: Query = _filter_manager(request, justificatifs_query)
|
||||
|
||||
# Retour des justificatifs en JSON
|
||||
restrict = not current_user.has_permission(Permission.AbsJustifView)
|
||||
data_set: list[dict] = []
|
||||
for justi in justificatifs_query.all():
|
||||
data = justi.to_dict(format_api=True)
|
||||
data = justi.to_dict(format_api=True, restrict=restrict)
|
||||
data_set.append(data)
|
||||
|
||||
return data_set
|
||||
@ -294,6 +305,7 @@ def justif_create(etudid: int = None, nip=None, ine=None):
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
set_sco_dept(etud.departement.acronym)
|
||||
|
||||
# Récupération des justificatifs à créer
|
||||
create_list: list[object] = request.get_json(force=True)
|
||||
|
@ -8,16 +8,14 @@
|
||||
ScoDoc 9 API : accès aux moduleimpl
|
||||
"""
|
||||
|
||||
from flask import g
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import (
|
||||
FormSemestre,
|
||||
ModuleImpl,
|
||||
)
|
||||
from app.models import ModuleImpl
|
||||
from app.scodoc import sco_liste_notes
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
@ -62,10 +60,7 @@ def moduleimpl(moduleimpl_id: int):
|
||||
}
|
||||
}
|
||||
"""
|
||||
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()
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
return modimpl.to_dict(convert_objects=True)
|
||||
|
||||
|
||||
@ -87,8 +82,36 @@ def moduleimpl_inscriptions(moduleimpl_id: int):
|
||||
...
|
||||
]
|
||||
"""
|
||||
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()
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
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
|
||||
|
@ -311,6 +311,13 @@ def group_create(partition_id: int): # partition-group-create
|
||||
args["group_name"] = args["group_name"].strip()
|
||||
if not GroupDescr.check_name(partition, args["group_name"]):
|
||||
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||
|
||||
# le numero est optionnel
|
||||
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)
|
||||
|
@ -7,7 +7,6 @@
|
||||
"""
|
||||
ScoDoc 9 API : accès aux utilisateurs
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
@ -15,13 +14,14 @@ from flask_login import current_user, login_required
|
||||
|
||||
from app import db, log
|
||||
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 is_valid_password
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Departement
|
||||
from app.models import Departement, ScoDocSiteConfig
|
||||
from app.scodoc import sco_edt_cal
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
@ -441,3 +441,63 @@ def role_delete(role_name: str):
|
||||
db.session.delete(role)
|
||||
db.session.commit()
|
||||
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
|
||||
|
@ -102,6 +102,8 @@ class User(UserMixin, ScoDocModel):
|
||||
token = db.Column(db.Text(), index=True, unique=True)
|
||||
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)
|
||||
Permission = Permission
|
||||
|
||||
@ -245,24 +247,26 @@ class User(UserMixin, ScoDocModel):
|
||||
def to_dict(self, include_email=True):
|
||||
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
|
||||
data = {
|
||||
"date_expiration": self.date_expiration.isoformat() + "Z"
|
||||
if self.date_expiration
|
||||
else None,
|
||||
"date_modif_passwd": self.date_modif_passwd.isoformat() + "Z"
|
||||
if self.date_modif_passwd
|
||||
else None,
|
||||
"date_created": self.date_created.isoformat() + "Z"
|
||||
if self.date_created
|
||||
else None,
|
||||
"date_expiration": (
|
||||
self.date_expiration.isoformat() + "Z" if self.date_expiration else None
|
||||
),
|
||||
"date_modif_passwd": (
|
||||
self.date_modif_passwd.isoformat() + "Z"
|
||||
if self.date_modif_passwd
|
||||
else None
|
||||
),
|
||||
"date_created": (
|
||||
self.date_created.isoformat() + "Z" if self.date_created else None
|
||||
),
|
||||
"dept": self.dept,
|
||||
"id": self.id,
|
||||
"active": self.active,
|
||||
"cas_id": self.cas_id,
|
||||
"cas_allow_login": self.cas_allow_login,
|
||||
"cas_allow_scodoc_login": self.cas_allow_scodoc_login,
|
||||
"cas_last_login": self.cas_last_login.isoformat() + "Z"
|
||||
if self.cas_last_login
|
||||
else None,
|
||||
"cas_last_login": (
|
||||
self.cas_last_login.isoformat() + "Z" if self.cas_last_login else None
|
||||
),
|
||||
"edt_id": self.edt_id,
|
||||
"status_txt": "actif" if self.active else "fermé",
|
||||
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None,
|
||||
@ -477,8 +481,8 @@ class User(UserMixin, ScoDocModel):
|
||||
return f"{nom} {scu.format_prenom(self.prenom)} ({self.user_name})"
|
||||
|
||||
@staticmethod
|
||||
def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]:
|
||||
"""Returns id from the string "Dupont Pierre (dupont)"
|
||||
def get_user_from_nomplogin(nomplogin: str) -> Optional["User"]:
|
||||
"""Returns User instance from the string "Dupont Pierre (dupont)"
|
||||
or None if user does not exist
|
||||
"""
|
||||
match = re.match(r".*\((.*)\)", nomplogin.strip())
|
||||
@ -486,7 +490,7 @@ class User(UserMixin, ScoDocModel):
|
||||
user_name = match.group(1)
|
||||
u = User.query.filter_by(user_name=user_name).first()
|
||||
if u:
|
||||
return u.id
|
||||
return u
|
||||
return None
|
||||
|
||||
def get_nom_fmt(self):
|
||||
|
@ -349,19 +349,12 @@ class BulletinBUT:
|
||||
raise ScoValueError("bulletin_etud: version de bulletin demandée invalide")
|
||||
res = self.res
|
||||
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 = {
|
||||
"version": "0",
|
||||
"type": "BUT",
|
||||
"date": datetime.datetime.utcnow().isoformat() + "Z",
|
||||
"publie": not formsemestre.bul_hide_xml,
|
||||
"etat_inscription": etud.inscription_etat(formsemestre.id),
|
||||
"etudiant": etud.to_dict_bul(),
|
||||
"formation": {
|
||||
"id": formsemestre.formation.id,
|
||||
@ -370,14 +363,20 @@ class BulletinBUT:
|
||||
"titre": formsemestre.formation.titre,
|
||||
},
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etat_inscription": etat_inscription,
|
||||
"options": sco_preferences.bulletin_option_affichage(
|
||||
formsemestre, self.prefs
|
||||
),
|
||||
}
|
||||
if not published:
|
||||
published = (not formsemestre.bul_hide_xml) or force_publishing
|
||||
if not published or d["etat_inscription"] is False:
|
||||
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)
|
||||
etud_groups = sco_groups.get_etud_formsemestre_groups(
|
||||
etud, formsemestre, only_to_show=True
|
||||
@ -410,7 +409,7 @@ class BulletinBUT:
|
||||
semestre_infos.update(
|
||||
sco_bulletins_json.dict_decision_jury(etud, formsemestre)
|
||||
)
|
||||
if etat_inscription == scu.INSCRIT:
|
||||
if d["etat_inscription"] == scu.INSCRIT:
|
||||
# moyenne des moyennes générales du semestre
|
||||
semestre_infos["notes"] = {
|
||||
"value": fmt_note(res.etud_moy_gen[etud.id]),
|
||||
@ -499,10 +498,8 @@ class BulletinBUT:
|
||||
d["etud"]["etat_civil"] = etud.etat_civil
|
||||
d.update(self.res.sem)
|
||||
etud_etat = self.res.get_etud_etat(etud.id)
|
||||
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
|
||||
etud_etat,
|
||||
self.prefs,
|
||||
decision_sem=d["semestre"].get("decision"),
|
||||
d["filigranne"] = sco_bulletins_pdf.get_filigranne_apc(
|
||||
etud_etat, self.prefs, etud.id, res=self.res
|
||||
)
|
||||
if etud_etat == scu.DEMISSION:
|
||||
d["demission"] = "(Démission)"
|
||||
|
@ -104,8 +104,10 @@ def _build_bulletin_but_infos(
|
||||
bulletins_sem = BulletinBUT(formsemestre)
|
||||
if fmt == "pdf":
|
||||
bul: dict = bulletins_sem.bulletin_etud_complet(etud)
|
||||
filigranne = bul["filigranne"]
|
||||
else: # la même chose avec un peu moins d'infos
|
||||
bul: dict = bulletins_sem.bulletin_etud(etud, force_publishing=True)
|
||||
filigranne = ""
|
||||
decision_ues = (
|
||||
{x["acronyme"]: x for x in bul["semestre"]["decision_ue"]}
|
||||
if "semestre" in bul and "decision_ue" in bul["semestre"]
|
||||
@ -131,6 +133,7 @@ def _build_bulletin_but_infos(
|
||||
"decision_ues": decision_ues,
|
||||
"ects_total": ects_total,
|
||||
"etud": etud,
|
||||
"filigranne": filigranne,
|
||||
"formsemestre": formsemestre,
|
||||
"logo": logo,
|
||||
"prefs": bulletins_sem.prefs,
|
||||
|
@ -48,6 +48,7 @@ def make_bulletin_but_court_pdf(
|
||||
ects_total: float = 0.0,
|
||||
etud: Identite = None,
|
||||
formsemestre: FormSemestre = None,
|
||||
filigranne=""
|
||||
logo: Logo = None,
|
||||
prefs: SemPreferences = None,
|
||||
title: str = "",
|
||||
@ -86,6 +87,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
decision_ues: dict = None,
|
||||
ects_total: float = 0.0,
|
||||
etud: Identite = None,
|
||||
filigranne="",
|
||||
formsemestre: FormSemestre = None,
|
||||
logo: Logo = None,
|
||||
prefs: SemPreferences = None,
|
||||
@ -95,7 +97,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
] = None,
|
||||
ues_acronyms: list[str] = None,
|
||||
):
|
||||
super().__init__(bul, authuser=current_user)
|
||||
super().__init__(bul, authuser=current_user, filigranne=filigranne)
|
||||
self.bul = bul
|
||||
self.cursus = cursus
|
||||
self.decision_ues = decision_ues
|
||||
@ -192,7 +194,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
"""Génère la partie "titre" du bulletin de notes.
|
||||
Renvoie une liste d'objets platypus
|
||||
"""
|
||||
# comme les bulletins standard, mais avec notre préférence
|
||||
# comme les bulletins standards, mais avec notre préférence
|
||||
return super().bul_title_pdf(preference_field=preference_field)
|
||||
|
||||
def bul_part_below(self, fmt="pdf") -> list:
|
||||
@ -404,6 +406,8 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
|
||||
def boite_identite(self) -> list:
|
||||
"Les informations sur l'identité et l'inscription de l'étudiant"
|
||||
parcour = self.formsemestre.etuds_inscriptions[self.etud.id].parcour
|
||||
|
||||
return [
|
||||
Paragraph(
|
||||
SU(f"""{self.etud.nomprenom}"""),
|
||||
@ -414,7 +418,8 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
f"""
|
||||
<b>{self.bul["demission"]}</b><br/>
|
||||
Formation: {self.formsemestre.titre_num()}<br/>
|
||||
Année scolaire: {self.formsemestre.annee_scolaire_str()}<br/>
|
||||
{'Parcours ' + parcour.code + '<br/>' if parcour else ''}
|
||||
Année universitaire: {self.formsemestre.annee_scolaire_str()}<br/>
|
||||
"""
|
||||
),
|
||||
style=self.style_base,
|
||||
|
@ -119,9 +119,13 @@ class EtudCursusBUT:
|
||||
|
||||
self.validation_par_competence_et_annee = {}
|
||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
validation_rcue: ApcValidationRCUE
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
niveau = validation_rcue.niveau()
|
||||
if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||
if (
|
||||
niveau is None
|
||||
or not niveau.competence.id in self.validation_par_competence_et_annee
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
previous_validation = self.validation_par_competence_et_annee.get(
|
||||
niveau.competence.id
|
||||
@ -443,8 +447,24 @@ def formsemestre_warning_apc_setup(
|
||||
}">formation n'est pas associée à un référentiel de compétence.</a>
|
||||
</div>
|
||||
"""
|
||||
# Vérifie les niveaux de chaque parcours
|
||||
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
|
||||
for parcour in formsemestre.parcours or [None]:
|
||||
annee = (formsemestre.semestre_id + 1) // 2
|
||||
niveaux_ids = {
|
||||
|
@ -380,14 +380,24 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
sco_codes.ADJ,
|
||||
] + self.codes
|
||||
explanation += f" et {self.nb_rcues_under_8} < 8"
|
||||
else:
|
||||
self.codes = [
|
||||
sco_codes.RED,
|
||||
sco_codes.NAR,
|
||||
sco_codes.PAS1NCI,
|
||||
sco_codes.ADJ,
|
||||
sco_codes.PASD, # voir #488 (discutable, conventions locales)
|
||||
] + self.codes
|
||||
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:
|
||||
codes = []
|
||||
self.codes = (
|
||||
codes
|
||||
+ [
|
||||
sco_codes.RED,
|
||||
sco_codes.NAR,
|
||||
sco_codes.PAS1NCI,
|
||||
sco_codes.ADJ,
|
||||
sco_codes.PASD, # voir #488 (discutable, conventions locales)
|
||||
]
|
||||
+ self.codes
|
||||
)
|
||||
explanation += f""" et {self.nb_rcues_under_8
|
||||
} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
|
||||
|
||||
@ -407,7 +417,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
+ '</div><div class="warning">'.join(messages)
|
||||
+ "</div>"
|
||||
)
|
||||
self.codes = [self.codes[0]] + sorted(self.codes[1:])
|
||||
self.codes = [self.codes[0]] + sorted((c or "") for c in self.codes[1:])
|
||||
|
||||
def passage_de_droit_en_but3(self) -> tuple[bool, str]:
|
||||
"""Vérifie si les conditions supplémentaires de passage BUT2 vers BUT3 sont satisfaites"""
|
||||
@ -514,19 +524,21 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
"""Les deux formsemestres auquel est inscrit l'étudiant (ni DEM ni DEF)
|
||||
du niveau auquel appartient formsemestre.
|
||||
|
||||
-> S_impair, S_pair
|
||||
-> S_impair, S_pair (de la même année scolaire)
|
||||
|
||||
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
|
||||
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
|
||||
return None, None
|
||||
|
||||
if formsemestre.semestre_id % 2:
|
||||
idx_autre = formsemestre.semestre_id + 1
|
||||
idx_autre = formsemestre.semestre_id + 1 # impair, autre = suivant
|
||||
else:
|
||||
idx_autre = formsemestre.semestre_id - 1
|
||||
idx_autre = formsemestre.semestre_id - 1 # pair: autre = précédent
|
||||
|
||||
# Cherche l'autre semestre de la même année scolaire:
|
||||
autre_formsemestre = None
|
||||
@ -539,6 +551,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
inscr.formsemestre.formation.referentiel_competence
|
||||
== formsemestre.formation.referentiel_competence
|
||||
)
|
||||
# Non bloqué
|
||||
and not inscr.formsemestre.block_moyennes
|
||||
# L'autre semestre
|
||||
and (inscr.formsemestre.semestre_id == idx_autre)
|
||||
# de la même année scolaire
|
||||
@ -610,6 +624,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
def next_semestre_ids(self, code: str) -> set[int]:
|
||||
"""Les indices des semestres dans lequels l'étudiant est autorisé
|
||||
à 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
|
||||
# est de droit pour tout étudiant.
|
||||
@ -653,6 +668,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
|
||||
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.
|
||||
|
||||
Si le code_annee est None, efface le code déjà enregistré.
|
||||
"""
|
||||
log("jury_but.DecisionsProposeesAnnee.record_form")
|
||||
code_annee = self.codes[0] # si pas dans le form, valeur par defaut
|
||||
@ -697,6 +714,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
def record(self, code: str, mark_recorded: bool = True) -> bool:
|
||||
"""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 le code est None, efface le code déjà enregistré.
|
||||
Si mark_recorded est vrai, positionne self.recorded
|
||||
"""
|
||||
if self.inscription_etat != scu.INSCRIT:
|
||||
@ -746,7 +764,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
return True
|
||||
|
||||
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:
|
||||
return
|
||||
if self.inscription_etat != scu.INSCRIT:
|
||||
|
@ -154,7 +154,7 @@ def pvjury_table_but(
|
||||
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
|
||||
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
|
||||
"_nom_target": url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud.id,
|
||||
),
|
||||
|
@ -97,7 +97,7 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
||||
<span class="avertissement_redoublement">{formsemestre_2.annee_scolaire_str()
|
||||
if formsemestre_2 else ""}</span>
|
||||
</div>
|
||||
<div class="titre">RCUE</div>
|
||||
<div class="titre" title="Décisions sur RCUEs enregistrées sur l'ensemble du cursus">RCUE</div>
|
||||
"""
|
||||
)
|
||||
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}
|
||||
{'annee_prec' if annee_prec else ''}
|
||||
">
|
||||
<div title="{ue.titre}">{ue.acronyme}</div>
|
||||
<div title="{ue.titre or ''}">{ue.acronyme}</div>
|
||||
<div class="but_note with_scoplement">
|
||||
<div>{moy_ue_str}</div>
|
||||
{scoplement}
|
||||
@ -447,7 +447,7 @@ def jury_but_semestriel(
|
||||
<div class="nom_etud">{etud.nomprenom}</div>
|
||||
</div>
|
||||
<div class="bull_photo"><a href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -667,10 +667,12 @@ class BonusCalais(BonusSportAdditif):
|
||||
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 :
|
||||
<ul>
|
||||
<li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||||
<li><b>en BUT</b> à la moyenne de chaque UE;
|
||||
</li>
|
||||
<li><b>en BUT et LP</b> à la moyenne des UE dont l'acronyme fini par <b>BS</b>
|
||||
(ex : UE2.1BS, UE32BS)
|
||||
<li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant;
|
||||
</li>
|
||||
<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>
|
||||
</ul>
|
||||
"""
|
||||
@ -692,12 +694,17 @@ class BonusCalais(BonusSportAdditif):
|
||||
else:
|
||||
self.classic_use_bonus_ues = True # pour les LP
|
||||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||
ues = self.formsemestre.get_ues(with_sport=False)
|
||||
ues_sans_bs = [
|
||||
ue for ue in ues if ue.acronyme[-2:].upper() != "BS"
|
||||
] # les 2 derniers cars forcés en majus
|
||||
for ue in ues_sans_bs:
|
||||
self.bonus_ues[ue.id] = 0.0
|
||||
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_sans_bs = [
|
||||
ue for ue in ues if ue.acronyme[-2:].upper() != "BS"
|
||||
] # les 2 derniers cars forcés en majus
|
||||
for ue in ues_sans_bs:
|
||||
self.bonus_ues[ue.id] = 0.0
|
||||
|
||||
|
||||
class BonusColmar(BonusSportAdditif):
|
||||
|
@ -56,6 +56,7 @@ class EvaluationEtat:
|
||||
|
||||
evaluation_id: int
|
||||
nb_attente: int
|
||||
nb_notes: int # nb notes d'étudiants inscrits au semestre et au modimpl
|
||||
is_complete: bool
|
||||
|
||||
def to_dict(self):
|
||||
@ -168,25 +169,34 @@ class ModuleImplResults:
|
||||
# NULL en base => ABS (= -999)
|
||||
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
|
||||
# Ce merge ne garde que les étudiants inscrits au module
|
||||
# et met à NULL les notes non présentes
|
||||
# et met à NULL (NaN) les notes non présentes
|
||||
# (notes non saisies ou etuds non inscrits au module):
|
||||
evals_notes = evals_notes.merge(
|
||||
eval_df, how="left", left_index=True, right_index=True
|
||||
)
|
||||
# Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
|
||||
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_notes_inscr.iloc[
|
||||
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
|
||||
].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.evaluations_etat[evaluation.id] = EvaluationEtat(
|
||||
evaluation_id=evaluation.id,
|
||||
nb_attente=len(eval_etudids_attente),
|
||||
nb_notes=int(nb_notes),
|
||||
is_complete=is_complete,
|
||||
)
|
||||
# au moins une note en ATT dans ce modimpl:
|
||||
# au moins une note en attente (ATT ou manquante en mode "immédiat") dans ce modimpl:
|
||||
self.en_attente = bool(self.etudids_attente)
|
||||
|
||||
# Force columns names to integers (evaluation ids)
|
||||
|
@ -100,7 +100,7 @@ def compute_sem_moys_apc_using_ects(
|
||||
|
||||
|
||||
def comp_ranks_series(notes: pd.Series) -> tuple[pd.Series, pd.Series]:
|
||||
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
|
||||
"""Calcul rangs à partir d'une série ("vecteur") de notes (index etudid, valeur
|
||||
numérique) en tenant compte des ex-aequos.
|
||||
|
||||
Result: couple (tuple)
|
||||
|
@ -273,7 +273,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
return s.index[s.notna()]
|
||||
|
||||
def etud_parcours_ues_ids(self, etudid: int) -> set[int]:
|
||||
"""Ensemble les id des UEs que l'étudiant doit valider dans ce semestre compte tenu
|
||||
"""Ensemble des id des UEs que l'étudiant doit valider dans ce semestre compte tenu
|
||||
du parcours dans lequel il est inscrit.
|
||||
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.
|
||||
|
@ -234,7 +234,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
||||
raise ScoValueError(
|
||||
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
|
||||
impossible à déterminer pour l'étudiant <a href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}" class="discretelink">{etud.nom_disp()}</a></p>
|
||||
<p>Il faut <a href="{
|
||||
url_for("notes.formsemestre_edit_uecoefs", scodoc_dept=g.scodoc_dept,
|
||||
|
@ -9,12 +9,13 @@
|
||||
|
||||
from collections import Counter, defaultdict
|
||||
from collections.abc import Generator
|
||||
import datetime
|
||||
from functools import cached_property
|
||||
from operator import attrgetter
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
@ -22,14 +23,19 @@ from app.comp import res_sem
|
||||
from app.comp.res_cache import ResultatsCache
|
||||
from app.comp.jury import ValidationsSemestre
|
||||
from app.comp.moy_mod import ModuleImplResults
|
||||
from app.models import FormSemestre, FormSemestreUECoef
|
||||
from app.models import Identite
|
||||
from app.models import ModuleImpl, ModuleImplInscription
|
||||
from app.models import ScolarAutorisationInscription
|
||||
from app.models.ues import UniteEns
|
||||
from app.models import (
|
||||
Evaluation,
|
||||
FormSemestre,
|
||||
FormSemestreUECoef,
|
||||
Identite,
|
||||
ModuleImpl,
|
||||
ModuleImplInscription,
|
||||
ScolarAutorisationInscription,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc.sco_cache import ResultatsSemestreCache
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoTemporaryError
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
@ -192,16 +198,82 @@ class ResultatsSemestre(ResultatsCache):
|
||||
*[mr.etudids_attente for mr in self.modimpls_results.values()]
|
||||
)
|
||||
|
||||
# # Etat des évaluations
|
||||
# # (se substitue à do_evaluation_etat, sans les moyennes par groupes)
|
||||
# def get_evaluations_etats(evaluation_id: int) -> dict:
|
||||
# """Renvoie dict avec les clés:
|
||||
# last_modif
|
||||
# nb_evals_completes
|
||||
# nb_evals_en_cours
|
||||
# nb_evals_vides
|
||||
# attente
|
||||
# """
|
||||
# Etat des évaluations
|
||||
def get_evaluation_etat(self, evaluation: Evaluation) -> dict:
|
||||
"""État d'une évaluation
|
||||
{
|
||||
"coefficient" : float, # 0 si None
|
||||
"description" : str, # de l'évaluation, "" si None
|
||||
"etat" {
|
||||
"evalcomplete" : bool,
|
||||
"last_modif" : datetime.datetime | None, # saisie de note la plus récente
|
||||
"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...
|
||||
def get_formsemestre_validations(self) -> ValidationsSemestre:
|
||||
|
@ -423,30 +423,37 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
)
|
||||
return evaluations
|
||||
|
||||
def get_evaluations_etats(self) -> list[dict]:
|
||||
"""Liste de toutes les évaluations du semestre
|
||||
[ {...evaluation et son etat...} ]"""
|
||||
# TODO: à moderniser (voir dans ResultatsSemestre)
|
||||
# utilisé par
|
||||
# do_evaluation_etat_in_sem
|
||||
def get_evaluations_etats(self) -> dict[int, dict]:
|
||||
""" "état" de chaque évaluation du semestre
|
||||
{
|
||||
evaluation_id : {
|
||||
"evalcomplete" : bool,
|
||||
"last_modif" : datetime | None
|
||||
"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
|
||||
|
||||
from app.scodoc import sco_evaluations
|
||||
|
||||
if not hasattr(self, "_evaluations_etats"):
|
||||
self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem(
|
||||
self.formsemestre.id
|
||||
)
|
||||
|
||||
return self._evaluations_etats
|
||||
|
||||
def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
|
||||
"""Liste des états des évaluations de ce module"""
|
||||
# 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
|
||||
]
|
||||
# ancienne version < 2024-02-02
|
||||
# def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
|
||||
# """Liste des états des évaluations de ce module
|
||||
# ordonnée selon (numero desc, date_debut desc)
|
||||
# """
|
||||
# # à moderniser: lent, recharge des données que l'on a déjà...
|
||||
# # remplacemé par ResultatsSemestre.get_mod_evaluation_etat_list
|
||||
# #
|
||||
# return [
|
||||
# e
|
||||
# for e in self.get_evaluations_etats()
|
||||
# if e["moduleimpl_id"] == moduleimpl_id
|
||||
# ]
|
||||
|
||||
def get_moduleimpls_attente(self):
|
||||
"""Liste des modimpls du semestre ayant des notes en attente"""
|
||||
|
@ -32,6 +32,7 @@ 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,
|
||||
@ -101,7 +102,7 @@ class AjoutAssiOrJustForm(FlaskForm):
|
||||
)
|
||||
|
||||
entry_date = StringField(
|
||||
"Date de dépot ou saisie",
|
||||
"Date de dépôt ou saisie",
|
||||
validators=[validators.Length(max=10)],
|
||||
render_kw={
|
||||
"class": "datepicker",
|
||||
@ -109,6 +110,16 @@ class AjoutAssiOrJustForm(FlaskForm):
|
||||
"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})
|
||||
|
||||
@ -136,6 +147,7 @@ class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
|
||||
"Module",
|
||||
choices={}, # will be populated dynamically
|
||||
)
|
||||
est_just = BooleanField("Justifiée")
|
||||
|
||||
|
||||
class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
||||
@ -161,3 +173,30 @@ class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
||||
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
|
||||
|
||||
parcours: list[ApcParcours] = ue.formation.referentiel_competence.parcours
|
||||
# Initialise un champs de saisie par parcours
|
||||
# Initialise un champ de saisie par parcours
|
||||
for parcour in parcours:
|
||||
ects = ue.get_ects(parcour, only_parcours=True)
|
||||
setattr(
|
||||
|
@ -34,52 +34,11 @@ import re
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import DecimalField, SubmitField, ValidationError
|
||||
from wtforms.fields.simple import StringField
|
||||
from wtforms.validators import Optional
|
||||
from wtforms.validators import Optional, Length
|
||||
|
||||
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):
|
||||
"""Le tick_time doit être entre 0 et 60 minutes"""
|
||||
if field.data < 1 or field.data > 59:
|
||||
@ -118,14 +77,36 @@ def check_ics_regexp(form, field):
|
||||
|
||||
class ConfigAssiduitesForm(FlaskForm):
|
||||
"Formulaire paramétrage Module Assiduité"
|
||||
|
||||
assi_morning_time = TimeField(
|
||||
"Début de la journée"
|
||||
) # TODO utiliser TextField + timepicker voir AjoutAssiOrJustForm
|
||||
assi_lunch_time = TimeField(
|
||||
"Heure de midi (date pivot entre matin et après-midi)"
|
||||
) # TODO
|
||||
assi_afternoon_time = TimeField("Fin de la journée") # TODO
|
||||
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(
|
||||
"Granularité de la timeline (temps en minutes)",
|
||||
|
@ -82,7 +82,7 @@ class ConfigCASForm(FlaskForm):
|
||||
|
||||
cas_attribute_id = StringField(
|
||||
label="Attribut CAS utilisé comme id (laissez vide pour prendre l'id par défaut)",
|
||||
description="""Le champs CAS qui sera considéré comme l'id unique des
|
||||
description="""Le champ CAS qui sera considéré comme l'id unique des
|
||||
comptes utilisateurs.""",
|
||||
)
|
||||
|
||||
|
@ -77,6 +77,9 @@ class ScoDocConfigurationForm(FlaskForm):
|
||||
Attention: si ce champ peut aussi être défini dans chaque département.""",
|
||||
validators=[Optional(), Email()],
|
||||
)
|
||||
user_require_email_institutionnel = BooleanField(
|
||||
"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é)"
|
||||
)
|
||||
@ -99,6 +102,7 @@ def configuration():
|
||||
"month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(),
|
||||
"email_from_addr": ScoDocSiteConfig.get("email_from_addr"),
|
||||
"disable_bul_pdf": ScoDocSiteConfig.is_bul_pdf_disabled(),
|
||||
"user_require_email_institutionnel": ScoDocSiteConfig.is_user_require_email_institutionnel_enabled(),
|
||||
}
|
||||
)
|
||||
if request.method == "POST" and (
|
||||
@ -151,6 +155,18 @@ def configuration():
|
||||
"Exports PDF "
|
||||
+ ("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 render_template(
|
||||
|
49
app/forms/main/config_rgpd.py
Normal file
49
app/forms/main/config_rgpd.py
Normal file
@ -0,0 +1,49 @@
|
||||
# -*- 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})
|
@ -52,7 +52,7 @@ class ScoDocModel(db.Model):
|
||||
def create_from_dict(cls, data: dict) -> "ScoDocModel":
|
||||
"""Create a new instance of the model with attributes given in dict.
|
||||
The instance is added to the session (but not flushed nor committed).
|
||||
Use only relevant arributes for the given model and ignore others.
|
||||
Use only relevant attributes for the given model and ignore others.
|
||||
"""
|
||||
if data:
|
||||
args = cls.convert_dict_fields(cls.filter_model_attributes(data))
|
||||
|
@ -2,8 +2,10 @@
|
||||
"""Gestion de l'assiduité (assiduités + justificatifs)
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask_login import current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.exc import DataError
|
||||
|
||||
from app import db, log, g, set_sco_dept
|
||||
from app.models import (
|
||||
@ -87,8 +89,10 @@ class Assiduite(ScoDocModel):
|
||||
lazy="select",
|
||||
)
|
||||
|
||||
def to_dict(self, format_api=True) -> dict:
|
||||
"""Retourne la représentation json de l'assiduité"""
|
||||
def to_dict(self, format_api=True, restrict: bool | None = None) -> dict:
|
||||
"""Retourne la représentation json de l'assiduité
|
||||
restrict n'est pas utilisé ici.
|
||||
"""
|
||||
etat = self.etat
|
||||
user: User | None = None
|
||||
if format_api:
|
||||
@ -249,50 +253,57 @@ class Assiduite(ScoDocModel):
|
||||
sco_abs_notification.abs_notify(etud.id, nouv_assiduite.date_debut)
|
||||
return nouv_assiduite
|
||||
|
||||
def set_moduleimpl(self, moduleimpl_id: int | str) -> bool:
|
||||
"""TODO"""
|
||||
# je ne comprend pas cette fonction WIP
|
||||
# moduleimpl_id peut être == "autre", ce qui plante
|
||||
# ci-dessous un fix temporaire en attendant explication de @iziram
|
||||
if moduleimpl_id is None:
|
||||
raise ScoValueError("invalid moduleimpl_id")
|
||||
try:
|
||||
moduleimpl_id_int = int(moduleimpl_id)
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("invalid moduleimpl_id") from exc
|
||||
# /fix
|
||||
moduleimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id_int)
|
||||
if moduleimpl is not None:
|
||||
# Vérification de l'inscription de l'étudiant
|
||||
if moduleimpl.est_inscrit(self.etudiant):
|
||||
self.moduleimpl_id = moduleimpl.id
|
||||
else:
|
||||
raise ScoValueError("L'étudiant n'est pas inscrit au module")
|
||||
elif isinstance(moduleimpl_id, str):
|
||||
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:
|
||||
self.external_data = {"module": moduleimpl_id}
|
||||
self.external_data = {"module": "autre"}
|
||||
else:
|
||||
self.external_data["module"] = moduleimpl_id
|
||||
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:
|
||||
# Vérification si module forcé
|
||||
formsemestre: FormSemestre = get_formsemestre_from_data(
|
||||
{
|
||||
"etudid": self.etudid,
|
||||
"date_debut": self.date_debut,
|
||||
"date_fin": self.date_fin,
|
||||
}
|
||||
)
|
||||
force: bool
|
||||
|
||||
if formsemestre:
|
||||
force = is_assiduites_module_forced(formsemestre_id=formsemestre.id)
|
||||
else:
|
||||
force = is_assiduites_module_forced(dept_id=self.etudiant.dept_id)
|
||||
|
||||
if force:
|
||||
raise ScoValueError("Module non renseigné")
|
||||
return True
|
||||
raise ScoValueError("L'étudiant n'est pas inscrit au module")
|
||||
|
||||
def supprime(self):
|
||||
"Supprime l'assiduité. Log et commit."
|
||||
@ -322,7 +333,7 @@ class Assiduite(ScoDocModel):
|
||||
return get_formsemestre_from_data(self.to_dict())
|
||||
|
||||
def get_module(self, traduire: bool = False) -> int | str:
|
||||
"TODO"
|
||||
"TODO documenter"
|
||||
if self.moduleimpl_id is not None:
|
||||
if traduire:
|
||||
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||
@ -338,6 +349,41 @@ class Assiduite(ScoDocModel):
|
||||
|
||||
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):
|
||||
"""
|
||||
@ -372,7 +418,7 @@ class Justificatif(ScoDocModel):
|
||||
|
||||
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épot au secrétariat, si différente
|
||||
# pourrait devenir date de dépôt au secrétariat, si différente
|
||||
|
||||
user_id = db.Column(
|
||||
db.Integer,
|
||||
@ -391,6 +437,14 @@ class Justificatif(ScoDocModel):
|
||||
etudiant = db.relationship(
|
||||
"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)
|
||||
|
||||
@ -402,20 +456,16 @@ class Justificatif(ScoDocModel):
|
||||
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) -> dict:
|
||||
"""transformation de l'objet en dictionnaire sérialisable"""
|
||||
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
|
||||
username = self.user_id
|
||||
user: User = self.user if self.user_id is not None else None
|
||||
|
||||
if format_api:
|
||||
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 = {
|
||||
"justif_id": self.justif_id,
|
||||
@ -424,11 +474,13 @@ class Justificatif(ScoDocModel):
|
||||
"date_debut": self.date_debut,
|
||||
"date_fin": self.date_fin,
|
||||
"etat": etat,
|
||||
"raison": self.raison,
|
||||
"fichier": self.fichier,
|
||||
"raison": None if restrict else self.raison,
|
||||
"fichier": None if restrict else self.fichier,
|
||||
"entry_date": self.entry_date,
|
||||
"user_id": username,
|
||||
"external_data": self.external_data,
|
||||
"user_id": None if user is None else user.id, # l'uid
|
||||
"user_name": None if user is None else user.user_name, # le login
|
||||
"user_nom_complet": None if user is None else user.get_nomcomplet(),
|
||||
"external_data": None if restrict else self.external_data,
|
||||
}
|
||||
return data
|
||||
|
||||
@ -575,6 +627,12 @@ def compute_assiduites_justified(
|
||||
Returns:
|
||||
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
|
||||
if justificatifs is None:
|
||||
justificatifs: list[Justificatif] = Justificatif.query.filter_by(
|
||||
|
@ -9,6 +9,7 @@ from app.models.but_refcomp import ApcNiveau
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
|
||||
class ApcValidationRCUE(db.Model):
|
||||
@ -76,10 +77,12 @@ class ApcValidationRCUE(db.Model):
|
||||
niveau = self.niveau()
|
||||
return niveau.annee if niveau else None
|
||||
|
||||
def niveau(self) -> ApcNiveau:
|
||||
def niveau(self) -> ApcNiveau | None:
|
||||
"""Le niveau de compétence associé à cet RCUE."""
|
||||
# Par convention, il est donné par la seconde UE
|
||||
return self.ue2.niveau_competence
|
||||
# à défaut (si l'UE a été désacciée entre temps), la première
|
||||
# et à défaut, renvoie None
|
||||
return self.ue2.niveau_competence or self.ue1.niveau_competence
|
||||
|
||||
def to_dict(self):
|
||||
"as a dict"
|
||||
@ -218,15 +221,18 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||
decisions["descr_decisions_rcue"] = ""
|
||||
decisions["descr_decisions_niveaux"] = ""
|
||||
# --- Année: prend la validation pour l'année scolaire et l'ordre de ce semestre
|
||||
annee_but = (formsemestre.semestre_id + 1) // 2
|
||||
validation = ApcValidationAnnee.query.filter_by(
|
||||
etudid=etud.id,
|
||||
annee_scolaire=formsemestre.annee_scolaire(),
|
||||
ordre=annee_but,
|
||||
referentiel_competence_id=formsemestre.formation.referentiel_competence_id,
|
||||
).first()
|
||||
if validation:
|
||||
decisions["decision_annee"] = validation.to_dict_bul()
|
||||
if sco_preferences.get_preference("bul_but_code_annuel", formsemestre.id):
|
||||
annee_but = (formsemestre.semestre_id + 1) // 2
|
||||
validation = ApcValidationAnnee.query.filter_by(
|
||||
etudid=etud.id,
|
||||
annee_scolaire=formsemestre.annee_scolaire(),
|
||||
ordre=annee_but,
|
||||
referentiel_competence_id=formsemestre.formation.referentiel_competence_id,
|
||||
).first()
|
||||
if validation:
|
||||
decisions["decision_annee"] = validation.to_dict_bul()
|
||||
else:
|
||||
decisions["decision_annee"] = None
|
||||
else:
|
||||
decisions["decision_annee"] = None
|
||||
return decisions
|
||||
|
@ -95,6 +95,7 @@ class ScoDocSiteConfig(db.Model):
|
||||
"month_debut_annee_scolaire": int,
|
||||
"month_debut_periode2": int,
|
||||
"disable_bul_pdf": bool,
|
||||
"user_require_email_institutionnel": bool,
|
||||
# CAS
|
||||
"cas_enable": bool,
|
||||
"cas_server": str,
|
||||
@ -243,6 +244,14 @@ class ScoDocSiteConfig(db.Model):
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
||||
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
|
||||
def is_bul_pdf_disabled(cls) -> bool:
|
||||
"""True si on interdit les exports PDF des bulltins"""
|
||||
@ -250,36 +259,14 @@ class ScoDocSiteConfig(db.Model):
|
||||
return cfg is not None and cfg.value
|
||||
|
||||
@classmethod
|
||||
def enable_entreprises(cls, enabled=True) -> bool:
|
||||
def enable_entreprises(cls, enabled: bool = True) -> bool:
|
||||
"""Active (ou déactive) le module entreprises. True si changement."""
|
||||
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
|
||||
return cls.set("enable_entreprises", "on" if enabled else "")
|
||||
|
||||
@classmethod
|
||||
def disable_bul_pdf(cls, enabled=True) -> bool:
|
||||
"""Interedit (ou autorise) les exports PDF. True si changement."""
|
||||
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
|
||||
"""Interdit (ou autorise) les exports PDF. True si changement."""
|
||||
return cls.set("disable_bul_pdf", "on" if enabled else "")
|
||||
|
||||
@classmethod
|
||||
def get(cls, name: str, default: str = "") -> str:
|
||||
@ -298,9 +285,10 @@ class ScoDocSiteConfig(db.Model):
|
||||
if cfg is None:
|
||||
cfg = ScoDocSiteConfig(name=name, value=value_str)
|
||||
else:
|
||||
cfg.value = str(value or "")
|
||||
cfg.value = value_str
|
||||
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.commit()
|
||||
@ -309,7 +297,7 @@ class ScoDocSiteConfig(db.Model):
|
||||
|
||||
@classmethod
|
||||
def _get_int_field(cls, name: str, default=None) -> int:
|
||||
"""Valeur d'un champs integer"""
|
||||
"""Valeur d'un champ integer"""
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
||||
if (cfg is None) or cfg.value is None:
|
||||
return default
|
||||
@ -323,7 +311,7 @@ class ScoDocSiteConfig(db.Model):
|
||||
default=None,
|
||||
range_values: tuple = (),
|
||||
) -> bool:
|
||||
"""Set champs integer. True si changement."""
|
||||
"""Set champ integer. True si changement."""
|
||||
if value != cls._get_int_field(name, default=default):
|
||||
if not isinstance(value, int) or (
|
||||
range_values and (value < range_values[0]) or (value > range_values[1])
|
||||
|
@ -19,7 +19,7 @@ from app.models.departements import Departement
|
||||
from app.models.scolar_event import ScolarEvent
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc.sco_bac import Baccalaureat
|
||||
from app.scodoc.sco_exceptions import ScoInvalidParamError, ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoGenError, ScoInvalidParamError, ScoValueError
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
@ -101,7 +101,12 @@ class Identite(models.ScoDocModel):
|
||||
adresses = db.relationship(
|
||||
"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")
|
||||
#
|
||||
dispense_ues = db.relationship(
|
||||
@ -119,6 +124,9 @@ class Identite(models.ScoDocModel):
|
||||
"Justificatif", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
|
||||
)
|
||||
|
||||
# Champs "protégés" par ViewEtudData (RGPD)
|
||||
protected_attrs = {"boursier"}
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
||||
@ -176,7 +184,7 @@ class Identite(models.ScoDocModel):
|
||||
def url_fiche(self) -> str:
|
||||
"url de la fiche étudiant"
|
||||
return url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id
|
||||
"scolar.fiche_etud", scodoc_dept=self.departement.acronym, etudid=self.id
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@ -230,6 +238,15 @@ class Identite(models.ScoDocModel):
|
||||
log(f"Identite.create {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."""
|
||||
@ -418,7 +435,7 @@ class Identite(models.ScoDocModel):
|
||||
return args_dict
|
||||
|
||||
def to_dict_short(self) -> dict:
|
||||
"""Les champs essentiels"""
|
||||
"""Les champs essentiels (aucune donnée perso protégée)"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"civilite": self.civilite,
|
||||
@ -433,9 +450,11 @@ class Identite(models.ScoDocModel):
|
||||
"prenom_etat_civil": self.prenom_etat_civil,
|
||||
}
|
||||
|
||||
def to_dict_scodoc7(self) -> dict:
|
||||
def to_dict_scodoc7(self, restrict=False, with_inscriptions=False) -> dict:
|
||||
"""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.pop("_sa_instance_state", None)
|
||||
@ -446,7 +465,9 @@ class Identite(models.ScoDocModel):
|
||||
e_dict["nomprenom"] = self.nomprenom
|
||||
adresse = self.adresses.first()
|
||||
if adresse:
|
||||
e_dict.update(adresse.to_dict())
|
||||
e_dict.update(adresse.to_dict(restrict=restrict))
|
||||
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
|
||||
|
||||
def to_dict_bul(self, include_urls=True):
|
||||
@ -461,9 +482,9 @@ class Identite(models.ScoDocModel):
|
||||
"civilite": self.civilite,
|
||||
"code_ine": self.code_ine or "",
|
||||
"code_nip": self.code_nip or "",
|
||||
"date_naissance": self.date_naissance.strftime("%d/%m/%Y")
|
||||
if self.date_naissance
|
||||
else "",
|
||||
"date_naissance": (
|
||||
self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""
|
||||
),
|
||||
"dept_acronym": self.departement.acronym,
|
||||
"dept_id": self.dept_id,
|
||||
"dept_naissance": self.dept_naissance or "",
|
||||
@ -481,7 +502,7 @@ class Identite(models.ScoDocModel):
|
||||
if include_urls and has_request_context():
|
||||
# test request context so we can use this func in tests under the flask shell
|
||||
d["fiche_url"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=self.id
|
||||
)
|
||||
d["photo_url"] = sco_photos.get_etud_photo_url(self.id)
|
||||
adresse = self.adresses.first()
|
||||
@ -490,16 +511,33 @@ class Identite(models.ScoDocModel):
|
||||
d["id"] = self.id # a été écrasé par l'id de adresse
|
||||
return d
|
||||
|
||||
def to_dict_api(self) -> dict:
|
||||
"""Représentation dictionnaire pour export API, avec adresses et admission."""
|
||||
def to_dict_api(self, restrict=False, with_annotations=False) -> dict:
|
||||
"""Représentation dictionnaire pour export API, avec adresses et admission.
|
||||
Si restrict, supprime les infos "personnelles" (boursier)
|
||||
"""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
admission = self.admission
|
||||
e["admission"] = admission.to_dict() if admission is not None else None
|
||||
e["adresses"] = [adr.to_dict() for adr in self.adresses]
|
||||
e["adresses"] = [adr.to_dict(restrict=restrict) for adr in self.adresses]
|
||||
e["dept_acronym"] = self.departement.acronym
|
||||
e.pop("departement", None)
|
||||
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
|
||||
|
||||
def inscriptions(self) -> list["FormSemestreInscription"]:
|
||||
@ -555,7 +593,9 @@ class Identite(models.ScoDocModel):
|
||||
return r[0] if r else None
|
||||
|
||||
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()
|
||||
if inscription_courante:
|
||||
titre_sem = inscription_courante.formsemestre.titre_mois()
|
||||
@ -566,7 +606,7 @@ class Identite(models.ScoDocModel):
|
||||
else:
|
||||
inscr_txt = "Inscrit en"
|
||||
|
||||
return {
|
||||
result = {
|
||||
"etat_in_cursem": inscription_courante.etat,
|
||||
"inscription_courante": inscription_courante,
|
||||
"inscription": titre_sem,
|
||||
@ -589,15 +629,20 @@ class Identite(models.ScoDocModel):
|
||||
inscription = "ancien"
|
||||
situation = "ancien élève"
|
||||
else:
|
||||
inscription = ("non inscrit",)
|
||||
inscription = "non inscrit"
|
||||
situation = inscription
|
||||
return {
|
||||
result = {
|
||||
"etat_in_cursem": "?",
|
||||
"inscription_courante": None,
|
||||
"inscription": inscription,
|
||||
"inscription_str": inscription,
|
||||
"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:
|
||||
"""État de l'inscription de cet étudiant au semestre:
|
||||
@ -718,6 +763,58 @@ 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(
|
||||
etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True
|
||||
) -> dict:
|
||||
@ -825,12 +922,25 @@ class Adresse(models.ScoDocModel):
|
||||
)
|
||||
description = db.Column(db.Text)
|
||||
|
||||
def to_dict(self, convert_nulls_to_str=False):
|
||||
"""Représentation dictionnaire,"""
|
||||
# Champs "protégés" par ViewEtudData (RGPD)
|
||||
protected_attrs = {
|
||||
"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.pop("_sa_instance_state", None)
|
||||
if convert_nulls_to_str:
|
||||
return {k: e[k] or "" for k in e}
|
||||
e = {k: v or "" for k, v in e.items()}
|
||||
if restrict:
|
||||
e = {k: v for (k, v) in e.items() if k not in self.protected_attrs}
|
||||
return e
|
||||
|
||||
|
||||
@ -885,12 +995,16 @@ class Admission(models.ScoDocModel):
|
||||
# classement (1..Ngr) par le jury dans le groupe APB
|
||||
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:
|
||||
"Le bac. utiliser bac.abbrev() pour avoir une chaine de caractères."
|
||||
return Baccalaureat(self.bac, specialite=self.specialite)
|
||||
|
||||
def to_dict(self, no_nulls=False):
|
||||
"""Représentation dictionnaire,"""
|
||||
def to_dict(self, no_nulls=False, restrict=False):
|
||||
"""Représentation dictionnaire. Si restrict, filtre les champs protégés (RGPD)."""
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
if no_nulls:
|
||||
@ -905,6 +1019,8 @@ class Admission(models.ScoDocModel):
|
||||
d[key] = 0
|
||||
elif isinstance(col_type, sqlalchemy.Boolean):
|
||||
d[key] = False
|
||||
if restrict:
|
||||
d = {k: v for (k, v) in d.items() if k in self.not_protected_attrs}
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
@ -972,10 +1088,16 @@ class EtudAnnotation(db.Model):
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
etudid = db.Column(db.Integer) # sans contrainte (compat ScoDoc 7))
|
||||
etudid = db.Column(db.Integer, db.ForeignKey(Identite.id))
|
||||
author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user
|
||||
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
|
||||
|
@ -5,7 +5,7 @@
|
||||
import datetime
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import g, url_for
|
||||
from flask import abort, g, url_for
|
||||
from flask_login import current_user
|
||||
import sqlalchemy as sa
|
||||
|
||||
@ -184,7 +184,7 @@ class Evaluation(db.Model):
|
||||
# ScoDoc7 output_formators
|
||||
e_dict["evaluation_id"] = self.id
|
||||
e_dict["date_debut"] = self.date_debut.isoformat() if self.date_debut else None
|
||||
e_dict["date_fin"] = self.date_debut.isoformat() if self.date_fin else None
|
||||
e_dict["date_fin"] = self.date_fin.isoformat() if self.date_fin else None
|
||||
e_dict["numero"] = self.numero or 0
|
||||
e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
||||
|
||||
@ -241,6 +241,25 @@ class Evaluation(db.Model):
|
||||
if k != "_sa_instance_state" and k != "id" and k in data:
|
||||
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
|
||||
def get_max_numero(cls, moduleimpl_id: int) -> int:
|
||||
"""Return max numero among evaluations in this
|
||||
@ -265,7 +284,9 @@ class Evaluation(db.Model):
|
||||
evaluations = moduleimpl.evaluations.order_by(
|
||||
Evaluation.date_debut, Evaluation.numero
|
||||
).all()
|
||||
all_numbered = all(e.numero is not None for e in evaluations)
|
||||
numeros_distincts = {e.numero for e in evaluations if e.numero is not None}
|
||||
# pas de None, pas de dupliqués
|
||||
all_numbered = len(numeros_distincts) == len(evaluations)
|
||||
if all_numbered and only_if_unumbered:
|
||||
return # all ok
|
||||
|
||||
@ -428,8 +449,8 @@ class Evaluation(db.Model):
|
||||
|
||||
def get_ue_poids_str(self) -> str:
|
||||
"""string describing poids, for excel cells and pdfs
|
||||
Note: si les poids ne sont pas initialisés (poids par défaut),
|
||||
ils ne sont pas affichés.
|
||||
Note: les poids nuls ou non initialisés (poids par défaut),
|
||||
ne sont pas affichés.
|
||||
"""
|
||||
# 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
|
||||
@ -440,7 +461,7 @@ class Evaluation(db.Model):
|
||||
for p in sorted(
|
||||
self.ue_poids, key=lambda p: (p.ue.numero or 0, p.ue.acronyme)
|
||||
)
|
||||
if evaluation_semestre_idx == p.ue.semestre_idx
|
||||
if evaluation_semestre_idx == p.ue.semestre_idx and (p.poids or 0) > 0
|
||||
]
|
||||
)
|
||||
|
||||
@ -584,20 +605,10 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
|
||||
if date_debut and date_fin:
|
||||
duration = data["date_fin"] - data["date_debut"]
|
||||
if duration.total_seconds() < 0 or duration > MAX_EVALUATION_DURATION:
|
||||
raise ScoValueError("Heures de l'évaluation incohérentes !")
|
||||
# # --- heures
|
||||
# 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)
|
||||
raise ScoValueError(
|
||||
"Heures de l'évaluation incohérentes !",
|
||||
dest_url="javascript:history.back();",
|
||||
)
|
||||
|
||||
|
||||
def heure_to_time(heure: str) -> datetime.time:
|
||||
|
@ -68,7 +68,7 @@ class FormSemestre(db.Model):
|
||||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||
titre = db.Column(db.Text(), nullable=False)
|
||||
date_debut = db.Column(db.Date(), nullable=False)
|
||||
date_fin = db.Column(db.Date(), nullable=False)
|
||||
date_fin = db.Column(db.Date(), nullable=False) # jour inclus
|
||||
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
|
||||
"identifiant emplois du temps (unicité non imposée)"
|
||||
etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true")
|
||||
@ -187,7 +187,7 @@ class FormSemestre(db.Model):
|
||||
def get_formsemestre(
|
||||
cls, formsemestre_id: int | str, dept_id: int = None
|
||||
) -> "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)
|
||||
@ -673,7 +673,7 @@ class FormSemestre(db.Model):
|
||||
) -> 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.now()
|
||||
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,
|
||||
|
@ -2,12 +2,14 @@
|
||||
"""ScoDoc models: moduleimpls
|
||||
"""
|
||||
import pandas as pd
|
||||
from flask import abort, g
|
||||
from flask_login import current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.comp import df_cache
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import APO_CODE_STR_LEN, ScoDocModel
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.evaluations import Evaluation
|
||||
from app.models.modules import Module
|
||||
@ -16,7 +18,7 @@ from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class ModuleImpl(db.Model):
|
||||
class ModuleImpl(ScoDocModel):
|
||||
"""Mise en oeuvre d'un module pour une annee/semestre"""
|
||||
|
||||
__tablename__ = "notes_moduleimpl"
|
||||
@ -35,7 +37,10 @@ class ModuleImpl(db.Model):
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
|
||||
responsable_id = db.Column(
|
||||
"responsable_id", db.Integer, db.ForeignKey("user.id", ondelete="SET NULL")
|
||||
)
|
||||
responsable = db.relationship("User", back_populates="modimpls")
|
||||
# formule de calcul moyenne:
|
||||
computation_expr = db.Column(db.Text())
|
||||
|
||||
@ -51,8 +56,8 @@ class ModuleImpl(db.Model):
|
||||
secondary="notes_modules_enseignants",
|
||||
lazy="dynamic",
|
||||
backref="moduleimpl",
|
||||
viewonly=True,
|
||||
)
|
||||
"enseignants du module (sans le responsable)"
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
||||
@ -82,6 +87,23 @@ class ModuleImpl(db.Model):
|
||||
df_cache.EvaluationsPoidsCache.set(self.id, 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):
|
||||
"""Invalide poids cachés"""
|
||||
df_cache.EvaluationsPoidsCache.delete(self.id)
|
||||
@ -169,7 +191,7 @@ class ModuleImpl(db.Model):
|
||||
return allow_ens and user.id in (ens.id for ens in self.enseignants)
|
||||
return True
|
||||
|
||||
def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
|
||||
def can_change_responsable(self, user: User, raise_exc=False) -> bool:
|
||||
"""Check if user can modify module resp.
|
||||
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
||||
= Admin, et dir des etud. (si option l'y autorise)
|
||||
@ -190,6 +212,27 @@ class ModuleImpl(db.Model):
|
||||
raise AccessDenied(f"Modification impossible pour {user}")
|
||||
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:
|
||||
"""
|
||||
Vérifie si l'étudiant est bien inscrit au moduleimpl (même si DEM ou DEF au semestre).
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""ScoDoc 9 models : Modules
|
||||
"""
|
||||
from flask import current_app
|
||||
from flask import current_app, g
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
@ -310,6 +310,14 @@ class Module(db.Model):
|
||||
return []
|
||||
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):
|
||||
"""Coefficients des modules vers les UE (APC, BUT)
|
||||
@ -372,6 +380,19 @@ class NotesTag(db.Model):
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
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
|
||||
notes_modules_tags = db.Table(
|
||||
|
@ -5,6 +5,7 @@ from flask import g
|
||||
import pandas as pd
|
||||
|
||||
from app import db, log
|
||||
from app import models
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.but_refcomp import ApcNiveau, ApcParcours
|
||||
@ -12,7 +13,7 @@ from app.models.modules import Module
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class UniteEns(db.Model):
|
||||
class UniteEns(models.ScoDocModel):
|
||||
"""Unité d'Enseignement (UE)"""
|
||||
|
||||
__tablename__ = "notes_ue"
|
||||
@ -81,7 +82,7 @@ class UniteEns(db.Model):
|
||||
'EXTERNE' if self.is_external else ''})>"""
|
||||
|
||||
def clone(self):
|
||||
"""Create a new copy of this ue.
|
||||
"""Create a new copy of this ue, add to session.
|
||||
Ne copie pas le code, ni le code Apogée, ni les liens au réf. de comp.
|
||||
(parcours et niveau).
|
||||
"""
|
||||
@ -100,8 +101,26 @@ class UniteEns(db.Model):
|
||||
coef_rcue=self.coef_rcue,
|
||||
color=self.color,
|
||||
)
|
||||
db.session.add(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):
|
||||
"""as a dict, with the same conversions as in ScoDoc7.
|
||||
If convert_objects, convert all attributes to native types
|
||||
|
44
app/pe/pe_affichage.py
Normal file
44
app/pe/pe_affichage.py
Normal file
@ -0,0 +1,44 @@
|
||||
##############################################################################
|
||||
# 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"
|
@ -1,517 +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)
|
||||
##############################################################################
|
||||
|
||||
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
|
286
app/pe/pe_comp.py
Normal file
286
app/pe/pe_comp.py
Normal file
@ -0,0 +1,286 @@
|
||||
# -*- 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
|
618
app/pe/pe_etudiant.py
Normal file
618
app/pe/pe_etudiant.py
Normal file
@ -0,0 +1,618 @@
|
||||
# -*- 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)
|
160
app/pe/pe_interclasstag.py
Normal file
160
app/pe/pe_interclasstag.py
Normal file
@ -0,0 +1,160 @@
|
||||
##############################################################################
|
||||
#
|
||||
# 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
|
734
app/pe/pe_jury.py
Normal file
734
app/pe/pe_jury.py
Normal file
@ -0,0 +1,734 @@
|
||||
# -*- 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
|
1272
app/pe/pe_jurype.py
1272
app/pe/pe_jurype.py
File diff suppressed because it is too large
Load Diff
269
app/pe/pe_rcs.py
Normal file
269
app/pe/pe_rcs.py
Normal file
@ -0,0 +1,269 @@
|
||||
##############################################################################
|
||||
# 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"]
|
217
app/pe/pe_rcstag.py
Normal file
217
app/pe/pe_rcstag.py
Normal file
@ -0,0 +1,217 @@
|
||||
# -*- 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
|
@ -1,511 +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 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"]
|
310
app/pe/pe_semtag.py
Normal file
310
app/pe/pe_semtag.py
Normal file
@ -0,0 +1,310 @@
|
||||
# -*- 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
|
@ -1,324 +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.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: list[dict], 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] is 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
|
263
app/pe/pe_tabletags.py
Normal file
263
app/pe/pe_tabletags.py
Normal file
@ -0,0 +1,263 @@
|
||||
# -*- 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
|
@ -1,348 +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.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
|
@ -1,960 +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 import log
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
|
||||
PE_DEBUG = 0
|
||||
|
||||
if not PE_DEBUG:
|
||||
# log to notes.log
|
||||
def pe_print(*a, **kw):
|
||||
# kw is ignored. log always add a newline
|
||||
log(" ".join(a))
|
||||
|
||||
else:
|
||||
pe_print = print # print function
|
||||
|
||||
|
||||
# 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"
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
||||
def print_semestres_description(sems, avec_affichage_debug=False):
|
||||
"""Dediee a l'affichage d'un semestre pour debug du module"""
|
||||
|
||||
def chaine_semestre(sem):
|
||||
desc = (
|
||||
"S"
|
||||
+ str(sem["semestre_id"])
|
||||
+ " "
|
||||
+ sem["modalite"]
|
||||
+ " "
|
||||
+ sem["anneescolaire"]
|
||||
)
|
||||
desc += " (" + sem["annee_debut"] + "/" + sem["annee_fin"] + ") "
|
||||
desc += str(sem["formation_id"]) + " / " + str(sem["formsemestre_id"])
|
||||
desc += " - " + sem["titre_num"]
|
||||
return desc
|
||||
|
||||
if avec_affichage_debug == True:
|
||||
if isinstance(sems, list):
|
||||
for sem in sems:
|
||||
pe_print(chaine_semestre(sem))
|
||||
else:
|
||||
pe_print(chaine_semestre(sems))
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
||||
def calcul_age(born):
|
||||
"""Calcule l'age à partir de la date de naissance sous forme d'une chaine de caractère 'jj/mm/aaaa'.
|
||||
Aucun test de validité sur le format de la date n'est fait.
|
||||
"""
|
||||
if not isinstance(born, str) or born == "":
|
||||
return ""
|
||||
|
||||
donnees = born.split("/")
|
||||
naissance = datetime.datetime(int(donnees[2]), int(donnees[1]), int(donnees[0]))
|
||||
today = datetime.date.today()
|
||||
return (
|
||||
today.year
|
||||
- naissance.year
|
||||
- ((today.month, today.day) < (naissance.month, naissance.day))
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
||||
def remove_accents(input_unicode_str):
|
||||
"""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):
|
||||
"""List of regular filenames in a directory (recursive)
|
||||
Excludes files and directories begining with .
|
||||
"""
|
||||
R = []
|
||||
for root, dirs, files in os.walk(path, topdown=True):
|
||||
dirs[:] = [d for d in dirs if d[0] != "."]
|
||||
R += [os.path.join(root, fn) for fn in files if fn[0] != "."]
|
||||
return R
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
||||
# Variable pour le debug des avislatex (en squeezant le calcul du jury souvent long)
|
||||
JURY_SYNTHESE_POUR_DEBUG = {
|
||||
"EID1810": {
|
||||
"nom": "ROUX",
|
||||
"entree": "2016",
|
||||
"civilite_str": "M.",
|
||||
"promo": 2016,
|
||||
"S2": {
|
||||
"groupe": {
|
||||
"informatique": (
|
||||
13.184230769230767,
|
||||
0.21666666666666667,
|
||||
"18",
|
||||
78,
|
||||
9.731491508491509,
|
||||
18.46846153846154,
|
||||
18.46846153846154,
|
||||
),
|
||||
"technique": (
|
||||
12.975409073359078,
|
||||
0.6166666666666666,
|
||||
"16",
|
||||
78,
|
||||
9.948540264387688,
|
||||
18.29285714285714,
|
||||
18.29285714285714,
|
||||
),
|
||||
"pe": (
|
||||
12.016584900684544,
|
||||
1.116666666666667,
|
||||
"20",
|
||||
78,
|
||||
9.83147528118408,
|
||||
17.691755169172936,
|
||||
17.691755169172936,
|
||||
),
|
||||
"mathematiques": (
|
||||
12.25,
|
||||
0.1,
|
||||
"15 ex",
|
||||
78,
|
||||
8.45153073717949,
|
||||
19.0625,
|
||||
19.0625,
|
||||
),
|
||||
"dut": (
|
||||
12.43750128724589,
|
||||
1.0,
|
||||
"19",
|
||||
78,
|
||||
10.151630181286441,
|
||||
17.881104750512645,
|
||||
17.881104750512645,
|
||||
),
|
||||
},
|
||||
"promo": {
|
||||
"informatique": (
|
||||
13.184230769230767,
|
||||
0.21666666666666667,
|
||||
"25",
|
||||
73,
|
||||
11.696187214611871,
|
||||
18.51346153846154,
|
||||
18.51346153846154,
|
||||
),
|
||||
"technique": (
|
||||
12.975409073359078,
|
||||
0.6166666666666666,
|
||||
"23",
|
||||
73,
|
||||
11.862307379173147,
|
||||
17.616047267953675,
|
||||
17.616047267953675,
|
||||
),
|
||||
"pe": (
|
||||
12.016584900684544,
|
||||
1.116666666666667,
|
||||
"28",
|
||||
73,
|
||||
11.571004424603757,
|
||||
16.706338951857248,
|
||||
16.706338951857248,
|
||||
),
|
||||
"mathematiques": (
|
||||
12.25,
|
||||
0.1,
|
||||
"18 ex",
|
||||
73,
|
||||
10.00886454908676,
|
||||
19.0625,
|
||||
19.0625,
|
||||
),
|
||||
"dut": (
|
||||
12.43750128724589,
|
||||
1.0,
|
||||
"25",
|
||||
73,
|
||||
11.88798432763965,
|
||||
17.397627309377608,
|
||||
17.397627309377608,
|
||||
),
|
||||
},
|
||||
},
|
||||
"S1": {
|
||||
"groupe": {
|
||||
"informatique": (
|
||||
16.064999999999998,
|
||||
0.16666666666666669,
|
||||
"11",
|
||||
82,
|
||||
11.020296296296294,
|
||||
19.325999999999997,
|
||||
19.325999999999997,
|
||||
),
|
||||
"technique": (
|
||||
14.513007894736845,
|
||||
0.6333333333333333,
|
||||
"11",
|
||||
82,
|
||||
11.195082967479676,
|
||||
18.309764912280702,
|
||||
18.309764912280702,
|
||||
),
|
||||
"pe": (
|
||||
13.260301515151516,
|
||||
1.1,
|
||||
"19",
|
||||
82,
|
||||
10.976036277232245,
|
||||
17.7460505050505,
|
||||
17.7460505050505,
|
||||
),
|
||||
"mathematiques": (
|
||||
11.142850000000001,
|
||||
0.13333333333333333,
|
||||
"34",
|
||||
82,
|
||||
10.314605121951217,
|
||||
19.75,
|
||||
19.75,
|
||||
),
|
||||
"dut": (
|
||||
13.54367375,
|
||||
1.0,
|
||||
"19",
|
||||
82,
|
||||
11.22193801880508,
|
||||
18.226902529333334,
|
||||
18.226902529333334,
|
||||
),
|
||||
},
|
||||
"promo": {
|
||||
"informatique": (
|
||||
16.064999999999998,
|
||||
0.16666666666666669,
|
||||
"15",
|
||||
73,
|
||||
13.265276712328768,
|
||||
19.325999999999997,
|
||||
19.325999999999997,
|
||||
),
|
||||
"technique": (
|
||||
14.513007894736845,
|
||||
0.6333333333333333,
|
||||
"16",
|
||||
73,
|
||||
12.996048795361693,
|
||||
18.309764912280702,
|
||||
18.309764912280702,
|
||||
),
|
||||
"pe": (
|
||||
13.260301515151516,
|
||||
1.1,
|
||||
"25",
|
||||
73,
|
||||
12.4107195879539,
|
||||
17.7460505050505,
|
||||
17.7460505050505,
|
||||
),
|
||||
"mathematiques": (
|
||||
11.142850000000001,
|
||||
0.13333333333333333,
|
||||
"39",
|
||||
73,
|
||||
11.320606952054794,
|
||||
19.75,
|
||||
19.75,
|
||||
),
|
||||
"dut": (
|
||||
13.54367375,
|
||||
1.0,
|
||||
"25",
|
||||
73,
|
||||
12.730581289342638,
|
||||
18.226902529333334,
|
||||
18.226902529333334,
|
||||
),
|
||||
},
|
||||
},
|
||||
"4S": {
|
||||
"groupe": {
|
||||
"informatique": (
|
||||
14.84359375,
|
||||
0.5333333333333333,
|
||||
"2",
|
||||
19,
|
||||
10.69933552631579,
|
||||
18.28646875,
|
||||
18.28646875,
|
||||
),
|
||||
"pe": (
|
||||
12.93828572598162,
|
||||
3.75,
|
||||
"4",
|
||||
19,
|
||||
11.861967145815218,
|
||||
15.737718967605682,
|
||||
15.737718967605682,
|
||||
),
|
||||
"mathematiques": (None, None, "1 ex", 19, None, None, None),
|
||||
"ptut": (None, None, "1 ex", 19, None, None, None),
|
||||
"dut": (
|
||||
13.511767410105122,
|
||||
4.0,
|
||||
"4",
|
||||
19,
|
||||
12.573349864933606,
|
||||
15.781651391587998,
|
||||
15.781651391587998,
|
||||
),
|
||||
},
|
||||
"promo": {
|
||||
"informatique": (
|
||||
16.075,
|
||||
0.1,
|
||||
"4",
|
||||
73,
|
||||
10.316541095890413,
|
||||
19.333333333333336,
|
||||
19.333333333333336,
|
||||
),
|
||||
"pe": (
|
||||
13.52416666666667,
|
||||
0.49999999999999994,
|
||||
"13",
|
||||
73,
|
||||
11.657102668465479,
|
||||
16.853208080808084,
|
||||
16.853208080808084,
|
||||
),
|
||||
"mathematiques": (
|
||||
None,
|
||||
None,
|
||||
"55 ex",
|
||||
73,
|
||||
7.705091805555555,
|
||||
19.8,
|
||||
19.8,
|
||||
),
|
||||
"dut": (
|
||||
14.425416666666665,
|
||||
1.0,
|
||||
"12",
|
||||
73,
|
||||
13.188168241098825,
|
||||
16.612613522048612,
|
||||
16.612613522048612,
|
||||
),
|
||||
},
|
||||
},
|
||||
"S4": {
|
||||
"groupe": {
|
||||
"informatique": (
|
||||
16.075,
|
||||
0.1,
|
||||
"1",
|
||||
19,
|
||||
8.799078947368422,
|
||||
16.075,
|
||||
16.075,
|
||||
),
|
||||
"technique": (
|
||||
13.835576923076923,
|
||||
0.4333333333333333,
|
||||
"4",
|
||||
19,
|
||||
12.238304655870447,
|
||||
16.521153846153847,
|
||||
16.521153846153847,
|
||||
),
|
||||
"pe": (
|
||||
13.52416666666667,
|
||||
0.49999999999999994,
|
||||
"4",
|
||||
19,
|
||||
12.292846491228072,
|
||||
16.25833333333334,
|
||||
16.25833333333334,
|
||||
),
|
||||
"dut": (
|
||||
14.425416666666665,
|
||||
1.0,
|
||||
"6",
|
||||
19,
|
||||
13.628367861842106,
|
||||
15.267566666666665,
|
||||
15.267566666666665,
|
||||
),
|
||||
},
|
||||
"promo": {
|
||||
"informatique": (
|
||||
16.075,
|
||||
0.1,
|
||||
"4",
|
||||
73,
|
||||
10.316541095890413,
|
||||
19.333333333333336,
|
||||
19.333333333333336,
|
||||
),
|
||||
"pe": (
|
||||
13.52416666666667,
|
||||
0.49999999999999994,
|
||||
"13",
|
||||
73,
|
||||
11.657102668465479,
|
||||
16.853208080808084,
|
||||
16.853208080808084,
|
||||
),
|
||||
"technique": (
|
||||
13.835576923076923,
|
||||
0.4333333333333333,
|
||||
"11",
|
||||
73,
|
||||
12.086685508009952,
|
||||
17.25909420289855,
|
||||
17.25909420289855,
|
||||
),
|
||||
"mathematiques": (
|
||||
None,
|
||||
None,
|
||||
"55 ex",
|
||||
73,
|
||||
7.705091805555555,
|
||||
19.8,
|
||||
19.8,
|
||||
),
|
||||
"ptut": (
|
||||
13.5,
|
||||
0.13333333333333333,
|
||||
"50",
|
||||
73,
|
||||
13.898173515981734,
|
||||
17.083333333333332,
|
||||
17.083333333333332,
|
||||
),
|
||||
"dut": (
|
||||
14.425416666666665,
|
||||
1.0,
|
||||
"12",
|
||||
73,
|
||||
13.188168241098825,
|
||||
16.612613522048612,
|
||||
16.612613522048612,
|
||||
),
|
||||
},
|
||||
},
|
||||
"1A": {
|
||||
"groupe": {
|
||||
"informatique": (
|
||||
14.43673913043478,
|
||||
0.38333333333333336,
|
||||
"16",
|
||||
78,
|
||||
11.046040002787066,
|
||||
18.85992173913043,
|
||||
18.85992173913043,
|
||||
),
|
||||
"technique": (
|
||||
13.754459142857144,
|
||||
1.25,
|
||||
"14",
|
||||
78,
|
||||
11.179785631638866,
|
||||
18.493250340136054,
|
||||
18.493250340136054,
|
||||
),
|
||||
"pe": (
|
||||
12.633767581547854,
|
||||
2.216666666666667,
|
||||
"18",
|
||||
78,
|
||||
10.912253971396854,
|
||||
18.39547581699347,
|
||||
18.39547581699347,
|
||||
),
|
||||
"mathematiques": (
|
||||
11.617342857142857,
|
||||
0.23333333333333334,
|
||||
"24",
|
||||
78,
|
||||
9.921286855287565,
|
||||
19.375000000000004,
|
||||
19.375000000000004,
|
||||
),
|
||||
"dut": (
|
||||
12.990587518622945,
|
||||
2.0,
|
||||
"18",
|
||||
78,
|
||||
11.2117147027821,
|
||||
18.391345156695156,
|
||||
18.391345156695156,
|
||||
),
|
||||
},
|
||||
"promo": {
|
||||
"informatique": (
|
||||
13.184230769230767,
|
||||
0.21666666666666667,
|
||||
"25",
|
||||
73,
|
||||
11.696187214611871,
|
||||
18.51346153846154,
|
||||
18.51346153846154,
|
||||
),
|
||||
"technique": (
|
||||
12.975409073359078,
|
||||
0.6166666666666666,
|
||||
"23",
|
||||
73,
|
||||
11.862307379173147,
|
||||
17.616047267953675,
|
||||
17.616047267953675,
|
||||
),
|
||||
"pe": (
|
||||
12.016584900684544,
|
||||
1.116666666666667,
|
||||
"28",
|
||||
73,
|
||||
11.571004424603757,
|
||||
16.706338951857248,
|
||||
16.706338951857248,
|
||||
),
|
||||
"mathematiques": (
|
||||
12.25,
|
||||
0.1,
|
||||
"18 ex",
|
||||
73,
|
||||
10.00886454908676,
|
||||
19.0625,
|
||||
19.0625,
|
||||
),
|
||||
"dut": (
|
||||
12.43750128724589,
|
||||
1.0,
|
||||
"25",
|
||||
73,
|
||||
11.88798432763965,
|
||||
17.397627309377608,
|
||||
17.397627309377608,
|
||||
),
|
||||
},
|
||||
},
|
||||
"2A": {
|
||||
"groupe": {
|
||||
"informatique": (
|
||||
15.88333333333333,
|
||||
0.15000000000000002,
|
||||
"2",
|
||||
19,
|
||||
9.805818713450288,
|
||||
17.346666666666668,
|
||||
17.346666666666668,
|
||||
),
|
||||
"pe": (
|
||||
13.378513043478259,
|
||||
1.5333333333333334,
|
||||
"6",
|
||||
19,
|
||||
12.099566454042717,
|
||||
16.06209927536232,
|
||||
16.06209927536232,
|
||||
),
|
||||
"technique": (
|
||||
13.965093333333336,
|
||||
1.1666666666666665,
|
||||
"5",
|
||||
19,
|
||||
12.51068332957394,
|
||||
16.472092380952386,
|
||||
16.472092380952386,
|
||||
),
|
||||
"mathematiques": (None, None, "1 ex", 19, None, None, None),
|
||||
"dut": (
|
||||
14.032947301587301,
|
||||
2.0,
|
||||
"4",
|
||||
19,
|
||||
13.043386086541773,
|
||||
15.574706269841268,
|
||||
15.574706269841268,
|
||||
),
|
||||
},
|
||||
"promo": {
|
||||
"informatique": (
|
||||
16.075,
|
||||
0.1,
|
||||
"4",
|
||||
73,
|
||||
10.316541095890413,
|
||||
19.333333333333336,
|
||||
19.333333333333336,
|
||||
),
|
||||
"pe": (
|
||||
13.52416666666667,
|
||||
0.49999999999999994,
|
||||
"13",
|
||||
73,
|
||||
11.657102668465479,
|
||||
16.853208080808084,
|
||||
16.853208080808084,
|
||||
),
|
||||
"technique": (
|
||||
13.835576923076923,
|
||||
0.4333333333333333,
|
||||
"11",
|
||||
73,
|
||||
12.086685508009952,
|
||||
17.25909420289855,
|
||||
17.25909420289855,
|
||||
),
|
||||
"mathematiques": (
|
||||
None,
|
||||
None,
|
||||
"55 ex",
|
||||
73,
|
||||
7.705091805555555,
|
||||
19.8,
|
||||
19.8,
|
||||
),
|
||||
"dut": (
|
||||
14.425416666666665,
|
||||
1.0,
|
||||
"12",
|
||||
73,
|
||||
13.188168241098825,
|
||||
16.612613522048612,
|
||||
16.612613522048612,
|
||||
),
|
||||
},
|
||||
},
|
||||
"nbSemestres": 4,
|
||||
"code_nip": "21414563",
|
||||
"prenom": "Baptiste",
|
||||
"age": "21",
|
||||
"lycee": "PONCET",
|
||||
"3S": {
|
||||
"groupe": {
|
||||
"informatique": (
|
||||
14.559423076923077,
|
||||
0.43333333333333335,
|
||||
"3",
|
||||
19,
|
||||
11.137856275303646,
|
||||
18.8095,
|
||||
18.8095,
|
||||
),
|
||||
"pe": (
|
||||
12.84815019664546,
|
||||
3.25,
|
||||
"4",
|
||||
19,
|
||||
11.795678015751701,
|
||||
15.657624449801428,
|
||||
15.657624449801428,
|
||||
),
|
||||
"technique": (
|
||||
13.860638395358142,
|
||||
1.9833333333333334,
|
||||
"3",
|
||||
19,
|
||||
12.395950358235925,
|
||||
17.340302131732695,
|
||||
17.340302131732695,
|
||||
),
|
||||
"mathematiques": (
|
||||
11.494044444444445,
|
||||
0.3,
|
||||
"6",
|
||||
19,
|
||||
9.771571754385965,
|
||||
14.405358333333334,
|
||||
14.405358333333334,
|
||||
),
|
||||
"dut": (
|
||||
13.207217657917942,
|
||||
3.0,
|
||||
"4",
|
||||
19,
|
||||
12.221677199297439,
|
||||
15.953012966561774,
|
||||
15.953012966561774,
|
||||
),
|
||||
},
|
||||
"promo": {
|
||||
"informatique": (15.5, 0.05, "13", 73, 10.52222222222222, 20.0, 20.0),
|
||||
"pe": (
|
||||
13.308035483870967,
|
||||
1.0333333333333334,
|
||||
"17",
|
||||
73,
|
||||
11.854843423685786,
|
||||
16.191317607526884,
|
||||
16.191317607526884,
|
||||
),
|
||||
"technique": (
|
||||
14.041625757575758,
|
||||
0.7333333333333333,
|
||||
"10",
|
||||
73,
|
||||
11.929466899200335,
|
||||
16.6400384469697,
|
||||
16.6400384469697,
|
||||
),
|
||||
"mathematiques": (
|
||||
11.0625,
|
||||
0.06666666666666667,
|
||||
"40",
|
||||
73,
|
||||
11.418430205479451,
|
||||
19.53,
|
||||
19.53,
|
||||
),
|
||||
"dut": (
|
||||
13.640477936507937,
|
||||
1.0,
|
||||
"14",
|
||||
73,
|
||||
12.097377866597594,
|
||||
16.97088994741667,
|
||||
16.97088994741667,
|
||||
),
|
||||
},
|
||||
},
|
||||
"bac": "STI2D",
|
||||
"S3": {
|
||||
"groupe": {
|
||||
"informatique": (15.5, 0.05, "5", 19, 12.842105263157896, 20.0, 20.0),
|
||||
"pe": (
|
||||
13.308035483870967,
|
||||
1.0333333333333334,
|
||||
"8",
|
||||
19,
|
||||
12.339608902093943,
|
||||
15.967147311827956,
|
||||
15.967147311827956,
|
||||
),
|
||||
"technique": (
|
||||
14.041625757575758,
|
||||
0.7333333333333333,
|
||||
"7",
|
||||
19,
|
||||
13.128539816586922,
|
||||
16.44310151515152,
|
||||
16.44310151515152,
|
||||
),
|
||||
"mathematiques": (
|
||||
11.0625,
|
||||
0.06666666666666667,
|
||||
"6",
|
||||
19,
|
||||
9.280921052631578,
|
||||
16.125,
|
||||
16.125,
|
||||
),
|
||||
"dut": (
|
||||
13.640477936507937,
|
||||
1.0,
|
||||
"8",
|
||||
19,
|
||||
12.83638061385213,
|
||||
15.881845873015871,
|
||||
15.881845873015871,
|
||||
),
|
||||
},
|
||||
"promo": {
|
||||
"informatique": (15.5, 0.05, "13", 73, 10.52222222222222, 20.0, 20.0),
|
||||
"pe": (
|
||||
13.308035483870967,
|
||||
1.0333333333333334,
|
||||
"17",
|
||||
73,
|
||||
11.854843423685786,
|
||||
16.191317607526884,
|
||||
16.191317607526884,
|
||||
),
|
||||
"technique": (
|
||||
14.041625757575758,
|
||||
0.7333333333333333,
|
||||
"10",
|
||||
73,
|
||||
11.929466899200335,
|
||||
16.6400384469697,
|
||||
16.6400384469697,
|
||||
),
|
||||
"mathematiques": (
|
||||
11.0625,
|
||||
0.06666666666666667,
|
||||
"40",
|
||||
73,
|
||||
11.418430205479451,
|
||||
19.53,
|
||||
19.53,
|
||||
),
|
||||
"dut": (
|
||||
13.640477936507937,
|
||||
1.0,
|
||||
"14",
|
||||
73,
|
||||
12.097377866597594,
|
||||
16.97088994741667,
|
||||
16.97088994741667,
|
||||
),
|
||||
},
|
||||
},
|
||||
"parcours": [
|
||||
{
|
||||
"nom_semestre_dans_parcours": "semestre 4 FAP 2016",
|
||||
"titreannee": "DUT RT UFA (PPN 2013), semestre 4 FAP 2016",
|
||||
},
|
||||
{
|
||||
"nom_semestre_dans_parcours": "semestre 3 FAP 2015-2016",
|
||||
"titreannee": "DUT RT UFA (PPN 2013), semestre 3 FAP 2015-2016",
|
||||
},
|
||||
{
|
||||
"nom_semestre_dans_parcours": "semestre 2 FI 2015",
|
||||
"titreannee": "DUT RT, semestre 2 FI 2015",
|
||||
},
|
||||
{
|
||||
"nom_semestre_dans_parcours": "semestre 1 FI 2014-2015",
|
||||
"titreannee": "DUT RT, semestre 1 FI 2014-2015",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
@ -35,148 +35,70 @@
|
||||
|
||||
"""
|
||||
|
||||
from flask import send_file, request
|
||||
from flask import flash, g, redirect, render_template, request, send_file, url_for
|
||||
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import FormSemestre
|
||||
from app.pe import pe_comp
|
||||
from app.pe import pe_jury
|
||||
from app.views import ScoData
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
from app.pe import pe_tools
|
||||
from app.pe import pe_jurype
|
||||
from app.pe import pe_avislatex
|
||||
from app.views import notes_bp as bp
|
||||
|
||||
|
||||
def _pe_view_sem_recap_form(formsemestre_id):
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Avis de poursuite d'études"),
|
||||
f"""<h2 class="formsemestre">Génération des avis de poursuites d'études</h2>
|
||||
<p class="help">
|
||||
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
|
||||
poursuites d'études.
|
||||
<br>
|
||||
De nombreux aspects sont paramétrables:
|
||||
<a href="https://scodoc.org/AvisPoursuiteEtudes"
|
||||
target="_blank" rel="noopener noreferrer">
|
||||
voir la documentation
|
||||
</a>.
|
||||
</p>
|
||||
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form"
|
||||
enctype="multipart/form-data">
|
||||
<div class="pe_template_up">
|
||||
Les templates sont généralement installés sur le serveur ou dans le
|
||||
paramétrage de ScoDoc.
|
||||
<br>
|
||||
Au besoin, vous pouvez spécifier ici votre propre fichier de template
|
||||
(<tt>un_avis.tex</tt>):
|
||||
<div class="pe_template_upb">Template:
|
||||
<input type="file" size="30" name="avis_tmpl_file"/>
|
||||
</div>
|
||||
<div class="pe_template_upb">Pied de page:
|
||||
<input type="file" size="30" name="footer_tmpl_file"/>
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit" value="Générer les documents"/>
|
||||
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}">
|
||||
</form>
|
||||
""",
|
||||
]
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
||||
# called from the web, POST or GET
|
||||
def pe_view_sem_recap(
|
||||
formsemestre_id,
|
||||
avis_tmpl_file=None,
|
||||
footer_tmpl_file=None,
|
||||
):
|
||||
@bp.route("/pe_view_sem_recap/<int:formsemestre_id>", methods=("GET", "POST"))
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def pe_view_sem_recap(formsemestre_id: int):
|
||||
"""Génération des avis de poursuite d'étude"""
|
||||
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if not formsemestre.formation.is_apc():
|
||||
raise ScoValueError(
|
||||
"""Le module de Poursuites d'Etudes
|
||||
n'est disponible que pour des formations BUT"""
|
||||
)
|
||||
|
||||
if formsemestre.formation.get_cursus().NB_SEM < 6:
|
||||
raise ScoValueError(
|
||||
"""Le module de Poursuites d'Etudes n'est pas prévu
|
||||
pour une formation de moins de 6 semestres"""
|
||||
)
|
||||
# L'année du diplome
|
||||
annee_diplome = pe_comp.get_annee_diplome_semestre(formsemestre)
|
||||
|
||||
# Cosemestres diplomants
|
||||
cosemestres = pe_comp.get_cosemestres_diplomants(annee_diplome)
|
||||
|
||||
if request.method == "GET":
|
||||
return _pe_view_sem_recap_form(formsemestre_id)
|
||||
prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id)
|
||||
|
||||
semBase = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
|
||||
jury = pe_jurype.JuryPE(semBase)
|
||||
# Ajout avis LaTeX au même zip:
|
||||
etudids = list(jury.syntheseJury.keys())
|
||||
|
||||
# Récupération du template latex, du footer latex et du tag identifiant les annotations relatives aux PE
|
||||
# (chaines unicodes, html non quoté)
|
||||
template_latex = ""
|
||||
# template fourni via le formulaire Web
|
||||
if avis_tmpl_file:
|
||||
try:
|
||||
template_latex = avis_tmpl_file.read().decode("utf-8")
|
||||
except UnicodeDecodeError as e:
|
||||
raise ScoValueError(
|
||||
"Données (template) invalides (caractères non UTF8 ?)"
|
||||
) from e
|
||||
else:
|
||||
# template indiqué dans préférences ScoDoc ?
|
||||
template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
|
||||
formsemestre_id, champ="pe_avis_latex_tmpl"
|
||||
return render_template(
|
||||
"pe/pe_view_sem_recap.j2",
|
||||
annee_diplome=annee_diplome,
|
||||
formsemestre=formsemestre,
|
||||
sco=ScoData(formsemestre=formsemestre),
|
||||
cosemestres=cosemestres,
|
||||
)
|
||||
|
||||
template_latex = template_latex.strip()
|
||||
if not template_latex:
|
||||
# pas de preference pour le template: utilise fichier du serveur
|
||||
template_latex = pe_avislatex.get_templates_from_distrib("avis")
|
||||
|
||||
# Footer:
|
||||
footer_latex = ""
|
||||
# template fourni via le formulaire Web
|
||||
if footer_tmpl_file:
|
||||
footer_latex = footer_tmpl_file.read().decode("utf-8")
|
||||
else:
|
||||
footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
|
||||
formsemestre_id, champ="pe_avis_latex_footer"
|
||||
)
|
||||
footer_latex = footer_latex.strip()
|
||||
if not footer_latex:
|
||||
# pas de preference pour le footer: utilise fichier du serveur
|
||||
footer_latex = pe_avislatex.get_templates_from_distrib(
|
||||
"footer"
|
||||
) # fallback: footer vides
|
||||
|
||||
tag_annotation_pe = pe_avislatex.get_code_latex_from_scodoc_preference(
|
||||
formsemestre_id, champ="pe_tag_annotation_avis_latex"
|
||||
)
|
||||
|
||||
# Ajout des annotations PE dans un fichier excel
|
||||
sT = pe_avislatex.table_syntheseAnnotationPE(jury.syntheseJury, tag_annotation_pe)
|
||||
if sT:
|
||||
jury.add_file_to_zip(
|
||||
jury.NOM_EXPORT_ZIP + "_annotationsPE" + scu.XLSX_SUFFIX, sT.excel()
|
||||
# request.method == "POST"
|
||||
jury = pe_jury.JuryPE(annee_diplome)
|
||||
if not jury.diplomes_ids:
|
||||
flash("aucun étudiant à considérer !")
|
||||
return redirect(
|
||||
url_for(
|
||||
"notes.pe_view_sem_recap",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
)
|
||||
|
||||
latex_pages = {} # Dictionnaire de la forme nom_fichier => contenu_latex
|
||||
for etudid in etudids:
|
||||
[nom_fichier, contenu_latex] = pe_avislatex.get_avis_poursuite_par_etudiant(
|
||||
jury,
|
||||
etudid,
|
||||
template_latex,
|
||||
tag_annotation_pe,
|
||||
footer_latex,
|
||||
prefs,
|
||||
)
|
||||
jury.add_file_to_zip("avis/" + nom_fichier + ".tex", contenu_latex)
|
||||
latex_pages[nom_fichier] = contenu_latex # Sauvegarde dans un dico
|
||||
|
||||
# Nouvelle version : 1 fichier par étudiant avec 1 fichier appelant créée ci-dessous
|
||||
doc_latex = "\n% -----\n".join(
|
||||
["\\include{" + nom + "}" for nom in sorted(latex_pages.keys())]
|
||||
)
|
||||
jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex)
|
||||
|
||||
# Ajoute image, LaTeX class file(s) and modeles
|
||||
pe_tools.add_pe_stuff_to_zip(jury.zipfile, jury.NOM_EXPORT_ZIP)
|
||||
data = jury.get_zipped_data()
|
||||
|
||||
return send_file(
|
||||
data,
|
||||
mimetype="application/zip",
|
||||
download_name=scu.sanitize_filename(jury.NOM_EXPORT_ZIP + ".zip"),
|
||||
download_name=scu.sanitize_filename(jury.nom_export_zip + ".zip"),
|
||||
as_attachment=True,
|
||||
)
|
||||
|
@ -396,7 +396,7 @@ class TF(object):
|
||||
self.values[field] = int(self.values[field])
|
||||
except ValueError:
|
||||
msg.append(
|
||||
f"valeur invalide ({self.values[field]}) pour le champs {field}"
|
||||
f"valeur invalide ({self.values[field]}) pour le champ {field}"
|
||||
)
|
||||
ok = False
|
||||
elif typ == "float" or typ == "real":
|
||||
@ -404,7 +404,7 @@ class TF(object):
|
||||
self.values[field] = float(self.values[field].replace(",", "."))
|
||||
except ValueError:
|
||||
msg.append(
|
||||
f"valeur invalide ({self.values[field]}) pour le champs {field}"
|
||||
f"valeur invalide ({self.values[field]}) pour le champ {field}"
|
||||
)
|
||||
ok = False
|
||||
if ok:
|
||||
@ -685,6 +685,11 @@ class TF(object):
|
||||
'<input type="text" name="%s" size="10" value="%s" class="datepicker">'
|
||||
% (field, values[field])
|
||||
)
|
||||
elif input_type == "time": # JavaScript widget for date input
|
||||
lem.append(
|
||||
f"""<input type="text" name="{field}" maxlength="5" size="5" value="{
|
||||
values[field]}" class="timepicker">"""
|
||||
)
|
||||
elif input_type == "text_suggest":
|
||||
lem.append(
|
||||
'<input type="text" name="%s" id="%s" size="%d" %s'
|
||||
|
@ -145,7 +145,9 @@ def sco_header(
|
||||
etudid=None,
|
||||
formsemestre_id=None,
|
||||
):
|
||||
"Main HTML page header for ScoDoc"
|
||||
"""Main HTML page header for ScoDoc
|
||||
Utilisé dans les anciennes pages. Les nouvelles pages utilisent le template Jinja.
|
||||
"""
|
||||
from app.scodoc.sco_formsemestre_status import formsemestre_page_title
|
||||
|
||||
if etudid is not None:
|
||||
@ -189,7 +191,12 @@ def sco_header(
|
||||
# jQuery UI
|
||||
# can modify loaded theme here
|
||||
H.append(
|
||||
f'<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n'
|
||||
f"""
|
||||
<link type="text/css" rel="stylesheet"
|
||||
href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
|
||||
<link type="text/css" rel="stylesheet"
|
||||
href="{scu.STATIC_DIR}/libjs/timepicker-1.3.5/jquery.timepicker.min.css" />
|
||||
"""
|
||||
)
|
||||
if init_google_maps:
|
||||
# It may be necessary to add an API key:
|
||||
@ -219,19 +226,26 @@ def sco_header(
|
||||
|
||||
# jQuery
|
||||
H.append(
|
||||
f"""<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>"""
|
||||
f"""
|
||||
<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>
|
||||
"""
|
||||
)
|
||||
# qTip
|
||||
if init_qtip:
|
||||
H.append(
|
||||
f"""<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
|
||||
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />"""
|
||||
<link type="text/css" rel="stylesheet"
|
||||
href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />
|
||||
"""
|
||||
)
|
||||
|
||||
H.append(
|
||||
f"""<script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/js/scodoc.js"></script>"""
|
||||
f"""<script
|
||||
src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/js/scodoc.js"></script>
|
||||
"""
|
||||
)
|
||||
if init_google_maps:
|
||||
H.append(
|
||||
|
@ -32,12 +32,68 @@ from flask import render_template, url_for
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app.models import Evaluation, GroupDescr, Identite, ModuleImpl, Partition
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from sco_version import SCOVERSION
|
||||
|
||||
|
||||
def retreive_formsemestre_from_request() -> int:
|
||||
"""Cherche si on a de quoi déduire le semestre affiché à partir des
|
||||
arguments de la requête:
|
||||
formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
|
||||
Returns None si pas défini.
|
||||
"""
|
||||
if request.method == "GET":
|
||||
args = request.args
|
||||
elif request.method == "POST":
|
||||
args = request.form
|
||||
else:
|
||||
return None
|
||||
formsemestre_id = None
|
||||
# Search formsemestre
|
||||
group_ids = args.get("group_ids", [])
|
||||
if "formsemestre_id" in args:
|
||||
formsemestre_id = args["formsemestre_id"]
|
||||
elif "moduleimpl_id" in args and args["moduleimpl_id"]:
|
||||
modimpl = db.session.get(ModuleImpl, args["moduleimpl_id"])
|
||||
if not modimpl:
|
||||
return None # suppressed ?
|
||||
formsemestre_id = modimpl.formsemestre_id
|
||||
elif "evaluation_id" in args:
|
||||
evaluation = db.session.get(Evaluation, args["evaluation_id"])
|
||||
if not evaluation:
|
||||
return None # evaluation suppressed ?
|
||||
formsemestre_id = evaluation.moduleimpl.formsemestre_id
|
||||
elif "group_id" in args:
|
||||
group = db.session.get(GroupDescr, args["group_id"])
|
||||
if not group:
|
||||
return None
|
||||
formsemestre_id = group.partition.formsemestre_id
|
||||
elif group_ids:
|
||||
if isinstance(group_ids, str):
|
||||
group_ids = group_ids.split(",")
|
||||
group_id = group_ids[0]
|
||||
group = db.session.get(GroupDescr, group_id)
|
||||
if not group:
|
||||
return None
|
||||
formsemestre_id = group.partition.formsemestre_id
|
||||
elif "partition_id" in args:
|
||||
partition = db.session.get(Partition, args["partition_id"])
|
||||
if not partition:
|
||||
return None
|
||||
formsemestre_id = partition.formsemestre_id
|
||||
|
||||
if formsemestre_id is None:
|
||||
return None # no current formsemestre
|
||||
try:
|
||||
return int(formsemestre_id)
|
||||
except ValueError:
|
||||
return None # no current formsemestre
|
||||
|
||||
|
||||
def sidebar_common():
|
||||
"partie commune à toutes les sidebar"
|
||||
home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept)
|
||||
@ -104,46 +160,58 @@ def sidebar(etudid: int = None):
|
||||
etudid = request.form.get("etudid", None)
|
||||
|
||||
if etudid is not None:
|
||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||
params.update(etud)
|
||||
params["fiche_url"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
)
|
||||
etud = Identite.get_etud(etudid)
|
||||
# compte les absences du semestre en cours
|
||||
H.append(
|
||||
"""<h2 id="insidebar-etud"><a href="%(fiche_url)s" class="sidebar">
|
||||
<font color="#FF0000">%(civilite_str)s %(nom_disp)s</font></a>
|
||||
</h2>
|
||||
<b>Absences</b>"""
|
||||
% params
|
||||
f"""<h2 id="insidebar-etud"><a href="{
|
||||
url_for(
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
)
|
||||
}" class="sidebar">
|
||||
<font color="#FF0000">{etud.civilite_str} {etud.nom_disp()}</font></a>
|
||||
</h2>
|
||||
<b>Absences</b>"""
|
||||
)
|
||||
if etud["cursem"]:
|
||||
cur_sem = etud["cursem"]
|
||||
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, cur_sem)
|
||||
inscription = etud.inscription_courante()
|
||||
if inscription:
|
||||
formsemestre = inscription.formsemestre
|
||||
nbabs, nbabsjust = sco_assiduites.formsemestre_get_assiduites_count(
|
||||
etudid, formsemestre
|
||||
)
|
||||
nbabsnj = nbabs - nbabsjust
|
||||
H.append(
|
||||
f"""<span title="absences du { cur_sem["date_debut"] } au {
|
||||
cur_sem["date_fin"] }">({
|
||||
sco_preferences.get_preference("assi_metrique", None)})
|
||||
f"""<span title="absences du {
|
||||
formsemestre.date_debut.strftime("%d/%m/%Y")
|
||||
} au {
|
||||
formsemestre.date_fin.strftime("%d/%m/%Y")
|
||||
}">({
|
||||
sco_preferences.get_preference("assi_metrique", None)})
|
||||
<br>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
|
||||
)
|
||||
H.append("<ul>")
|
||||
if current_user.has_permission(Permission.AbsChange):
|
||||
# essaie de conserver le semestre actuellement en vue
|
||||
cur_formsemestre_id = retreive_formsemestre_from_request()
|
||||
H.append(
|
||||
f"""
|
||||
<li><a href="{ url_for('assiduites.ajout_assiduite_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Ajouter</a></li>
|
||||
<li><a href="{ url_for('assiduites.ajout_justificatif_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Justifier</a></li>
|
||||
<li><a href="{
|
||||
url_for('assiduites.ajout_assiduite_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Ajouter</a></li>
|
||||
<li><a href="{
|
||||
url_for('assiduites.ajout_justificatif_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid,
|
||||
formsemestre_id=cur_formsemestre_id,
|
||||
)
|
||||
}">Justifier</a></li>
|
||||
"""
|
||||
)
|
||||
if sco_preferences.get_preference("handle_billets_abs"):
|
||||
H.append(
|
||||
f"""<li><a href="{ url_for('absences.billets_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Billets</a></li>"""
|
||||
f"""<li><a href="{
|
||||
url_for('absences.billets_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Billets</a></li>"""
|
||||
)
|
||||
H.append(
|
||||
f"""
|
||||
|
@ -265,7 +265,7 @@ def DBUpdateArgs(cnx, table, vals, where=None, commit=False, convert_empty_to_nu
|
||||
# log('vals=%s\n'%vals)
|
||||
except psycopg2.errors.StringDataRightTruncation as exc:
|
||||
cnx.rollback()
|
||||
raise ScoValueError("champs de texte trop long !") from exc
|
||||
raise ScoValueError("champ de texte trop long !") from exc
|
||||
except:
|
||||
cnx.rollback() # get rid of this transaction
|
||||
log('Exception in DBUpdateArgs:\n\treq="%s"\n\tvals="%s"\n' % (req, vals))
|
||||
@ -300,6 +300,7 @@ class EditableTable(object):
|
||||
output_formators={},
|
||||
input_formators={},
|
||||
aux_tables=[],
|
||||
convert_empty_to_nulls=True, # les arguments vides sont traduits en NULL
|
||||
convert_null_outputs_to_empty=True,
|
||||
html_quote=False, # changed in 9.0.10
|
||||
fields_creators={}, # { field : [ sql_command_to_create_it ] }
|
||||
@ -321,6 +322,7 @@ class EditableTable(object):
|
||||
self.output_formators = output_formators
|
||||
self.input_formators = input_formators
|
||||
self.convert_null_outputs_to_empty = convert_null_outputs_to_empty
|
||||
self.convert_empty_to_nulls = convert_empty_to_nulls
|
||||
self.html_quote = html_quote
|
||||
self.fields_creators = fields_creators
|
||||
self.filter_nulls = filter_nulls
|
||||
@ -351,6 +353,7 @@ class EditableTable(object):
|
||||
self.table_name,
|
||||
vals,
|
||||
commit=True,
|
||||
convert_empty_to_nulls=self.convert_empty_to_nulls,
|
||||
return_id=(self.id_name is not None),
|
||||
ignore_conflicts=self.insert_ignore_conflicts,
|
||||
)
|
||||
@ -444,7 +447,7 @@ def dictfilter(d, fields, filter_nulls=True):
|
||||
"""returns a copy of d with only keys listed in "fields" and non null values"""
|
||||
r = {}
|
||||
for f in fields:
|
||||
if f in d and (d[f] != None or not filter_nulls):
|
||||
if f in d and (d[f] is not None or not filter_nulls):
|
||||
try:
|
||||
val = d[f].strip()
|
||||
except:
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -129,7 +129,7 @@ def table_billets(
|
||||
] = f'id="{billet.etudiant.id}" class="etudinfo"'
|
||||
if with_links:
|
||||
billet_dict["_nomprenom_target"] = url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=billet_dict["etudid"],
|
||||
)
|
||||
|
@ -34,7 +34,7 @@ Il suffit d'appeler abs_notify() après chaque ajout d'absence.
|
||||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
from flask import current_app, g, url_for
|
||||
from flask import g, url_for
|
||||
from flask_mail import Message
|
||||
|
||||
from app import db
|
||||
@ -42,6 +42,7 @@ from app import email
|
||||
from app import log
|
||||
from app.auth.models import User
|
||||
from app.models.absences import AbsenceNotification
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.events import Scolog
|
||||
from app.models.formsemestre import FormSemestre
|
||||
import app.scodoc.notesdb as ndb
|
||||
@ -108,7 +109,6 @@ def do_abs_notify(
|
||||
return # abort
|
||||
|
||||
# Vérification fréquence (pour ne pas envoyer de mails trop souvent)
|
||||
# TODO Mettre la fréquence dans les préférences assiduités
|
||||
abs_notify_max_freq = sco_preferences.get_preference("abs_notify_max_freq")
|
||||
destinations_filtered = []
|
||||
for email_addr in destinations:
|
||||
@ -175,9 +175,15 @@ def abs_notify_get_destinations(
|
||||
if prefs["abs_notify_email"]:
|
||||
destinations.append(prefs["abs_notify_email"])
|
||||
if prefs["abs_notify_etud"]:
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
if etud["email_default"]:
|
||||
destinations.append(etud["email_default"])
|
||||
etud = Identite.get_etud(etudid)
|
||||
adresse = etud.adresses.first()
|
||||
if adresse:
|
||||
# Mail à utiliser pour les envois vers l'étudiant:
|
||||
# choix qui pourrait être controlé par une preference
|
||||
# ici priorité au mail institutionnel:
|
||||
email_default = adresse.email or adresse.emailperso
|
||||
if email_default:
|
||||
destinations.append(email_default)
|
||||
|
||||
# Notification (à chaque fois) des resp. de modules ayant des évaluations
|
||||
# à cette date
|
||||
@ -262,16 +268,18 @@ def abs_notification_message(
|
||||
"""
|
||||
from app.scodoc import sco_bulletins
|
||||
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
etud = Identite.get_etud(etudid)
|
||||
|
||||
# Variables accessibles dans les balises du template: %(nom_variable)s :
|
||||
values = sco_bulletins.make_context_dict(formsemestre, etud)
|
||||
values = sco_bulletins.make_context_dict(
|
||||
formsemestre, etud.to_dict_scodoc7(with_inscriptions=True)
|
||||
)
|
||||
|
||||
values["nbabs"] = nbabs
|
||||
values["nbabsjust"] = nbabsjust
|
||||
values["nbabsnonjust"] = nbabs - nbabsjust
|
||||
values["url_ficheetud"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid, _external=True
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid, _external=True
|
||||
)
|
||||
|
||||
template = prefs["abs_notification_mail_tmpl"]
|
||||
@ -281,7 +289,7 @@ def abs_notification_message(
|
||||
log("abs_notification_message: empty template, not sending message")
|
||||
return None
|
||||
|
||||
subject = f"""[ScoDoc] Trop d'absences pour {etud["nomprenom"]}"""
|
||||
subject = f"""[ScoDoc] Trop d'absences pour {etud.nomprenom}"""
|
||||
msg = Message(subject, sender=email.get_from_addr(formsemestre.departement.acronym))
|
||||
msg.body = txt
|
||||
return msg
|
||||
|
@ -62,7 +62,7 @@ def can_edit_etud_archive(authuser):
|
||||
|
||||
|
||||
def etud_list_archives_html(etud: Identite):
|
||||
"""HTML snippet listing archives"""
|
||||
"""HTML snippet listing archives."""
|
||||
can_edit = can_edit_etud_archive(current_user)
|
||||
etud_archive_id = etud.id
|
||||
L = []
|
||||
@ -138,21 +138,18 @@ def etud_upload_file_form(etudid):
|
||||
"""Page with a form to choose and upload a file, with a description."""
|
||||
# check permission
|
||||
if not can_edit_etud_archive(current_user):
|
||||
raise AccessDenied("opération non autorisée pour %s" % current_user)
|
||||
etuds = sco_etud.get_etud_info(filled=True)
|
||||
if not etuds:
|
||||
raise ScoValueError("étudiant inexistant")
|
||||
etud = etuds[0]
|
||||
raise AccessDenied(f"opération non autorisée pour {current_user}")
|
||||
etud = Identite.get_etud(etudid)
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Chargement d'un document associé à %(nomprenom)s" % etud,
|
||||
page_title=f"Chargement d'un document associé à {etud.nomprenom}",
|
||||
),
|
||||
"""<h2>Chargement d'un document associé à %(nomprenom)s</h2>
|
||||
"""
|
||||
% etud,
|
||||
"""<p>Le fichier ne doit pas dépasser %sMo.</p>
|
||||
"""
|
||||
% (scu.CONFIG.ETUD_MAX_FILE_SIZE // (1024 * 1024)),
|
||||
f"""<h2>Chargement d'un document associé à {etud.nomprenom}</h2>
|
||||
|
||||
<p>Le fichier ne doit pas dépasser {
|
||||
scu.CONFIG.ETUD_MAX_FILE_SIZE // (1024 * 1024)}Mo.</p>
|
||||
""",
|
||||
]
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
@ -176,20 +173,12 @@ def etud_upload_file_form(etudid):
|
||||
if tf[0] == 0:
|
||||
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
else:
|
||||
data = tf[2]["datafile"].read()
|
||||
descr = tf[2]["description"]
|
||||
filename = tf[2]["datafile"].filename
|
||||
etud_archive_id = etud["etudid"]
|
||||
_store_etud_file_to_new_archive(
|
||||
etud_archive_id, data, filename, description=descr
|
||||
)
|
||||
return flask.redirect(
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
return flask.redirect(etud.url_fiche())
|
||||
data = tf[2]["datafile"].read()
|
||||
descr = tf[2]["description"]
|
||||
filename = tf[2]["datafile"].filename
|
||||
_store_etud_file_to_new_archive(etudid, data, filename, description=descr)
|
||||
return flask.redirect(etud.url_fiche())
|
||||
|
||||
|
||||
def _store_etud_file_to_new_archive(
|
||||
@ -209,26 +198,23 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
|
||||
# check permission
|
||||
if not can_edit_etud_archive(current_user):
|
||||
raise AccessDenied(f"opération non autorisée pour {current_user}")
|
||||
etuds = sco_etud.get_etud_info(filled=True)
|
||||
if not etuds:
|
||||
raise ScoValueError("étudiant inexistant")
|
||||
etud = etuds[0]
|
||||
etud_archive_id = etud["etudid"]
|
||||
etud = Identite.get_etud(etudid)
|
||||
etud_archive_id = etudid
|
||||
archive_id = ETUDS_ARCHIVER.get_id_from_name(
|
||||
etud_archive_id, archive_name, dept_id=etud["dept_id"]
|
||||
etud_archive_id, archive_name, dept_id=etud.dept_id
|
||||
)
|
||||
if not dialog_confirmed:
|
||||
return scu.confirm_dialog(
|
||||
"""<h2>Confirmer la suppression des fichiers ?</h2>
|
||||
<p>Fichier associé le %s à l'étudiant %s</p>
|
||||
<p>La suppression sera définitive.</p>"""
|
||||
% (
|
||||
ETUDS_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"),
|
||||
etud["nomprenom"],
|
||||
),
|
||||
f"""<h2>Confirmer la suppression des fichiers ?</h2>
|
||||
<p>Fichier associé le {
|
||||
ETUDS_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M")
|
||||
} à l'étudiant {etud.nomprenom}
|
||||
</p>
|
||||
<p>La suppression sera définitive.</p>
|
||||
""",
|
||||
dest_url="",
|
||||
cancel_url=url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etudid,
|
||||
head_message="annulation",
|
||||
@ -236,22 +222,17 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
|
||||
parameters={"etudid": etudid, "archive_name": archive_name},
|
||||
)
|
||||
|
||||
ETUDS_ARCHIVER.delete_archive(archive_id, dept_id=etud["dept_id"])
|
||||
ETUDS_ARCHIVER.delete_archive(archive_id, dept_id=etud.dept_id)
|
||||
flash("Archive supprimée")
|
||||
return flask.redirect(
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
return flask.redirect(etud.url_fiche())
|
||||
|
||||
|
||||
def etud_get_archived_file(etudid, archive_name, filename):
|
||||
"""Send file to client."""
|
||||
etuds = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
||||
if not etuds:
|
||||
raise ScoValueError("étudiant inexistant")
|
||||
etud = etuds[0]
|
||||
etud_archive_id = etud["etudid"]
|
||||
etud = Identite.get_etud(etudid)
|
||||
etud_archive_id = etud.id
|
||||
return ETUDS_ARCHIVER.get_archived_file(
|
||||
etud_archive_id, archive_name, filename, dept_id=etud["dept_id"]
|
||||
etud_archive_id, archive_name, filename, dept_id=etud.dept_id
|
||||
)
|
||||
|
||||
|
||||
|
@ -17,7 +17,15 @@ from app import log
|
||||
|
||||
class Trace:
|
||||
"""gestionnaire de la trace des fichiers justificatifs
|
||||
XXX TODO à documenter: rôle et format des fichier strace
|
||||
|
||||
Role des fichiers traces :
|
||||
- Sauvegarder la date de dépôt du fichier
|
||||
- Sauvegarder la date de suppression du fichier (dans le cas de plusieurs fichiers pour un même justif)
|
||||
- Sauvegarder l'user_id de l'utilisateur ayant déposé le fichier (=> permet de montrer les fichiers qu'aux personnes qui l'on déposé / qui ont le rôle AssiJustifView)
|
||||
|
||||
_trace.csv :
|
||||
nom_fichier_srv,datetime_depot,datetime_suppr,user_id
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, path: str) -> None:
|
||||
@ -39,7 +47,7 @@ class Trace:
|
||||
continue
|
||||
entry_date: datetime = is_iso_formated(csv[1], True)
|
||||
delete_date: datetime = is_iso_formated(csv[2], True)
|
||||
user_id = csv[3]
|
||||
user_id = csv[3].strip()
|
||||
self.content[fname] = [entry_date, delete_date, user_id]
|
||||
|
||||
if os.path.isfile(self.path):
|
||||
@ -84,7 +92,14 @@ class Trace:
|
||||
self, fnames: list[str] = None
|
||||
) -> dict[str, list[datetime, datetime, str]]:
|
||||
"""Récupère la trace pour les noms de fichiers.
|
||||
si aucun nom n'est donné, récupère tous les fichiers"""
|
||||
si aucun nom n'est donné, récupère tous les fichiers
|
||||
|
||||
retour :
|
||||
{
|
||||
"nom_fichier_srv": [datetime_depot, datetime_suppr/None, user_id],
|
||||
...
|
||||
}
|
||||
"""
|
||||
|
||||
if fnames is None:
|
||||
return self.content
|
||||
@ -101,7 +116,7 @@ class JustificatifArchiver(BaseArchiver):
|
||||
|
||||
TOTALK:
|
||||
- oid -> etudid
|
||||
- archive_id -> date de création de l'archive (une archive par dépot de document)
|
||||
- archive_id -> date de création de l'archive (une archive par dépôt de document)
|
||||
|
||||
justificatif
|
||||
└── <dept_id>
|
||||
@ -215,8 +230,7 @@ class JustificatifArchiver(BaseArchiver):
|
||||
filenames = self.list_archive(archive_id, dept_id=etud.dept_id)
|
||||
trace: Trace = Trace(archive_id)
|
||||
traced = trace.get_trace(filenames)
|
||||
|
||||
return [(key, value[2]) for key, value in traced.items()]
|
||||
return [(key, value[2]) for key, value in traced.items() if value is not None]
|
||||
|
||||
def get_justificatif_file(self, archive_name: str, etud: Identite, filename: str):
|
||||
"""
|
||||
|
@ -1,22 +1,28 @@
|
||||
"""
|
||||
Ecrit par Matthias Hartmann.
|
||||
"""
|
||||
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from pytz import UTC
|
||||
|
||||
from flask import g
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import log, db
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log, db, set_sco_dept
|
||||
from app.models import (
|
||||
Identite,
|
||||
FormSemestre,
|
||||
FormSemestreInscription,
|
||||
ModuleImpl,
|
||||
ModuleImplInscription,
|
||||
ScoDocSiteConfig,
|
||||
)
|
||||
from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_justified
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_etud
|
||||
from app.models import ScoDocSiteConfig
|
||||
from flask import g
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
class CountCalculator:
|
||||
@ -93,9 +99,9 @@ class CountCalculator:
|
||||
evening if evening else ScoDocSiteConfig.get("assi_afternoon_time", "18:00")
|
||||
)
|
||||
|
||||
self.non_work_days: list[
|
||||
scu.NonWorkDays
|
||||
] = scu.NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
||||
self.non_work_days: list[scu.NonWorkDays] = (
|
||||
scu.NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
|
||||
delta_total: timedelta = datetime.combine(
|
||||
date.min, self.evening
|
||||
@ -170,7 +176,7 @@ class CountCalculator:
|
||||
"""Récupère une clé de dictionnaire en fonction de l'état de l'assiduité
|
||||
et si elle est justifié
|
||||
"""
|
||||
keys: dict[EtatAssiduite, str] = {
|
||||
keys: dict[scu.EtatAssiduite, str] = {
|
||||
scu.EtatAssiduite.ABSENT: "absent",
|
||||
scu.EtatAssiduite.RETARD: "retard",
|
||||
scu.EtatAssiduite.PRESENT: "present",
|
||||
@ -188,7 +194,7 @@ class CountCalculator:
|
||||
if assi.etat != scu.EtatAssiduite.PRESENT:
|
||||
_key: str = scu.EtatAssiduite.inverse().get(assi.etat).name.lower()
|
||||
if key not in self.data[_key]["demi"]:
|
||||
self.data[_key]["demi"].append(day)
|
||||
self.data[_key]["demi"].append(key)
|
||||
|
||||
if key not in self.data["total"]["demi"]:
|
||||
self.data["total"]["demi"].append(key)
|
||||
@ -349,6 +355,11 @@ def get_assiduites_stats(
|
||||
assiduites: Query, metric: str = "all", filtered: dict[str, object] = None
|
||||
) -> dict[str, int | float]:
|
||||
"""Compte les assiduités en fonction des filtres"""
|
||||
# XXX TODO-assiduite : documenter !!!
|
||||
# Que sont les filtres ? Quelles valeurs ?
|
||||
# documenter permet de faire moins de bug: qualité du code non satisfaisante.
|
||||
#
|
||||
# + on se perd entre les clés en majuscules et en minuscules. Pourquoi
|
||||
|
||||
if filtered is not None:
|
||||
deb, fin = None, None
|
||||
@ -366,6 +377,10 @@ def get_assiduites_stats(
|
||||
assiduites = filter_by_formsemestre(
|
||||
assiduites, Assiduite, filtered[key]
|
||||
)
|
||||
case "formsemestre_modimpls":
|
||||
assiduites = filter_by_modimpls(
|
||||
assiduites, Assiduite, filtered[key]
|
||||
)
|
||||
case "est_just":
|
||||
assiduites = filter_assiduites_by_est_just(
|
||||
assiduites, filtered[key]
|
||||
@ -390,13 +405,18 @@ def get_assiduites_stats(
|
||||
|
||||
# Récupération des états
|
||||
etats: list[str] = (
|
||||
filtered["etat"].split(",")
|
||||
if "etat" in filtered
|
||||
else ["absent", "present", "retard"]
|
||||
filtered["etat"].split(",") if "etat" in filtered else scu.EtatAssiduite.all()
|
||||
)
|
||||
|
||||
# être sur que les états sont corrects
|
||||
etats = [etat for etat in etats if etat.upper() in scu.EtatAssiduite.all()]
|
||||
|
||||
# Préparation du dictionnaire de retour avec les valeurs du calcul
|
||||
count: dict = calculator.to_dict(only_total=False)
|
||||
for etat in etats:
|
||||
# TODO-assiduite: on se perd entre les lower et upper.
|
||||
# Pourquoi EtatAssiduite est en majuscules si tout le reste est en minuscules ?
|
||||
etat = etat.lower()
|
||||
if etat != "present":
|
||||
output[etat] = count[etat]
|
||||
output[etat]["justifie"] = count[etat + "_just"]
|
||||
@ -448,8 +468,6 @@ def filter_by_date(
|
||||
if date_fin is None:
|
||||
date_fin = datetime.max
|
||||
|
||||
date_deb = scu.localize_datetime(date_deb) # TODO A modifier (timezone ?)
|
||||
date_fin = scu.localize_datetime(date_fin)
|
||||
if not strict:
|
||||
return collection.filter(
|
||||
collection_cls.date_debut <= date_fin, collection_cls.date_fin >= date_deb
|
||||
@ -481,7 +499,9 @@ def filter_by_formsemestre(
|
||||
formsemestre: FormSemestre,
|
||||
) -> Query:
|
||||
"""
|
||||
Filtrage d'une collection en fonction d'un formsemestre
|
||||
Filtrage d'une collection : conserve les élements tels que
|
||||
- l'étudiant est inscrit au formsemestre
|
||||
- et la plage de dates intersecte celle du formsemestre
|
||||
"""
|
||||
|
||||
if formsemestre is None:
|
||||
@ -496,14 +516,46 @@ def filter_by_formsemestre(
|
||||
.filter(FormSemestreInscription.formsemestre_id == formsemestre.id)
|
||||
)
|
||||
|
||||
form_date_debut = formsemestre.date_debut + timedelta(days=1)
|
||||
form_date_fin = formsemestre.date_fin + timedelta(days=1)
|
||||
|
||||
collection_result = collection_result.filter(
|
||||
collection_class.date_debut >= form_date_debut
|
||||
collection_class.date_debut <= formsemestre.date_fin
|
||||
).filter(collection_class.date_fin >= formsemestre.date_debut)
|
||||
|
||||
return collection_result
|
||||
|
||||
|
||||
def filter_by_modimpls(
|
||||
collection_query: Assiduite | Justificatif,
|
||||
collection_class: Assiduite | Justificatif,
|
||||
formsemestre: FormSemestre,
|
||||
) -> Query:
|
||||
"""
|
||||
Filtrage d'une collection d'assiduités: conserve les élements
|
||||
- si l'étudiant est inscrit au formsemestre
|
||||
- Et que l'assiduité concerne un moduleimpl de ce formsemestre
|
||||
|
||||
Ne fait rien sur les justificatifs.
|
||||
Ne fait rien si formsemestre is None
|
||||
"""
|
||||
if (collection_class != Assiduite) or (formsemestre is None):
|
||||
return collection_query
|
||||
|
||||
# restreint aux inscrits:
|
||||
collection_result = (
|
||||
collection_query.join(Identite, collection_class.etudid == Identite.id)
|
||||
.join(
|
||||
FormSemestreInscription,
|
||||
Identite.id == FormSemestreInscription.etudid,
|
||||
)
|
||||
.filter(FormSemestreInscription.formsemestre_id == formsemestre.id)
|
||||
)
|
||||
|
||||
return collection_result.filter(collection_class.date_fin <= form_date_fin)
|
||||
collection_result = (
|
||||
collection_result.join(ModuleImpl)
|
||||
.join(ModuleImplInscription)
|
||||
.filter(ModuleImplInscription.etudid == collection_class.etudid)
|
||||
)
|
||||
|
||||
return collection_result
|
||||
|
||||
|
||||
def justifies(justi: Justificatif, obj: bool = False) -> list[int] | Query:
|
||||
@ -556,15 +608,19 @@ def get_all_justified(
|
||||
return after
|
||||
|
||||
|
||||
def create_absence(
|
||||
def create_absence_billet(
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etudid: int,
|
||||
description: str = None,
|
||||
est_just: bool = False,
|
||||
) -> int:
|
||||
"""TODO: doc, dire quand l'utiliser"""
|
||||
# TODO
|
||||
"""
|
||||
Permet de rapidement créer une absence.
|
||||
**UTILISÉ UNIQUEMENT POUR LES BILLETS**
|
||||
Ne pas utiliser autre par.
|
||||
TALK: Vérifier si nécessaire
|
||||
"""
|
||||
etud: Identite = Identite.query.filter_by(etudid=etudid).first_or_404()
|
||||
assiduite_unique: Assiduite = Assiduite.create_assiduite(
|
||||
etud=etud,
|
||||
@ -646,8 +702,7 @@ def get_assiduites_count_in_interval(
|
||||
"""
|
||||
date_debut_iso = date_debut_iso or date_debut.isoformat()
|
||||
date_fin_iso = date_fin_iso or date_fin.isoformat()
|
||||
# TODO Question: pourquoi ne pas cacher toutes les métriques, si l'API les veut toutes ?
|
||||
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}{metrique}_assiduites"
|
||||
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites"
|
||||
|
||||
r = sco_cache.AbsSemEtudCache.get(key)
|
||||
if not r or moduleimpl_id is not None:
|
||||
@ -663,27 +718,28 @@ def get_assiduites_count_in_interval(
|
||||
|
||||
calculator: CountCalculator = CountCalculator()
|
||||
calculator.compute_assiduites(assiduites)
|
||||
calcul : dict = calculator.to_dict(only_total=False)
|
||||
nb_abs: dict = calcul["absent"][metrique]
|
||||
nb_abs_just: dict = calcul["absent_just"][metrique]
|
||||
calcul: dict = calculator.to_dict(only_total=False)
|
||||
|
||||
r = (nb_abs, nb_abs_just)
|
||||
r = calcul
|
||||
if moduleimpl_id is None:
|
||||
ans = sco_cache.AbsSemEtudCache.set(key, r)
|
||||
if not ans:
|
||||
log("warning: get_assiduites_count failed to cache")
|
||||
return r
|
||||
|
||||
nb_abs: dict = r["absent"][metrique]
|
||||
nb_abs_just: dict = r["absent_just"][metrique]
|
||||
return (nb_abs, nb_abs_just)
|
||||
|
||||
|
||||
def invalidate_assiduites_count(etudid: int, sem: dict):
|
||||
"""Invalidate (clear) cached counts"""
|
||||
date_debut = sem["date_debut_iso"]
|
||||
date_fin = sem["date_fin_iso"]
|
||||
for met in scu.AssiduitesMetrics.TAG:
|
||||
key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites"
|
||||
sco_cache.AbsSemEtudCache.delete(key)
|
||||
key = str(etudid) + "_" + date_debut + "_" + date_fin + "_assiduites"
|
||||
sco_cache.AbsSemEtudCache.delete(key)
|
||||
|
||||
|
||||
# Non utilisé
|
||||
def invalidate_assiduites_count_sem(sem: dict):
|
||||
"""Invalidate (clear) cached abs counts for all the students of this semestre"""
|
||||
inscriptions = (
|
||||
@ -752,3 +808,14 @@ def simple_invalidate_cache(obj: dict, etudid: str | int = None):
|
||||
etudid = etudid if etudid is not None else obj["etudid"]
|
||||
invalidate_assiduites_etud_date(etudid, date_debut)
|
||||
invalidate_assiduites_etud_date(etudid, date_fin)
|
||||
|
||||
# mettre à jour le scodoc_dept en fonction de l'étudiant
|
||||
etud = Identite.query.filter_by(etudid=etudid).first_or_404()
|
||||
set_sco_dept(etud.departement.acronym)
|
||||
|
||||
# Invalide les caches des tableaux de l'étudiant
|
||||
sco_cache.RequeteTableauAssiduiteCache.delete_pattern(
|
||||
pattern=f"tableau-etud-{etudid}*"
|
||||
)
|
||||
# Invalide les tableaux "bilan dept"
|
||||
sco_cache.RequeteTableauAssiduiteCache.delete_pattern(pattern=f"tableau-dept*")
|
||||
|
@ -795,7 +795,9 @@ def etud_descr_situation_semestre(
|
||||
descr_mention = ""
|
||||
|
||||
# Décisions APC / BUT
|
||||
if pv.get("decision_annee", {}):
|
||||
if pv.get("decision_annee", {}) and sco_preferences.get_preference(
|
||||
"bul_but_code_annuel", formsemestre.id
|
||||
):
|
||||
infos["descr_decision_annee"] = "Décision année: " + pv.get(
|
||||
"decision_annee", {}
|
||||
).get("code", "")
|
||||
|
@ -60,6 +60,7 @@ import traceback
|
||||
from flask import g, request
|
||||
|
||||
from app import log, ScoValueError
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import FormSemestre, Identite
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
@ -165,9 +166,9 @@ def process_field(
|
||||
values={pprint.pformat(cdict)}
|
||||
"""
|
||||
)
|
||||
text = f"""<para><i>format invalide: champs</i> {missing_key} <i>inexistant !</i></para>"""
|
||||
text = f"""<para><i>format invalide: champ</i> {missing_key} <i>inexistant !</i></para>"""
|
||||
scu.flash_once(
|
||||
f"Attention: format PDF invalide (champs {field}, clef {missing_key})"
|
||||
f"Attention: format PDF invalide (champ {field}, clef {missing_key})"
|
||||
)
|
||||
raise
|
||||
except: # pylint: disable=bare-except
|
||||
@ -318,14 +319,34 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
|
||||
return pdfdoc, filename
|
||||
|
||||
|
||||
def get_filigranne(etud_etat: str, prefs, decision_sem=None) -> str:
|
||||
"""Texte à placer en "filigranne" sur le bulletin pdf"""
|
||||
def get_filigranne(
|
||||
etud_etat: str, prefs, decision_sem: str | None | bool = None
|
||||
) -> str:
|
||||
"""Texte à placer en "filigranne" sur le bulletin pdf.
|
||||
etud_etat : etat de l'inscription (I ou D)
|
||||
decision_sem = code jury ou vide
|
||||
"""
|
||||
if etud_etat == scu.DEMISSION:
|
||||
return "Démission"
|
||||
elif etud_etat == codes_cursus.DEF:
|
||||
if etud_etat == codes_cursus.DEF:
|
||||
return "Défaillant"
|
||||
elif (prefs["bul_show_temporary"] and not decision_sem) or prefs[
|
||||
if (prefs["bul_show_temporary"] and not decision_sem) or prefs[
|
||||
"bul_show_temporary_forced"
|
||||
]:
|
||||
return prefs["bul_temporary_txt"]
|
||||
return ""
|
||||
|
||||
|
||||
def get_filigranne_apc(
|
||||
etud_etat: str, prefs, etudid: int, res: ResultatsSemestreBUT
|
||||
) -> str:
|
||||
"""Texte à placer en "filigranne" sur le bulletin pdf.
|
||||
Version optimisée pour BUT
|
||||
"""
|
||||
if prefs["bul_show_temporary_forced"]:
|
||||
return get_filigranne(etud_etat, prefs)
|
||||
if prefs["bul_show_temporary"]:
|
||||
# requete les décisions de jury
|
||||
decision_sem = res.etud_has_decision(etudid)
|
||||
return get_filigranne(etud_etat, prefs, decision_sem=decision_sem)
|
||||
return get_filigranne(etud_etat, prefs)
|
||||
|
@ -121,6 +121,33 @@ class ScoDocCache:
|
||||
for oid in oids:
|
||||
cls.delete(oid)
|
||||
|
||||
@classmethod
|
||||
def delete_pattern(cls, pattern: str, std_prefix=True) -> int:
|
||||
"""Delete all keys matching pattern.
|
||||
The pattern starts with flask_cache_<dept_acronym>.
|
||||
If std_prefix is true (default), the prefix is added
|
||||
to the given pattern.
|
||||
Examples:
|
||||
'TABASSI_tableau-etud-1234:*'
|
||||
Or, with std_prefix false, 'flask_cache_RT_TABASSI_tableau-etud-1234:*'
|
||||
|
||||
Returns number of keys deleted.
|
||||
"""
|
||||
# see https://stackoverflow.com/questions/36708461/flask-cache-list-keys-based-on-a-pattern
|
||||
assert CACHE.cache.__class__.__name__ == "RedisCache" # Redis specific
|
||||
import redis
|
||||
|
||||
if std_prefix:
|
||||
pattern = "flask_cache_" + g.scodoc_dept + "_" + cls.prefix + "_" + pattern
|
||||
|
||||
r = redis.Redis()
|
||||
count = 0
|
||||
for key in r.scan_iter(pattern):
|
||||
log(f"{cls.__name__}.delete_pattern({key})")
|
||||
r.delete(key)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
class EvaluationCache(ScoDocCache):
|
||||
"""Cache for evaluations.
|
||||
@ -369,3 +396,13 @@ class ValidationsSemestreCache(ScoDocCache):
|
||||
|
||||
prefix = "VSC"
|
||||
timeout = 60 * 60 # ttl 1 heure (en phase de mise au point)
|
||||
|
||||
|
||||
class RequeteTableauAssiduiteCache(ScoDocCache):
|
||||
"""
|
||||
clé : "<titre_tableau>:<type_obj>:<show_pres>:<show_retard>:<show_desc>:<order_col>:<order>"
|
||||
Valeur = liste de dicts
|
||||
"""
|
||||
|
||||
prefix = "TABASSI"
|
||||
timeout = 60 * 60 # Une heure
|
||||
|
@ -39,7 +39,6 @@ from app import log
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc import sco_cache, sco_etud
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc.codes_cursus import (
|
||||
CMP,
|
||||
ADC,
|
||||
|
@ -134,10 +134,10 @@ def table_debouche_etudids(etudids, keep_numeric=True):
|
||||
"nom": etud["nom"],
|
||||
"prenom": etud["prenom"],
|
||||
"_nom_target": url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
),
|
||||
"_prenom_target": url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
),
|
||||
"_nom_td_attrs": 'id="%s" class="etudinfo"' % (etud["etudid"]),
|
||||
# 'debouche' : etud['debouche'],
|
||||
|
@ -126,53 +126,59 @@ def html_edit_formation_apc(
|
||||
UniteEns.type != codes_cursus.UE_SPORT,
|
||||
).first()
|
||||
H += [
|
||||
render_template(
|
||||
"pn/form_mods.j2",
|
||||
formation=formation,
|
||||
titre=f"Ressources du S{semestre_idx}",
|
||||
create_element_msg="créer une nouvelle ressource",
|
||||
# matiere_parent=matiere_parent,
|
||||
modules=ressources_in_sem,
|
||||
module_type=ModuleType.RESSOURCE,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
semestre_id=semestre_idx,
|
||||
)
|
||||
if ues_by_sem[semestre_idx].count() > 0
|
||||
else "",
|
||||
render_template(
|
||||
"pn/form_mods.j2",
|
||||
formation=formation,
|
||||
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
|
||||
create_element_msg="créer une nouvelle SAÉ",
|
||||
# matiere_parent=matiere_parent,
|
||||
modules=saes_in_sem,
|
||||
module_type=ModuleType.SAE,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
semestre_id=semestre_idx,
|
||||
)
|
||||
if ues_by_sem[semestre_idx].count() > 0
|
||||
else "",
|
||||
render_template(
|
||||
"pn/form_mods.j2",
|
||||
formation=formation,
|
||||
titre=f"Autres modules (non BUT) du S{semestre_idx}",
|
||||
create_element_msg="créer un nouveau module",
|
||||
modules=other_modules_in_sem,
|
||||
module_type=ModuleType.STANDARD,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
semestre_id=semestre_idx,
|
||||
)
|
||||
if ues_by_sem[semestre_idx].count() > 0
|
||||
else """<span class="fontred">créer une UE pour pouvoir ajouter des modules</span>""",
|
||||
(
|
||||
render_template(
|
||||
"pn/form_mods.j2",
|
||||
formation=formation,
|
||||
titre=f"Ressources du S{semestre_idx}",
|
||||
create_element_msg="créer une nouvelle ressource",
|
||||
# matiere_parent=matiere_parent,
|
||||
modules=ressources_in_sem,
|
||||
module_type=ModuleType.RESSOURCE,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
semestre_id=semestre_idx,
|
||||
)
|
||||
if ues_by_sem[semestre_idx].count() > 0
|
||||
else ""
|
||||
),
|
||||
(
|
||||
render_template(
|
||||
"pn/form_mods.j2",
|
||||
formation=formation,
|
||||
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
|
||||
create_element_msg="créer une nouvelle SAÉ",
|
||||
# matiere_parent=matiere_parent,
|
||||
modules=saes_in_sem,
|
||||
module_type=ModuleType.SAE,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
semestre_id=semestre_idx,
|
||||
)
|
||||
if ues_by_sem[semestre_idx].count() > 0
|
||||
else ""
|
||||
),
|
||||
(
|
||||
render_template(
|
||||
"pn/form_mods.j2",
|
||||
formation=formation,
|
||||
titre=f"Autres modules (non BUT) du S{semestre_idx}",
|
||||
create_element_msg="créer un nouveau module",
|
||||
modules=other_modules_in_sem,
|
||||
module_type=ModuleType.STANDARD,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
semestre_id=semestre_idx,
|
||||
)
|
||||
if ues_by_sem[semestre_idx].count() > 0
|
||||
else """<span class="fontred">créer une UE pour pouvoir ajouter des modules</span>"""
|
||||
),
|
||||
]
|
||||
|
||||
return "\n".join(H)
|
||||
@ -202,7 +208,7 @@ def html_ue_infos(ue):
|
||||
)
|
||||
return render_template(
|
||||
"pn/ue_infos.j2",
|
||||
titre=f"UE {ue.acronyme} {ue.titre}",
|
||||
titre=f"UE {ue.acronyme} {ue.titre or ''}",
|
||||
ue=ue,
|
||||
formsemestres=formsemestres,
|
||||
nb_etuds_valid_ue=nb_etuds_valid_ue,
|
||||
|
@ -104,7 +104,7 @@ def matiere_create(ue_id=None):
|
||||
default_numero = max([mat.numero for mat in ue.matieres] or [9]) + 1
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Création d'une matière"),
|
||||
f"""<h2>Création d'une matière dans l'UE {ue.titre} ({ue.acronyme})</h2>
|
||||
f"""<h2>Création d'une matière dans l'UE {ue.titre or ''} ({ue.acronyme})</h2>
|
||||
<p class="help">Les matières sont des groupes de modules dans une UE
|
||||
d'une formation donnée. Les matières servent surtout pour la
|
||||
présentation (bulletins, etc) mais <em>n'ont pas de rôle dans le calcul
|
||||
|
@ -85,7 +85,7 @@ _moduleEditor = ndb.EditableTable(
|
||||
"heures_tp": ndb.float_null_is_zero,
|
||||
"numero": ndb.int_null_is_zero,
|
||||
"coefficient": ndb.float_null_is_zero,
|
||||
"module_type": ndb.int_null_is_zero
|
||||
"module_type": ndb.int_null_is_zero,
|
||||
#'ects' : ndb.float_null_is_null
|
||||
},
|
||||
)
|
||||
@ -387,14 +387,16 @@ def module_edit(
|
||||
"scodoc/help/modules.j2",
|
||||
is_apc=is_apc,
|
||||
semestre_id=semestre_id,
|
||||
formsemestres=FormSemestre.query.filter(
|
||||
ModuleImpl.formsemestre_id == FormSemestre.id,
|
||||
ModuleImpl.module_id == module_id,
|
||||
)
|
||||
.order_by(FormSemestre.date_debut)
|
||||
.all()
|
||||
if not create
|
||||
else None,
|
||||
formsemestres=(
|
||||
FormSemestre.query.filter(
|
||||
ModuleImpl.formsemestre_id == FormSemestre.id,
|
||||
ModuleImpl.module_id == module_id,
|
||||
)
|
||||
.order_by(FormSemestre.date_debut)
|
||||
.all()
|
||||
if not create
|
||||
else None
|
||||
),
|
||||
create=create,
|
||||
),
|
||||
]
|
||||
@ -413,9 +415,11 @@ def module_edit(
|
||||
}
|
||||
if module:
|
||||
module_types |= {
|
||||
scu.ModuleType(module.module_type)
|
||||
if module.module_type
|
||||
else scu.ModuleType.STANDARD
|
||||
(
|
||||
scu.ModuleType(module.module_type)
|
||||
if module.module_type
|
||||
else scu.ModuleType.STANDARD
|
||||
)
|
||||
}
|
||||
# Numéro du module
|
||||
# cherche le numero adéquat (pour placer le module en fin de liste)
|
||||
@ -571,15 +575,17 @@ def module_edit(
|
||||
"input_type": "menu",
|
||||
"title": "Rattachement :" if is_apc else "Matière :",
|
||||
"explanation": (
|
||||
"UE de rattachement, utilisée notamment pour les malus"
|
||||
+ (
|
||||
" (module utilisé, ne peut pas être changé de semestre)"
|
||||
if in_use
|
||||
else ""
|
||||
(
|
||||
"UE de rattachement, utilisée notamment pour les malus"
|
||||
+ (
|
||||
" (module utilisé, ne peut pas être changé de semestre)"
|
||||
if in_use
|
||||
else ""
|
||||
)
|
||||
)
|
||||
)
|
||||
if is_apc
|
||||
else "un module appartient à une seule matière.",
|
||||
if is_apc
|
||||
else "un module appartient à une seule matière."
|
||||
),
|
||||
"labels": mat_names,
|
||||
"allowed_values": ue_mat_ids,
|
||||
"enabled": unlocked,
|
||||
@ -733,7 +739,7 @@ def module_edit(
|
||||
"title": f"""<span class="fontred">{scu.EMO_WARNING }
|
||||
L'UE <a class="stdlink" href="{
|
||||
url_for("notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue.id)
|
||||
}">{ue.acronyme} {ue.titre}</a>
|
||||
}">{ue.acronyme} {ue.titre or ''}</a>
|
||||
n'est pas associée à un niveau de compétences
|
||||
</span>""",
|
||||
},
|
||||
@ -766,12 +772,14 @@ def module_edit(
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
descr,
|
||||
html_foot_markup=f"""<div class="sco_tag_module_edit"><span
|
||||
html_foot_markup=(
|
||||
f"""<div class="sco_tag_module_edit"><span
|
||||
class="sco_tag_edit"><textarea data-module_id="{module_id}" class="module_tag_editor"
|
||||
>{','.join(sco_tag_module.module_tag_list(module_id))}</textarea></span></div>
|
||||
"""
|
||||
if not create
|
||||
else "",
|
||||
if not create
|
||||
else ""
|
||||
),
|
||||
initvalues=module_dict if module else {},
|
||||
submitlabel="Modifier ce module" if module else "Créer ce module",
|
||||
cancelbutton="Annuler",
|
||||
@ -814,7 +822,7 @@ def module_edit(
|
||||
tf[2]["matiere_id"] = matiere.id
|
||||
else:
|
||||
matiere_id = sco_edit_matiere.do_matiere_create(
|
||||
{"ue_id": ue.id, "titre": ue.titre, "numero": 1},
|
||||
{"ue_id": ue.id, "titre": ue.titre or "", "numero": 1},
|
||||
)
|
||||
tf[2]["matiere_id"] = matiere_id
|
||||
|
||||
|
@ -89,6 +89,7 @@ _ueEditor = ndb.EditableTable(
|
||||
"color",
|
||||
"niveau_competence_id",
|
||||
),
|
||||
convert_empty_to_nulls=False, # necessaire pour ue_code == ""
|
||||
sortkey="numero",
|
||||
input_formators={
|
||||
"type": ndb.int_null_is_zero,
|
||||
@ -110,7 +111,7 @@ def ue_list(*args, **kw):
|
||||
return _ueEditor.list(cnx, *args, **kw)
|
||||
|
||||
|
||||
def do_ue_create(args):
|
||||
def do_ue_create(args, allow_empty_ue_code=False):
|
||||
"create an ue"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# check duplicates
|
||||
@ -120,19 +121,27 @@ def do_ue_create(args):
|
||||
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
|
||||
(chaque UE doit avoir un acronyme unique dans la formation)"""
|
||||
)
|
||||
if (
|
||||
(not "ue_code" in args)
|
||||
or (args["ue_code"] is None)
|
||||
or (not args["ue_code"].strip())
|
||||
):
|
||||
# évite les conflits de code
|
||||
while True:
|
||||
cursor = db.session.execute(sa.text("select notes_newid_ucod();"))
|
||||
code = cursor.fetchone()[0]
|
||||
if UniteEns.query.filter_by(ue_code=code).count() == 0:
|
||||
break
|
||||
args["ue_code"] = code
|
||||
if "ue_code" not in args or args["ue_code"] is None or not args["ue_code"].strip():
|
||||
if allow_empty_ue_code:
|
||||
args["ue_code"] = ""
|
||||
else:
|
||||
# évite les conflits: génère nouveau ue_code
|
||||
while True:
|
||||
cursor = db.session.execute(sa.text("select notes_newid_ucod();"))
|
||||
code = cursor.fetchone()[0]
|
||||
if UniteEns.query.filter_by(ue_code=code).count() == 0:
|
||||
break
|
||||
args["ue_code"] = code
|
||||
|
||||
# last checks
|
||||
if not args.get("acronyme"):
|
||||
raise ScoValueError("acronyme vide")
|
||||
args["coefficient"] = args.get("coefficient", None)
|
||||
if args["coefficient"] == "":
|
||||
args["coefficient"] = None
|
||||
|
||||
# create
|
||||
# XXX TODO utiliser UniteEns.create_from_dict
|
||||
ue_id = _ueEditor.create(cnx, args)
|
||||
log(f"do_ue_create: created {ue_id} with {args}")
|
||||
|
||||
@ -158,7 +167,7 @@ def do_ue_delete(ue: UniteEns, delete_validations=False, force=False):
|
||||
if not ue.can_be_deleted():
|
||||
raise ScoNonEmptyFormationObject(
|
||||
f"UE (id={ue.id}, dud)",
|
||||
msg=ue.titre,
|
||||
msg=f"{ue.titre or ''} ({ue.acronyme})",
|
||||
dest_url=url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
@ -302,11 +311,13 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
<p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé.
|
||||
Seuls les <em>modules</em> ont des coefficients.
|
||||
</p>""",
|
||||
f"""
|
||||
(
|
||||
f"""
|
||||
<h4>UE du semestre S{ue.semestre_idx}</h4>
|
||||
"""
|
||||
if is_apc and ue
|
||||
else "",
|
||||
if is_apc and ue
|
||||
else ""
|
||||
),
|
||||
]
|
||||
|
||||
ue_types = cursus.ALLOWED_UE_TYPES
|
||||
@ -628,8 +639,8 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
|
||||
)
|
||||
if not ue.can_be_deleted():
|
||||
raise ScoNonEmptyFormationObject(
|
||||
f"UE",
|
||||
msg=ue.titre,
|
||||
"UE",
|
||||
msg=f"{ue.titre or ''} ({ue.acronyme})",
|
||||
dest_url=url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
@ -640,7 +651,7 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
|
||||
|
||||
if not dialog_confirmed:
|
||||
return scu.confirm_dialog(
|
||||
f"<h2>Suppression de l'UE {ue.titre} ({ue.acronyme})</h2>",
|
||||
f"<h2>Suppression de l'UE {ue.titre or ''} ({ue.acronyme})</h2>",
|
||||
dest_url="",
|
||||
parameters={"ue_id": ue.id},
|
||||
cancel_url=url_for(
|
||||
@ -669,6 +680,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
||||
semestre_idx = None
|
||||
else:
|
||||
semestre_idx = int(semestre_idx)
|
||||
show_tags = scu.to_bool(request.args.get("show_tags", 0))
|
||||
locked = formation.has_locked_sems(semestre_idx)
|
||||
semestre_ids = range(1, parcours.NB_SEM + 1)
|
||||
# transition: on requete ici via l'ORM mais on utilise les fonctions ScoDoc7
|
||||
@ -874,11 +886,13 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
||||
)
|
||||
# Description des UE/matières/modules
|
||||
H.append(
|
||||
"""
|
||||
f"""
|
||||
<div class="formation_ue_list">
|
||||
<div class="ue_list_tit">Programme pédagogique:</div>
|
||||
<form>
|
||||
<input type="checkbox" class="sco_tag_checkbox">montrer les tags des modules</input>
|
||||
<input type="checkbox" class="sco_tag_checkbox"
|
||||
{'checked' if show_tags else ''}
|
||||
> Montrer les tags des modules voire en ajouter <i>(ceux correspondant aux titres des compétences étant ajoutés par défaut)</i></input>
|
||||
</form>
|
||||
"""
|
||||
)
|
||||
@ -977,6 +991,13 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
||||
formation_id=formation_id)
|
||||
}">Table récapitulative de la formation</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{
|
||||
url_for('notes.formation_tag_modules_by_type', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation_id,
|
||||
semestre_idx=1 if semestre_idx is None else semestre_idx
|
||||
)
|
||||
}">Tagguer tous les modules par leur type</a> (tag <tt>res</tt>, <tt>sae</tt>).
|
||||
</li>
|
||||
<li><a class="stdlink" href="{
|
||||
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation_id, fmt='xml')
|
||||
@ -1060,6 +1081,7 @@ def _html_select_semestre_idx(formation_id, semestre_ids, semestre_idx):
|
||||
htm += f"""
|
||||
</select>
|
||||
<input type="hidden" name="formation_id" value="{formation_id}"></input>
|
||||
<input type="hidden" name="show_tags" value="0"></input>
|
||||
</form>"""
|
||||
return htm
|
||||
|
||||
@ -1430,7 +1452,7 @@ def ue_sharing_code(ue_code: str = "", ue_id: int = None, hide_ue_id: int = None
|
||||
H.append("<ul>")
|
||||
for ue in ues:
|
||||
H.append(
|
||||
f"""<li>{ue.acronyme} ({ue.titre}) dans
|
||||
f"""<li>{ue.acronyme} ({ue.titre or ''}) dans
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)}"
|
||||
|
@ -177,12 +177,26 @@ def formsemestre_edt_dict(
|
||||
TODO: spécifier intervalle de dates start et end
|
||||
"""
|
||||
t0 = time.time()
|
||||
group_ids_set = set(group_ids) if group_ids else set()
|
||||
try:
|
||||
events_scodoc, _ = load_and_convert_ics(formsemestre)
|
||||
except ScoValueError as exc:
|
||||
return exc.args[0]
|
||||
# Génération des événements pour le calendrier html
|
||||
edt_dict = translate_calendar(
|
||||
events_scodoc, group_ids, show_modules_titles=show_modules_titles
|
||||
)
|
||||
log(
|
||||
f"formsemestre_edt_dict: loaded edt for {formsemestre} in {(time.time()-t0):g}s"
|
||||
)
|
||||
return edt_dict
|
||||
|
||||
|
||||
def translate_calendar(
|
||||
events_scodoc: list[dict],
|
||||
group_ids: list[int] = None,
|
||||
show_modules_titles=True,
|
||||
) -> list[dict]:
|
||||
"""Génération des événements pour le calendrier html"""
|
||||
group_ids_set = set(group_ids) if group_ids else set()
|
||||
promo_icon = f"""<img height="18px" src="{scu.STATIC_DIR}/icons/promo.svg"
|
||||
title="promotion complète" alt="promotion"/>"""
|
||||
abs_icon = f"""<img height="28px" src="{scu.STATIC_DIR}/icons/absences.svg"
|
||||
@ -211,7 +225,6 @@ def formsemestre_edt_dict(
|
||||
url_for(
|
||||
"assiduites.signal_assiduites_group",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
heure_deb=event["heure_deb"],
|
||||
heure_fin=event["heure_fin"],
|
||||
@ -271,17 +284,17 @@ def formsemestre_edt_dict(
|
||||
"start": event["start"],
|
||||
"end": event["end"],
|
||||
"backgroundColor": event["group_bg_color"],
|
||||
"raw": event["raw"],
|
||||
# Infos brutes pour usage API éventuel
|
||||
"edt_ens_ids": event["edt_ens_ids"],
|
||||
"ens_user_names": ens_user_names,
|
||||
"group_id": group.id if group else None,
|
||||
"group_edt_id": event["edt_group"],
|
||||
"moduleimpl_id": modimpl.id if modimpl else None,
|
||||
"UID": event["UID"], # icalendar event UID
|
||||
}
|
||||
events_cal.append(d)
|
||||
log(
|
||||
f"formsemestre_edt_dict: loaded edt for {formsemestre} in {(time.time()-t0):g}s"
|
||||
)
|
||||
|
||||
return events_cal
|
||||
|
||||
|
||||
@ -302,7 +315,35 @@ def get_ics_uid_pattern() -> re.Pattern:
|
||||
|
||||
|
||||
def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[str]]:
|
||||
"""Chargement fichier ics, filtrage et extraction des identifiants.
|
||||
"""Chargement fichier ics.
|
||||
Renvoie une liste d'évènements, et la liste des identifiants de groupes
|
||||
trouvés (utilisée pour l'aide).
|
||||
"""
|
||||
# Chargement du calendier ics
|
||||
_, calendar = formsemestre_load_calendar(formsemestre)
|
||||
if not calendar:
|
||||
return [], []
|
||||
# --- Correspondances id edt -> id scodoc pour groupes
|
||||
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
|
||||
default_group = formsemestre.get_default_group()
|
||||
# --- Correspondances id edt -> id scodoc pour modimpls
|
||||
edt2modimpl = formsemestre_retreive_modimpls_from_edt_id(formsemestre)
|
||||
return convert_ics(
|
||||
calendar,
|
||||
edt2group=edt2group,
|
||||
default_group=default_group,
|
||||
edt2modimpl=edt2modimpl,
|
||||
)
|
||||
|
||||
|
||||
def convert_ics(
|
||||
calendar: icalendar.cal.Calendar,
|
||||
edt2group: dict[str, GroupDescr] = None,
|
||||
default_group: GroupDescr = None,
|
||||
edt2modimpl: dict[str, ModuleImpl] = None,
|
||||
) -> tuple[list[dict], list[str]]:
|
||||
"""Filtrage et extraction des identifiants des évènements calendrier.
|
||||
|
||||
Renvoie une liste d'évènements, et la liste des identifiants de groupes
|
||||
trouvés (utilisée pour l'aide).
|
||||
|
||||
@ -310,10 +351,6 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
- False si extraction regexp non configuré
|
||||
- "tous" (promo) si pas de correspondance trouvée.
|
||||
"""
|
||||
# Chargement du calendier ics
|
||||
_, calendar = formsemestre_load_calendar(formsemestre)
|
||||
if not calendar:
|
||||
return []
|
||||
# --- Paramètres d'extraction
|
||||
edt_ics_title_field = ScoDocSiteConfig.get("edt_ics_title_field")
|
||||
edt_ics_title_regexp = ScoDocSiteConfig.get("edt_ics_title_regexp")
|
||||
@ -348,15 +385,13 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
edt_ics_uid_field = ScoDocSiteConfig.get("edt_ics_uid_field")
|
||||
edt_ics_uid_pattern = get_ics_uid_pattern()
|
||||
|
||||
# --- Correspondances id edt -> id scodoc pour groupes, modules et enseignants
|
||||
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
|
||||
# --- Groupes
|
||||
group_colors = {
|
||||
group_name: _COLOR_PALETTE[i % (len(_COLOR_PALETTE) - 1) + 1]
|
||||
for i, group_name in enumerate(edt2group)
|
||||
}
|
||||
edt_groups_ids = set() # les ids de groupes normalisés tels que dans l'ics
|
||||
default_group = formsemestre.get_default_group()
|
||||
edt2modimpl = formsemestre_retreive_modimpls_from_edt_id(formsemestre)
|
||||
|
||||
edt2user: dict[str, User | None] = {} # construit au fur et à mesure (cache)
|
||||
# ---
|
||||
events = [e for e in calendar.walk() if e.name == "VEVENT"]
|
||||
@ -371,29 +406,6 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
)
|
||||
# title remplacé par le nom du module scodoc quand il est trouvé
|
||||
title = title_edt
|
||||
# --- Group
|
||||
if edt_ics_group_pattern:
|
||||
edt_group = extract_event_edt_id(
|
||||
event, edt_ics_group_field, edt_ics_group_pattern
|
||||
)
|
||||
edt_groups_ids.add(edt_group)
|
||||
# si pas de groupe dans l'event, ou si groupe non reconnu,
|
||||
# prend toute la promo ("tous")
|
||||
group: GroupDescr = (
|
||||
edt2group.get(edt_group, default_group)
|
||||
if edt_group
|
||||
else default_group
|
||||
)
|
||||
group_bg_color = (
|
||||
group_colors.get(edt_group, _EVENT_DEFAULT_COLOR)
|
||||
if group
|
||||
else "lightgrey"
|
||||
)
|
||||
else:
|
||||
edt_group = ""
|
||||
group = False
|
||||
group_bg_color = _EVENT_DEFAULT_COLOR
|
||||
|
||||
# --- ModuleImpl
|
||||
if edt_ics_mod_pattern:
|
||||
edt_module = extract_event_edt_id(
|
||||
@ -405,6 +417,34 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
else:
|
||||
modimpl = False
|
||||
edt_module = ""
|
||||
# --- Group
|
||||
if edt_ics_group_pattern:
|
||||
edt_group = extract_event_edt_id(
|
||||
event, edt_ics_group_field, edt_ics_group_pattern
|
||||
)
|
||||
edt_groups_ids.add(edt_group)
|
||||
# si pas de groupe dans l'event, ou si groupe non reconnu,
|
||||
# prend toute la promo ("tous")
|
||||
event_default_group = (
|
||||
default_group
|
||||
if default_group
|
||||
else (modimpl.formsemestre.get_default_group() if modimpl else None)
|
||||
)
|
||||
group: GroupDescr = (
|
||||
edt2group.get(edt_group, event_default_group)
|
||||
if edt_group
|
||||
else event_default_group
|
||||
)
|
||||
group_bg_color = (
|
||||
group_colors.get(edt_group, _EVENT_DEFAULT_COLOR)
|
||||
if group
|
||||
else "lightgrey"
|
||||
)
|
||||
else:
|
||||
edt_group = ""
|
||||
group = False
|
||||
group_bg_color = _EVENT_DEFAULT_COLOR
|
||||
|
||||
# --- Enseignants
|
||||
users: list[User] = []
|
||||
if edt_ics_uid_pattern:
|
||||
@ -446,6 +486,8 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
"jour": event.decoded("dtstart").date().isoformat(),
|
||||
"start": event.decoded("dtstart").isoformat(),
|
||||
"end": event.decoded("dtend").isoformat(),
|
||||
"UID": event.decoded("UID").decode("utf-8"),
|
||||
"raw": event.to_ical().decode("utf-8"),
|
||||
}
|
||||
)
|
||||
return events_sco, sorted(edt_groups_ids)
|
||||
|
@ -542,7 +542,9 @@ def view_scodoc_etuds(semset_id, title="", nip_list="", fmt="html"):
|
||||
etuds = [sco_etud.get_etud_info(code_nip=nip, filled=True)[0] for nip in nips]
|
||||
|
||||
for e in etuds:
|
||||
tgt = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"])
|
||||
tgt = url_for(
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"]
|
||||
)
|
||||
e["_nom_target"] = tgt
|
||||
e["_prenom_target"] = tgt
|
||||
e["_nom_td_attrs"] = f"""id="{e['etudid']}" class="etudinfo" """
|
||||
@ -769,10 +771,10 @@ def view_apo_csv(etape_apo="", semset_id="", fmt="html"):
|
||||
e["in_scodoc"] = e["nip"] not in nips_no_sco
|
||||
e["in_scodoc_str"] = {True: "oui", False: "non"}[e["in_scodoc"]]
|
||||
if e["in_scodoc"]:
|
||||
e["_in_scodoc_str_target"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, code_nip=e["nip"]
|
||||
)
|
||||
e.update(sco_etud.get_etud_info(code_nip=e["nip"], filled=True)[0])
|
||||
e["_in_scodoc_str_target"] = url_for(
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"]
|
||||
)
|
||||
e["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"],)
|
||||
e["_prenom_td_attrs"] = 'id="pre-%s" class="etudinfo"' % (e["etudid"],)
|
||||
else:
|
||||
|
@ -692,7 +692,7 @@ class EtapeBilan:
|
||||
@staticmethod
|
||||
def link_etu(etudid, nom):
|
||||
return '<a class="stdlink" href="%s">%s</a>' % (
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
nom,
|
||||
)
|
||||
|
||||
|
@ -39,6 +39,7 @@ from app import db, email
|
||||
from app import log
|
||||
from app.models import Admission, Identite
|
||||
from app.models.etudiants import (
|
||||
check_etud_duplicate_code,
|
||||
input_civilite,
|
||||
input_civilite_etat_civil,
|
||||
make_etud_args,
|
||||
@ -64,7 +65,7 @@ def format_etud_ident(etud: dict):
|
||||
|
||||
Note: par rapport à Identite.to_dict_bul(),
|
||||
ajoute les champs:
|
||||
'email_default', 'nom_disp', 'nom_usuel', 'civilite_etat_civil_str', 'ne', 'civilite_str'
|
||||
'nom_disp', 'nom_usuel', 'civilite_etat_civil_str', 'ne', 'civilite_str'
|
||||
"""
|
||||
etud["nom"] = format_nom(etud["nom"])
|
||||
if "nom_usuel" in etud:
|
||||
@ -98,10 +99,6 @@ def format_etud_ident(etud: dict):
|
||||
etud["ne"] = "e"
|
||||
else: # 'X'
|
||||
etud["ne"] = "(e)"
|
||||
# Mail à utiliser pour les envois vers l'étudiant:
|
||||
# choix qui pourrait être controé par une preference
|
||||
# ici priorité au mail institutionnel:
|
||||
etud["email_default"] = etud.get("email", "") or etud.get("emailperso", "")
|
||||
|
||||
|
||||
def force_uppercase(s):
|
||||
@ -117,36 +114,6 @@ def _format_etat_civil(etud: dict) -> str:
|
||||
return etud["nomprenom"]
|
||||
|
||||
|
||||
def format_lycee(nomlycee):
|
||||
nomlycee = nomlycee.strip()
|
||||
s = nomlycee.lower()
|
||||
if s[:5] == "lycee" or s[:5] == "lycée":
|
||||
return nomlycee[5:]
|
||||
else:
|
||||
return nomlycee
|
||||
|
||||
|
||||
def format_telephone(n):
|
||||
if n is None:
|
||||
return ""
|
||||
if len(n) < 7:
|
||||
return n
|
||||
else:
|
||||
n = n.replace(" ", "").replace(".", "")
|
||||
i = 0
|
||||
r = ""
|
||||
j = len(n) - 1
|
||||
while j >= 0:
|
||||
r = n[j] + r
|
||||
if i % 2 == 1 and j != 0:
|
||||
r = " " + r
|
||||
i += 1
|
||||
j -= 1
|
||||
if len(r) == 13 and r[0] != "0":
|
||||
r = "0" + r
|
||||
return r
|
||||
|
||||
|
||||
def format_pays(s):
|
||||
"laisse le pays seulement si != FRANCE"
|
||||
if s.upper() != "FRANCE":
|
||||
@ -263,74 +230,12 @@ def check_nom_prenom_homonyms(
|
||||
return True, query.all()
|
||||
|
||||
|
||||
def _check_duplicate_code(cnx, args, code_name, disable_notify=False, edit=True):
|
||||
"""Vérifie que le code n'est pas dupliqué.
|
||||
Raises ScoGenError si problème.
|
||||
"""
|
||||
etudid = args.get("etudid", None)
|
||||
if args.get(code_name, None):
|
||||
etuds = identite_list(cnx, {code_name: str(args[code_name])})
|
||||
duplicate = False
|
||||
if edit:
|
||||
duplicate = (len(etuds) > 1) or (
|
||||
(len(etuds) == 1) and etuds[0]["id"] != args["etudid"]
|
||||
)
|
||||
else:
|
||||
duplicate = len(etuds) > 0
|
||||
if duplicate:
|
||||
listh = [] # liste des doubles
|
||||
for e in etuds:
|
||||
listh.append(
|
||||
f"""Autre étudiant: <a href="{
|
||||
url_for(
|
||||
"scolar.ficheEtud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=e["etudid"]
|
||||
)}">{e['nom']} {e['prenom']}</a>"""
|
||||
)
|
||||
if etudid:
|
||||
OK = "retour à la fiche étudiant"
|
||||
dest_endpoint = "scolar.ficheEtud"
|
||||
parameters = {"etudid": etudid}
|
||||
else:
|
||||
if "tf_submitted" in args:
|
||||
del args["tf_submitted"]
|
||||
OK = "Continuer"
|
||||
dest_endpoint = "scolar.etudident_create_form"
|
||||
parameters = args
|
||||
else:
|
||||
OK = "Annuler"
|
||||
dest_endpoint = "notes.index_html"
|
||||
parameters = {}
|
||||
if not disable_notify:
|
||||
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) }
|
||||
">{OK}</a>
|
||||
</p>
|
||||
"""
|
||||
else:
|
||||
err_page = f"""<h3>Code étudiant ({code_name}) dupliqué !</h3>"""
|
||||
log(f"*** error: code {code_name} duplique: {args[code_name]}")
|
||||
raise ScoGenError(err_page)
|
||||
|
||||
|
||||
def identite_edit(cnx, args, disable_notify=False):
|
||||
"""Modifie l'identite d'un étudiant.
|
||||
Si pref notification et difference, envoie message notification, sauf si disable_notify
|
||||
"""
|
||||
_check_duplicate_code(
|
||||
cnx, args, "code_nip", disable_notify=disable_notify, edit=True
|
||||
)
|
||||
_check_duplicate_code(
|
||||
cnx, args, "code_ine", disable_notify=disable_notify, edit=True
|
||||
)
|
||||
check_etud_duplicate_code(args, "code_nip", edit=True)
|
||||
check_etud_duplicate_code(args, "code_ine", edit=True)
|
||||
notify_to = None
|
||||
if not disable_notify:
|
||||
try:
|
||||
@ -359,16 +264,14 @@ def identite_edit(cnx, args, disable_notify=False):
|
||||
|
||||
def identite_create(cnx, args):
|
||||
"check unique etudid, then create"
|
||||
_check_duplicate_code(cnx, args, "code_nip", edit=False)
|
||||
_check_duplicate_code(cnx, args, "code_ine", edit=False)
|
||||
check_etud_duplicate_code(args, "code_nip", edit=False)
|
||||
check_etud_duplicate_code(args, "code_ine", edit=False)
|
||||
|
||||
if "etudid" in args:
|
||||
etudid = args["etudid"]
|
||||
r = identite_list(cnx, {"etudid": etudid})
|
||||
if r:
|
||||
raise ScoValueError(
|
||||
"Code identifiant (etudid) déjà utilisé ! (%s)" % etudid
|
||||
)
|
||||
raise ScoValueError(f"Code identifiant (etudid) déjà utilisé ! ({etudid})")
|
||||
return _identiteEditor.create(cnx, args)
|
||||
|
||||
|
||||
@ -619,7 +522,7 @@ def create_etud(cnx, args: dict = None):
|
||||
etud_dict = etudident_list(cnx, {"etudid": etudid})[0]
|
||||
fill_etuds_info([etud_dict])
|
||||
etud_dict["url"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_INSCR,
|
||||
@ -724,19 +627,28 @@ def get_etablissements():
|
||||
|
||||
|
||||
def get_lycee_infos(codelycee):
|
||||
E = get_etablissements()
|
||||
return E.get(codelycee, None)
|
||||
etablissements = get_etablissements()
|
||||
return etablissements.get(codelycee, None)
|
||||
|
||||
|
||||
def format_lycee_from_code(codelycee):
|
||||
def format_lycee_from_code(codelycee: str) -> str:
|
||||
"Description lycee à partir du code"
|
||||
E = get_etablissements()
|
||||
if codelycee in E:
|
||||
e = E[codelycee]
|
||||
etablissements = get_etablissements()
|
||||
if codelycee in etablissements:
|
||||
e = etablissements[codelycee]
|
||||
nomlycee = e["name"]
|
||||
return "%s (%s)" % (nomlycee, e["commune"])
|
||||
return f"{nomlycee} ({e['commune']})"
|
||||
return f"{codelycee} (établissement inconnu)"
|
||||
|
||||
|
||||
def format_lycee(nomlycee: str) -> str:
|
||||
"mise en forme nom de lycée"
|
||||
nomlycee = nomlycee.strip()
|
||||
s = nomlycee.lower()
|
||||
if s[:5] == "lycee" or s[:5] == "lycée":
|
||||
return nomlycee[5:]
|
||||
else:
|
||||
return "%s (établissement inconnu)" % codelycee
|
||||
return nomlycee
|
||||
|
||||
|
||||
def etud_add_lycee_infos(etud):
|
||||
@ -821,49 +733,20 @@ def fill_etuds_info(etuds: list[dict], add_admission=True):
|
||||
# nettoyage champs souvent vides
|
||||
etud["codepostallycee"] = etud.get("codepostallycee", "") or ""
|
||||
etud["nomlycee"] = etud.get("nomlycee", "") or ""
|
||||
if etud.get("nomlycee"):
|
||||
etud["ilycee"] = "Lycée " + format_lycee(etud["nomlycee"])
|
||||
if etud["villelycee"]:
|
||||
etud["ilycee"] += " (%s)" % etud.get("villelycee", "")
|
||||
etud["ilycee"] += "<br>"
|
||||
else:
|
||||
if etud.get("codelycee"):
|
||||
etud["ilycee"] = format_lycee_from_code(etud["codelycee"])
|
||||
else:
|
||||
etud["ilycee"] = ""
|
||||
rap = ""
|
||||
if etud.get("rapporteur") or etud.get("commentaire"):
|
||||
rap = "Note du rapporteur"
|
||||
if etud.get("rapporteur"):
|
||||
rap += " (%s)" % etud["rapporteur"]
|
||||
rap += ": "
|
||||
if etud.get("commentaire"):
|
||||
rap += "<em>%s</em>" % etud["commentaire"]
|
||||
etud["rap"] = rap
|
||||
|
||||
if etud.get("telephone"):
|
||||
etud["telephonestr"] = "<b>Tél.:</b> " + format_telephone(etud["telephone"])
|
||||
else:
|
||||
etud["telephonestr"] = ""
|
||||
if etud.get("telephonemobile"):
|
||||
etud["telephonemobilestr"] = "<b>Mobile:</b> " + format_telephone(
|
||||
etud["telephonemobile"]
|
||||
)
|
||||
else:
|
||||
etud["telephonemobilestr"] = ""
|
||||
|
||||
|
||||
# voir Identite.inscription_descr et Identite.to_dict_scodoc7(with_inscriptions=True)
|
||||
def etud_inscriptions_infos(etudid: int, ne="") -> dict:
|
||||
"""Dict avec les informations sur les semestres passés et courant.
|
||||
{
|
||||
"sems" : , # trie les semestres par date de debut, le plus recent d'abord
|
||||
"ins" : ,
|
||||
"cursem" : ,
|
||||
"inscription" : ,
|
||||
"inscriptionstr" : ,
|
||||
"inscription_formsemestre_id" : ,
|
||||
"etatincursem" : ,
|
||||
"situation" : ,
|
||||
"inscription" : , # cursem["titremois"]
|
||||
"inscriptionstr" : , # "Inscrit en " + cursem["titremois"]
|
||||
"inscription_formsemestre_id" : , # cursem["formsemestre_id"]
|
||||
"etatincursem" : , # curi["etat"]
|
||||
"situation" : , # descr_situation_etud(etudid, ne)
|
||||
}
|
||||
"""
|
||||
from app.scodoc import sco_formsemestre
|
||||
|
@ -156,7 +156,7 @@ def evaluation_check_absences_html(
|
||||
H.append(
|
||||
f"""<li><a class="discretelink" href="{
|
||||
url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
)
|
||||
}">{etud.nomprenom}</a>"""
|
||||
)
|
||||
|
@ -162,50 +162,48 @@ def do_evaluation_get_all_notes(
|
||||
return d
|
||||
|
||||
|
||||
def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
|
||||
def moduleimpl_evaluation_move(evaluation_id: int, after=0):
|
||||
"""Move before/after previous one (decrement/increment numero)
|
||||
(published)
|
||||
"""
|
||||
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
|
||||
redirect = int(redirect)
|
||||
evaluation = Evaluation.get_evaluation(evaluation_id)
|
||||
# access: can change eval ?
|
||||
if not evaluation.moduleimpl.can_edit_evaluation(current_user):
|
||||
raise AccessDenied(
|
||||
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
||||
)
|
||||
Evaluation.moduleimpl_evaluation_renumber(
|
||||
evaluation.moduleimpl, only_if_unumbered=True
|
||||
)
|
||||
e = get_evaluations_dict(args={"evaluation_id": evaluation_id})[0]
|
||||
|
||||
after = int(after) # 0: deplace avant, 1 deplace apres
|
||||
if after not in (0, 1):
|
||||
raise ValueError('invalid value for "after"')
|
||||
mod_evals = get_evaluations_dict({"moduleimpl_id": e["moduleimpl_id"]})
|
||||
if len(mod_evals) > 1:
|
||||
idx = [p["evaluation_id"] for p in mod_evals].index(evaluation_id)
|
||||
|
||||
Evaluation.moduleimpl_evaluation_renumber(
|
||||
evaluation.moduleimpl, only_if_unumbered=True
|
||||
)
|
||||
mod_evaluations = evaluation.moduleimpl.evaluations.all()
|
||||
if len(mod_evaluations) > 1:
|
||||
idx = [e.id for e in mod_evaluations].index(evaluation.id)
|
||||
neigh = None # object to swap with
|
||||
if after == 0 and idx > 0:
|
||||
neigh = mod_evals[idx - 1]
|
||||
elif after == 1 and idx < len(mod_evals) - 1:
|
||||
neigh = mod_evals[idx + 1]
|
||||
neigh = mod_evaluations[idx - 1]
|
||||
elif after == 1 and idx < len(mod_evaluations) - 1:
|
||||
neigh = mod_evaluations[idx + 1]
|
||||
if neigh: #
|
||||
if neigh["numero"] == e["numero"]:
|
||||
if neigh.numero == evaluation.numero:
|
||||
log("Warning: moduleimpl_evaluation_move: forcing renumber")
|
||||
Evaluation.moduleimpl_evaluation_renumber(
|
||||
evaluation.moduleimpl, only_if_unumbered=False
|
||||
)
|
||||
else:
|
||||
# swap numero with neighbor
|
||||
e["numero"], neigh["numero"] = neigh["numero"], e["numero"]
|
||||
do_evaluation_edit(e)
|
||||
do_evaluation_edit(neigh)
|
||||
evaluation.numero, neigh.numero = neigh.numero, evaluation.numero
|
||||
db.session.add(evaluation)
|
||||
db.session.add(neigh)
|
||||
db.session.commit()
|
||||
# redirect to moduleimpl page:
|
||||
if redirect:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=e["moduleimpl_id"],
|
||||
)
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=evaluation.moduleimpl.id,
|
||||
)
|
||||
)
|
||||
|
@ -37,7 +37,7 @@ from flask_login import current_user
|
||||
from flask import request
|
||||
|
||||
from app import db
|
||||
from app.models import Evaluation, FormSemestre, ModuleImpl
|
||||
from app.models import Evaluation, Module, ModuleImpl
|
||||
from app.models.evaluations import heure_to_time
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
@ -47,7 +47,6 @@ from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
|
||||
@ -58,27 +57,20 @@ def evaluation_create_form(
|
||||
page_title="Évaluation",
|
||||
):
|
||||
"Formulaire création/édition d'une évaluation (pas de ses notes)"
|
||||
evaluation: Evaluation
|
||||
if evaluation_id is not None:
|
||||
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
if evaluation is None:
|
||||
raise ScoValueError("Cette évaluation n'existe pas ou plus !")
|
||||
moduleimpl_id = evaluation.moduleimpl_id
|
||||
#
|
||||
modimpl: ModuleImpl = (
|
||||
ModuleImpl.query.filter_by(id=moduleimpl_id)
|
||||
.join(FormSemestre)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
.first_or_404()
|
||||
)
|
||||
modimpl_o = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[
|
||||
0
|
||||
]
|
||||
mod = modimpl_o["module"]
|
||||
formsemestre_id = modimpl_o["formsemestre_id"]
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
formsemestre_id = modimpl.formsemestre_id
|
||||
formsemestre = modimpl.formsemestre
|
||||
module: Module = modimpl.module
|
||||
sem_ues = formsemestre.get_ues(with_sport=False)
|
||||
is_malus = mod["module_type"] == ModuleType.MALUS
|
||||
is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE)
|
||||
is_malus = module.module_type == ModuleType.MALUS
|
||||
is_apc = module.module_type in (ModuleType.RESSOURCE, ModuleType.SAE)
|
||||
preferences = sco_preferences.SemPreferences(formsemestre.id)
|
||||
can_edit_poids = not preferences["but_disable_edit_poids_evaluations"]
|
||||
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible
|
||||
@ -98,9 +90,11 @@ def evaluation_create_form(
|
||||
# création nouvel
|
||||
if moduleimpl_id is None:
|
||||
raise ValueError("missing moduleimpl_id parameter")
|
||||
numeros = [(e.numero or 0) for e in modimpl.evaluations]
|
||||
initvalues = {
|
||||
"note_max": 20,
|
||||
"jour": time.strftime("%d/%m/%Y", time.localtime()),
|
||||
"note_max": 20,
|
||||
"numero": (max(numeros) + 1) if numeros else 0,
|
||||
"publish_incomplete": is_malus,
|
||||
"visibulletin": 1,
|
||||
}
|
||||
@ -128,18 +122,7 @@ def evaluation_create_form(
|
||||
min_note_max_str = scu.fmt_note(min_note_max)
|
||||
else:
|
||||
min_note_max_str = "0"
|
||||
#
|
||||
H = [
|
||||
f"""<h3>{action} en
|
||||
{scu.MODULE_TYPE_NAMES[mod["module_type"]]} <a class="stdlink" href="{
|
||||
url_for("notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
||||
}">{mod["code"] or "module sans code"} {mod["titre"]}</a> {link}</h3>
|
||||
"""
|
||||
]
|
||||
|
||||
heures = [f"{h:02d}h{m:02d}" for h in range(8, 19) for m in (0, 30)]
|
||||
#
|
||||
initvalues["coefficient"] = initvalues.get("coefficient", 1.0)
|
||||
vals = scu.get_request_args()
|
||||
#
|
||||
@ -164,6 +147,7 @@ def evaluation_create_form(
|
||||
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
|
||||
("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
|
||||
("moduleimpl_id", {"default": moduleimpl_id, "input_type": "hidden"}),
|
||||
("numero", {"default": initvalues["numero"], "input_type": "hidden"}),
|
||||
(
|
||||
"jour",
|
||||
{
|
||||
@ -178,9 +162,7 @@ def evaluation_create_form(
|
||||
{
|
||||
"title": "Heure de début",
|
||||
"explanation": "heure du début de l'épreuve",
|
||||
"input_type": "menu",
|
||||
"allowed_values": heures,
|
||||
"labels": heures,
|
||||
"input_type": "time",
|
||||
},
|
||||
),
|
||||
(
|
||||
@ -188,9 +170,7 @@ def evaluation_create_form(
|
||||
{
|
||||
"title": "Heure de fin",
|
||||
"explanation": "heure de fin de l'épreuve",
|
||||
"input_type": "menu",
|
||||
"allowed_values": heures,
|
||||
"labels": heures,
|
||||
"input_type": "time",
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -303,7 +283,7 @@ def evaluation_create_form(
|
||||
"coef. mod.:" +str(coef_ue) if coef_ue
|
||||
else "ce module n'a pas de coef. dans cette UE"
|
||||
})</span>
|
||||
<span class="eval_coef_ue_titre">{ue.titre}</span>
|
||||
<span class="eval_coef_ue_titre">{ue.titre or ''}</span>
|
||||
""",
|
||||
"allow_null": False,
|
||||
# ok si poids nul ou coef vers l'UE nul:
|
||||
@ -327,6 +307,16 @@ def evaluation_create_form(
|
||||
dest_url = url_for(
|
||||
"notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id
|
||||
)
|
||||
H = [
|
||||
f"""<h3>{action} en
|
||||
{scu.MODULE_TYPE_NAMES[module.module_type]} <a class="stdlink" href="{
|
||||
url_for("notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
||||
}">{module.code or "module sans code"} {
|
||||
module.titre or module.abbrev or "(sans titre)"
|
||||
}</a> {link}</h3>
|
||||
"""
|
||||
]
|
||||
if tf[0] == 0:
|
||||
head = html_sco_header.sco_header(page_title=page_title)
|
||||
return (
|
||||
@ -335,6 +325,7 @@ def evaluation_create_form(
|
||||
+ "\n"
|
||||
+ tf[1]
|
||||
+ render_template("scodoc/help/evaluations.j2", is_apc=is_apc)
|
||||
+ render_template("sco_timepicker.j2")
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
elif tf[0] == -1:
|
||||
|
@ -40,7 +40,7 @@ from app import db
|
||||
from app.auth.models import User
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Evaluation, FormSemestre
|
||||
from app.models import Evaluation, FormSemestre, ModuleImpl
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
@ -280,86 +280,21 @@ def do_evaluation_etat(
|
||||
}
|
||||
|
||||
|
||||
def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
|
||||
"""Liste les évaluations de tous les modules de ce semestre.
|
||||
Triée par module, numero desc, date_debut desc
|
||||
Donne pour chaque eval son état (voir do_evaluation_etat)
|
||||
{ evaluation_id,nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif ... }
|
||||
|
||||
Exemple:
|
||||
[ {
|
||||
'coefficient': 1.0,
|
||||
'description': 'QCM et cas pratiques',
|
||||
'etat': {
|
||||
'evalattente': False,
|
||||
'evalcomplete': True,
|
||||
'evaluation_id': 'GEAEVAL82883',
|
||||
'gr_incomplets': [],
|
||||
'gr_moyennes': [{
|
||||
'gr_median': '12.00', # sur 20
|
||||
'gr_moy': '11.88',
|
||||
'gr_nb_att': 0,
|
||||
'gr_nb_notes': 166,
|
||||
'group_id': 'GEAG266762',
|
||||
'group_name': None
|
||||
}],
|
||||
'groups': {'GEAG266762': {'etudid': 'GEAEID80603',
|
||||
'group_id': 'GEAG266762',
|
||||
'group_name': None,
|
||||
'partition_id': 'GEAP266761'}
|
||||
},
|
||||
'last_modif': datetime.datetime(2015, 12, 3, 15, 15, 16),
|
||||
'median': '12.00',
|
||||
'moy': '11.84',
|
||||
'nb_abs': 2,
|
||||
'nb_att': 0,
|
||||
'nb_inscrits': 166,
|
||||
'nb_neutre': 0,
|
||||
'nb_notes': 168,
|
||||
'nb_notes_total': 169
|
||||
},
|
||||
'evaluation_id': 'GEAEVAL82883',
|
||||
'evaluation_type': 0,
|
||||
'heure_debut': datetime.time(8, 0),
|
||||
'heure_fin': datetime.time(9, 30),
|
||||
'jour': datetime.date(2015, 11, 3), // vide => 1/1/1900
|
||||
'moduleimpl_id': 'GEAMIP80490',
|
||||
'note_max': 20.0,
|
||||
'numero': 0,
|
||||
'publish_incomplete': 0,
|
||||
'visibulletin': 1} ]
|
||||
|
||||
"""
|
||||
req = """SELECT E.id AS evaluation_id, E.*
|
||||
FROM notes_evaluation E, notes_moduleimpl MI
|
||||
WHERE MI.formsemestre_id = %(formsemestre_id)s
|
||||
and MI.id = E.moduleimpl_id
|
||||
ORDER BY MI.id, numero desc, date_debut desc
|
||||
"""
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
res = cursor.dictfetchall()
|
||||
# etat de chaque evaluation:
|
||||
for r in res:
|
||||
if with_etat:
|
||||
r["etat"] = do_evaluation_etat(r["evaluation_id"])
|
||||
r["jour"] = r["date_debut"] or datetime.date(1900, 1, 1)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def _eval_etat(evals):
|
||||
"""evals: list of mappings (etats)
|
||||
-> nb_eval_completes, nb_evals_en_cours,
|
||||
nb_evals_vides, date derniere modif
|
||||
def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
|
||||
"""Synthétise les états d'une liste d'évaluations
|
||||
evals: list of mappings (etats),
|
||||
utilise e["etat"]["evalcomplete"], e["etat"]["nb_notes"], e["etat"]["last_modif"]
|
||||
->
|
||||
nb_eval_completes (= prises en compte)
|
||||
nb_evals_en_cours (= avec des notes, mais pas complete)
|
||||
nb_evals_vides (= sans aucune note)
|
||||
date derniere modif
|
||||
|
||||
Une eval est "complete" ssi tous les etudiants *inscrits* ont une note.
|
||||
|
||||
"""
|
||||
nb_evals_completes, nb_evals_en_cours, nb_evals_vides = 0, 0, 0
|
||||
dates = []
|
||||
for e in evals:
|
||||
for e in etat_evals:
|
||||
if e["etat"]["evalcomplete"]:
|
||||
nb_evals_completes += 1
|
||||
elif e["etat"]["nb_notes"] == 0:
|
||||
@ -370,11 +305,8 @@ def _eval_etat(evals):
|
||||
if last_modif is not None:
|
||||
dates.append(e["etat"]["last_modif"])
|
||||
|
||||
if dates:
|
||||
dates = scu.sort_dates(dates)
|
||||
last_modif = dates[-1] # date de derniere modif d'une note dans un module
|
||||
else:
|
||||
last_modif = ""
|
||||
# date de derniere modif d'une note dans un module
|
||||
last_modif = sorted(dates)[-1] if dates else ""
|
||||
|
||||
return {
|
||||
"nb_evals_completes": nb_evals_completes,
|
||||
@ -384,37 +316,42 @@ def _eval_etat(evals):
|
||||
}
|
||||
|
||||
|
||||
def do_evaluation_etat_in_sem(formsemestre_id):
|
||||
"""-> nb_eval_completes, nb_evals_en_cours, nb_evals_vides,
|
||||
date derniere modif, attente
|
||||
|
||||
XXX utilisé par
|
||||
- formsemestre_status_head
|
||||
- gen_formsemestre_recapcomplet_xml
|
||||
- gen_formsemestre_recapcomplet_json
|
||||
|
||||
"nb_evals_completes"
|
||||
"nb_evals_en_cours"
|
||||
"nb_evals_vides"
|
||||
"date_derniere_note"
|
||||
"last_modif"
|
||||
"attente"
|
||||
def do_evaluation_etat_in_sem(formsemestre: FormSemestre) -> dict:
|
||||
"""-> { nb_eval_completes, nb_evals_en_cours, nb_evals_vides,
|
||||
date derniere modif, attente }
|
||||
"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
# Note: utilisé par
|
||||
# - formsemestre_status_head
|
||||
# nb_evals_completes, nb_evals_en_cours, nb_evals_vides, last_modif
|
||||
# pour la ligne
|
||||
# Évaluations: 20 ok, 8 en cours, 5 vides (dernière note saisie le 11/01/2024 à 19h49)
|
||||
# attente
|
||||
#
|
||||
# - gen_formsemestre_recapcomplet_xml
|
||||
# - gen_formsemestre_recapcomplet_json
|
||||
# nb_evals_completes, nb_evals_en_cours, nb_evals_vides, last_modif
|
||||
#
|
||||
# "nb_evals_completes"
|
||||
# "nb_evals_en_cours"
|
||||
# "nb_evals_vides"
|
||||
# "last_modif"
|
||||
# "attente"
|
||||
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
evals = nt.get_evaluations_etats()
|
||||
etat = _eval_etat(evals)
|
||||
evaluations_etats = nt.get_evaluations_etats()
|
||||
# raccordement moche...
|
||||
etat = _summarize_evals_etats([{"etat": v} for v in evaluations_etats.values()])
|
||||
# Ajoute information sur notes en attente
|
||||
etat["attente"] = len(nt.get_moduleimpls_attente()) > 0
|
||||
return etat
|
||||
|
||||
|
||||
def do_evaluation_etat_in_mod(nt, moduleimpl_id):
|
||||
def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl):
|
||||
"""état des évaluations dans ce module"""
|
||||
evals = nt.get_mod_evaluation_etat_list(moduleimpl_id)
|
||||
etat = _eval_etat(evals)
|
||||
etat_evals = nt.get_mod_evaluation_etat_list(modimpl)
|
||||
etat = _summarize_evals_etats(etat_evals)
|
||||
# Il y a-t-il des notes en attente dans ce module ?
|
||||
etat["attente"] = nt.modimpls_results[moduleimpl_id].en_attente
|
||||
etat["attente"] = nt.modimpls_results[modimpl.id].en_attente
|
||||
return etat
|
||||
|
||||
|
||||
@ -691,7 +628,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True)
|
||||
group_ids=group_id,
|
||||
evaluation_id=evaluation.id,
|
||||
date_debut=evaluation.date_debut.isoformat(),
|
||||
date_fin=evaluation.date_fin.isoformat(),
|
||||
date_fin=evaluation.date_fin.isoformat() if evaluation.date_fin else "",
|
||||
)
|
||||
}">absences ce jour</a>
|
||||
</span>
|
||||
|
@ -230,3 +230,15 @@ class APIInvalidParams(Exception):
|
||||
|
||||
class ScoFormationConflict(Exception):
|
||||
"""Conflit cohérence formation (APC)"""
|
||||
|
||||
|
||||
class ScoTemporaryError(ScoValueError):
|
||||
"""Erreurs temporaires rarissimes (caches ?)"""
|
||||
|
||||
def __init__(self, msg: str = ""):
|
||||
msg = """
|
||||
<p>"Erreur temporaire</p>
|
||||
<p>Veuillez ré-essayer. Si le problème persiste, merci de contacter l'assistance ScoDoc
|
||||
</p>
|
||||
"""
|
||||
super().__init__(msg)
|
||||
|
@ -173,9 +173,10 @@ def _build_results_list(dpv_by_sem, etuds_infos):
|
||||
"nom_usuel": etud["nom_usuel"],
|
||||
"prenom": etud["prenom"],
|
||||
"civilite_str": etud["civilite_str"],
|
||||
"_nom_target": "%s"
|
||||
% url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
"_nom_td_attrs": 'id="%s" class="etudinfo"' % etudid,
|
||||
"_nom_target": url_for(
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
),
|
||||
"_nom_td_attrs": f'id="{etudid}" class="etudinfo"',
|
||||
"bac": bac.abbrev(),
|
||||
"parcours": dec["parcours"],
|
||||
}
|
||||
|
@ -145,7 +145,7 @@ def search_etud_in_dept(expnom=""):
|
||||
if "dest_url" in vals:
|
||||
endpoint = vals["dest_url"]
|
||||
else:
|
||||
endpoint = "scolar.ficheEtud"
|
||||
endpoint = "scolar.fiche_etud"
|
||||
if "parameters_keys" in vals:
|
||||
for key in vals["parameters_keys"].split(","):
|
||||
url_args[key] = vals[key]
|
||||
@ -328,8 +328,9 @@ def table_etud_in_accessible_depts(expnom=None):
|
||||
"""
|
||||
result, accessible_depts = search_etud_in_accessible_depts(expnom=expnom)
|
||||
H = [
|
||||
"""<div class="table_etud_in_accessible_depts">""",
|
||||
"""<h3>Recherche multi-département de "<tt>%s</tt>"</h3>""" % expnom,
|
||||
f"""<div class="table_etud_in_accessible_depts">
|
||||
<h3>Recherche multi-département de "<tt>{expnom}</tt>"</h3>
|
||||
""",
|
||||
]
|
||||
for etuds in result:
|
||||
if etuds:
|
||||
@ -337,9 +338,9 @@ def table_etud_in_accessible_depts(expnom=None):
|
||||
# H.append('<h3>Département %s</h3>' % DeptId)
|
||||
for e in etuds:
|
||||
e["_nomprenom_target"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=dept_id, etudid=e["etudid"]
|
||||
"scolar.fiche_etud", scodoc_dept=dept_id, etudid=e["etudid"]
|
||||
)
|
||||
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
|
||||
e["_nomprenom_td_attrs"] = f"""id="{e['etudid']}" class="etudinfo" """
|
||||
|
||||
tab = GenTable(
|
||||
titles={"nomprenom": "Étudiants en " + dept_id},
|
||||
|
@ -103,6 +103,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response:
|
||||
"heures_cours": mod.heures_cours,
|
||||
"heures_td": mod.heures_td,
|
||||
"heures_tp": mod.heures_tp,
|
||||
"tags": ", ".join(t.title for t in mod.tags if t.title),
|
||||
"_css_row_class": f"mod {mod.type_abbrv()}",
|
||||
}
|
||||
)
|
||||
@ -117,6 +118,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response:
|
||||
"heures_cours",
|
||||
"heures_td",
|
||||
"heures_tp",
|
||||
"tags",
|
||||
]
|
||||
if not formation.is_apc():
|
||||
columns_ids.insert(columns_ids.index("ects"), "coef")
|
||||
@ -132,6 +134,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response:
|
||||
"heures_cours": "Cours (h)",
|
||||
"heures_td": "TD (h)",
|
||||
"heures_tp": "TP (h)",
|
||||
"tags": "Tags",
|
||||
"ects": "ECTS",
|
||||
}
|
||||
|
||||
|
@ -168,16 +168,14 @@ def formsemestre_associate_new_version(
|
||||
formation_id=new_formation_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
else:
|
||||
raise ScoValueError("Méthode invalide")
|
||||
)
|
||||
raise ScoValueError("Méthode invalide")
|
||||
|
||||
|
||||
def do_formsemestres_associate_new_version(
|
||||
|
@ -117,6 +117,7 @@ def formation_export_dict(
|
||||
ues = ues.all()
|
||||
ues.sort(key=lambda u: (u.semestre_idx or 0, u.numero or 0, u.acronyme))
|
||||
f_dict["ue"] = []
|
||||
ue: UniteEns
|
||||
for ue in ues:
|
||||
ue_dict = ue.to_dict()
|
||||
f_dict["ue"].append(ue_dict)
|
||||
@ -142,8 +143,8 @@ def formation_export_dict(
|
||||
|
||||
if not export_codes_apo:
|
||||
ue_dict.pop("code_apogee", None)
|
||||
if ue_dict["ects"] is None:
|
||||
del ue_dict["ects"]
|
||||
if ue_dict.get("ects") is None:
|
||||
ue_dict.pop("ects", None)
|
||||
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
|
||||
mats.sort(key=lambda m: m["numero"] or 0)
|
||||
ue_dict["matiere"] = mats
|
||||
@ -363,7 +364,9 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
ue_info[1]["niveau_competence_id"] = _formation_retreive_apc_niveau(
|
||||
referentiel_competence_id, ue_info[1]
|
||||
)
|
||||
ue_id = sco_edit_ue.do_ue_create(ue_info[1])
|
||||
# Note: si le code est indiqué "" dans le xml, il faut le conserver vide
|
||||
# pour la comparaison ultérieure des formations XXX
|
||||
ue_id = sco_edit_ue.do_ue_create(ue_info[1], allow_empty_ue_code=True)
|
||||
ue: UniteEns = db.session.get(UniteEns, ue_id)
|
||||
assert ue
|
||||
if xml_ue_id:
|
||||
|
@ -241,7 +241,7 @@ def do_formsemestre_create(args, silent=False):
|
||||
write_formsemestre_etapes(args)
|
||||
if args["responsables"]:
|
||||
args["formsemestre_id"] = formsemestre_id
|
||||
write_formsemestre_responsables(args)
|
||||
_write_formsemestre_responsables(args)
|
||||
|
||||
# create default partition
|
||||
partition_id = sco_groups.partition_create(
|
||||
@ -275,7 +275,7 @@ def do_formsemestre_edit(sem, cnx=None, **kw):
|
||||
|
||||
_formsemestreEditor.edit(cnx, sem, **kw)
|
||||
write_formsemestre_etapes(sem)
|
||||
write_formsemestre_responsables(sem)
|
||||
_write_formsemestre_responsables(sem)
|
||||
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=sem["formsemestre_id"]
|
||||
@ -296,8 +296,12 @@ def read_formsemestre_responsables(formsemestre_id: int) -> list[int]: # py3.9+
|
||||
return [x["responsable_id"] for x in r]
|
||||
|
||||
|
||||
def write_formsemestre_responsables(sem):
|
||||
return _write_formsemestre_aux(sem, "responsables", "responsable_id")
|
||||
def _write_formsemestre_responsables(sem): # TODO old, à ré-écrire avec models
|
||||
if sem and "responsables" in sem:
|
||||
sem["responsables"] = [
|
||||
uid for uid in sem["responsables"] if (uid is not None) and (uid != -1)
|
||||
]
|
||||
_write_formsemestre_aux(sem, "responsables", "responsable_id")
|
||||
|
||||
|
||||
# ---------------------- Coefs des UE
|
||||
@ -362,10 +366,11 @@ def read_formsemestre_etapes(formsemestre_id): # OBSOLETE
|
||||
return [ApoEtapeVDI(x["etape_apo"]) for x in r if x["etape_apo"]]
|
||||
|
||||
|
||||
def write_formsemestre_etapes(sem):
|
||||
def write_formsemestre_etapes(sem): # TODO old, à ré-écrire avec models
|
||||
return _write_formsemestre_aux(sem, "etapes", "etape_apo")
|
||||
|
||||
|
||||
# TODO old, à ré-écrire avec models
|
||||
def _write_formsemestre_aux(sem, fieldname, valuename):
|
||||
"""fieldname: 'etapes' ou 'responsables'
|
||||
valuename: 'etape_apo' ou 'responsable_id'
|
||||
|
@ -577,7 +577,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
{
|
||||
"input_type": "boolcheckbox",
|
||||
"title": "Bloquer moyennes",
|
||||
"explanation": "empêcher le calcul des moyennes d'UE et générale.",
|
||||
"explanation": "empêcher le calcul des moyennes d'UE et générale (en BUT, empèche sa prise en compte dans les jurys annuels)",
|
||||
},
|
||||
),
|
||||
(
|
||||
@ -857,17 +857,20 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
tf[2]["bul_hide_xml"] = True
|
||||
# remap les identifiants de responsables:
|
||||
for field in resp_fields:
|
||||
tf[2][field] = User.get_user_id_from_nomplogin(tf[2][field])
|
||||
resp = User.get_user_from_nomplogin(tf[2][field])
|
||||
tf[2][field] = resp.id if resp else -1
|
||||
tf[2]["responsables"] = []
|
||||
for field in resp_fields:
|
||||
if tf[2][field]:
|
||||
tf[2]["responsables"].append(tf[2][field])
|
||||
for module_id in tf[2]["tf-checked"]:
|
||||
mod_resp_id = User.get_user_id_from_nomplogin(tf[2][module_id])
|
||||
if mod_resp_id is None:
|
||||
mod_resp = User.get_user_from_nomplogin(tf[2][module_id])
|
||||
if mod_resp is None:
|
||||
# Si un module n'a pas de responsable (ou inconnu),
|
||||
# l'affecte au 1er directeur des etudes:
|
||||
mod_resp_id = tf[2]["responsable_id"]
|
||||
else:
|
||||
mod_resp_id = mod_resp.id
|
||||
tf[2][module_id] = mod_resp_id
|
||||
|
||||
# etapes:
|
||||
@ -1227,9 +1230,12 @@ def formsemestre_clone(formsemestre_id):
|
||||
"formsemestre_status?formsemestre_id=%s" % formsemestre_id
|
||||
)
|
||||
else:
|
||||
resp = User.get_user_from_nomplogin(tf[2]["responsable_id"])
|
||||
if not resp:
|
||||
raise ScoValueError("id responsable invalide")
|
||||
new_formsemestre_id = do_formsemestre_clone(
|
||||
formsemestre_id,
|
||||
User.get_user_id_from_nomplogin(tf[2]["responsable_id"]),
|
||||
resp.id,
|
||||
tf[2]["date_debut"],
|
||||
tf[2]["date_fin"],
|
||||
clone_evaluations=tf[2]["clone_evaluations"],
|
||||
@ -1273,29 +1279,24 @@ def do_formsemestre_clone(
|
||||
log(f"created formsemestre {formsemestre_id}")
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
# 2- create moduleimpls
|
||||
mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id)
|
||||
for mod_orig in mods_orig:
|
||||
args = mod_orig.copy()
|
||||
modimpl_orig: ModuleImpl
|
||||
for modimpl_orig in formsemestre_orig.modimpls:
|
||||
args = modimpl_orig.to_dict(with_module=False)
|
||||
args["formsemestre_id"] = formsemestre_id
|
||||
mid = sco_moduleimpl.do_moduleimpl_create(args)
|
||||
# copy notes_modules_enseignants
|
||||
ens = sco_moduleimpl.do_ens_list(
|
||||
args={"moduleimpl_id": mod_orig["moduleimpl_id"]}
|
||||
)
|
||||
for e in ens:
|
||||
args = e.copy()
|
||||
args["moduleimpl_id"] = mid
|
||||
sco_moduleimpl.do_ens_create(args)
|
||||
modimpl_new = ModuleImpl.create_from_dict(args)
|
||||
db.session.flush()
|
||||
# copy enseignants
|
||||
for ens in modimpl_orig.enseignants:
|
||||
modimpl_new.enseignants.append(ens)
|
||||
db.session.add(modimpl_new)
|
||||
# optionally, copy evaluations
|
||||
if clone_evaluations:
|
||||
for e in Evaluation.query.filter_by(
|
||||
moduleimpl_id=mod_orig["moduleimpl_id"]
|
||||
):
|
||||
for e in Evaluation.query.filter_by(moduleimpl_id=modimpl_orig.id):
|
||||
# copie en enlevant la date
|
||||
new_eval = e.clone(
|
||||
not_copying=("date_debut", "date_fin", "moduleimpl_id")
|
||||
)
|
||||
new_eval.moduleimpl_id = mid
|
||||
new_eval.moduleimpl_id = modimpl_new.id
|
||||
# Copie les poids APC de l'évaluation
|
||||
new_eval.set_ue_poids_dict(e.get_ue_poids_dict())
|
||||
db.session.commit()
|
||||
@ -1592,7 +1593,9 @@ def formsemestre_edit_options(formsemestre_id):
|
||||
ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
|
||||
if not ok:
|
||||
return err
|
||||
return sco_preferences.SemPreferences(formsemestre_id).edit(categories=["bul"])
|
||||
return sco_preferences.SemPreferences(formsemestre_id).edit(
|
||||
categories=["bul", "bul_but_pdf"]
|
||||
)
|
||||
|
||||
|
||||
def formsemestre_change_publication_bul(
|
||||
|
@ -102,7 +102,7 @@ def formsemestre_ext_create_form(etudid, formsemestre_id):
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid, only_ext=1) }">
|
||||
inscrire à un autre semestre</a>"
|
||||
</p>
|
||||
<h3><a href="{ url_for('scolar.ficheEtud',
|
||||
<h3><a href="{ url_for('scolar.fiche_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}" class="stdlink">Étudiant {etud.nomprenom}</a></h3>
|
||||
""",
|
||||
@ -221,7 +221,7 @@ def formsemestre_ext_create_form(etudid, formsemestre_id):
|
||||
tf[2]["formation_id"] = orig_sem["formation_id"]
|
||||
formsemestre_ext_create(etudid, tf[2])
|
||||
return flask.redirect(
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
|
||||
|
||||
@ -258,13 +258,17 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid):
|
||||
submitlabel="Enregistrer ces validations",
|
||||
cancelbutton="Annuler",
|
||||
initvalues=initvalues,
|
||||
cssclass="tf_ext_edit_ue_validations ext_apc"
|
||||
if formsemestre.formation.is_apc()
|
||||
else "tf_ext_edit_ue_validations",
|
||||
cssclass=(
|
||||
"tf_ext_edit_ue_validations ext_apc"
|
||||
if formsemestre.formation.is_apc()
|
||||
else "tf_ext_edit_ue_validations"
|
||||
),
|
||||
# En APC, stocke les coefficients pour l'affichage de la moyenne en direct
|
||||
form_attrs=f"""data-ue_coefs='[{', '.join(str(ue.ects or 0) for ue in ues)}]'"""
|
||||
if formsemestre.formation.is_apc()
|
||||
else "",
|
||||
form_attrs=(
|
||||
f"""data-ue_coefs='[{', '.join(str(ue.ects or 0) for ue in ues)}]'"""
|
||||
if formsemestre.formation.is_apc()
|
||||
else ""
|
||||
),
|
||||
)
|
||||
if tf[0] == -1:
|
||||
return "<h4>annulation</h4>"
|
||||
@ -421,12 +425,18 @@ def _ue_form_description(
|
||||
"input_type": "text",
|
||||
"size": 4,
|
||||
"template": itemtemplate,
|
||||
"title": "<tt>"
|
||||
+ (f"S{ue.semestre_idx} " if ue.semestre_idx is not None else "")
|
||||
+ f"<b>{ue.acronyme}</b></tt> {ue.titre}"
|
||||
+ f" ({ue.ects} ECTS)"
|
||||
if ue.ects is not None
|
||||
else "",
|
||||
"title": (
|
||||
"<tt>"
|
||||
+ (
|
||||
f"S{ue.semestre_idx} "
|
||||
if ue.semestre_idx is not None
|
||||
else ""
|
||||
)
|
||||
+ f"<b>{ue.acronyme}</b></tt> {ue.titre or ''}"
|
||||
+ f" ({ue.ects} ECTS)"
|
||||
if ue.ects is not None
|
||||
else ""
|
||||
),
|
||||
"attributes": [coef_disabled],
|
||||
},
|
||||
)
|
||||
|
@ -400,7 +400,7 @@ def formsemestre_inscription_with_modules_form(etudid, only_ext=False):
|
||||
H.append("<p>aucune session de formation !</p>")
|
||||
H.append(
|
||||
f"""<h3>ou</h3> <a class="stdlink" href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">retour à la fiche de {etud.nomprenom}</a>"""
|
||||
)
|
||||
return "\n".join(H) + footer
|
||||
@ -440,7 +440,7 @@ def formsemestre_inscription_with_modules(
|
||||
dans le semestre {formsemestre.titre_mois()}
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="{url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
<li><a href="{url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}" class="stdlink">retour à la fiche de {etud.nomprenom}</a>
|
||||
</li>
|
||||
<li><a href="{url_for(
|
||||
@ -501,7 +501,7 @@ def formsemestre_inscription_with_modules(
|
||||
method="formsemestre_inscription_with_modules",
|
||||
)
|
||||
return flask.redirect(
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
else:
|
||||
# formulaire choix groupe
|
||||
@ -656,7 +656,7 @@ function chkbx_select(field_id, state) {
|
||||
return "\n".join(H) + "\n" + tf[1] + footer
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
else:
|
||||
# Inscriptions aux modules choisis
|
||||
@ -697,7 +697,7 @@ function chkbx_select(field_id, state) {
|
||||
"""<h3>Aucune modification à effectuer</h3>
|
||||
<p><a class="stdlink" href="%s">retour à la fiche étudiant</a></p>
|
||||
"""
|
||||
% url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
% url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
return "\n".join(H) + footer
|
||||
|
||||
@ -755,7 +755,7 @@ function chkbx_select(field_id, state) {
|
||||
etudid,
|
||||
modulesimpls_ainscrire,
|
||||
modulesimpls_adesinscrire,
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
)
|
||||
)
|
||||
return "\n".join(H) + footer
|
||||
@ -820,7 +820,7 @@ def do_moduleimpl_incription_options(
|
||||
<p><a class="stdlink" href="%s">
|
||||
Retour à la fiche étudiant</a></p>
|
||||
"""
|
||||
% url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
% url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
html_sco_header.sco_footer(),
|
||||
]
|
||||
return "\n".join(H)
|
||||
@ -885,7 +885,7 @@ def formsemestre_inscrits_ailleurs(formsemestre_id):
|
||||
'<li><a href="%s" class="discretelink">%s</a> : '
|
||||
% (
|
||||
url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud["etudid"],
|
||||
),
|
||||
|
@ -51,13 +51,14 @@ from app.models import (
|
||||
NotesNotes,
|
||||
)
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import (
|
||||
ScoValueError,
|
||||
ScoInvalidIdType,
|
||||
)
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import sco_archives_formsemestre
|
||||
@ -75,12 +76,14 @@ from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_users
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.html_sidebar import retreive_formsemestre_from_request
|
||||
from app.scodoc.sco_formsemestre_custommenu import formsemestre_custommenu_html
|
||||
import sco_version
|
||||
|
||||
|
||||
def _build_menu_stats(formsemestre_id):
|
||||
def _build_menu_stats(formsemestre: FormSemestre):
|
||||
"Définition du menu 'Statistiques'"
|
||||
formsemestre_id = formsemestre.id
|
||||
return [
|
||||
{
|
||||
"title": "Statistiques...",
|
||||
@ -109,7 +112,7 @@ def _build_menu_stats(formsemestre_id):
|
||||
"title": "Lycées d'origine",
|
||||
"endpoint": "notes.formsemestre_etuds_lycees",
|
||||
"args": {"formsemestre_id": formsemestre_id},
|
||||
"enabled": True,
|
||||
"enabled": current_user.has_permission(Permission.ViewEtudData),
|
||||
},
|
||||
{
|
||||
"title": 'Table "poursuite"',
|
||||
@ -121,7 +124,8 @@ def _build_menu_stats(formsemestre_id):
|
||||
"title": "Documents Avis Poursuite Etudes (xp)",
|
||||
"endpoint": "notes.pe_view_sem_recap",
|
||||
"args": {"formsemestre_id": formsemestre_id},
|
||||
"enabled": True, # current_app.config["TESTING"] or current_app.config["DEBUG"],
|
||||
"enabled": formsemestre.formation.is_apc(),
|
||||
# current_app.config["TESTING"] or current_app.config["DEBUG"],
|
||||
},
|
||||
{
|
||||
"title": 'Table "débouchés"',
|
||||
@ -277,9 +281,11 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
|
||||
|
||||
menu_inscriptions = [
|
||||
{
|
||||
"title": "Gérer les inscriptions aux UE et modules"
|
||||
if formsemestre.formation.is_apc()
|
||||
else "Gérer les inscriptions aux modules",
|
||||
"title": (
|
||||
"Gérer les inscriptions aux UE et modules"
|
||||
if formsemestre.formation.is_apc()
|
||||
else "Gérer les inscriptions aux modules"
|
||||
),
|
||||
"endpoint": "notes.moduleimpl_inscriptions_stats",
|
||||
"args": {"formsemestre_id": formsemestre_id},
|
||||
}
|
||||
@ -336,6 +342,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
|
||||
formsemestre_id, fix_if_missing=True
|
||||
),
|
||||
},
|
||||
"enabled": current_user.has_permission(Permission.ViewEtudData),
|
||||
},
|
||||
{
|
||||
"title": "Vérifier inscriptions multiples",
|
||||
@ -459,7 +466,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
|
||||
},
|
||||
]
|
||||
|
||||
menu_stats = _build_menu_stats(formsemestre_id)
|
||||
menu_stats = _build_menu_stats(formsemestre)
|
||||
H = [
|
||||
'<ul id="sco_menu">',
|
||||
htmlutils.make_menu("Semestre", menu_semestre),
|
||||
@ -474,57 +481,6 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def retreive_formsemestre_from_request() -> int:
|
||||
"""Cherche si on a de quoi déduire le semestre affiché à partir des
|
||||
arguments de la requête:
|
||||
formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
|
||||
Returns None si pas défini.
|
||||
"""
|
||||
if request.method == "GET":
|
||||
args = request.args
|
||||
elif request.method == "POST":
|
||||
args = request.form
|
||||
else:
|
||||
return None
|
||||
formsemestre_id = None
|
||||
# Search formsemestre
|
||||
group_ids = args.get("group_ids", [])
|
||||
if "formsemestre_id" in args:
|
||||
formsemestre_id = args["formsemestre_id"]
|
||||
elif "moduleimpl_id" in args and args["moduleimpl_id"]:
|
||||
modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=args["moduleimpl_id"])
|
||||
if not modimpl:
|
||||
return None # suppressed ?
|
||||
modimpl = modimpl[0]
|
||||
formsemestre_id = modimpl["formsemestre_id"]
|
||||
elif "evaluation_id" in args:
|
||||
E = sco_evaluation_db.get_evaluations_dict(
|
||||
{"evaluation_id": args["evaluation_id"]}
|
||||
)
|
||||
if not E:
|
||||
return None # evaluation suppressed ?
|
||||
E = E[0]
|
||||
modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
formsemestre_id = modimpl["formsemestre_id"]
|
||||
elif "group_id" in args:
|
||||
group = sco_groups.get_group(args["group_id"])
|
||||
formsemestre_id = group["formsemestre_id"]
|
||||
elif group_ids:
|
||||
if isinstance(group_ids, str):
|
||||
group_ids = group_ids.split(",")
|
||||
group_id = group_ids[0]
|
||||
group = sco_groups.get_group(group_id)
|
||||
formsemestre_id = group["formsemestre_id"]
|
||||
elif "partition_id" in args:
|
||||
partition = sco_groups.get_partition(args["partition_id"])
|
||||
formsemestre_id = partition["formsemestre_id"]
|
||||
|
||||
if not formsemestre_id:
|
||||
return None # no current formsemestre
|
||||
|
||||
return int(formsemestre_id)
|
||||
|
||||
|
||||
# Element HTML decrivant un semestre (barre de menu et infos)
|
||||
def formsemestre_page_title(formsemestre_id=None):
|
||||
"""Element HTML decrivant un semestre (barre de menu et infos)
|
||||
@ -665,17 +621,15 @@ def formsemestre_description_table(
|
||||
if ue.color:
|
||||
for k in list(ue_info.keys()):
|
||||
if not k.startswith("_"):
|
||||
ue_info[
|
||||
f"_{k}_td_attrs"
|
||||
] = f'style="background-color: {ue.color} !important;"'
|
||||
ue_info[f"_{k}_td_attrs"] = (
|
||||
f'style="background-color: {ue.color} !important;"'
|
||||
)
|
||||
if not is_apc:
|
||||
# n'affiche la ligne UE qu'en formation classique
|
||||
# car l'UE de rattachement n'a pas d'intérêt en BUT
|
||||
rows.append(ue_info)
|
||||
|
||||
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||
moduleimpl_id=modimpl.id
|
||||
)
|
||||
mod_nb_inscrits = nt.modimpls_results[modimpl.id].nb_inscrits_module
|
||||
enseignants = ", ".join(ens.get_prenomnom() for ens in modimpl.enseignants)
|
||||
|
||||
row = {
|
||||
@ -684,7 +638,7 @@ def formsemestre_description_table(
|
||||
"Code": modimpl.module.code or "",
|
||||
"Module": modimpl.module.abbrev or modimpl.module.titre,
|
||||
"_Module_class": "scotext",
|
||||
"Inscrits": len(mod_inscrits),
|
||||
"Inscrits": mod_nb_inscrits,
|
||||
"Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"],
|
||||
"_Responsable_class": "scotext",
|
||||
"Enseignants": enseignants,
|
||||
@ -726,7 +680,7 @@ def formsemestre_description_table(
|
||||
|
||||
if with_evals:
|
||||
# Ajoute lignes pour evaluations
|
||||
evals = nt.get_mod_evaluation_etat_list(modimpl.id)
|
||||
evals = nt.get_mod_evaluation_etat_list(modimpl)
|
||||
evals.reverse() # ordre chronologique
|
||||
# Ajoute etat:
|
||||
eval_rows = []
|
||||
@ -917,7 +871,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
||||
<a class="btn" href="{
|
||||
url_for("assiduites.bilan_dept",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre=formsemestre.id,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
<button>Justificatifs en attente</button></a>
|
||||
@ -988,10 +942,10 @@ def html_expr_diagnostic(diagnostics):
|
||||
|
||||
def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None):
|
||||
"""En-tête HTML des pages "semestre" """
|
||||
sem: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
if not sem:
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
if not formsemestre:
|
||||
raise ScoValueError("Semestre inexistant (il a peut être été supprimé ?)")
|
||||
formation: Formation = sem.formation
|
||||
formation: Formation = formsemestre.formation
|
||||
parcours = formation.get_cursus()
|
||||
|
||||
page_title = page_title or "Modules de "
|
||||
@ -1003,25 +957,25 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
|
||||
f"""<table>
|
||||
<tr><td class="fichetitre2">Formation: </td><td>
|
||||
<a href="{url_for('notes.ue_table',
|
||||
scodoc_dept=g.scodoc_dept, formation_id=sem.formation.id)}"
|
||||
scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)}"
|
||||
class="discretelink" title="Formation {
|
||||
formation.acronyme}, v{formation.version}">{formation.titre}</a>
|
||||
""",
|
||||
]
|
||||
if sem.semestre_id >= 0:
|
||||
H.append(f", {parcours.SESSION_NAME} {sem.semestre_id}")
|
||||
if sem.modalite:
|
||||
H.append(f" en {sem.modalite}")
|
||||
if sem.etapes:
|
||||
if formsemestre.semestre_id >= 0:
|
||||
H.append(f", {parcours.SESSION_NAME} {formsemestre.semestre_id}")
|
||||
if formsemestre.modalite:
|
||||
H.append(f" en {formsemestre.modalite}")
|
||||
if formsemestre.etapes:
|
||||
H.append(
|
||||
f""" (étape <b><tt>{
|
||||
sem.etapes_apo_str() or "-"
|
||||
formsemestre.etapes_apo_str() or "-"
|
||||
}</tt></b>)"""
|
||||
)
|
||||
H.append("</td></tr>")
|
||||
if formation.is_apc():
|
||||
# Affiche les parcours BUT cochés. Si aucun, tous ceux du référentiel.
|
||||
sem_parcours = sem.get_parcours_apc()
|
||||
sem_parcours = formsemestre.get_parcours_apc()
|
||||
H.append(
|
||||
f"""
|
||||
<tr><td class="fichetitre2">Parcours: </td>
|
||||
@ -1030,7 +984,7 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
|
||||
"""
|
||||
)
|
||||
|
||||
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id)
|
||||
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre)
|
||||
H.append(
|
||||
'<tr><td class="fichetitre2">Évaluations: </td><td> %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides'
|
||||
% evals
|
||||
@ -1048,11 +1002,11 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
|
||||
"""<span class="fontred">Il y a des notes en attente !</span>
|
||||
Le classement des étudiants n'a qu'une valeur indicative."""
|
||||
)
|
||||
if sem.bul_hide_xml:
|
||||
if formsemestre.bul_hide_xml:
|
||||
warnings.append("""Bulletins non publiés sur la passerelle.""")
|
||||
if sem.block_moyennes:
|
||||
if formsemestre.block_moyennes:
|
||||
warnings.append("Calcul des moyennes bloqué !")
|
||||
if sem.semestre_id >= 0 and not sem.est_sur_une_annee():
|
||||
if formsemestre.semestre_id >= 0 and not formsemestre.est_sur_une_annee():
|
||||
warnings.append("""<em>Ce semestre couvre plusieurs années scolaires !</em>""")
|
||||
if warnings:
|
||||
H += [
|
||||
@ -1074,18 +1028,14 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
|
||||
# S'assure que les groupes de parcours sont à jour:
|
||||
if int(check_parcours):
|
||||
formsemestre.setup_parcours_groups()
|
||||
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
|
||||
formsemestre_id=formsemestre_id
|
||||
)
|
||||
modimpls = formsemestre.modimpls_sorted
|
||||
nt = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
||||
# Construit la liste de tous les enseignants de ce semestre:
|
||||
mails_enseignants = set(u.email for u in formsemestre.responsables)
|
||||
for modimpl in modimpls:
|
||||
mails_enseignants.add(sco_users.user_info(modimpl["responsable_id"])["email"])
|
||||
mails_enseignants |= set(
|
||||
[sco_users.user_info(m["ens_id"])["email"] for m in modimpl["ens"]]
|
||||
)
|
||||
mails_enseignants.add(sco_users.user_info(modimpl.responsable_id)["email"])
|
||||
mails_enseignants |= {u.email for u in modimpl.enseignants if u.email}
|
||||
|
||||
can_edit = formsemestre.can_be_edited_by(current_user)
|
||||
can_change_all_notes = current_user.has_permission(Permission.EditAllNotes) or (
|
||||
@ -1102,9 +1052,11 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
|
||||
formsemestre_id=formsemestre_id, page_title="Tableau de bord"
|
||||
),
|
||||
formsemestre_warning_apc_setup(formsemestre, nt),
|
||||
formsemestre_warning_etuds_sans_note(formsemestre, nt)
|
||||
if can_change_all_notes
|
||||
else "",
|
||||
(
|
||||
formsemestre_warning_etuds_sans_note(formsemestre, nt)
|
||||
if can_change_all_notes
|
||||
else ""
|
||||
),
|
||||
"""<p style="font-size: 130%"><b>Tableau de bord : </b>""",
|
||||
]
|
||||
if formsemestre.est_courant():
|
||||
@ -1135,13 +1087,13 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
|
||||
if nt.parcours.APC_SAE:
|
||||
# BUT: tableau ressources puis SAE
|
||||
ressources = [
|
||||
m for m in modimpls if m["module"]["module_type"] == ModuleType.RESSOURCE
|
||||
m for m in modimpls if m.module.module_type == ModuleType.RESSOURCE
|
||||
]
|
||||
saes = [m for m in modimpls if m["module"]["module_type"] == ModuleType.SAE]
|
||||
saes = [m for m in modimpls if m.module.module_type == ModuleType.SAE]
|
||||
autres = [
|
||||
m
|
||||
for m in modimpls
|
||||
if m["module"]["module_type"] not in (ModuleType.RESSOURCE, ModuleType.SAE)
|
||||
if m.module.module_type not in (ModuleType.RESSOURCE, ModuleType.SAE)
|
||||
]
|
||||
H += [
|
||||
f"""
|
||||
@ -1182,7 +1134,7 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
|
||||
modimpls_classic = [
|
||||
m
|
||||
for m in modimpls
|
||||
if m["module"]["module_type"] not in (ModuleType.RESSOURCE, ModuleType.SAE)
|
||||
if m.module.module_type not in (ModuleType.RESSOURCE, ModuleType.SAE)
|
||||
]
|
||||
H += [
|
||||
"<p>",
|
||||
@ -1214,8 +1166,10 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
|
||||
adrlist = list(mails_enseignants - {None, ""})
|
||||
if adrlist:
|
||||
H.append(
|
||||
'<p><a class="stdlink" href="mailto:?cc=%s">Courrier aux %d enseignants du semestre</a></p>'
|
||||
% (",".join(adrlist), len(adrlist))
|
||||
f"""<p>
|
||||
<a class="stdlink" href="mailto:?cc={','.join(adrlist)}">Courrier aux {
|
||||
len(adrlist)} enseignants du semestre</a>
|
||||
</p>"""
|
||||
)
|
||||
return "".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
@ -1235,7 +1189,7 @@ _TABLEAU_MODULES_FOOT = """</table>"""
|
||||
|
||||
|
||||
def formsemestre_tableau_modules(
|
||||
modimpls: list[dict],
|
||||
modimpls: list[ModuleImpl],
|
||||
nt,
|
||||
formsemestre: FormSemestre,
|
||||
can_edit=True,
|
||||
@ -1246,11 +1200,11 @@ def formsemestre_tableau_modules(
|
||||
H = []
|
||||
prev_ue_id = None
|
||||
for modimpl in modimpls:
|
||||
mod: Module = db.session.get(Module, modimpl["module_id"])
|
||||
mod: Module = modimpl.module
|
||||
moduleimpl_status_url = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=modimpl["moduleimpl_id"],
|
||||
moduleimpl_id=modimpl.id,
|
||||
)
|
||||
mod_descr = "Module " + (mod.titre or "")
|
||||
if mod.is_apc():
|
||||
@ -1267,54 +1221,52 @@ def formsemestre_tableau_modules(
|
||||
mod_descr += " (pas de coefficients) "
|
||||
else:
|
||||
mod_descr += ", coef. " + str(mod.coefficient)
|
||||
mod_ens = sco_users.user_info(modimpl["responsable_id"])["nomcomplet"]
|
||||
if modimpl["ens"]:
|
||||
mod_ens = sco_users.user_info(modimpl.responsable_id)["nomcomplet"]
|
||||
if modimpl.enseignants.count():
|
||||
mod_ens += " (resp.), " + ", ".join(
|
||||
[sco_users.user_info(e["ens_id"])["nomcomplet"] for e in modimpl["ens"]]
|
||||
[u.get_nomcomplet() for u in modimpl.enseignants]
|
||||
)
|
||||
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||
moduleimpl_id=modimpl["moduleimpl_id"]
|
||||
)
|
||||
|
||||
ue = modimpl["ue"]
|
||||
if show_ues and (prev_ue_id != ue["ue_id"]):
|
||||
prev_ue_id = ue["ue_id"]
|
||||
titre = ue["titre"]
|
||||
mod_nb_inscrits = nt.modimpls_results[modimpl.id].nb_inscrits_module
|
||||
ue = modimpl.module.ue
|
||||
if show_ues and (prev_ue_id != ue.id):
|
||||
prev_ue_id = ue.id
|
||||
titre = ue.titre or ""
|
||||
if use_ue_coefs:
|
||||
titre += f""" <b>(coef. {ue["coefficient"] or 0.0})</b>"""
|
||||
titre += f""" <b>(coef. {ue.coefficient or 0.0})</b>"""
|
||||
H.append(
|
||||
f"""<tr class="formsemestre_status_ue"><td colspan="4">
|
||||
<span class="status_ue_acro">{ue["acronyme"]}</span>
|
||||
<span class="status_ue_acro">{ue.acronyme}</span>
|
||||
<span class="status_ue_title">{titre}</span>
|
||||
</td><td colspan="2">"""
|
||||
)
|
||||
|
||||
expr = sco_compute_moy.get_ue_expression(
|
||||
formsemestre.id, ue["ue_id"], html_quote=True
|
||||
formsemestre.id, ue.id, html_quote=True
|
||||
)
|
||||
if expr:
|
||||
H.append(
|
||||
f""" <span class="formula" title="mode de calcul de la moyenne d'UE">{expr}</span>
|
||||
<span class="warning">formule inutilisée en 9.2: <a href="{
|
||||
url_for("notes.delete_ue_expr", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ue_id=ue["ue_id"] )
|
||||
url_for("notes.delete_ue_expr", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ue_id=ue.id )
|
||||
}
|
||||
">supprimer</a></span>"""
|
||||
)
|
||||
|
||||
H.append("</td></tr>")
|
||||
|
||||
if modimpl["ue"]["type"] != codes_cursus.UE_STANDARD:
|
||||
if ue.type != codes_cursus.UE_STANDARD:
|
||||
fontorange = " fontorange" # style css additionnel
|
||||
else:
|
||||
fontorange = ""
|
||||
|
||||
etat = sco_evaluations.do_evaluation_etat_in_mod(nt, modimpl["moduleimpl_id"])
|
||||
etat = sco_evaluations.do_evaluation_etat_in_mod(nt, modimpl)
|
||||
# if nt.parcours.APC_SAE:
|
||||
# tbd style si module non conforme
|
||||
if (
|
||||
etat["nb_evals_completes"] > 0
|
||||
and etat["nb_evals_en_cours"] == 0
|
||||
and etat["nb_evals_vides"] == 0
|
||||
and not etat["attente"]
|
||||
):
|
||||
H.append(f'<tr class="formsemestre_status_green{fontorange}">')
|
||||
else:
|
||||
@ -1328,10 +1280,10 @@ def formsemestre_tableau_modules(
|
||||
<td class="scotext"><a href="{moduleimpl_status_url}" title="{mod_descr}"
|
||||
class="formsemestre_status_link">{mod.abbrev or mod.titre or ""}</a>
|
||||
</td>
|
||||
<td class="formsemestre_status_inscrits">{len(mod_inscrits)}</td>
|
||||
<td class="formsemestre_status_inscrits">{mod_nb_inscrits}</td>
|
||||
<td class="resp scotext">
|
||||
<a class="discretelink" href="{moduleimpl_status_url}" title="{mod_ens}">{
|
||||
sco_users.user_info(modimpl["responsable_id"])["prenomnom"]
|
||||
sco_users.user_info(modimpl.responsable_id)["prenomnom"]
|
||||
}</a>
|
||||
</td>
|
||||
<td>
|
||||
@ -1368,6 +1320,7 @@ def formsemestre_tableau_modules(
|
||||
if nb_evals != 0:
|
||||
H.append(
|
||||
f"""<a href="{moduleimpl_status_url}"
|
||||
title="les évaluations 'ok' sont celles prises en compte dans les calculs"
|
||||
class="formsemestre_status_link">{nb_evals} prévues,
|
||||
{etat["nb_evals_completes"]} ok</a>"""
|
||||
)
|
||||
@ -1378,17 +1331,14 @@ def formsemestre_tableau_modules(
|
||||
etat["nb_evals_en_cours"]
|
||||
} en cours</a></span>"""
|
||||
)
|
||||
if etat["attente"]:
|
||||
H.append(
|
||||
f""" <span><a class="redlink" href="{moduleimpl_status_url}"
|
||||
title="Il y a des notes en attente">[en attente]</a></span>"""
|
||||
)
|
||||
if etat["attente"]:
|
||||
H.append(
|
||||
f""" <span><a class="redlink" href="{moduleimpl_status_url}"
|
||||
title="Il y a des notes en attente">[en attente]</a></span>"""
|
||||
)
|
||||
elif mod.module_type == ModuleType.MALUS:
|
||||
nb_malus_notes = sum(
|
||||
[
|
||||
e["etat"]["nb_notes"]
|
||||
for e in nt.get_mod_evaluation_etat_list(modimpl["moduleimpl_id"])
|
||||
]
|
||||
e["etat"]["nb_notes"] for e in nt.get_mod_evaluation_etat_list(modimpl)
|
||||
)
|
||||
H.append(
|
||||
f"""<td class="malus">
|
||||
@ -1457,7 +1407,7 @@ def formsemestre_warning_etuds_sans_note(
|
||||
noms = ", ".join(
|
||||
[
|
||||
f"""<a href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
}" class="discretelink">{etud.nomprenom}</a>"""
|
||||
for etud in etuds
|
||||
]
|
||||
@ -1519,13 +1469,13 @@ def formsemestre_note_etuds_sans_notes(
|
||||
a déjà des notes"""
|
||||
)
|
||||
return redirect(
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
else:
|
||||
noms = "</li><li>".join(
|
||||
[
|
||||
f"""<a href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
}" class="discretelink">{etud.nomprenom}</a>"""
|
||||
for etud in etuds
|
||||
]
|
||||
|
@ -117,8 +117,8 @@ def formsemestre_validation_etud_form(
|
||||
if read_only:
|
||||
check = True
|
||||
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
etud_d = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud_d, formsemestre_id)
|
||||
if not Se.sem["etat"]:
|
||||
raise ScoValueError("validation: semestre verrouille")
|
||||
|
||||
@ -132,7 +132,7 @@ def formsemestre_validation_etud_form(
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title=f"Parcours {etud['nomprenom']}",
|
||||
page_title=f"Parcours {etud.nomprenom}",
|
||||
javascripts=["js/recap_parcours.js"],
|
||||
)
|
||||
]
|
||||
@ -177,26 +177,22 @@ def formsemestre_validation_etud_form(
|
||||
H.append('<table style="width: 100%"><tr><td>')
|
||||
if not check:
|
||||
H.append(
|
||||
'<h2 class="formsemestre">%s: validation %s%s</h2>Parcours: %s'
|
||||
% (
|
||||
etud["nomprenom"],
|
||||
Se.parcours.SESSION_NAME_A,
|
||||
Se.parcours.SESSION_NAME,
|
||||
Se.get_cursus_descr(),
|
||||
)
|
||||
f"""<h2 class="formsemestre">{etud.nomprenom}: validation {
|
||||
Se.parcours.SESSION_NAME_A}{Se.parcours.SESSION_NAME
|
||||
}</h2>Parcours: {Se.get_cursus_descr()}
|
||||
"""
|
||||
)
|
||||
else:
|
||||
H.append(
|
||||
'<h2 class="formsemestre">Parcours de %s</h2>%s'
|
||||
% (etud["nomprenom"], Se.get_cursus_descr())
|
||||
f"""<h2 class="formsemestre">Parcours de {etud.nomprenom}</h2>{Se.get_cursus_descr()}"""
|
||||
)
|
||||
|
||||
H.append(
|
||||
'</td><td style="text-align: right;"><a href="%s">%s</a></td></tr></table>'
|
||||
% (
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
sco_photos.etud_photo_html(etud, title="fiche de %s" % etud["nom"]),
|
||||
)
|
||||
f"""</td><td style="text-align: right;"><a href="{
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a></td></tr>
|
||||
</table>
|
||||
"""
|
||||
)
|
||||
|
||||
etud_etat = nt.get_etud_etat(etudid)
|
||||
@ -210,7 +206,7 @@ def formsemestre_validation_etud_form(
|
||||
<div class="warning">
|
||||
Impossible de statuer sur cet étudiant:
|
||||
il est démissionnaire ou défaillant (voir <a href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">sa fiche</a>)
|
||||
</div>
|
||||
"""
|
||||
@ -289,7 +285,7 @@ def formsemestre_validation_etud_form(
|
||||
etudid=etudid, origin_formsemestre_id=formsemestre_id
|
||||
).all()
|
||||
if autorisations:
|
||||
H.append(". Autorisé%s à s'inscrire en " % etud["ne"])
|
||||
H.append(f". Autorisé{etud.e} à s'inscrire en ")
|
||||
H.append(", ".join([f"S{aut.semestre_id}" for aut in autorisations]) + ".")
|
||||
H.append("</p>")
|
||||
|
||||
@ -732,7 +728,9 @@ def formsemestre_recap_parcours_table(
|
||||
)
|
||||
# Dispense BUT ?
|
||||
if (etudid, ue.id) in nt.dispense_ues:
|
||||
moy_ue_txt = "❎" if (ue_status and ue_status["is_capitalized"]) else "⭕"
|
||||
moy_ue_txt = (
|
||||
"❎" if (ue_status and ue_status["is_capitalized"]) else "⭕"
|
||||
)
|
||||
explanation_ue.append("non inscrit (dispense)")
|
||||
else:
|
||||
moy_ue_txt = scu.fmt_note(moy_ue)
|
||||
@ -1102,7 +1100,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
|
||||
|
||||
ue_names = ["Choisir..."] + [
|
||||
f"""{('S'+str(ue.semestre_idx)+' : ') if ue.semestre_idx is not None else ''
|
||||
}{ue.acronyme} {ue.titre} ({ue.ue_code or ""})"""
|
||||
}{ue.acronyme} {ue.titre or ''} ({ue.ue_code or ""})"""
|
||||
for ue in ues
|
||||
]
|
||||
ue_ids = [""] + [ue.id for ue in ues]
|
||||
|
@ -52,7 +52,7 @@ from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc.sco_etud import etud_sort_key
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoPermissionDenied
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [
|
||||
@ -118,6 +118,16 @@ def groups_view(
|
||||
init_qtip=True,
|
||||
)
|
||||
}
|
||||
<style>
|
||||
div.multiselect-container.dropdown-menu {{
|
||||
min-width: 180px;
|
||||
}}
|
||||
span.warning_unauthorized {{
|
||||
color: pink;
|
||||
font-style: italic;
|
||||
margin-left: 12px;
|
||||
}}
|
||||
</style>
|
||||
<div id="group-tabs">
|
||||
<!-- Menu choix groupe -->
|
||||
{form_groups_choice(groups_infos, submit_on_change=True)}
|
||||
@ -474,15 +484,12 @@ def groups_table(
|
||||
"""
|
||||
from app.scodoc import sco_report
|
||||
|
||||
# log(
|
||||
# "enter groups_table %s: %s"
|
||||
# % (groups_infos.members[0]["nom"], groups_infos.members[0].get("etape", "-"))
|
||||
# )
|
||||
can_view_etud_data = int(current_user.has_permission(Permission.ViewEtudData))
|
||||
with_codes = int(with_codes)
|
||||
with_paiement = int(with_paiement)
|
||||
with_archives = int(with_archives)
|
||||
with_annotations = int(with_annotations)
|
||||
with_bourse = int(with_bourse)
|
||||
with_paiement = int(with_paiement) and can_view_etud_data
|
||||
with_archives = int(with_archives) and can_view_etud_data
|
||||
with_annotations = int(with_annotations) and can_view_etud_data
|
||||
with_bourse = int(with_bourse) and can_view_etud_data
|
||||
|
||||
base_url_np = groups_infos.base_url + f"&with_codes={with_codes}"
|
||||
base_url = (
|
||||
@ -527,7 +534,8 @@ def groups_table(
|
||||
if fmt != "html": # ne mentionne l'état que en Excel (style en html)
|
||||
columns_ids.append("etat")
|
||||
columns_ids.append("email")
|
||||
columns_ids.append("emailperso")
|
||||
if can_view_etud_data:
|
||||
columns_ids.append("emailperso")
|
||||
|
||||
if fmt == "moodlecsv":
|
||||
columns_ids = ["email", "semestre_groupe"]
|
||||
@ -561,7 +569,7 @@ def groups_table(
|
||||
else:
|
||||
etud["_emailperso_target"] = ""
|
||||
fiche_url = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
)
|
||||
etud["_nom_disp_target"] = fiche_url
|
||||
etud["_nom_disp_order"] = etud_sort_key(etud)
|
||||
@ -616,7 +624,7 @@ def groups_table(
|
||||
+ "+".join(sorted(moodle_groupenames))
|
||||
)
|
||||
else:
|
||||
filename = "etudiants_%s" % groups_infos.groups_filename
|
||||
filename = f"etudiants_{groups_infos.groups_filename}"
|
||||
|
||||
prefs = sco_preferences.SemPreferences(groups_infos.formsemestre_id)
|
||||
tab = GenTable(
|
||||
@ -664,28 +672,33 @@ def groups_table(
|
||||
"""
|
||||
]
|
||||
if groups_infos.members:
|
||||
Of = []
|
||||
menu_options = []
|
||||
options = {
|
||||
"with_paiement": "Paiement inscription",
|
||||
"with_archives": "Fichiers archivés",
|
||||
"with_annotations": "Annotations",
|
||||
"with_codes": "Codes",
|
||||
"with_bourse": "Statut boursier",
|
||||
"with_codes": "Affiche codes",
|
||||
}
|
||||
for option in options:
|
||||
if can_view_etud_data:
|
||||
options.update(
|
||||
{
|
||||
"with_paiement": "Paiement inscription",
|
||||
"with_archives": "Fichiers archivés",
|
||||
"with_annotations": "Annotations",
|
||||
"with_bourse": "Statut boursier",
|
||||
}
|
||||
)
|
||||
for option, label in options.items():
|
||||
if locals().get(option, False):
|
||||
selected = "selected"
|
||||
else:
|
||||
selected = ""
|
||||
Of.append(
|
||||
"""<option value="%s" %s>%s</option>"""
|
||||
% (option, selected, options[option])
|
||||
menu_options.append(
|
||||
f"""<option value="{option}" {selected}>{label}</option>"""
|
||||
)
|
||||
|
||||
H.extend(
|
||||
[
|
||||
"""<span style="margin-left: 2em;"><select name="group_list_options" id="group_list_options" class="multiselect" multiple="multiple">""",
|
||||
"\n".join(Of),
|
||||
"""<span style="margin-left: 2em;">
|
||||
<select name="group_list_options" id="group_list_options" class="multiselect" multiple="multiple">""",
|
||||
"\n".join(menu_options),
|
||||
"""</select></span>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
@ -701,6 +714,9 @@ def groups_table(
|
||||
});
|
||||
</script>
|
||||
""",
|
||||
"""<span class="warning_unauthorized">accès aux données personnelles interdit</span>"""
|
||||
if not can_view_etud_data
|
||||
else "",
|
||||
]
|
||||
)
|
||||
H.append("</div></form>")
|
||||
@ -708,41 +724,45 @@ def groups_table(
|
||||
H.extend(
|
||||
[
|
||||
tab.html(),
|
||||
"<ul>",
|
||||
'<li><a class="stdlink" href="%s&fmt=xlsappel">Feuille d\'appel Excel</a></li>'
|
||||
% (tab.base_url,),
|
||||
'<li><a class="stdlink" href="%s&fmt=xls">Table Excel</a></li>'
|
||||
% (tab.base_url,),
|
||||
'<li><a class="stdlink" href="%s&fmt=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a></li>'
|
||||
% (tab.base_url,),
|
||||
"""<li>
|
||||
<a class="stdlink" href="export_groups_as_moodle_csv?formsemestre_id=%s">Fichier CSV pour Moodle (tous les groupes)</a>
|
||||
f"""
|
||||
<ul>
|
||||
<li><a class="stdlink" href="{tab.base_url}&fmt=xlsappel">Feuille d'appel Excel</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{tab.base_url}&fmt=xls">Table Excel</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{tab.base_url}&fmt=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="stdlink" href="export_groups_as_moodle_csv?formsemestre_id={groups_infos.formsemestre_id}">
|
||||
Fichier CSV pour Moodle (tous les groupes)</a>
|
||||
<em>(voir le paramétrage pour modifier le format des fichiers Moodle exportés)</em>
|
||||
</li>"""
|
||||
% groups_infos.formsemestre_id,
|
||||
</li>""",
|
||||
]
|
||||
)
|
||||
if amail_inst:
|
||||
H.append(
|
||||
'<li><a class="stdlink" href="mailto:?bcc=%s">Envoyer un mail collectif au groupe de %s (via %d adresses institutionnelles)</a></li>'
|
||||
% (
|
||||
",".join(amail_inst),
|
||||
groups_infos.groups_titles,
|
||||
len(amail_inst),
|
||||
)
|
||||
f"""<li>
|
||||
<a class="stdlink" href="mailto:?bcc={','.join(amail_inst)
|
||||
}">Envoyer un mail collectif au groupe de {groups_infos.groups_titles}
|
||||
(via {len(amail_inst)} adresses institutionnelles)</a>
|
||||
</li>"""
|
||||
)
|
||||
|
||||
if amail_perso:
|
||||
H.append(
|
||||
'<li><a class="stdlink" href="mailto:?bcc=%s">Envoyer un mail collectif au groupe de %s (via %d adresses personnelles)</a></li>'
|
||||
% (
|
||||
",".join(amail_perso),
|
||||
groups_infos.groups_titles,
|
||||
len(amail_perso),
|
||||
if can_view_etud_data:
|
||||
if amail_perso:
|
||||
H.append(
|
||||
f"""<li>
|
||||
<a class="stdlink" href="mailto:?bcc={','.join(amail_perso)
|
||||
}">Envoyer un mail collectif au groupe de {groups_infos.groups_titles}
|
||||
(via {len(amail_perso)} adresses personnelles)</a>
|
||||
</li>"""
|
||||
)
|
||||
)
|
||||
else:
|
||||
H.append("<li><em>Adresses personnelles non renseignées</em></li>")
|
||||
else:
|
||||
H.append("<li><em>Adresses personnelles non renseignées</em></li>")
|
||||
H.append(
|
||||
"""<li class="unauthorized">adresses mail personnelles protégées</li>"""
|
||||
)
|
||||
|
||||
H.append("</ul>")
|
||||
|
||||
@ -772,6 +792,10 @@ def groups_table(
|
||||
filename = "liste_%s" % groups_infos.groups_filename
|
||||
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
|
||||
elif fmt == "allxls":
|
||||
if not can_view_etud_data:
|
||||
raise ScoPermissionDenied(
|
||||
"Vous n'avez pas la permission requise (ViewEtudData)"
|
||||
)
|
||||
# feuille Excel avec toutes les infos etudiants
|
||||
if not groups_infos.members:
|
||||
return ""
|
||||
@ -829,7 +853,9 @@ def groups_table(
|
||||
etud, groups_infos.formsemestre_id
|
||||
)
|
||||
m["parcours"] = Se.get_cursus_descr()
|
||||
m["code_cursus"], _ = sco_report.get_code_cursus_etud(etud)
|
||||
m["code_cursus"], _ = sco_report.get_code_cursus_etud(
|
||||
etud["etudid"], sems=etud["sems"]
|
||||
)
|
||||
rows = [[m.get(k, "") for k in keys] for m in groups_infos.members]
|
||||
title = "etudiants_%s" % groups_infos.groups_filename
|
||||
xls = sco_excel.excel_simple_table(titles=titles, lines=rows, sheet_name=title)
|
||||
@ -879,8 +905,9 @@ def tab_absences_html(groups_infos, etat=None):
|
||||
% groups_infos.groups_query_args,
|
||||
"""<li><a class="stdlink" href="trombino?%s&fmt=pdflist">Liste d'appel avec photos</a></li>"""
|
||||
% groups_infos.groups_query_args,
|
||||
"""<li><a class="stdlink" href="groups_export_annotations?%s">Liste des annotations</a></li>"""
|
||||
% groups_infos.groups_query_args,
|
||||
f"""<li><a class="stdlink" href="groups_export_annotations?{groups_infos.groups_query_args}">Liste des annotations</a></li>"""
|
||||
if authuser.has_permission(Permission.ViewEtudData)
|
||||
else """<li class="unauthorized" title="non autorisé">Liste des annotations</li>""",
|
||||
"</ul>",
|
||||
]
|
||||
)
|
||||
@ -899,14 +926,19 @@ def tab_absences_html(groups_infos, etat=None):
|
||||
"""
|
||||
)
|
||||
# Lien pour ajout fichiers étudiants
|
||||
if authuser.has_permission(Permission.EtudAddAnnotations):
|
||||
text = "Télécharger des fichiers associés aux étudiants (e.g. dossiers d'admission)"
|
||||
if authuser.has_permission(
|
||||
Permission.EtudAddAnnotations
|
||||
) and authuser.has_permission(Permission.ViewEtudData):
|
||||
H.append(
|
||||
f"""<li><a class="stdlink" href="{
|
||||
url_for('scolar.etudarchive_import_files_form',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
group_id=group_id
|
||||
)}">Télécharger des fichiers associés aux étudiants (e.g. dossiers d'admission)</a></li>"""
|
||||
)}">{text}</a></li>"""
|
||||
)
|
||||
else:
|
||||
H.append(f"""<li class="unauthorized" title="non autorisé">{text}</li>""")
|
||||
|
||||
H.append("</ul></div>")
|
||||
return "".join(H)
|
||||
|
@ -508,15 +508,20 @@ def students_import_admission(
|
||||
H = [html_sco_header.sco_header(page_title="Import données admissions")]
|
||||
H.append("<p>Import terminé !</p>")
|
||||
H.append(
|
||||
'<p><a class="stdlink" href="%s">Continuer</a></p>'
|
||||
% url_for(
|
||||
f"""<p><a class="stdlink" href="{ url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
}">Continuer</a></p>"""
|
||||
)
|
||||
if diag:
|
||||
H.append("<p>Diagnostic: <ul><li>%s</li></ul></p>" % "</li><li>".join(diag))
|
||||
H.append(
|
||||
f"""<p>Diagnostic: <ul><li>{
|
||||
"</li><li>".join(diag)
|
||||
}</li></ul></p>
|
||||
"""
|
||||
)
|
||||
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
@ -669,7 +669,7 @@ def etuds_select_boxes(
|
||||
elink = """<a class="discretelink %s" href="%s">%s</a>""" % (
|
||||
c,
|
||||
url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud["etudid"],
|
||||
),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user