Compare commits
132 Commits
master
...
pe-comp-av
Author | SHA1 | Date |
---|---|---|
Cléo Baras | aa85150f47 | |
Cléo Baras | dd98e19624 | |
Cléo Baras | 1712dcf8f7 | |
Cléo Baras | dbe7f5d482 | |
Cléo Baras | 5827a37a31 | |
Cléo Baras | 24237eb7b7 | |
Emmanuel Viennet | 959a98d0a2 | |
Cléo Baras | c91ab67951 | |
Cléo Baras | 1bed4bb720 | |
Cléo Baras | 7026746385 | |
Cléo Baras | 817b54d334 | |
Cléo Baras | 16a12dad59 | |
Cléo Baras | 99bb0f471b | |
Emmanuel Viennet | 35a038fd3a | |
Emmanuel Viennet | b46556c189 | |
Iziram | 71f90f5261 | |
Iziram | 1b037d6c7c | |
Emmanuel Viennet | 60a97b7baf | |
Iziram | 0332553587 | |
Iziram | 958cf435c8 | |
Iziram | c69e9c34a0 | |
Iziram | 17f8771b0b | |
Iziram | 7eb41fb2eb | |
Iziram | a79ca4a17d | |
Emmanuel Viennet | 411ef8ae0d | |
Emmanuel Viennet | 169bf17fdd | |
Emmanuel Viennet | 75d4c110a8 | |
Emmanuel Viennet | 9003a2ca87 | |
Emmanuel Viennet | 55ecaa45a9 | |
Emmanuel Viennet | ab39454a0d | |
Iziram | 5158bd0c8f | |
Iziram | 21b2e0f582 | |
Emmanuel Viennet | e56cbfc5a2 | |
Emmanuel Viennet | 9cdab8d1ed | |
Cléo Baras | 7cdba43e86 | |
Iziram | 079348bb87 | |
Iziram | c882e0d6a0 | |
Cléo Baras | 9c7576154c | |
Cléo Baras | ce0d5ec9fd | |
Iziram | 3d6be2f200 | |
Cléo Baras | e675064cae | |
Iziram | 185e061f01 | |
Iziram | e4c889ec8a | |
Emmanuel Viennet | 7ef45e0bac | |
Iziram | f242fee5ff | |
Emmanuel Viennet | c960d943d2 | |
Emmanuel Viennet | 741168a065 | |
Emmanuel Viennet | 5c9126d263 | |
Emmanuel Viennet | ce63b7f2f5 | |
Emmanuel Viennet | 5e5cb015d0 | |
Cléo Baras | 5fc1800f70 | |
Cléo Baras | 2459356245 | |
Cléo Baras | b1602f0cf3 | |
Cléo Baras | ba28d5f3c8 | |
Cléo Baras | b9b9a172c7 | |
Cléo Baras | c2a66b607f | |
Cléo Baras | 802e8f4648 | |
Iziram | 3184d5d92e | |
Cléo Baras | cf7d7d2db8 | |
Cléo Baras | 5ea65433be | |
Cléo Baras | 35a20c3307 | |
Cléo Baras | 8acd9a12d4 | |
Cléo Baras | 2020114c1b | |
Cléo Baras | a93aa19449 | |
Iziram | c620c3b0e1 | |
Iziram | c2e77846b9 | |
Cléo Baras | 28b25ad681 | |
Cléo Baras | 5ea79c03a3 | |
Emmanuel Viennet | fdcf6388f5 | |
Iziram | 9dcaf70e18 | |
Emmanuel Viennet | 20d4b4e1b3 | |
Emmanuel Viennet | aaaf41250a | |
Iziram | b3b47a755f | |
Emmanuel Viennet | bc5292b165 | |
Emmanuel Viennet | ee601071f5 | |
Emmanuel Viennet | 0cf3b0a782 | |
Emmanuel Viennet | 49a5ec488d | |
Cléo Baras | a50bbe9223 | |
Cléo Baras | 57d616da1a | |
Emmanuel Viennet | c0a965d774 | |
Emmanuel Viennet | 1c01d987be | |
Cléo Baras | 21a794a760 | |
Emmanuel Viennet | 41944bcd29 | |
Cléo Baras | 960f8a3462 | |
Cléo Baras | 6821a02956 | |
Emmanuel Viennet | 47a42d897e | |
Emmanuel Viennet | 7f32f1fb99 | |
Cléo Baras | eb56182407 | |
Cléo Baras | 02b057ca5a | |
Cléo Baras | eff28d64f9 | |
Emmanuel Viennet | 81fab97018 | |
Emmanuel Viennet | a8a711b30a | |
Emmanuel Viennet | 46cdaf75b8 | |
Emmanuel Viennet | d1d89cc427 | |
Emmanuel Viennet | 61d35ddac0 | |
Emmanuel Viennet | c492cf550a | |
Emmanuel Viennet | 2dd7154036 | |
Emmanuel Viennet | 13e7bd4512 | |
Emmanuel Viennet | f1ce70e6de | |
Emmanuel Viennet | a8ff540e95 | |
Emmanuel Viennet | cc3f5d393f | |
Emmanuel Viennet | 7c794c01d1 | |
Cléo Baras | 746314b2fb | |
Emmanuel Viennet | 624ea39edd | |
Emmanuel Viennet | 853bc31422 | |
Emmanuel Viennet | 09d59848d6 | |
Emmanuel Viennet | f31eca97bb | |
Emmanuel Viennet | 3844ae46d1 | |
Emmanuel Viennet | fae9fbdd09 | |
Cléo Baras | 40a57a9b86 | |
Cléo Baras | b5125fa3d7 | |
Cléo Baras | 0f446fe0d3 | |
Cléo Baras | 5f656b431b | |
Cléo Baras | 83059cd995 | |
Cléo Baras | 8de1a44583 | |
Cléo Baras | 491d600bd4 | |
Emmanuel Viennet | 56aa5fbba3 | |
Cléo Baras | d6a75b176e | |
Emmanuel Viennet | e6d61fcd8a | |
Cléo Baras | 70f399e8b7 | |
Cléo Baras | 68bd20f8de | |
Cléo Baras | 1716daafde | |
Cléo Baras | 5e49384a90 | |
Cléo Baras | 828c619c74 | |
Cléo Baras | b8cb592ac9 | |
Cléo Baras | d8381884dc | |
Cléo Baras | 883028216f | |
Emmanuel Viennet | d140240909 | |
Cléo Baras | 267dbb6460 | |
Cléo Baras | 02a73de04d | |
Cléo Baras | e78a2d3ffe | |
Emmanuel Viennet | bcb801662a |
|
@ -20,14 +20,8 @@ from app.api import api_bp as bp
|
|||
from app.api import api_web_bp
|
||||
from app.api import get_model_api_object, tools
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import (
|
||||
Identite,
|
||||
Justificatif,
|
||||
Departement,
|
||||
FormSemestre,
|
||||
)
|
||||
from app.models import Identite, Justificatif, Departement, FormSemestre, Scolog
|
||||
from app.models.assiduites import (
|
||||
compute_assiduites_justified,
|
||||
get_formsemestre_from_data,
|
||||
)
|
||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||
|
@ -315,7 +309,6 @@ def justif_create(etudid: int = None, nip=None, ine=None):
|
|||
|
||||
errors: list[dict] = []
|
||||
success: list[dict] = []
|
||||
justifs: list[Justificatif] = []
|
||||
|
||||
# énumération des justificatifs
|
||||
for i, data in enumerate(create_list):
|
||||
|
@ -327,11 +320,9 @@ def justif_create(etudid: int = None, nip=None, ine=None):
|
|||
errors.append({"indice": i, "message": obj})
|
||||
else:
|
||||
success.append({"indice": i, "message": obj})
|
||||
justifs.append(justi)
|
||||
justi.justifier_assiduites()
|
||||
scass.simple_invalidate_cache(data, etud.id)
|
||||
|
||||
# Actualisation des assiduités justifiées en fonction de tous les nouveaux justificatifs
|
||||
compute_assiduites_justified(etud.etudid, justifs)
|
||||
return {"errors": errors, "success": success}
|
||||
|
||||
|
||||
|
@ -500,9 +491,16 @@ def justif_edit(justif_id: int):
|
|||
return json_error(404, err)
|
||||
|
||||
# Mise à jour du justificatif
|
||||
justificatif_unique.dejustifier_assiduites()
|
||||
db.session.add(justificatif_unique)
|
||||
db.session.commit()
|
||||
|
||||
Scolog.logdb(
|
||||
method="edit_justificatif",
|
||||
etudid=justificatif_unique.etudiant.id,
|
||||
msg=f"justificatif modif: {justificatif_unique}",
|
||||
)
|
||||
|
||||
# Génération du dictionnaire de retour
|
||||
# La couverture correspond
|
||||
# - aux assiduités précédemment justifiées par le justificatif
|
||||
|
@ -510,11 +508,7 @@ def justif_edit(justif_id: int):
|
|||
retour = {
|
||||
"couverture": {
|
||||
"avant": avant_ids,
|
||||
"apres": compute_assiduites_justified(
|
||||
justificatif_unique.etudid,
|
||||
[justificatif_unique],
|
||||
True,
|
||||
),
|
||||
"apres": justificatif_unique.justifier_assiduites(),
|
||||
}
|
||||
}
|
||||
# Invalide le cache
|
||||
|
@ -591,14 +585,10 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
|
|||
|
||||
# On invalide le cache
|
||||
scass.simple_invalidate_cache(justificatif_unique.to_dict())
|
||||
# On actualise les assiduités justifiées de l'étudiant concerné
|
||||
justificatif_unique.dejustifier_assiduites()
|
||||
# On supprime le justificatif
|
||||
db.session.delete(justificatif_unique)
|
||||
# On actualise les assiduités justifiées de l'étudiant concerné
|
||||
compute_assiduites_justified(
|
||||
justificatif_unique.etudid,
|
||||
Justificatif.query.filter_by(etudid=justificatif_unique.etudid).all(),
|
||||
True,
|
||||
)
|
||||
|
||||
return (200, "OK")
|
||||
|
||||
|
@ -699,7 +689,6 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
|
|||
@as_json
|
||||
@permission_required(Permission.AbsChange)
|
||||
def justif_remove(justif_id: int = None):
|
||||
# XXX TODO pas de test unitaire
|
||||
"""
|
||||
Supression d'un fichier ou d'une archive
|
||||
{
|
||||
|
|
|
@ -104,9 +104,11 @@ class BulletinBUT:
|
|||
"competence": None, # XXX TODO lien avec référentiel
|
||||
"moyenne": None,
|
||||
# Le bonus sport appliqué sur cette UE
|
||||
"bonus": fmt_note(res.bonus_ues[ue.id][etud.id])
|
||||
if res.bonus_ues is not None and ue.id in res.bonus_ues
|
||||
else fmt_note(0.0),
|
||||
"bonus": (
|
||||
fmt_note(res.bonus_ues[ue.id][etud.id])
|
||||
if res.bonus_ues is not None and ue.id in res.bonus_ues
|
||||
else fmt_note(0.0)
|
||||
),
|
||||
"malus": fmt_note(res.malus[ue.id][etud.id]),
|
||||
"capitalise": None, # "AAAA-MM-JJ" TODO #sco93
|
||||
"ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
|
||||
|
@ -181,14 +183,16 @@ class BulletinBUT:
|
|||
"is_external": ue_capitalisee.is_external,
|
||||
"date_capitalisation": ue_capitalisee.event_date,
|
||||
"formsemestre_id": ue_capitalisee.formsemestre_id,
|
||||
"bul_orig_url": url_for(
|
||||
"notes.formsemestre_bulletinetud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud.id,
|
||||
formsemestre_id=ue_capitalisee.formsemestre_id,
|
||||
)
|
||||
if ue_capitalisee.formsemestre_id
|
||||
else None,
|
||||
"bul_orig_url": (
|
||||
url_for(
|
||||
"notes.formsemestre_bulletinetud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud.id,
|
||||
formsemestre_id=ue_capitalisee.formsemestre_id,
|
||||
)
|
||||
if ue_capitalisee.formsemestre_id
|
||||
else None
|
||||
),
|
||||
"ressources": {}, # sans détail en BUT
|
||||
"saes": {},
|
||||
}
|
||||
|
@ -227,13 +231,15 @@ class BulletinBUT:
|
|||
"id": modimpl.id,
|
||||
"titre": modimpl.module.titre,
|
||||
"code_apogee": modimpl.module.code_apogee,
|
||||
"url": url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=modimpl.id,
|
||||
)
|
||||
if has_request_context()
|
||||
else "na",
|
||||
"url": (
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=modimpl.id,
|
||||
)
|
||||
if has_request_context()
|
||||
else "na"
|
||||
),
|
||||
"moyenne": {
|
||||
# # moyenne indicative de module: moyenne des UE,
|
||||
# # ignorant celles sans notes (nan)
|
||||
|
@ -242,18 +248,20 @@ class BulletinBUT:
|
|||
# "max": fmt_note(moyennes_etuds.max()),
|
||||
# "moy": fmt_note(moyennes_etuds.mean()),
|
||||
},
|
||||
"evaluations": [
|
||||
self.etud_eval_results(etud, e)
|
||||
for e in modimpl.evaluations
|
||||
if (e.visibulletin or version == "long")
|
||||
and (e.id in modimpl_results.evaluations_etat)
|
||||
and (
|
||||
modimpl_results.evaluations_etat[e.id].is_complete
|
||||
or self.prefs["bul_show_all_evals"]
|
||||
)
|
||||
]
|
||||
if version != "short"
|
||||
else [],
|
||||
"evaluations": (
|
||||
[
|
||||
self.etud_eval_results(etud, e)
|
||||
for e in modimpl.evaluations
|
||||
if (e.visibulletin or version == "long")
|
||||
and (e.id in modimpl_results.evaluations_etat)
|
||||
and (
|
||||
modimpl_results.evaluations_etat[e.id].is_complete
|
||||
or self.prefs["bul_show_all_evals"]
|
||||
)
|
||||
]
|
||||
if version != "short"
|
||||
else []
|
||||
),
|
||||
}
|
||||
return d
|
||||
|
||||
|
@ -274,35 +282,43 @@ class BulletinBUT:
|
|||
poids = collections.defaultdict(lambda: 0.0)
|
||||
d = {
|
||||
"id": e.id,
|
||||
"coef": fmt_note(e.coefficient)
|
||||
if e.evaluation_type == scu.EVALUATION_NORMALE
|
||||
else None,
|
||||
"coef": (
|
||||
fmt_note(e.coefficient)
|
||||
if e.evaluation_type == Evaluation.EVALUATION_NORMALE
|
||||
else None
|
||||
),
|
||||
"date_debut": e.date_debut.isoformat() if e.date_debut else None,
|
||||
"date_fin": e.date_fin.isoformat() if e.date_fin else None,
|
||||
"description": e.description,
|
||||
"evaluation_type": e.evaluation_type,
|
||||
"note": {
|
||||
"value": fmt_note(
|
||||
eval_notes[etud.id],
|
||||
note_max=e.note_max,
|
||||
),
|
||||
"min": fmt_note(notes_ok.min(), note_max=e.note_max),
|
||||
"max": fmt_note(notes_ok.max(), note_max=e.note_max),
|
||||
"moy": fmt_note(notes_ok.mean(), note_max=e.note_max),
|
||||
},
|
||||
"note": (
|
||||
{
|
||||
"value": fmt_note(
|
||||
eval_notes[etud.id],
|
||||
note_max=e.note_max,
|
||||
),
|
||||
"min": fmt_note(notes_ok.min(), note_max=e.note_max),
|
||||
"max": fmt_note(notes_ok.max(), note_max=e.note_max),
|
||||
"moy": fmt_note(notes_ok.mean(), note_max=e.note_max),
|
||||
}
|
||||
if not e.is_blocked()
|
||||
else {}
|
||||
),
|
||||
"poids": poids,
|
||||
"url": url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
evaluation_id=e.id,
|
||||
)
|
||||
if has_request_context()
|
||||
else "na",
|
||||
"url": (
|
||||
url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
evaluation_id=e.id,
|
||||
)
|
||||
if has_request_context()
|
||||
else "na"
|
||||
),
|
||||
# deprecated (supprimer avant #sco9.7)
|
||||
"date": e.date_debut.isoformat() if e.date_debut else None,
|
||||
"heure_debut": e.date_debut.time().isoformat("minutes")
|
||||
if e.date_debut
|
||||
else None,
|
||||
"heure_debut": (
|
||||
e.date_debut.time().isoformat("minutes") if e.date_debut else None
|
||||
),
|
||||
"heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None,
|
||||
}
|
||||
return d
|
||||
|
@ -343,7 +359,7 @@ class BulletinBUT:
|
|||
"short" : ne descend pas plus bas que les modules.
|
||||
|
||||
- Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
|
||||
(bulletins non publiés).
|
||||
(bulletins non publiés sur la passerelle).
|
||||
"""
|
||||
if version not in scu.BULLETINS_VERSIONS_BUT:
|
||||
raise ScoValueError("bulletin_etud: version de bulletin demandée invalide")
|
||||
|
@ -377,7 +393,7 @@ class BulletinBUT:
|
|||
else:
|
||||
etud_ues_ids = res.etud_ues_ids(etud.id)
|
||||
|
||||
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
|
||||
nbabsnj, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id)
|
||||
etud_groups = sco_groups.get_etud_formsemestre_groups(
|
||||
etud, formsemestre, only_to_show=True
|
||||
)
|
||||
|
@ -392,7 +408,7 @@ class BulletinBUT:
|
|||
}
|
||||
if self.prefs["bul_show_abs"]:
|
||||
semestre_infos["absences"] = {
|
||||
"injustifie": nbabs - nbabsjust,
|
||||
"injustifie": nbabsnj,
|
||||
"total": nbabs,
|
||||
"metrique": {
|
||||
"H.": "Heure(s)",
|
||||
|
@ -509,7 +525,7 @@ class BulletinBUT:
|
|||
d["demission"] = ""
|
||||
|
||||
# --- Absences
|
||||
d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id)
|
||||
_, d["nbabsjust"], d["nbabs"] = self.res.formsemestre.get_abs_count(etud.id)
|
||||
|
||||
# --- Decision Jury
|
||||
infos, _ = sco_bulletins.etud_descr_situation_semestre(
|
||||
|
|
|
@ -119,6 +119,12 @@ def _build_bulletin_but_infos(
|
|||
refcomp = formsemestre.formation.referentiel_competence
|
||||
if refcomp is None:
|
||||
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
|
||||
|
||||
warn_html = cursus_but.formsemestre_warning_apc_setup(
|
||||
formsemestre, bulletins_sem.res
|
||||
)
|
||||
if warn_html:
|
||||
raise ScoValueError("<b>Formation mal configurée pour le BUT</b>" + warn_html)
|
||||
ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau(
|
||||
refcomp, etud
|
||||
)
|
||||
|
|
|
@ -24,7 +24,7 @@ from reportlab.lib.colors import blue
|
|||
from reportlab.lib.units import cm, mm
|
||||
from reportlab.platypus import Paragraph, Spacer
|
||||
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.models import Evaluation, ScoDocSiteConfig
|
||||
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
||||
from app.scodoc import gen_tables
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
|
@ -422,7 +422,11 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||
def evaluations_rows(self, rows, evaluations: list[dict], ue_acros=()):
|
||||
"lignes des évaluations"
|
||||
for e in evaluations:
|
||||
coef = e["coef"] if e["evaluation_type"] == scu.EVALUATION_NORMALE else "*"
|
||||
coef = (
|
||||
e["coef"]
|
||||
if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE
|
||||
else "*"
|
||||
)
|
||||
t = {
|
||||
"titre": f"{e['description'] or ''}",
|
||||
"moyenne": e["note"]["value"],
|
||||
|
@ -431,7 +435,10 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||
),
|
||||
"coef": coef,
|
||||
"_coef_pdf": Paragraph(
|
||||
f"<para align=right fontSize={self.small_fontsize}><i>{coef}</i></para>"
|
||||
f"""<para align=right fontSize={self.small_fontsize}><i>{
|
||||
coef if e["evaluation_type"] != Evaluation.EVALUATION_BONUS
|
||||
else "bonus"
|
||||
}</i></para>"""
|
||||
),
|
||||
"_pdf_style": [
|
||||
(
|
||||
|
|
|
@ -38,14 +38,11 @@ import datetime
|
|||
from xml.etree import ElementTree
|
||||
from xml.etree.ElementTree import Element
|
||||
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.but import bulletin_but
|
||||
from app.models import BulAppreciations, FormSemestre, Identite
|
||||
from app.models import BulAppreciations, FormSemestre, Identite, UniteEns
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_photos
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_xml
|
||||
|
@ -202,12 +199,12 @@ def bulletin_but_xml_compat(
|
|||
if e.visibulletin or version == "long":
|
||||
x_eval = Element(
|
||||
"evaluation",
|
||||
date_debut=e.date_debut.isoformat()
|
||||
if e.date_debut
|
||||
else "",
|
||||
date_fin=e.date_fin.isoformat()
|
||||
if e.date_debut
|
||||
else "",
|
||||
date_debut=(
|
||||
e.date_debut.isoformat() if e.date_debut else ""
|
||||
),
|
||||
date_fin=(
|
||||
e.date_fin.isoformat() if e.date_debut else ""
|
||||
),
|
||||
coefficient=str(e.coefficient),
|
||||
# pas les poids en XML compat
|
||||
evaluation_type=str(e.evaluation_type),
|
||||
|
@ -215,9 +212,9 @@ def bulletin_but_xml_compat(
|
|||
# notes envoyées sur 20, ceci juste pour garder trace:
|
||||
note_max_origin=str(e.note_max),
|
||||
# --- deprecated
|
||||
jour=e.date_debut.isoformat()
|
||||
if e.date_debut
|
||||
else "",
|
||||
jour=(
|
||||
e.date_debut.isoformat() if e.date_debut else ""
|
||||
),
|
||||
heure_debut=e.heure_debut(),
|
||||
heure_fin=e.heure_fin(),
|
||||
)
|
||||
|
@ -244,7 +241,7 @@ def bulletin_but_xml_compat(
|
|||
|
||||
# --- Absences
|
||||
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
|
||||
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
|
||||
_, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id)
|
||||
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
|
||||
|
||||
# -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py ---------
|
||||
|
@ -294,17 +291,18 @@ def bulletin_but_xml_compat(
|
|||
"decisions_ue"
|
||||
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
|
||||
for ue_id in decision["decisions_ue"].keys():
|
||||
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
|
||||
doc.append(
|
||||
Element(
|
||||
"decision_ue",
|
||||
ue_id=str(ue["ue_id"]),
|
||||
numero=quote_xml_attr(ue["numero"]),
|
||||
acronyme=quote_xml_attr(ue["acronyme"]),
|
||||
titre=quote_xml_attr(ue["titre"]),
|
||||
code=decision["decisions_ue"][ue_id]["code"],
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
if ue:
|
||||
doc.append(
|
||||
Element(
|
||||
"decision_ue",
|
||||
ue_id=str(ue.id),
|
||||
numero=quote_xml_attr(ue.numero),
|
||||
acronyme=quote_xml_attr(ue.acronyme),
|
||||
titre=quote_xml_attr(ue.titre or ""),
|
||||
code=decision["decisions_ue"][ue_id]["code"],
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
for aut in decision["autorisations"]:
|
||||
doc.append(
|
||||
|
|
|
@ -23,29 +23,21 @@ from app.comp.res_but import ResultatsSemestreBUT
|
|||
from app.comp.res_compat import NotesTableCompat
|
||||
|
||||
from app.models.but_refcomp import (
|
||||
ApcAnneeParcours,
|
||||
ApcCompetence,
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
ApcParcoursNiveauCompetence,
|
||||
ApcReferentielCompetences,
|
||||
)
|
||||
from app.models import Scolog, ScolarAutorisationInscription
|
||||
from app.models.but_validations import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
)
|
||||
from app.models.ues import UEParcours
|
||||
from app.models.but_validations import ApcValidationRCUE
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES, UE_STANDARD
|
||||
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||
|
||||
from app.scodoc import sco_cursus_dut
|
||||
|
||||
|
||||
|
@ -122,10 +114,13 @@ class EtudCursusBUT:
|
|||
validation_rcue: ApcValidationRCUE
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
niveau = validation_rcue.niveau()
|
||||
if (
|
||||
niveau is None
|
||||
or not niveau.competence.id in self.validation_par_competence_et_annee
|
||||
):
|
||||
if niveau is None:
|
||||
raise ScoValueError(
|
||||
"""UE d'un RCUE non associée à un niveau de compétence.
|
||||
Vérifiez la formation et les associations de ses UEs.
|
||||
"""
|
||||
)
|
||||
if 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
|
||||
|
@ -440,11 +435,16 @@ def formsemestre_warning_apc_setup(
|
|||
"""
|
||||
if not formsemestre.formation.is_apc():
|
||||
return ""
|
||||
url_formation = url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formsemestre.formation.id,
|
||||
semestre_idx=formsemestre.semestre_id,
|
||||
)
|
||||
if formsemestre.formation.referentiel_competence is None:
|
||||
return f"""<div class="formsemestre_status_warning">
|
||||
La <a class="stdlink" href="{
|
||||
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)
|
||||
}">formation n'est pas associée à un référentiel de compétence.</a>
|
||||
La <a class="stdlink" href="{url_formation}">formation
|
||||
n'est pas associée à un référentiel de compétence.</a>
|
||||
</div>
|
||||
"""
|
||||
H = []
|
||||
|
@ -462,7 +462,9 @@ def formsemestre_warning_apc_setup(
|
|||
)
|
||||
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"""
|
||||
"""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]:
|
||||
|
@ -489,7 +491,8 @@ def formsemestre_warning_apc_setup(
|
|||
if not H:
|
||||
return ""
|
||||
return f"""<div class="formsemestre_status_warning">
|
||||
Problème dans la configuration de la formation:
|
||||
Problème dans la
|
||||
<a class="stdlink" href="{url_formation}">configuration de la formation</a>:
|
||||
<ul>
|
||||
<li>{ '</li><li>'.join(H) }</li>
|
||||
</ul>
|
||||
|
@ -502,6 +505,79 @@ def formsemestre_warning_apc_setup(
|
|||
"""
|
||||
|
||||
|
||||
def formation_semestre_niveaux_warning(formation: Formation, semestre_idx: int) -> str:
|
||||
"""Vérifie que tous les niveaux de compétences de cette année de formation
|
||||
ont bien des UEs.
|
||||
Afin de ne pas générer trop de messages, on ne considère que les parcours
|
||||
du référentiel de compétences pour lesquels au moins une UE a été associée.
|
||||
|
||||
Renvoie fragment de html
|
||||
"""
|
||||
annee = (semestre_idx - 1) // 2 + 1 # année BUT
|
||||
ref_comp: ApcReferentielCompetences = formation.referentiel_competence
|
||||
if not ref_comp:
|
||||
return "" # détecté ailleurs...
|
||||
niveaux_sans_ue_by_parcour = {} # { parcour.code : [ ue ... ] }
|
||||
parcours_ids = {
|
||||
uep.parcours_id
|
||||
for uep in UEParcours.query.join(UniteEns).filter_by(
|
||||
formation_id=formation.id, type=UE_STANDARD
|
||||
)
|
||||
}
|
||||
for parcour in ref_comp.parcours:
|
||||
if parcour.id not in parcours_ids:
|
||||
continue # saute parcours associés à aucune UE (tous semestres)
|
||||
niveaux_sans_ue = []
|
||||
niveaux = ApcNiveau.niveaux_annee_de_parcours(parcour, annee, ref_comp)
|
||||
# print(f"\n# Parcours {parcour.code} : {len(niveaux)} niveaux")
|
||||
for niveau in niveaux:
|
||||
ues = [ue for ue in formation.ues if ue.niveau_competence_id == niveau.id]
|
||||
if not ues:
|
||||
niveaux_sans_ue.append(niveau)
|
||||
# print( niveau.competence.titre + " " + str(niveau.ordre) + "\t" + str(ue) )
|
||||
if niveaux_sans_ue:
|
||||
niveaux_sans_ue_by_parcour[parcour.code] = niveaux_sans_ue
|
||||
#
|
||||
H = []
|
||||
for parcour_code, niveaux in niveaux_sans_ue_by_parcour.items():
|
||||
H.append(
|
||||
f"""<li>Parcours {parcour_code} : {
|
||||
len(niveaux)} niveaux sans UEs
|
||||
<span>
|
||||
{ ', '.join( f'{niveau.competence.titre} {niveau.ordre}'
|
||||
for niveau in niveaux
|
||||
)
|
||||
}
|
||||
</span>
|
||||
</li>
|
||||
"""
|
||||
)
|
||||
# Combien de compétences de tronc commun ?
|
||||
_, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee)
|
||||
nb_niveaux_tc = len(niveaux_by_parcours["TC"])
|
||||
nb_ues_tc = len(
|
||||
formation.query_ues_parcour(None)
|
||||
.filter(UniteEns.semestre_idx == semestre_idx)
|
||||
.all()
|
||||
)
|
||||
if nb_niveaux_tc != nb_ues_tc:
|
||||
H.append(
|
||||
f"""<li>{nb_niveaux_tc} niveaux de compétences de tronc commun,
|
||||
mais {nb_ues_tc} UEs de tronc commun ! (c'est normal si
|
||||
vous avez des UEs différenciées par parcours)</li>"""
|
||||
)
|
||||
|
||||
if H:
|
||||
return f"""<div class="formation_semestre_niveaux_warning">
|
||||
<div>Problèmes détectés à corriger :</div>
|
||||
<ul>
|
||||
{"".join(H)}
|
||||
</ul>
|
||||
</div>
|
||||
"""
|
||||
return "" # no problem detected
|
||||
|
||||
|
||||
def ue_associee_au_niveau_du_parcours(
|
||||
ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S"
|
||||
) -> UniteEns:
|
||||
|
|
|
@ -77,7 +77,7 @@ from app.models.but_refcomp import (
|
|||
ApcNiveau,
|
||||
ApcParcours,
|
||||
)
|
||||
from app.models import Scolog, ScolarAutorisationInscription
|
||||
from app.models import Evaluation, Scolog, ScolarAutorisationInscription
|
||||
from app.models.but_validations import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
|
@ -260,11 +260,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
else []
|
||||
)
|
||||
# ---- Niveaux et RCUEs
|
||||
niveaux_by_parcours = (
|
||||
formsemestre.formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
self.annee_but, [self.parcour] if self.parcour else None
|
||||
)[1]
|
||||
)
|
||||
niveaux_by_parcours = formsemestre.formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
self.annee_but, [self.parcour] if self.parcour else None
|
||||
)[
|
||||
1
|
||||
]
|
||||
self.niveaux_competences = niveaux_by_parcours["TC"] + (
|
||||
niveaux_by_parcours[self.parcour.id] if self.parcour else []
|
||||
)
|
||||
|
@ -358,13 +358,17 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
# self.codes = [] # pas de décision annuelle sur semestres impairs
|
||||
elif self.inscription_etat != scu.INSCRIT:
|
||||
self.codes = [
|
||||
sco_codes.DEM
|
||||
if self.inscription_etat == scu.DEMISSION
|
||||
else sco_codes.DEF,
|
||||
(
|
||||
sco_codes.DEM
|
||||
if self.inscription_etat == scu.DEMISSION
|
||||
else sco_codes.DEF
|
||||
),
|
||||
# propose aussi d'autres codes, au cas où...
|
||||
sco_codes.DEM
|
||||
if self.inscription_etat != scu.DEMISSION
|
||||
else sco_codes.DEF,
|
||||
(
|
||||
sco_codes.DEM
|
||||
if self.inscription_etat != scu.DEMISSION
|
||||
else sco_codes.DEF
|
||||
),
|
||||
sco_codes.ABAN,
|
||||
sco_codes.ABL,
|
||||
sco_codes.EXCLU,
|
||||
|
@ -595,11 +599,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
# Ordonne par numéro d'UE
|
||||
niv_rcue = sorted(
|
||||
self.rcue_by_niveau.items(),
|
||||
key=lambda x: x[1].ue_1.numero
|
||||
if x[1].ue_1
|
||||
else x[1].ue_2.numero
|
||||
if x[1].ue_2
|
||||
else 0,
|
||||
key=lambda x: (
|
||||
x[1].ue_1.numero if x[1].ue_1 else x[1].ue_2.numero if x[1].ue_2 else 0
|
||||
),
|
||||
)
|
||||
return {
|
||||
niveau_id: DecisionsProposeesRCUE(self, rcue, self.inscription_etat)
|
||||
|
@ -816,9 +818,15 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
Return: True si au moins un code modifié et enregistré.
|
||||
"""
|
||||
modif = False
|
||||
# Vérification notes en attente dans formsemestre origine
|
||||
if only_validantes and self.has_notes_en_attente():
|
||||
return False
|
||||
if only_validantes:
|
||||
if self.has_notes_en_attente():
|
||||
# notes en attente dans formsemestre origine
|
||||
return False
|
||||
if Evaluation.get_evaluations_blocked_for_etud(
|
||||
self.formsemestre, self.etud
|
||||
):
|
||||
# évaluation(s) qui seront débloquées dans le futur
|
||||
return False
|
||||
|
||||
# Toujours valider dans l'ordre UE, RCUE, Année
|
||||
annee_scolaire = self.formsemestre.annee_scolaire()
|
||||
|
@ -1488,9 +1496,11 @@ class DecisionsProposeesUE(DecisionsProposees):
|
|||
self.validation = None # cache toute validation
|
||||
self.explanation = "non inscrit (dem. ou déf.)"
|
||||
self.codes = [
|
||||
sco_codes.DEM
|
||||
if res.get_etud_etat(etud.id) == scu.DEMISSION
|
||||
else sco_codes.DEF
|
||||
(
|
||||
sco_codes.DEM
|
||||
if res.get_etud_etat(etud.id) == scu.DEMISSION
|
||||
else sco_codes.DEF
|
||||
)
|
||||
]
|
||||
return
|
||||
|
||||
|
|
|
@ -331,250 +331,6 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
|||
"""
|
||||
|
||||
|
||||
def jury_but_semestriel(
|
||||
formsemestre: FormSemestre,
|
||||
etud: Identite,
|
||||
read_only: bool,
|
||||
navigation_div: str = "",
|
||||
) -> str:
|
||||
"""Page: formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel)."""
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||
parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res)
|
||||
inscription_etat = etud.inscription_etat(formsemestre.id)
|
||||
semestre_terminal = (
|
||||
formsemestre.semestre_id >= formsemestre.formation.get_cursus().NB_SEM
|
||||
)
|
||||
autorisations_passage = ScolarAutorisationInscription.query.filter_by(
|
||||
etudid=etud.id,
|
||||
origin_formsemestre_id=formsemestre.id,
|
||||
).all()
|
||||
# Par défaut: autorisé à passer dans le semestre suivant si sem. impair,
|
||||
# ou si décision déjà enregistrée:
|
||||
est_autorise_a_passer = (formsemestre.semestre_id % 2) or (
|
||||
formsemestre.semestre_id + 1
|
||||
) in (a.semestre_id for a in autorisations_passage)
|
||||
decisions_ues = {
|
||||
ue.id: DecisionsProposeesUE(etud, formsemestre, ue, inscription_etat)
|
||||
for ue in ues
|
||||
}
|
||||
for dec_ue in decisions_ues.values():
|
||||
dec_ue.compute_codes()
|
||||
|
||||
if request.method == "POST":
|
||||
if not read_only:
|
||||
for key in request.form:
|
||||
code = request.form[key]
|
||||
# Codes d'UE
|
||||
code_match = re.match(r"^code_ue_(\d+)$", key)
|
||||
if code_match:
|
||||
ue_id = int(code_match.group(1))
|
||||
dec_ue = decisions_ues.get(ue_id)
|
||||
if not dec_ue:
|
||||
raise ScoValueError(f"UE invalide ue_id={ue_id}")
|
||||
dec_ue.record(code)
|
||||
db.session.commit()
|
||||
flash("codes enregistrés")
|
||||
if not semestre_terminal:
|
||||
if request.form.get("autorisation_passage"):
|
||||
if not formsemestre.semestre_id + 1 in (
|
||||
a.semestre_id for a in autorisations_passage
|
||||
):
|
||||
ScolarAutorisationInscription.delete_autorisation_etud(
|
||||
etud.id, formsemestre.id
|
||||
)
|
||||
ScolarAutorisationInscription.autorise_etud(
|
||||
etud.id,
|
||||
formsemestre.formation.formation_code,
|
||||
formsemestre.id,
|
||||
formsemestre.semestre_id + 1,
|
||||
)
|
||||
db.session.commit()
|
||||
flash(
|
||||
f"""autorisation de passage en S{formsemestre.semestre_id + 1
|
||||
} enregistrée"""
|
||||
)
|
||||
else:
|
||||
if est_autorise_a_passer:
|
||||
ScolarAutorisationInscription.delete_autorisation_etud(
|
||||
etud.id, formsemestre.id
|
||||
)
|
||||
db.session.commit()
|
||||
flash(
|
||||
f"autorisation de passage en S{formsemestre.semestre_id + 1} annulée"
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_JURY,
|
||||
obj=formsemestre.id,
|
||||
text=f"""Saisie décision jury dans {formsemestre.html_link_status()}""",
|
||||
url=url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
),
|
||||
)
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_validation_but",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=etud.id,
|
||||
)
|
||||
)
|
||||
# GET
|
||||
if formsemestre.semestre_id % 2 == 0:
|
||||
warning = f"""<div class="warning">
|
||||
Cet étudiant de S{formsemestre.semestre_id} ne peut pas passer
|
||||
en jury BUT annuel car il lui manque le semestre précédent.
|
||||
</div>"""
|
||||
else:
|
||||
warning = ""
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title=f"Validation BUT S{formsemestre.semestre_id}",
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=etud.id,
|
||||
cssstyles=("css/jury_but.css",),
|
||||
javascripts=("js/jury_but.js",),
|
||||
),
|
||||
f"""
|
||||
<div class="jury_but">
|
||||
<div>
|
||||
<div class="bull_head">
|
||||
<div>
|
||||
<div class="titre_parcours">Jury BUT S{formsemestre.id}
|
||||
- Parcours {(parcour.libelle if parcour else False) or "non spécifié"}
|
||||
</div>
|
||||
<div class="nom_etud">{etud.nomprenom}</div>
|
||||
</div>
|
||||
<div class="bull_photo"><a href="{
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Jury sur un semestre BUT isolé (ne concerne que les UEs)</h3>
|
||||
{warning}
|
||||
</div>
|
||||
|
||||
<form method="post" class="jury_but_box" id="jury_but">
|
||||
""",
|
||||
]
|
||||
|
||||
erase_span = ""
|
||||
if not read_only:
|
||||
# Requête toutes les validations (pas seulement celles du deca courant),
|
||||
# au cas où: changement d'architecture, saisie en mode classique, ...
|
||||
validations = ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etud.id, formsemestre_id=formsemestre.id
|
||||
).all()
|
||||
if validations:
|
||||
erase_span = f"""<a href="{
|
||||
url_for("notes.formsemestre_jury_but_erase",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id,
|
||||
etudid=etud.id, only_one_sem=1)
|
||||
}" class="stdlink">effacer les décisions enregistrées</a>"""
|
||||
else:
|
||||
erase_span = (
|
||||
"Cet étudiant n'a aucune décision enregistrée pour ce semestre."
|
||||
)
|
||||
|
||||
H.append(
|
||||
f"""
|
||||
<div class="but_section_annee">
|
||||
</div>
|
||||
<div><b>Unités d'enseignement de S{formsemestre.semestre_id}:</b></div>
|
||||
"""
|
||||
)
|
||||
if not ues:
|
||||
H.append(
|
||||
"""<div class="warning">Aucune UE ! Vérifiez votre programme de
|
||||
formation, et l'association UEs / Niveaux de compétences</div>"""
|
||||
)
|
||||
else:
|
||||
H.append(
|
||||
"""
|
||||
<div class="but_annee">
|
||||
<div class="titre"></div>
|
||||
<div class="titre"></div>
|
||||
<div class="titre"></div>
|
||||
<div class="titre"></div>
|
||||
"""
|
||||
)
|
||||
for ue in ues:
|
||||
dec_ue = decisions_ues[ue.id]
|
||||
H.append("""<div class="but_niveau_titre"><div></div></div>""")
|
||||
H.append(
|
||||
_gen_but_niveau_ue(
|
||||
ue,
|
||||
dec_ue,
|
||||
disabled=read_only,
|
||||
)
|
||||
)
|
||||
H.append(
|
||||
"""<div style=""></div>
|
||||
<div class=""></div>"""
|
||||
)
|
||||
H.append("</div>") # but_annee
|
||||
|
||||
div_autorisations_passage = (
|
||||
f"""
|
||||
<div class="but_autorisations_passage">
|
||||
<span>Autorisé à passer en :</span>
|
||||
{ ", ".join( ["S" + str(a.semestre_id or '') for a in autorisations_passage ] )}
|
||||
</div>
|
||||
"""
|
||||
if autorisations_passage
|
||||
else """<div class="but_autorisations_passage but_explanation">pas d'autorisations de passage enregistrées.</div>"""
|
||||
)
|
||||
H.append(div_autorisations_passage)
|
||||
|
||||
if read_only:
|
||||
H.append(
|
||||
f"""<div class="but_explanation">
|
||||
{"Vous n'avez pas la permission de modifier ces décisions."
|
||||
if formsemestre.etat
|
||||
else "Semestre verrouillé."}
|
||||
Les champs entourés en vert sont enregistrés.
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
else:
|
||||
if formsemestre.semestre_id < formsemestre.formation.get_cursus().NB_SEM:
|
||||
H.append(
|
||||
f"""
|
||||
<div class="but_settings">
|
||||
<input type="checkbox" name="autorisation_passage" value="1" {
|
||||
"checked" if est_autorise_a_passer else ""}>
|
||||
<em>autoriser à passer dans le semestre S{formsemestre.semestre_id+1}</em>
|
||||
</input>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
else:
|
||||
H.append("""<div class="help">dernier semestre de la formation.</div>""")
|
||||
H.append(
|
||||
f"""
|
||||
<div class="but_buttons">
|
||||
<span><input type="submit" value="Enregistrer ces décisions"></span>
|
||||
<span>{erase_span}</span>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
H.append(navigation_div)
|
||||
H.append("</div>")
|
||||
H.append(
|
||||
render_template(
|
||||
"but/documentation_codes_jury.j2",
|
||||
nom_univ=f"""Export {sco_preferences.get_preference("InstituteName")
|
||||
or sco_preferences.get_preference("UnivName")
|
||||
or "Apogée"}""",
|
||||
codes=ScoDocSiteConfig.get_codes_apo_dict(),
|
||||
)
|
||||
)
|
||||
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
# -------------
|
||||
def infos_fiche_etud_html(etudid: int) -> str:
|
||||
"""Section html pour fiche etudiant
|
||||
|
|
|
@ -35,7 +35,6 @@ moyenne générale d'une UE.
|
|||
"""
|
||||
import dataclasses
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import sqlalchemy as sa
|
||||
|
@ -151,17 +150,18 @@ class ModuleImplResults:
|
|||
self.evaluations_completes_dict = {}
|
||||
for evaluation in moduleimpl.evaluations:
|
||||
eval_df = self._load_evaluation_notes(evaluation)
|
||||
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
|
||||
# ou évaluation déclarée "à prise en compte immédiate"
|
||||
# Les évaluations de rattrapage et 2eme session sont toujours complètes
|
||||
# is_complete ssi
|
||||
# tous les inscrits (non dem) au module ont une note
|
||||
# ou évaluation déclarée "à prise en compte immédiate"
|
||||
# ou rattrapage, 2eme session, bonus
|
||||
# ET pas bloquée par date (is_blocked)
|
||||
|
||||
etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
|
||||
is_complete = (
|
||||
(evaluation.evaluation_type == scu.EVALUATION_RATTRAPAGE)
|
||||
or (evaluation.evaluation_type == scu.EVALUATION_SESSION2)
|
||||
(evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE)
|
||||
or (evaluation.publish_incomplete)
|
||||
or (not etudids_sans_note)
|
||||
)
|
||||
) and not evaluation.is_blocked()
|
||||
self.evaluations_completes.append(is_complete)
|
||||
self.evaluations_completes_dict[evaluation.id] = is_complete
|
||||
self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note
|
||||
|
@ -186,7 +186,7 @@ class ModuleImplResults:
|
|||
].index
|
||||
)
|
||||
if evaluation.publish_incomplete:
|
||||
# et en "imédiat", tous ceux sans note
|
||||
# et en "immédiat", tous ceux sans note
|
||||
eval_etudids_attente |= etudids_sans_note
|
||||
# Synthèse pour état du module:
|
||||
self.etudids_attente |= eval_etudids_attente
|
||||
|
@ -240,19 +240,20 @@ class ModuleImplResults:
|
|||
).formsemestre.inscriptions
|
||||
]
|
||||
|
||||
def get_evaluations_coefs(self, moduleimpl: ModuleImpl) -> np.array:
|
||||
def get_evaluations_coefs(self, modimpl: ModuleImpl) -> np.array:
|
||||
"""Coefficients des évaluations.
|
||||
Les coefs des évals incomplètes et non "normales" (session 2, rattrapage)
|
||||
sont zéro.
|
||||
Les coefs des évals incomplètes, rattrapage, session 2, bonus sont forcés à zéro.
|
||||
Résultat: 2d-array of floats, shape (nb_evals, 1)
|
||||
"""
|
||||
return (
|
||||
np.array(
|
||||
[
|
||||
e.coefficient
|
||||
if e.evaluation_type == scu.EVALUATION_NORMALE
|
||||
else 0.0
|
||||
for e in moduleimpl.evaluations
|
||||
(
|
||||
e.coefficient
|
||||
if e.evaluation_type == Evaluation.EVALUATION_NORMALE
|
||||
else 0.0
|
||||
)
|
||||
for e in modimpl.evaluations
|
||||
],
|
||||
dtype=float,
|
||||
)
|
||||
|
@ -276,7 +277,7 @@ class ModuleImplResults:
|
|||
) / [e.note_max / 20.0 for e in moduleimpl.evaluations]
|
||||
|
||||
def get_eval_notes_dict(self, evaluation_id: int) -> dict:
|
||||
"""Notes d'une évaulation, brutes, sous forme d'un dict
|
||||
"""Notes d'une évaluation, brutes, sous forme d'un dict
|
||||
{ etudid : valeur }
|
||||
avec les valeurs float, ou "ABS" ou EXC
|
||||
"""
|
||||
|
@ -285,7 +286,7 @@ class ModuleImplResults:
|
|||
for (etudid, x) in self.evals_notes[evaluation_id].items()
|
||||
}
|
||||
|
||||
def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl):
|
||||
def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl) -> Evaluation | None:
|
||||
"""L'évaluation de rattrapage de ce module, ou None s'il n'en a pas.
|
||||
Rattrapage: la moyenne du module est la meilleure note entre moyenne
|
||||
des autres évals et la note eval rattrapage.
|
||||
|
@ -293,25 +294,41 @@ class ModuleImplResults:
|
|||
eval_list = [
|
||||
e
|
||||
for e in moduleimpl.evaluations
|
||||
if e.evaluation_type == scu.EVALUATION_RATTRAPAGE
|
||||
if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE
|
||||
]
|
||||
if eval_list:
|
||||
return eval_list[0]
|
||||
return None
|
||||
|
||||
def get_evaluation_session2(self, moduleimpl: ModuleImpl):
|
||||
def get_evaluation_session2(self, moduleimpl: ModuleImpl) -> Evaluation | None:
|
||||
"""L'évaluation de deuxième session de ce module, ou None s'il n'en a pas.
|
||||
Session 2: remplace la note de moyenne des autres évals.
|
||||
"""
|
||||
eval_list = [
|
||||
e
|
||||
for e in moduleimpl.evaluations
|
||||
if e.evaluation_type == scu.EVALUATION_SESSION2
|
||||
if e.evaluation_type == Evaluation.EVALUATION_SESSION2
|
||||
]
|
||||
if eval_list:
|
||||
return eval_list[0]
|
||||
return None
|
||||
|
||||
def get_evaluations_bonus(self, modimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"""Les évaluations bonus de ce module, ou liste vide s'il n'en a pas."""
|
||||
return [
|
||||
e
|
||||
for e in modimpl.evaluations
|
||||
if e.evaluation_type == Evaluation.EVALUATION_BONUS
|
||||
]
|
||||
|
||||
def get_evaluations_bonus_idx(self, modimpl: ModuleImpl) -> list[int]:
|
||||
"""Les indices des évaluations bonus"""
|
||||
return [
|
||||
i
|
||||
for (i, e) in enumerate(modimpl.evaluations)
|
||||
if e.evaluation_type == Evaluation.EVALUATION_BONUS
|
||||
]
|
||||
|
||||
|
||||
class ModuleImplResultsAPC(ModuleImplResults):
|
||||
"Calcul des moyennes de modules à la mode BUT"
|
||||
|
@ -356,7 +373,7 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||
# et dans dans evals_poids_etuds
|
||||
# (rappel: la comparaison est toujours false face à un NaN)
|
||||
# shape: (nb_etuds, nb_evals, nb_ues)
|
||||
poids_stacked = np.stack([evals_poids] * nb_etuds)
|
||||
poids_stacked = np.stack([evals_poids] * nb_etuds) # nb_etuds, nb_evals, nb_ues
|
||||
evals_poids_etuds = np.where(
|
||||
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
|
||||
poids_stacked,
|
||||
|
@ -364,10 +381,20 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||
)
|
||||
# Calcule la moyenne pondérée sur les notes disponibles:
|
||||
evals_notes_stacked = np.stack([evals_notes_20] * nb_ues, axis=2)
|
||||
# evals_notes_stacked shape: nb_etuds, nb_evals, nb_ues
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etuds_moy_module = np.sum(
|
||||
evals_poids_etuds * evals_notes_stacked, axis=1
|
||||
) / np.sum(evals_poids_etuds, axis=1)
|
||||
# etuds_moy_module shape: nb_etuds x nb_ues
|
||||
|
||||
# Application des évaluations bonus:
|
||||
etuds_moy_module = self.apply_bonus(
|
||||
etuds_moy_module,
|
||||
modimpl,
|
||||
evals_poids_df,
|
||||
evals_notes_stacked,
|
||||
)
|
||||
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
eval_session2 = self.get_evaluation_session2(modimpl)
|
||||
|
@ -416,6 +443,30 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||
)
|
||||
return self.etuds_moy_module
|
||||
|
||||
def apply_bonus(
|
||||
self,
|
||||
etuds_moy_module: pd.DataFrame,
|
||||
modimpl: ModuleImpl,
|
||||
evals_poids_df: pd.DataFrame,
|
||||
evals_notes_stacked: np.ndarray,
|
||||
):
|
||||
"""Ajoute les points des évaluations bonus.
|
||||
Il peut y avoir un nb quelconque d'évaluations bonus.
|
||||
Les points sont directement ajoutés (ils peuvent être négatifs).
|
||||
"""
|
||||
evals_bonus = self.get_evaluations_bonus(modimpl)
|
||||
if not evals_bonus:
|
||||
return etuds_moy_module
|
||||
poids_stacked = np.stack([evals_poids_df.values] * len(etuds_moy_module))
|
||||
for evaluation in evals_bonus:
|
||||
eval_idx = evals_poids_df.index.get_loc(evaluation.id)
|
||||
etuds_moy_module += (
|
||||
evals_notes_stacked[:, eval_idx, :] * poids_stacked[:, eval_idx, :]
|
||||
)
|
||||
# Clip dans [0,20]
|
||||
etuds_moy_module.clip(0, 20, out=etuds_moy_module)
|
||||
return etuds_moy_module
|
||||
|
||||
|
||||
def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
||||
"""Charge poids des évaluations d'un module et retourne un dataframe
|
||||
|
@ -532,6 +583,13 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
|||
evals_coefs_etuds * evals_notes_20, axis=1
|
||||
) / np.sum(evals_coefs_etuds, axis=1)
|
||||
|
||||
# Application des évaluations bonus:
|
||||
etuds_moy_module = self.apply_bonus(
|
||||
etuds_moy_module,
|
||||
modimpl,
|
||||
evals_notes_20,
|
||||
)
|
||||
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
eval_session2 = self.get_evaluation_session2(modimpl)
|
||||
if eval_session2:
|
||||
|
@ -571,3 +629,22 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
|||
)
|
||||
|
||||
return self.etuds_moy_module
|
||||
|
||||
def apply_bonus(
|
||||
self,
|
||||
etuds_moy_module: np.ndarray,
|
||||
modimpl: ModuleImpl,
|
||||
evals_notes_20: np.ndarray,
|
||||
):
|
||||
"""Ajoute les points des évaluations bonus.
|
||||
Il peut y avoir un nb quelconque d'évaluations bonus.
|
||||
Les points sont directement ajoutés (ils peuvent être négatifs).
|
||||
"""
|
||||
evals_bonus_idx = self.get_evaluations_bonus_idx(modimpl)
|
||||
if not evals_bonus_idx:
|
||||
return etuds_moy_module
|
||||
for eval_idx in evals_bonus_idx:
|
||||
etuds_moy_module += evals_notes_20[:, eval_idx]
|
||||
# Clip dans [0,20]
|
||||
etuds_moy_module.clip(0, 20, out=etuds_moy_module)
|
||||
return etuds_moy_module
|
||||
|
|
|
@ -205,11 +205,12 @@ class ResultatsSemestre(ResultatsCache):
|
|||
"coefficient" : float, # 0 si None
|
||||
"description" : str, # de l'évaluation, "" si None
|
||||
"etat" {
|
||||
"blocked" : bool, # vrai si prise en compte bloquée
|
||||
"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,
|
||||
"evaluation_id" : int,
|
||||
"jour" : datetime.datetime, # e.date_debut or datetime.datetime(1900, 1, 1)
|
||||
"publish_incomplete" : bool,
|
||||
}
|
||||
|
@ -230,15 +231,16 @@ class ResultatsSemestre(ResultatsCache):
|
|||
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),
|
||||
"coefficient": evaluation.coefficient,
|
||||
"description": evaluation.description,
|
||||
"etat": {
|
||||
"blocked": evaluation.is_blocked(),
|
||||
"evalcomplete": etat.is_complete,
|
||||
"nb_notes": etat.nb_notes,
|
||||
"last_modif": last_modif,
|
||||
},
|
||||
"evaluation_id": evaluation.id,
|
||||
"jour": evaluation.date_debut or datetime.datetime(1900, 1, 1),
|
||||
"publish_incomplete": evaluation.publish_incomplete,
|
||||
}
|
||||
|
||||
|
@ -432,9 +434,24 @@ class ResultatsSemestre(ResultatsCache):
|
|||
ue_cap_dict["compense_formsemestre_id"] = None
|
||||
return ue_cap_dict
|
||||
|
||||
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
|
||||
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict | None:
|
||||
"""L'état de l'UE pour cet étudiant.
|
||||
Result: dict, ou None si l'UE n'est pas dans ce semestre.
|
||||
{
|
||||
"is_capitalized": # vrai si la version capitalisée est celle prise en compte (meilleure)
|
||||
"was_capitalized":# si elle a été capitalisée (meilleure ou pas)
|
||||
"is_external": # si UE externe
|
||||
"coef_ue": 0.0,
|
||||
"cur_moy_ue": 0.0, # moyenne de l'UE courante
|
||||
"moy": 0.0, # moyenne prise en compte
|
||||
"event_date": # date de la capiltalisation éventuelle (ou None)
|
||||
"ue": ue_dict, # l'UE, comme un dict
|
||||
"formsemestre_id": None,
|
||||
"capitalized_ue_id": None, # l'id de l'UE capitalisée, ou None
|
||||
"ects_pot": 0.0, # deprecated (les ECTS liés à cette UE)
|
||||
"ects": 0.0, # les ECTS acquis grace à cette UE
|
||||
"ects_ue": # les ECTS liés à cette UE
|
||||
}
|
||||
"""
|
||||
ue: UniteEns = db.session.get(UniteEns, ue_id)
|
||||
ue_dict = ue.to_dict()
|
||||
|
@ -455,7 +472,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||
"ects": 0.0,
|
||||
"ects_ue": ue.ects,
|
||||
}
|
||||
if not ue_id in self.etud_moy_ue:
|
||||
if not ue_id in self.etud_moy_ue or not etudid in self.etud_moy_ue[ue_id]:
|
||||
return None
|
||||
if not self.validations:
|
||||
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
|
||||
|
@ -512,11 +529,13 @@ class ResultatsSemestre(ResultatsCache):
|
|||
"is_external": ue_cap["is_external"] if is_capitalized else ue.is_external,
|
||||
"coef_ue": coef_ue,
|
||||
"ects_pot": ue.ects or 0.0,
|
||||
"ects": self.validations.decisions_jury_ues.get(etudid, {})
|
||||
.get(ue.id, {})
|
||||
.get("ects", 0.0)
|
||||
if self.validations.decisions_jury_ues
|
||||
else 0.0,
|
||||
"ects": (
|
||||
self.validations.decisions_jury_ues.get(etudid, {})
|
||||
.get(ue.id, {})
|
||||
.get("ects", 0.0)
|
||||
if self.validations.decisions_jury_ues
|
||||
else 0.0
|
||||
),
|
||||
"ects_ue": ue.ects,
|
||||
"cur_moy_ue": cur_moy_ue,
|
||||
"moy": moy_ue,
|
||||
|
|
|
@ -58,7 +58,6 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
self.moy_moy = "NA"
|
||||
self.moy_gen_rangs_by_group = {} # { group_id : (Series, Series) }
|
||||
self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}}
|
||||
self.expr_diagnostics = ""
|
||||
self.parcours = self.formsemestre.formation.get_cursus()
|
||||
self._modimpls_dict_by_ue = {} # local cache
|
||||
|
||||
|
@ -217,9 +216,9 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
# Rangs / UEs:
|
||||
for ue in ues:
|
||||
group_moys_ue = self.etud_moy_ue[ue.id][group_members]
|
||||
self.ue_rangs_by_group.setdefault(ue.id, {})[
|
||||
group.id
|
||||
] = moy_sem.comp_ranks_series(group_moys_ue * mask_inscr)
|
||||
self.ue_rangs_by_group.setdefault(ue.id, {})[group.id] = (
|
||||
moy_sem.comp_ranks_series(group_moys_ue * mask_inscr)
|
||||
)
|
||||
|
||||
def get_etud_rang(self, etudid: int) -> str:
|
||||
"""Le rang (classement) de l'étudiant dans le semestre.
|
||||
|
|
|
@ -126,6 +126,7 @@ class AjoutAssiOrJustForm(FlaskForm):
|
|||
|
||||
class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
|
||||
"Formulaire de saisie d'une assiduité pour un étudiant"
|
||||
|
||||
description = TextAreaField(
|
||||
"Description",
|
||||
render_kw={
|
||||
|
@ -152,6 +153,7 @@ class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
|
|||
|
||||
class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
||||
"Formulaire de saisie d'un justificatif pour un étudiant"
|
||||
|
||||
raison = TextAreaField(
|
||||
"Raison",
|
||||
render_kw={
|
||||
|
@ -176,6 +178,12 @@ class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
|||
|
||||
|
||||
class ChoixDateForm(FlaskForm):
|
||||
"""
|
||||
Formulaire de choix de date
|
||||
(utilisé par la page de choix de date
|
||||
si la date courante n'est pas dans le semestre)
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Init form, adding a filed for our error messages"
|
||||
super().__init__(*args, **kwargs)
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
##############################################################################
|
||||
#
|
||||
# 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 options génération table poursuite études (PE)
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import BooleanField, HiddenField, SubmitField
|
||||
|
||||
|
||||
class ParametrageClasseurPE(FlaskForm):
|
||||
"Formulaire paramétrage génération classeur PE"
|
||||
# cohorte_restreinte = BooleanField(
|
||||
# "Restreindre aux étudiants inscrits dans le semestre (sans interclassement de promotion) (à venir)"
|
||||
# )
|
||||
moyennes_tags = BooleanField(
|
||||
"Générer les moyennes sur les tags de modules personnalisés (cf. programme de formation)",
|
||||
default=True,
|
||||
render_kw={"checked": ""},
|
||||
)
|
||||
moyennes_ue_res_sae = BooleanField(
|
||||
"Générer les moyennes des ressources et des SAEs",
|
||||
default=True,
|
||||
render_kw={"checked": ""},
|
||||
)
|
||||
moyennes_ues_rcues = BooleanField(
|
||||
"Générer les moyennes par RCUEs (compétences) et leurs synthèses HTML étudiant par étudiant",
|
||||
default=True,
|
||||
render_kw={"checked": ""},
|
||||
)
|
||||
|
||||
min_max_moy = BooleanField("Afficher les colonnes min/max/moy")
|
||||
|
||||
# synthese_individuelle_etud = BooleanField(
|
||||
# "Générer (suppose les RCUES)"
|
||||
# )
|
||||
publipostage = BooleanField(
|
||||
"Nomme les moyennes pour publipostage",
|
||||
# default=False,
|
||||
# render_kw={"checked": ""},
|
||||
)
|
||||
|
||||
classeurs_detailles = BooleanField(
|
||||
"Générer des classeurs intermédiaires (pour debuggage)", default=False
|
||||
)
|
||||
|
||||
submit = SubmitField("Générer les classeurs poursuites d'études")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
|
@ -5,7 +5,6 @@ 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 (
|
||||
|
@ -89,6 +88,8 @@ class Assiduite(ScoDocModel):
|
|||
lazy="select",
|
||||
)
|
||||
|
||||
# Argument "restrict" obligatoire car on override la fonction "to_dict" de ScoDocModel
|
||||
# pylint: disable-next=unused-argument
|
||||
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.
|
||||
|
@ -307,6 +308,9 @@ class Assiduite(ScoDocModel):
|
|||
|
||||
def supprime(self):
|
||||
"Supprime l'assiduité. Log et commit."
|
||||
|
||||
# Obligatoire car import circulaire sinon
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from app.scodoc import sco_assiduites as scass
|
||||
|
||||
if g.scodoc_dept is None and self.etudiant.dept_id is not None:
|
||||
|
@ -356,7 +360,7 @@ class Assiduite(ScoDocModel):
|
|||
|
||||
date: str = self.entry_date.strftime("%d/%m/%Y à %H:%M")
|
||||
utilisateur: str = ""
|
||||
if self.user != None:
|
||||
if self.user is not None:
|
||||
self.user: User
|
||||
utilisateur = f"par {self.user.get_prenomnom()}"
|
||||
|
||||
|
@ -515,6 +519,8 @@ class Justificatif(ScoDocModel):
|
|||
def create_justificatif(
|
||||
cls,
|
||||
etudiant: Identite,
|
||||
# On a besoin des arguments mais on utilise "locals" pour les récupérer
|
||||
# pylint: disable=unused-argument
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatJustificatif,
|
||||
|
@ -538,8 +544,10 @@ class Justificatif(ScoDocModel):
|
|||
|
||||
def supprime(self):
|
||||
"Supprime le justificatif. Log et commit."
|
||||
|
||||
# Obligatoire car import circulaire sinon
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from app.scodoc import sco_assiduites as scass
|
||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||
|
||||
# Récupération de l'archive du justificatif
|
||||
archive_name: str = self.fichier
|
||||
|
@ -566,11 +574,7 @@ class Justificatif(ScoDocModel):
|
|||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
# On actualise les assiduités justifiées de l'étudiant concerné
|
||||
compute_assiduites_justified(
|
||||
self.etudid,
|
||||
Justificatif.query.filter_by(etudid=self.etudid).all(),
|
||||
True,
|
||||
)
|
||||
self.dejustifier_assiduites()
|
||||
|
||||
def get_fichiers(self) -> tuple[list[str], int]:
|
||||
"""Renvoie la liste des noms de fichiers justicatifs
|
||||
|
@ -592,6 +596,64 @@ class Justificatif(ScoDocModel):
|
|||
accessible_filenames.append(filename[0])
|
||||
return accessible_filenames, len(filenames)
|
||||
|
||||
def justifier_assiduites(
|
||||
self,
|
||||
) -> list[int]:
|
||||
"""Justifie les assiduités sur la période de validité du justificatif"""
|
||||
log(f"justifier_assiduites: {self}")
|
||||
assiduites_justifiees: list[int] = []
|
||||
if self.etat != EtatJustificatif.VALIDE:
|
||||
return []
|
||||
# On récupère les assiduités de l'étudiant sur la période donnée
|
||||
assiduites: Query = self.etudiant.assiduites.filter(
|
||||
Assiduite.date_debut >= self.date_debut,
|
||||
Assiduite.date_fin <= self.date_fin,
|
||||
Assiduite.etat != EtatAssiduite.PRESENT,
|
||||
)
|
||||
# Pour chaque assiduité, on la justifie
|
||||
for assi in assiduites:
|
||||
assi.est_just = True
|
||||
assiduites_justifiees.append(assi.assiduite_id)
|
||||
db.session.add(assi)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return assiduites_justifiees
|
||||
|
||||
def dejustifier_assiduites(self) -> list[int]:
|
||||
"""
|
||||
Déjustifie les assiduités sur la période du justificatif
|
||||
"""
|
||||
assiduites_dejustifiees: list[int] = []
|
||||
|
||||
# On récupère les assiduités de l'étudiant sur la période donnée
|
||||
assiduites: Query = self.etudiant.assiduites.filter(
|
||||
Assiduite.date_debut >= self.date_debut,
|
||||
Assiduite.date_fin <= self.date_fin,
|
||||
Assiduite.etat != EtatAssiduite.PRESENT,
|
||||
)
|
||||
assi: Assiduite
|
||||
for assi in assiduites:
|
||||
# On récupère les justificatifs qui justifient l'assiduité `assi`
|
||||
assi_justifs: list[int] = get_justifs_from_date(
|
||||
self.etudiant.etudid,
|
||||
assi.date_debut,
|
||||
assi.date_fin,
|
||||
long=False,
|
||||
valid=True,
|
||||
)
|
||||
# Si il n'y a pas d'autre justificatif valide, on déjustifie l'assiduité
|
||||
if len(assi_justifs) == 0 or (
|
||||
len(assi_justifs) == 1 and assi_justifs[0] == self.justif_id
|
||||
):
|
||||
assi.est_just = False
|
||||
assiduites_dejustifiees.append(assi.assiduite_id)
|
||||
db.session.add(assi)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return assiduites_dejustifiees
|
||||
|
||||
|
||||
def is_period_conflicting(
|
||||
date_debut: datetime,
|
||||
|
@ -615,72 +677,6 @@ def is_period_conflicting(
|
|||
return count > 0
|
||||
|
||||
|
||||
def compute_assiduites_justified(
|
||||
etudid: int, justificatifs: list[Justificatif] = None, reset: bool = False
|
||||
) -> list[int]:
|
||||
"""
|
||||
Args:
|
||||
etudid (int): l'identifiant de l'étudiant
|
||||
justificatifs (list[Justificatif]): La liste des justificatifs qui seront utilisés
|
||||
reset (bool, optional): remet à false les assiduites non justifiés. Defaults to False.
|
||||
|
||||
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(
|
||||
etudid=etudid
|
||||
).all()
|
||||
|
||||
# On ne prend que les justificatifs valides
|
||||
justificatifs = [j for j in justificatifs if j.etat == EtatJustificatif.VALIDE]
|
||||
|
||||
# On récupère les assiduités de l'étudiant
|
||||
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
|
||||
|
||||
assiduites_justifiees: list[int] = []
|
||||
|
||||
for assi in assiduites:
|
||||
# On ne justifie pas les Présences
|
||||
if assi.etat == EtatAssiduite.PRESENT:
|
||||
continue
|
||||
|
||||
# On récupère les justificatifs qui justifient l'assiduité `assi`
|
||||
assi_justificatifs = Justificatif.query.filter(
|
||||
Justificatif.etudid == assi.etudid,
|
||||
Justificatif.date_debut <= assi.date_debut,
|
||||
Justificatif.date_fin >= assi.date_fin,
|
||||
Justificatif.etat == EtatJustificatif.VALIDE,
|
||||
).all()
|
||||
|
||||
# Si au moins un justificatif possède une période qui couvre l'assiduité
|
||||
if any(
|
||||
assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin
|
||||
for j in justificatifs + assi_justificatifs
|
||||
):
|
||||
# On justifie l'assiduité
|
||||
# On ajoute l'id de l'assiduité à la liste des assiduités justifiées
|
||||
assi.est_just = True
|
||||
assiduites_justifiees.append(assi.assiduite_id)
|
||||
db.session.add(assi)
|
||||
elif reset:
|
||||
# Si le paramètre reset est Vrai alors les assiduités non justifiées
|
||||
# sont remise en "non justifiée"
|
||||
assi.est_just = False
|
||||
db.session.add(assi)
|
||||
# On valide la session
|
||||
db.session.commit()
|
||||
# On renvoie la liste des assiduite_id des assiduités justifiées
|
||||
return assiduites_justifiees
|
||||
|
||||
|
||||
def get_assiduites_justif(assiduite_id: int, long: bool) -> list[int | dict]:
|
||||
"""
|
||||
get_assiduites_justif Récupération des justificatifs d'une assiduité
|
||||
|
|
|
@ -125,7 +125,7 @@ class Identite(models.ScoDocModel):
|
|||
)
|
||||
|
||||
# Champs "protégés" par ViewEtudData (RGPD)
|
||||
protected_attrs = {"boursier"}
|
||||
protected_attrs = {"boursier", "nationalite"}
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
|
|
|
@ -10,6 +10,7 @@ from flask_login import current_user
|
|||
import sqlalchemy as sa
|
||||
|
||||
from app import db, log
|
||||
from app import models
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.events import ScolarNews
|
||||
from app.models.notes import NotesNotes
|
||||
|
@ -23,10 +24,8 @@ MAX_EVALUATION_DURATION = datetime.timedelta(days=365)
|
|||
NOON = datetime.time(12, 00)
|
||||
DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
|
||||
|
||||
VALID_EVALUATION_TYPES = {0, 1, 2}
|
||||
|
||||
|
||||
class Evaluation(db.Model):
|
||||
class Evaluation(models.ScoDocModel):
|
||||
"""Evaluation (contrôle, examen, ...)"""
|
||||
|
||||
__tablename__ = "notes_evaluation"
|
||||
|
@ -38,9 +37,9 @@ class Evaluation(db.Model):
|
|||
)
|
||||
date_debut = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||
date_fin = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||
description = db.Column(db.Text)
|
||||
note_max = db.Column(db.Float)
|
||||
coefficient = db.Column(db.Float)
|
||||
description = db.Column(db.Text, nullable=False)
|
||||
note_max = db.Column(db.Float, nullable=False)
|
||||
coefficient = db.Column(db.Float, nullable=False)
|
||||
visibulletin = db.Column(
|
||||
db.Boolean, nullable=False, default=True, server_default="true"
|
||||
)
|
||||
|
@ -48,15 +47,30 @@ class Evaluation(db.Model):
|
|||
publish_incomplete = db.Column(
|
||||
db.Boolean, nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# type d'evaluation: 0 normale, 1 rattrapage, 2 "2eme session"
|
||||
"prise en compte immédiate"
|
||||
evaluation_type = db.Column(
|
||||
db.Integer, nullable=False, default=0, server_default="0"
|
||||
)
|
||||
"type d'evaluation: 0 normale, 1 rattrapage, 2 2eme session, 3 bonus"
|
||||
blocked_until = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||
"date de prise en compte"
|
||||
BLOCKED_FOREVER = datetime.datetime(2666, 12, 31, tzinfo=scu.TIME_ZONE)
|
||||
# ordre de presentation (par défaut, le plus petit numero
|
||||
# est la plus ancienne eval):
|
||||
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
||||
|
||||
EVALUATION_NORMALE = 0 # valeurs stockées en base, ne pas changer !
|
||||
EVALUATION_RATTRAPAGE = 1
|
||||
EVALUATION_SESSION2 = 2
|
||||
EVALUATION_BONUS = 3
|
||||
VALID_EVALUATION_TYPES = {
|
||||
EVALUATION_NORMALE,
|
||||
EVALUATION_RATTRAPAGE,
|
||||
EVALUATION_SESSION2,
|
||||
EVALUATION_BONUS,
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<Evaluation {self.id} {
|
||||
self.date_debut.isoformat() if self.date_debut else ''} "{
|
||||
|
@ -70,15 +84,17 @@ class Evaluation(db.Model):
|
|||
date_fin: datetime.datetime = None,
|
||||
description=None,
|
||||
note_max=None,
|
||||
blocked_until=None,
|
||||
coefficient=None,
|
||||
visibulletin=None,
|
||||
publish_incomplete=None,
|
||||
evaluation_type=None,
|
||||
numero=None,
|
||||
**kw, # ceci pour absorber les éventuel arguments excedentaires
|
||||
):
|
||||
) -> "Evaluation":
|
||||
"""Create an evaluation. Check permission and all arguments.
|
||||
Ne crée pas les poids vers les UEs.
|
||||
Add to session, do not commit.
|
||||
"""
|
||||
if not moduleimpl.can_edit_evaluation(current_user):
|
||||
raise AccessDenied(
|
||||
|
@ -87,13 +103,15 @@ class Evaluation(db.Model):
|
|||
args = locals()
|
||||
del args["cls"]
|
||||
del args["kw"]
|
||||
check_convert_evaluation_args(moduleimpl, args)
|
||||
check_and_convert_evaluation_args(args, moduleimpl)
|
||||
# Check numeros
|
||||
Evaluation.moduleimpl_evaluation_renumber(moduleimpl, only_if_unumbered=True)
|
||||
if not "numero" in args or args["numero"] is None:
|
||||
args["numero"] = cls.get_new_numero(moduleimpl, args["date_debut"])
|
||||
#
|
||||
evaluation = Evaluation(**args)
|
||||
db.session.add(evaluation)
|
||||
db.session.flush()
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=moduleimpl.formsemestre_id)
|
||||
url = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
|
@ -196,6 +214,10 @@ class Evaluation(db.Model):
|
|||
def to_dict_api(self) -> dict:
|
||||
"Représentation dict pour API JSON"
|
||||
return {
|
||||
"blocked": self.is_blocked(),
|
||||
"blocked_until": (
|
||||
self.blocked_until.isoformat() if self.blocked_until else ""
|
||||
),
|
||||
"coefficient": self.coefficient,
|
||||
"date_debut": self.date_debut.isoformat() if self.date_debut else "",
|
||||
"date_fin": self.date_fin.isoformat() if self.date_fin else "",
|
||||
|
@ -210,9 +232,9 @@ class Evaluation(db.Model):
|
|||
"visibulletin": self.visibulletin,
|
||||
# Deprecated (supprimer avant #sco9.7)
|
||||
"date": self.date_debut.date().isoformat() if self.date_debut else "",
|
||||
"heure_debut": self.date_debut.time().isoformat()
|
||||
if self.date_debut
|
||||
else "",
|
||||
"heure_debut": (
|
||||
self.date_debut.time().isoformat() if self.date_debut else ""
|
||||
),
|
||||
"heure_fin": self.date_fin.time().isoformat() if self.date_fin else "",
|
||||
}
|
||||
|
||||
|
@ -232,15 +254,6 @@ class Evaluation(db.Model):
|
|||
|
||||
return e_dict
|
||||
|
||||
def from_dict(self, data):
|
||||
"""Set evaluation attributes from given dict values."""
|
||||
check_convert_evaluation_args(self.moduleimpl, data)
|
||||
if data.get("numero") is None:
|
||||
data["numero"] = Evaluation.get_max_numero(self.moduleimpl.id) + 1
|
||||
for k in self.__dict__:
|
||||
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
|
||||
|
@ -358,19 +371,6 @@ class Evaluation(db.Model):
|
|||
Chaine vide si non renseignée."""
|
||||
return self.date_fin.time().isoformat("minutes") if self.date_fin else ""
|
||||
|
||||
def clone(self, not_copying=()):
|
||||
"""Clone, not copying the given attrs
|
||||
Attention: la copie n'a pas d'id avant le prochain commit
|
||||
"""
|
||||
d = dict(self.__dict__)
|
||||
d.pop("id") # get rid of id
|
||||
d.pop("_sa_instance_state") # get rid of SQLAlchemy special attr
|
||||
for k in not_copying:
|
||||
d.pop(k)
|
||||
copy = self.__class__(**d)
|
||||
db.session.add(copy)
|
||||
return copy
|
||||
|
||||
def is_matin(self) -> bool:
|
||||
"Evaluation commençant le matin (faux si pas de date)"
|
||||
if not self.date_debut:
|
||||
|
@ -383,6 +383,14 @@ class Evaluation(db.Model):
|
|||
return False
|
||||
return self.date_debut.time() >= NOON
|
||||
|
||||
def is_blocked(self, now=None) -> bool:
|
||||
"True si prise en compte bloquée"
|
||||
if self.blocked_until is None:
|
||||
return False
|
||||
if now is None:
|
||||
now = datetime.datetime.now(scu.TIME_ZONE)
|
||||
return self.blocked_until > now
|
||||
|
||||
def set_default_poids(self) -> bool:
|
||||
"""Initialize les poids vers les UE à leurs valeurs par défaut
|
||||
C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.
|
||||
|
@ -471,6 +479,29 @@ class Evaluation(db.Model):
|
|||
"""
|
||||
return NotesNotes.query.filter_by(etudid=etud.id, evaluation_id=self.id).first()
|
||||
|
||||
@classmethod
|
||||
def get_evaluations_blocked_for_etud(
|
||||
cls, formsemestre, etud: Identite
|
||||
) -> list["Evaluation"]:
|
||||
"""Liste des évaluations de ce semestre avec note pour cet étudiant et date blocage
|
||||
et date blocage < FOREVER.
|
||||
Si non vide, une note apparaitra dans le futur pour cet étudiant: il faut
|
||||
donc interdire la saisie du jury.
|
||||
"""
|
||||
now = datetime.datetime.now(scu.TIME_ZONE)
|
||||
return (
|
||||
Evaluation.query.filter(
|
||||
Evaluation.blocked_until != None, # pylint: disable=C0121
|
||||
Evaluation.blocked_until >= now,
|
||||
)
|
||||
.join(ModuleImpl)
|
||||
.filter_by(formsemestre_id=formsemestre.id)
|
||||
.join(ModuleImplInscription)
|
||||
.filter_by(etudid=etud.id)
|
||||
.join(NotesNotes)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
class EvaluationUEPoids(db.Model):
|
||||
"""Poids des évaluations (BUT)
|
||||
|
@ -528,7 +559,7 @@ def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
|
|||
return e_dict
|
||||
|
||||
|
||||
def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
|
||||
def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
|
||||
"""Check coefficient, dates and duration, raises exception if invalid.
|
||||
Convert date and time strings to date and time objects.
|
||||
|
||||
|
@ -543,7 +574,7 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
|
|||
# --- evaluation_type
|
||||
try:
|
||||
data["evaluation_type"] = int(data.get("evaluation_type", 0) or 0)
|
||||
if not data["evaluation_type"] in VALID_EVALUATION_TYPES:
|
||||
if not data["evaluation_type"] in Evaluation.VALID_EVALUATION_TYPES:
|
||||
raise ScoValueError("invalid evaluation_type value")
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("invalid evaluation_type value") from exc
|
||||
|
@ -568,7 +599,7 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
|
|||
if coef < 0:
|
||||
raise ScoValueError("invalid coefficient value (must be positive or null)")
|
||||
data["coefficient"] = coef
|
||||
# --- date de l'évaluation
|
||||
# --- date de l'évaluation dans le semestre ?
|
||||
formsemestre = moduleimpl.formsemestre
|
||||
date_debut = data.get("date_debut", None)
|
||||
if date_debut:
|
||||
|
@ -609,6 +640,8 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
|
|||
"Heures de l'évaluation incohérentes !",
|
||||
dest_url="javascript:history.back();",
|
||||
)
|
||||
if "blocked_until" in data:
|
||||
data["blocked_until"] = data["blocked_until"] or None
|
||||
|
||||
|
||||
def heure_to_time(heure: str) -> datetime.time:
|
||||
|
@ -638,3 +671,6 @@ def _moduleimpl_evaluation_insert_before(
|
|||
db.session.add(e)
|
||||
db.session.commit()
|
||||
return n
|
||||
|
||||
|
||||
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
||||
|
|
|
@ -93,6 +93,10 @@ class FormSemestre(db.Model):
|
|||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
"Si vrai, la moyenne générale indicative BUT n'est pas calculée"
|
||||
mode_calcul_moyennes = db.Column(
|
||||
db.Integer, nullable=False, default=0, server_default="0"
|
||||
)
|
||||
"pour usage futur"
|
||||
gestion_semestrielle = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
|
@ -871,7 +875,7 @@ class FormSemestre(db.Model):
|
|||
|
||||
def get_abs_count(self, etudid):
|
||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||
tuple (nb abs, nb abs justifiées)
|
||||
tuple (nb abs non just, nb abs justifiées, nb abs total)
|
||||
Utilise un cache.
|
||||
"""
|
||||
from app.scodoc import sco_assiduites
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
"""ScoDoc 9 models : Modules
|
||||
"""
|
||||
|
||||
from flask import current_app, g
|
||||
|
||||
from app import db
|
||||
from app import models
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models.but_refcomp import ApcParcours, app_critiques_modules, parcours_modules
|
||||
from app.models.but_refcomp import (
|
||||
ApcParcours,
|
||||
ApcReferentielCompetences,
|
||||
app_critiques_modules,
|
||||
parcours_modules,
|
||||
)
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
class Module(db.Model):
|
||||
class Module(models.ScoDocModel):
|
||||
"""Module"""
|
||||
|
||||
__tablename__ = "notes_modules"
|
||||
|
@ -76,6 +83,55 @@ class Module(db.Model):
|
|||
return f"""<Module{ModuleType(self.module_type or ModuleType.STANDARD).name
|
||||
} id={self.id} code={self.code!r} semestre_id={self.semestre_id}>"""
|
||||
|
||||
@classmethod
|
||||
def convert_dict_fields(cls, args: dict) -> dict:
|
||||
"""Convert fields in the given dict. No other side effect.
|
||||
returns: dict to store in model's db.
|
||||
"""
|
||||
# s'assure que ects etc est non ''
|
||||
fs_empty_stored_as_nulls = {
|
||||
"coefficient",
|
||||
"ects",
|
||||
"heures_cours",
|
||||
"heures_td",
|
||||
"heures_tp",
|
||||
}
|
||||
args_dict = {}
|
||||
for key, value in args.items():
|
||||
if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
|
||||
if key in fs_empty_stored_as_nulls and value == "":
|
||||
value = None
|
||||
args_dict[key] = value
|
||||
|
||||
return args_dict
|
||||
|
||||
@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.
|
||||
Add 'id' to excluded."""
|
||||
# on ne peut pas affecter directement parcours
|
||||
return super().filter_model_attributes(data, (excluded or set()) | {"parcours"})
|
||||
|
||||
@classmethod
|
||||
def create_from_dict(cls, data: dict) -> "Module":
|
||||
"""Create from given dict, add parcours"""
|
||||
mod = super().create_from_dict(data)
|
||||
for p in data.get("parcours", []) or []:
|
||||
if isinstance(p, ApcParcours):
|
||||
parcour: ApcParcours = p
|
||||
else:
|
||||
pid = int(p)
|
||||
query = ApcParcours.query.filter_by(id=pid)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(ApcReferentielCompetences).filter_by(
|
||||
dept_id=g.scodoc_dept_id
|
||||
)
|
||||
parcour: ApcParcours = query.first()
|
||||
if parcour is None:
|
||||
raise ScoValueError("Parcours invalide")
|
||||
mod.parcours.append(parcour)
|
||||
return mod
|
||||
|
||||
def clone(self):
|
||||
"""Create a new copy of this module."""
|
||||
mod = Module(
|
||||
|
|
|
@ -126,7 +126,7 @@ class ScolarFormSemestreValidation(db.Model):
|
|||
def ects(self) -> float:
|
||||
"Les ECTS acquis par cette validation. (0 si ce n'est pas une validation d'UE)"
|
||||
return (
|
||||
self.ue.ects
|
||||
self.ue.ects or 0.0
|
||||
if (self.ue is not None) and (self.code in CODES_UE_VALIDES)
|
||||
else 0.0
|
||||
)
|
||||
|
|
|
@ -0,0 +1,343 @@
|
|||
##############################################################################
|
||||
#
|
||||
# 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.models import Identite
|
||||
from app.pe import pe_affichage
|
||||
from app.pe.moys import pe_tabletags, pe_moy, pe_moytag, pe_sxtag
|
||||
from app.pe.rcss import pe_rcs
|
||||
import app.pe.pe_comp as pe_comp
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
class InterClassTag(pe_tabletags.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'), qu'il soit
|
||||
de type SemX ou RCSemX,
|
||||
en reportant les moyennes obtenues sur à la version tagguée
|
||||
du RCS (de type SxTag ou RCSTag).
|
||||
Sont ensuite calculés les classements (uniquement)
|
||||
sur les étudiants diplômes.
|
||||
|
||||
Args:
|
||||
nom_rcs: Le nom de l'aggrégat
|
||||
type_interclassement: Le type d'interclassement (par UE ou par compétences)
|
||||
etudiants_diplomes: L'identité des étudiants diplômés
|
||||
rcss: Un dictionnaire {(nom_rcs, fid_final): RCS} donnant soit
|
||||
les SemX soit les RCSemX recencés par le jury PE
|
||||
rcstag: Un dictionnaire {(nom_rcs, fid_final): RCSTag} donnant
|
||||
soit les SxTag (associés aux SemX)
|
||||
soit les RCSTags (associés au RCSemX) calculés par le jury PE
|
||||
suivis: Un dictionnaire associé à chaque étudiant son rcss
|
||||
(de la forme ``{etudid: {nom_rcs: RCS_suivi}}``)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
nom_rcs: str,
|
||||
type_interclassement: str,
|
||||
etudiants_diplomes: dict[int, Identite],
|
||||
rcss: dict[(str, int) : pe_rcs.RCS],
|
||||
rcstags: dict[(str, int) : pe_tabletags.TableTag],
|
||||
suivis: dict[int:dict],
|
||||
):
|
||||
pe_tabletags.TableTag.__init__(self)
|
||||
|
||||
self.nom_rcs: str = nom_rcs
|
||||
"""Le nom du RCS interclassé"""
|
||||
|
||||
# Le type d'interclassement
|
||||
self.type = type_interclassement
|
||||
|
||||
pe_affichage.pe_print(
|
||||
f"*** Interclassement par 🗂️{type_interclassement} pour le RCS ⏯️{nom_rcs}"
|
||||
)
|
||||
|
||||
# Les informations sur les étudiants diplômés
|
||||
self.etuds: list[Identite] = list(etudiants_diplomes.values())
|
||||
"""Identités des étudiants diplômés"""
|
||||
self.add_etuds(self.etuds)
|
||||
|
||||
self.diplomes_ids = set(etudiants_diplomes.keys())
|
||||
"""Etudids des étudiants diplômés"""
|
||||
|
||||
# Les RCS de l'aggrégat (SemX ou RCSemX)
|
||||
self.rcss: dict[(str, int), pe_rcs.RCS] = {}
|
||||
"""Ensemble des SemX ou des RCSemX associés à l'aggrégat"""
|
||||
for (nom, fid), rcs in rcss.items():
|
||||
if nom == nom_rcs:
|
||||
self.rcss[(nom, fid)] = rcss
|
||||
|
||||
# Les données tagguées
|
||||
self.rcstags: dict[(str, int), pe_tabletags.TableTag] = {}
|
||||
"""Ensemble des SxTag ou des RCSTags associés à l'aggrégat"""
|
||||
for rcs_id in self.rcss:
|
||||
self.rcstags[rcs_id] = rcstags[rcs_id]
|
||||
|
||||
# Les RCS (SemX ou RCSemX) suivis par les étudiants du jury,
|
||||
# en ne gardant que ceux associés aux diplomés
|
||||
self.suivis: dict[int, pe_rcs.RCS] = {}
|
||||
"""Association entre chaque étudiant et le SxTag ou RCSTag à prendre
|
||||
pour l'aggrégat"""
|
||||
for etudid in self.diplomes_ids:
|
||||
self.suivis[etudid] = suivis[etudid][nom_rcs]
|
||||
|
||||
# Les données sur les tags
|
||||
self.tags_sorted = self._do_taglist()
|
||||
"""Liste des tags (triés par ordre alphabétique)"""
|
||||
aff = pe_affichage.repr_tags(self.tags_sorted)
|
||||
pe_affichage.pe_print(f"--> Tags : {aff}")
|
||||
|
||||
# Les données sur les UEs (si SxTag) ou compétences (si RCSTag)
|
||||
self.champs_sorted = self._do_ues_ou_competences_list()
|
||||
"""Les champs (UEs ou compétences) de l'interclassement"""
|
||||
if self.type == pe_moytag.CODE_MOY_UE:
|
||||
pe_affichage.pe_print(
|
||||
f"--> UEs : {pe_affichage.aff_UEs(self.champs_sorted)}"
|
||||
)
|
||||
else:
|
||||
pe_affichage.pe_print(
|
||||
f"--> Compétences : {pe_affichage.aff_competences(self.champs_sorted)}"
|
||||
)
|
||||
|
||||
# Etudids triés
|
||||
self.etudids_sorted = sorted(list(self.diplomes_ids))
|
||||
|
||||
self.nom = self.get_repr()
|
||||
"""Représentation textuelle de l'interclassement"""
|
||||
|
||||
# Synthétise les moyennes/classements par tag
|
||||
self.moyennes_tags: dict[str, pe_moytag.MoyennesTag] = {}
|
||||
for tag in self.tags_sorted:
|
||||
# Les moyennes tous modules confondus
|
||||
notes_gen = self.compute_notes_matrice(tag)
|
||||
|
||||
# Les coefficients de la moyenne générale
|
||||
coeffs = self.compute_coeffs_matrice(tag)
|
||||
aff = pe_affichage.repr_profil_coeffs(coeffs, with_index=True)
|
||||
pe_affichage.pe_print(f"--> Moyenne 👜{tag} avec coeffs: {aff} ")
|
||||
|
||||
infos = {"aggregat": self.nom_rcs, "cohorte": pe_moytag.CHAMP_PROMO}
|
||||
|
||||
self.moyennes_tags[tag] = pe_moytag.MoyennesTag(
|
||||
tag,
|
||||
self.type,
|
||||
notes_gen,
|
||||
coeffs, # limite les moyennes aux étudiants de la promo
|
||||
infos,
|
||||
)
|
||||
|
||||
def get_repr(self) -> str:
|
||||
"""Une représentation textuelle"""
|
||||
return f"{self.nom_rcs} par {self.type}"
|
||||
|
||||
def _do_taglist(self):
|
||||
"""Synthétise les tags à partir des TableTags (SXTag ou RCSTag)
|
||||
|
||||
Returns:
|
||||
Une liste de tags triés par ordre alphabétique
|
||||
"""
|
||||
tags = []
|
||||
for rcstag in self.rcstags.values():
|
||||
tags.extend(rcstag.tags_sorted)
|
||||
return sorted(set(tags))
|
||||
|
||||
def compute_notes_matrice(self, tag) -> pd.DataFrame:
|
||||
"""Construit la matrice de notes (etudids x champs) en
|
||||
reportant les moyennes obtenues par les étudiants
|
||||
aux semestres de l'aggrégat pour le tag visé.
|
||||
|
||||
Les champs peuvent être des acronymes d'UEs ou des compétences.
|
||||
|
||||
Args:
|
||||
tag: Le tag visé
|
||||
Return:
|
||||
Le dataFrame (etudids x champs)
|
||||
reportant les moyennes des étudiants aux champs
|
||||
"""
|
||||
# etudids_sorted: Les etudids des étudiants (diplômés) triés
|
||||
# champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice
|
||||
|
||||
# Partant d'un dataframe vierge
|
||||
df = pd.DataFrame(np.nan, index=self.etudids_sorted, columns=self.champs_sorted)
|
||||
|
||||
for rcstag in self.rcstags.values():
|
||||
# Charge les moyennes au tag d'un RCStag
|
||||
if tag in rcstag.moyennes_tags:
|
||||
moytag = rcstag.moyennes_tags[tag]
|
||||
|
||||
notes = moytag.matrice_notes # dataframe etudids x ues
|
||||
|
||||
# Etudiants/Champs communs entre le RCSTag et les données interclassées
|
||||
(
|
||||
etudids_communs,
|
||||
champs_communs,
|
||||
) = pe_comp.find_index_and_columns_communs(df, notes)
|
||||
|
||||
# Injecte les notes par tag
|
||||
df.loc[etudids_communs, champs_communs] = notes.loc[
|
||||
etudids_communs, champs_communs
|
||||
]
|
||||
|
||||
return df
|
||||
|
||||
def compute_coeffs_matrice(self, tag) -> pd.DataFrame:
|
||||
"""Idem que compute_notes_matrices mais pour les coeffs
|
||||
|
||||
Args:
|
||||
tag: Le tag visé
|
||||
Return:
|
||||
Le dataFrame (etudids x champs)
|
||||
reportant les moyennes des étudiants aux champs
|
||||
"""
|
||||
# etudids_sorted: Les etudids des étudiants (diplômés) triés
|
||||
# champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice
|
||||
|
||||
# Partant d'un dataframe vierge
|
||||
df = pd.DataFrame(np.nan, index=self.etudids_sorted, columns=self.champs_sorted)
|
||||
|
||||
for rcstag in self.rcstags.values():
|
||||
if tag in rcstag.moyennes_tags:
|
||||
# Charge les coeffs au tag d'un RCStag
|
||||
coeffs: pd.DataFrame = rcstag.moyennes_tags[tag].matrice_coeffs
|
||||
|
||||
# Etudiants/Champs communs entre le RCSTag et les données interclassées
|
||||
(
|
||||
etudids_communs,
|
||||
champs_communs,
|
||||
) = pe_comp.find_index_and_columns_communs(df, coeffs)
|
||||
|
||||
# Injecte les coeffs par tag
|
||||
df.loc[etudids_communs, champs_communs] = coeffs.loc[
|
||||
etudids_communs, champs_communs
|
||||
]
|
||||
|
||||
return df
|
||||
|
||||
def _do_ues_ou_competences_list(self) -> list[str]:
|
||||
"""Synthétise les champs (UEs ou compétences) sur lesquels
|
||||
sont calculés les moyennes.
|
||||
|
||||
Returns:
|
||||
Un dictionnaire {'acronyme_ue' : 'compétences'}
|
||||
"""
|
||||
dict_champs = []
|
||||
for rcstag in self.rcstags.values():
|
||||
if isinstance(rcstag, pe_sxtag.SxTag):
|
||||
champs = rcstag.acronymes_sorted
|
||||
else: # pe_rcstag.RCSTag
|
||||
champs = rcstag.competences_sorted
|
||||
dict_champs.extend(champs)
|
||||
return sorted(set(dict_champs))
|
||||
|
||||
def has_tags(self):
|
||||
"""Indique si l'interclassement a des tags (cas d'un
|
||||
interclassement sur un S5 qui n'a pas eu lieu)
|
||||
"""
|
||||
return len(self.tags_sorted) > 0
|
||||
|
||||
def _un_rcstag_significatif(self, rcsstags: dict[(str, int):pe_tabletags]):
|
||||
"""Renvoie un rcstag significatif (ayant des tags et des notes aux tags)
|
||||
parmi le dictionnaire de rcsstags"""
|
||||
for rcstag_id, rcstag in rcsstags.items():
|
||||
moystags: pe_moytag.MoyennesTag = rcstag.moyennes_tags
|
||||
for tag, moystag in moystags.items():
|
||||
tags_tries = moystag.get_all_significant_tags()
|
||||
if tags_tries:
|
||||
return moystag
|
||||
return None
|
||||
|
||||
def compute_df_synthese_moyennes_tag(
|
||||
self, tag, aggregat=None, type_colonnes=False, options={"min_max_moy": True}
|
||||
) -> pd.DataFrame:
|
||||
"""Construit le dataframe retraçant pour les données des moyennes
|
||||
pour affichage dans la synthèse du jury PE. (cf. to_df())
|
||||
|
||||
Args:
|
||||
etudids_sorted: Les etudids des étudiants (diplômés) triés
|
||||
champs_sorted: Les champs (UE ou compétences) à faire apparaitre dans la matrice
|
||||
Return:
|
||||
Le dataFrame (etudids x champs)
|
||||
reportant les moyennes des étudiants aux champs
|
||||
"""
|
||||
if aggregat:
|
||||
assert (
|
||||
aggregat == self.nom_rcs
|
||||
), "L'interclassement ciblé ne correspond pas à l'aggrégat visé"
|
||||
|
||||
etudids_sorted = sorted(list(self.diplomes_ids))
|
||||
|
||||
if not self.rcstags:
|
||||
return None
|
||||
|
||||
# Partant d'un dataframe vierge
|
||||
initialisation = False
|
||||
df = pd.DataFrame()
|
||||
|
||||
# Pour chaque rcs (suivi) associe la liste des etudids l'ayant suivi
|
||||
asso_rcs_etudids = {}
|
||||
for etudid in etudids_sorted:
|
||||
rcs = self.suivis[etudid]
|
||||
if rcs:
|
||||
if rcs.rcs_id not in asso_rcs_etudids:
|
||||
asso_rcs_etudids[rcs.rcs_id] = []
|
||||
asso_rcs_etudids[rcs.rcs_id].append(etudid)
|
||||
|
||||
for rcs_id, etudids in asso_rcs_etudids.items():
|
||||
# Charge ses moyennes au RCSTag suivi
|
||||
rcstag = self.rcstags[rcs_id] # Le SxTag ou RCSTag
|
||||
# Charge la moyenne
|
||||
if tag in rcstag.moyennes_tags:
|
||||
moytag: pd.DataFrame = rcstag.moyennes_tags[tag]
|
||||
df_moytag = moytag.to_df(options=options)
|
||||
|
||||
# Modif les colonnes au regard du 1er df_moytag significatif lu
|
||||
if not initialisation:
|
||||
df = pd.DataFrame(
|
||||
np.nan, index=etudids_sorted, columns=df_moytag.columns
|
||||
)
|
||||
colonnes = list(df_moytag.columns)
|
||||
for col in colonnes:
|
||||
if col.endswith("rang"):
|
||||
df[col] = df[col].astype(str)
|
||||
initialisation = True
|
||||
|
||||
# Injecte les notes des étudiants
|
||||
df.loc[etudids, :] = df_moytag.loc[etudids, :]
|
||||
|
||||
return df
|
|
@ -0,0 +1,203 @@
|
|||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app.comp.moy_sem import comp_ranks_series
|
||||
from app.pe import pe_affichage
|
||||
|
||||
|
||||
class Moyenne:
|
||||
COLONNES = [
|
||||
"note",
|
||||
"classement",
|
||||
"rang",
|
||||
"min",
|
||||
"max",
|
||||
"moy",
|
||||
"nb_etuds",
|
||||
"nb_inscrits",
|
||||
]
|
||||
"""Colonnes du df"""
|
||||
|
||||
@classmethod
|
||||
def get_colonnes_synthese(cls, with_min_max_moy):
|
||||
"""Renvoie le nom des colonnes à prendre en compte pour la génération
|
||||
d'un dataFrame résumant les données d'un objet pe_moy.Moyenne"""
|
||||
if with_min_max_moy:
|
||||
return ["note", "rang", "min", "max", "moy"]
|
||||
else:
|
||||
return ["note", "rang"]
|
||||
|
||||
def __init__(self, notes: pd.Series, infos: dict[str]):
|
||||
"""Classe centralisant la synthèse des moyennes/class/stat d'une série
|
||||
de notes pour un groupe d'étudiants (déduits des notes).
|
||||
|
||||
Sont génerés des Séries/DataFrame donnant :
|
||||
|
||||
* les "notes" : notes (float),
|
||||
* des "classements" : classements (float),
|
||||
* des "min" : la note minimum sur tout le groupe d'étudiants,
|
||||
* des "max" : la note maximum sur tout le groupe d'étudiants,
|
||||
* des "moy" : la moyenne des notes sur tout le groupe d'étudiants,
|
||||
* des "nb_inscrits" : le nombre d'étudiants ayant une note (non NaN)
|
||||
|
||||
Args:
|
||||
notes: Une (pandas.)Série de notes
|
||||
infos: Un dictionnaire donnant les informations sur la moyenne (aggrégat,
|
||||
tag, intitule, cohorte, groupe)
|
||||
"""
|
||||
self.notes = notes
|
||||
"""Les notes"""
|
||||
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 note est non nan/renseignée"""
|
||||
self.df: pd.DataFrame = self.comp_moy_et_stat(self.notes)
|
||||
"""Le dataframe retraçant les moyennes/classements/statistiques"""
|
||||
self.infos = {
|
||||
"aggregat": infos["aggregat"],
|
||||
"tag": infos["tag"],
|
||||
"intitule": infos["intitule"],
|
||||
"cohorte": infos["cohorte"],
|
||||
}
|
||||
"""Dictionnaire donnant des informations sur la note (aggrégat, cohorte, tag, type_de_moyenne)"""
|
||||
# self.synthese = self.to_dict()
|
||||
# """La synthèse (dictionnaire) des notes/classements/statistiques"""
|
||||
|
||||
def __repr__(self):
|
||||
"""Représentation textuelle d'un objet Moyenne
|
||||
sur la base de ses `infos`.
|
||||
"""
|
||||
repr = get_repr(
|
||||
self.infos["aggregat"],
|
||||
self.infos["tag"],
|
||||
self.infos["intitule"],
|
||||
self.infos["cohorte"],
|
||||
)
|
||||
return f"Moyenne {repr}"
|
||||
|
||||
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 (pouvant être une moyenne d'un tag à une UE ou une moyenne générale
|
||||
d'un 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=Moyenne.COLONNES,
|
||||
)
|
||||
|
||||
# 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["nb_etuds"] = df["nb_etuds"].astype(int)
|
||||
|
||||
# Les étudiants dont la note n'est pas nulle
|
||||
inscrits_ids = notes[notes.notnull()].index.to_list()
|
||||
df.loc[inscrits_ids, "nb_inscrits"] = len(inscrits_ids)
|
||||
# df["nb_inscrits"] = df["nb_inscrits"].astype(int)
|
||||
|
||||
# Le classement des inscrits
|
||||
notes_non_nulles = notes[inscrits_ids]
|
||||
(class_str, class_int) = comp_ranks_series(notes_non_nulles)
|
||||
df.loc[inscrits_ids, "classement"] = class_int
|
||||
# df["classement"] = df["classement"].astype(int)
|
||||
|
||||
# Le rang (classement/nb_inscrit)
|
||||
df["rang"] = df["rang"].astype(str)
|
||||
df.loc[inscrits_ids, "rang"] = (
|
||||
df.loc[inscrits_ids, "classement"].astype(int).astype(str)
|
||||
+ "/"
|
||||
+ df.loc[inscrits_ids, "nb_inscrits"].astype(int).astype(str)
|
||||
)
|
||||
|
||||
# Les stat (des inscrits)
|
||||
df.loc[inscrits_ids, "min"] = notes.min()
|
||||
df.loc[inscrits_ids, "max"] = notes.max()
|
||||
df.loc[inscrits_ids, "moy"] = notes.mean()
|
||||
|
||||
return df
|
||||
|
||||
def to_df(self, with_min_max_moy=None):
|
||||
"""Renvoie le df de synthèse (tel qu'attendu dans les exports Excel),
|
||||
en limitant les colonnes à celles attendues (dépendantes de l'option
|
||||
``with_min_max_moy``)
|
||||
"""
|
||||
colonnes_synthese = Moyenne.get_colonnes_synthese(
|
||||
with_min_max_moy=with_min_max_moy
|
||||
)
|
||||
# Copie le df modélisant les données
|
||||
df = self.df[colonnes_synthese].copy()
|
||||
df["rang"] = df["rang"].replace("nan", "")
|
||||
|
||||
# Remplace les noms de colonnes par leur intitulé dans le tableur excel
|
||||
cols = []
|
||||
for critere in colonnes_synthese:
|
||||
nom_col = get_colonne_df(
|
||||
self.infos["aggregat"],
|
||||
self.infos["tag"],
|
||||
self.infos["intitule"], # UEs ou compétences
|
||||
self.infos["cohorte"],
|
||||
critere,
|
||||
)
|
||||
cols += [nom_col]
|
||||
df.columns = cols
|
||||
return df
|
||||
|
||||
def to_json(self) -> dict:
|
||||
"""Renvoie un dictionnaire de synthèse des moyennes/classements/statistiques générale (but)"""
|
||||
df = self.to_df(with_min_max_moy=True)
|
||||
resultat = df.to_json(orient="index")
|
||||
return resultat
|
||||
|
||||
def has_notes(self) -> bool:
|
||||
"""Indique si la moyenne est significative (c'est-à-dire à des notes) et/ou des inscrits"""
|
||||
return len(self.inscrits_ids) > 0
|
||||
|
||||
|
||||
def get_repr(aggregat, tag, intitule, cohorte):
|
||||
"""Renvoie une représentation textuelle "aggregat|tag|intitule|cohorte"
|
||||
pour représenter une moyenne
|
||||
"""
|
||||
liste_champs = []
|
||||
if aggregat != None:
|
||||
liste_champs += [aggregat]
|
||||
liste_champs += [tag, intitule]
|
||||
if cohorte != None:
|
||||
liste_champs += [cohorte]
|
||||
return "|".join(liste_champs)
|
||||
|
||||
|
||||
def get_colonne_df(aggregat, tag, intitule, cohorte, critere):
|
||||
"""Renvoie la chaine de caractère "aggregat|tag|intitule|cohorte|critere"
|
||||
utilisé pour désigner les colonnes du df.
|
||||
|
||||
Args:
|
||||
aggregat: Un nom d'aggrégat (généralement "S1" ou "3S")
|
||||
pouvant être optionnel (si `None`)
|
||||
tag: Un nom de tags (par ex. "maths")
|
||||
intitule: Un nom d'UE ou de compétences ou "Général"
|
||||
cohorte: Une cohorte pour les interclassements (généralement
|
||||
Groupe ou Promo
|
||||
pouvant être optionnel (si `None`)
|
||||
critere: Un critère correspondant à l'une des colonnes
|
||||
d'une pe_moy.Moyenne
|
||||
Returns:
|
||||
Une chaine de caractères indiquant les champs séparés par
|
||||
un ``"|"``, généralement de la forme
|
||||
"S1|maths|UE|Groupe|note"
|
||||
"""
|
||||
liste_champs = [get_repr(aggregat, tag, intitule, cohorte), critere]
|
||||
return "|".join(liste_champs)
|
|
@ -0,0 +1,210 @@
|
|||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app import comp
|
||||
from app.comp.moy_sem import comp_ranks_series
|
||||
from app.pe.moys import pe_moy
|
||||
from app.pe.moys.pe_moy import get_colonne_df
|
||||
import re
|
||||
|
||||
CODE_MOY_UE = "UEs"
|
||||
CODE_MOY_COMPETENCES = "Compétences"
|
||||
CHAMP_GENERAL = "Général" # Nom du champ de la moyenne générale
|
||||
CHAMP_GROUPE = "groupe"
|
||||
CHAMP_PROMO = "promo"
|
||||
|
||||
|
||||
class MoyennesTag:
|
||||
def __init__(
|
||||
self,
|
||||
tag: str,
|
||||
type_moyenne: str,
|
||||
matrice_notes: pd.DataFrame, # etudids x UEs|comp
|
||||
matrice_coeffs: pd.DataFrame, # etudids x UEs|comp
|
||||
infos: dict,
|
||||
):
|
||||
"""Classe centralisant un ensemble de moyennes/class/stat,
|
||||
obtenu par un groupe d'étudiants, à un tag donné,
|
||||
en stockant les moyennes aux UEs|Compétences
|
||||
et la moyenne générale (toutes UEs confondues).
|
||||
|
||||
|
||||
|
||||
Args:
|
||||
tag: Un tag
|
||||
matrice_notes: Les moyennes (etudid x acronymes_ues|compétences)
|
||||
aux différentes UEs ou compétences
|
||||
matrice_coeffs: Les coeffs (etudid x acronymes_ues|compétences)
|
||||
aux différentes UEs ou compétences
|
||||
infos: Informations (aggrégat, cohorte ayant servi à calculer les moyennes)
|
||||
"""
|
||||
self.tag = tag
|
||||
"""Le tag associé aux moyennes"""
|
||||
|
||||
self.type = type_moyenne
|
||||
"""Le type de moyennes (par UEs ou par compétences)"""
|
||||
|
||||
self.infos = {
|
||||
"aggregat": infos["aggregat"],
|
||||
"tag": tag,
|
||||
"cohorte": infos["cohorte"],
|
||||
}
|
||||
"""Info sur les éléments (aggrégat, cohorte) ayant servi à calculer les moyennes"""
|
||||
|
||||
# Les moyennes par UE/compétences (ressources/SAEs confondues)
|
||||
self.matrice_notes: pd.DataFrame = matrice_notes
|
||||
"""Les notes par UEs ou Compétences (DataFrame etudids x UEs|comp)"""
|
||||
|
||||
self.matrice_coeffs: pd.DataFrame = matrice_coeffs
|
||||
"""Les coeffs à appliquer pour le calcul des moyennes générales
|
||||
(toutes UE ou compétences confondues). NaN si étudiant non inscrit"""
|
||||
|
||||
self.intitules: list[str] = list(self.matrice_notes.columns)
|
||||
"""Les intitules (acronymes d'UE ou compétences) renseignés dans les moyennes"""
|
||||
assert len(self.intitules) == len(
|
||||
set(self.intitules)
|
||||
), "Des champs de moyennes en doublons"
|
||||
|
||||
self.etudids: list[int] = list(self.matrice_notes.index)
|
||||
"""Les étudids renseignés dans les moyennes"""
|
||||
|
||||
self.moyennes_dict: dict[str, pe_moy.Moyenne] = {}
|
||||
"""Dictionnaire associant à chaque UE|Compétence ses données moyenne/class/stat"""
|
||||
for col in self.intitules: # if ue.type != UE_SPORT:
|
||||
# Les moyennes tous modules confondus
|
||||
notes = matrice_notes[col]
|
||||
infos = self.infos | {"intitule": col}
|
||||
self.moyennes_dict[col] = pe_moy.Moyenne(notes, infos)
|
||||
|
||||
# Les moyennes générales (toutes UEs confondues)
|
||||
self.notes_gen = pd.Series(np.nan, index=self.matrice_notes.index)
|
||||
"""Notes de la moyenne générale (toutes UEs|Comp confondues)"""
|
||||
if self.has_notes():
|
||||
self.notes_gen = self.compute_moy_gen(
|
||||
self.matrice_notes, self.matrice_coeffs
|
||||
)
|
||||
infos = self.infos | {"intitule": CHAMP_GENERAL}
|
||||
self.moyenne_gen = pe_moy.Moyenne(self.notes_gen, infos)
|
||||
"""Dataframe retraçant les moyennes/classements/statistiques général (toutes UESs confondues et modules confondus)"""
|
||||
|
||||
def has_notes(self):
|
||||
"""Détermine si les moyennes (aux UEs ou aux compétences)
|
||||
ont des notes
|
||||
|
||||
Returns:
|
||||
True si la moytag a des notes, False sinon
|
||||
"""
|
||||
for col, moy in self.moyennes_dict.items():
|
||||
if not moy.has_notes():
|
||||
return False
|
||||
return True
|
||||
# notes = self.matrice_notes
|
||||
|
||||
# nbre_nan = notes.isna().sum().sum()
|
||||
# nbre_notes_potentielles = len(notes.index) * len(notes.columns)
|
||||
# if nbre_nan == nbre_notes_potentielles:
|
||||
# return False
|
||||
# else:
|
||||
# return True
|
||||
|
||||
def compute_moy_gen(self, moys: pd.DataFrame, coeffs: pd.DataFrame) -> pd.Series:
|
||||
"""Calcule la moyenne générale (toutes UE/compétences confondus), en pondérant
|
||||
les notes obtenues aux UEs|Compétences par les coeff (ici les crédits ECTS).
|
||||
|
||||
Args:
|
||||
moys: Les moyennes (etudids x acronymes_ues/compétences)
|
||||
coeff: Les coeff (etudids x acronymes_ues/compétences)
|
||||
"""
|
||||
|
||||
# Calcule la moyenne générale dans le semestre (pondérée par le ECTS)
|
||||
try:
|
||||
moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects(
|
||||
moys,
|
||||
coeffs.fillna(0.0),
|
||||
# formation_id=self.formsemestre.formation_id,
|
||||
skip_empty_ues=True,
|
||||
)
|
||||
return moy_gen_tag
|
||||
except TypeError as e:
|
||||
raise TypeError(
|
||||
"Pb dans le calcul de la moyenne toutes UEs/compétences confondues"
|
||||
)
|
||||
|
||||
def to_df(self, options={"min_max_moy": True}) -> pd.DataFrame:
|
||||
"""Renvoie le df synthétisant l'ensemble des données connues.
|
||||
|
||||
Adapte :
|
||||
* les noms des colonnes aux données fournies dans l'attribut
|
||||
``infos`` (nom d'aggrégat, type de cohorte).
|
||||
* à l'option ``min_max_moy`` (limitant les colonnes)
|
||||
"""
|
||||
if "min_max_moy" not in options or options["min_max_moy"]:
|
||||
with_min_max_moy = True
|
||||
else:
|
||||
with_min_max_moy = False
|
||||
|
||||
# Les étudiants triés par etudid
|
||||
etudids_sorted = sorted(self.etudids)
|
||||
|
||||
# Le dataFrame à générer
|
||||
df = pd.DataFrame(index=etudids_sorted)
|
||||
|
||||
# Ajout des notes pour tous les champs
|
||||
champs = list(self.intitules)
|
||||
for champ in champs:
|
||||
moy: pe_moy.Moyenne = self.moyennes_dict[champ]
|
||||
df_champ = moy.to_df(
|
||||
with_min_max_moy=with_min_max_moy
|
||||
) # le dataframe (les colonnes ayant été renommées)
|
||||
colonnes_renommees = ajout_numero_a_colonnes(
|
||||
list(df.columns), list(df_champ.columns)
|
||||
)
|
||||
if colonnes_renommees:
|
||||
df_champ.columns = colonnes_renommees
|
||||
df = df.join(df_champ)
|
||||
|
||||
# Ajoute la moy générale
|
||||
df_moy_gen = self.moyenne_gen.to_df(with_min_max_moy=with_min_max_moy)
|
||||
colonnes_renommees = ajout_numero_a_colonnes(
|
||||
list(df.columns), list(df_moy_gen.columns)
|
||||
)
|
||||
if colonnes_renommees:
|
||||
df_moy_gen.columns = colonnes_renommees
|
||||
df = df.join(df_moy_gen)
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def ajout_numero_a_colonnes(colonnes, colonnes_a_ajouter):
|
||||
"""Partant d'une liste de noms de colonnes, vérifie si les noms des colonnes_a_ajouter
|
||||
n'entre pas en conflit (aka ne sont pas déjà présent dans colonnes).
|
||||
Si nom, renvoie `None`.
|
||||
Si oui, propose une liste de noms de colonnes_a_ajouter dans laquelle chaque nom
|
||||
est suivi d'un `"(X)"` (où X est un numéro choisi au regard des noms de colonnes).
|
||||
Les noms des colonnes sont de la forme "S1|maths|UE|Groupe|note (1)"
|
||||
|
||||
Devrait être supprimé à terme, car les noms des colonnes sont théoriquement prévus pour être
|
||||
unique/sans doublons.
|
||||
"""
|
||||
assert len(colonnes) == len(set(colonnes)), "Il y a déjà des doublons dans colonnes"
|
||||
colonnes_sans_numero = [col.split(" (")[0] for col in colonnes]
|
||||
|
||||
conflits = set(colonnes_sans_numero).intersection(colonnes_a_ajouter)
|
||||
if not conflits:
|
||||
# Pas de conflit
|
||||
return None
|
||||
|
||||
pattern = r"\((\d*)\)"
|
||||
p = re.compile(pattern)
|
||||
numeros = []
|
||||
for col in colonnes:
|
||||
numeros.extend(p.findall(col))
|
||||
|
||||
if numeros:
|
||||
numeros = [int(num) for num in numeros]
|
||||
num_max = max(numeros)
|
||||
else:
|
||||
num_max = 0
|
||||
|
||||
ajouts = [f"{col} ({num_max+1})" for col in colonnes_a_ajouter]
|
||||
return ajouts
|
|
@ -0,0 +1,455 @@
|
|||
# -*- 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.models import FormSemestre
|
||||
from app.pe import pe_affichage
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from app.pe.rcss import pe_rcs, pe_rcsemx
|
||||
import app.pe.moys.pe_sxtag as pe_sxtag
|
||||
import app.pe.pe_comp as pe_comp
|
||||
from app.pe.moys import pe_tabletags, pe_moytag
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
class RCSemXTag(pe_tabletags.TableTag):
|
||||
def __init__(
|
||||
self,
|
||||
rcsemx: pe_rcsemx.RCSemX,
|
||||
sxstags: dict[(str, int) : pe_sxtag.SxTag],
|
||||
semXs_suivis: dict[int, dict],
|
||||
):
|
||||
"""Calcule les moyennes par tag (orientées compétences)
|
||||
d'un regroupement de SxTag, 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 même semestre terminal.
|
||||
|
||||
Args:
|
||||
rcsemx: Le RCSemX (identifié par un nom et l'id de son semestre terminal)
|
||||
sxstags: Les données sur les SemX taggués
|
||||
semXs_suivis: Les données indiquant quels SXTags sont à prendre en compte
|
||||
pour chaque étudiant
|
||||
"""
|
||||
pe_tabletags.TableTag.__init__(self)
|
||||
|
||||
self.rcs_id: tuple(str, int) = rcsemx.rcs_id
|
||||
"""Identifiant du RCSemXTag (identique au RCSemX sur lequel il s'appuie)"""
|
||||
|
||||
self.rcsemx: pe_rcsemx.RCSemX = rcsemx
|
||||
"""Le regroupement RCSemX associé au RCSemXTag"""
|
||||
|
||||
self.semXs_suivis = semXs_suivis
|
||||
"""Les semXs suivis par les étudiants"""
|
||||
|
||||
self.nom = self.get_repr()
|
||||
"""Représentation textuelle du RSCtag"""
|
||||
|
||||
# Les données du semestre final
|
||||
self.formsemestre_final: FormSemestre = rcsemx.formsemestre_final
|
||||
"""Le semestre final"""
|
||||
self.fid_final: int = rcsemx.formsemestre_final.formsemestre_id
|
||||
"""Le fid du semestre final"""
|
||||
|
||||
# Affichage pour debug
|
||||
pe_affichage.pe_print(f"*** {self.get_repr(verbose=True)}")
|
||||
|
||||
# Les données aggrégés (RCRCF + SxTags)
|
||||
self.semXs_aggreges: dict[(str, int) : pe_rcsemx.RCSemX] = rcsemx.semXs_aggreges
|
||||
"""Les SemX aggrégés"""
|
||||
self.sxstags_aggreges = {}
|
||||
"""Les SxTag associés aux SemX aggrégés"""
|
||||
try:
|
||||
for rcf_id in self.semXs_aggreges:
|
||||
self.sxstags_aggreges[rcf_id] = sxstags[rcf_id]
|
||||
except:
|
||||
raise ValueError("Semestres SxTag manquants")
|
||||
self.sxtags_connus = sxstags # Tous les sxstags connus
|
||||
|
||||
# Les étudiants (etuds, états civils & etudis)
|
||||
sems_dans_aggregat = rcsemx.noms_semestres_aggreges
|
||||
sxtag_final = self.sxstags_aggreges[(sems_dans_aggregat[-1], self.rcs_id[1])]
|
||||
self.etuds = sxtag_final.etuds
|
||||
"""Les étudiants (extraits du semestre final)"""
|
||||
self.add_etuds(self.etuds)
|
||||
self.etudids_sorted = sorted(self.etudids)
|
||||
"""Les étudids triés"""
|
||||
|
||||
# Les compétences (extraites de tous les Sxtags)
|
||||
self.acronymes_ues_to_competences = self._do_acronymes_to_competences()
|
||||
"""L'association acronyme d'UEs -> compétence (extraites des SxTag aggrégés)"""
|
||||
|
||||
self.competences_sorted = sorted(
|
||||
set(self.acronymes_ues_to_competences.values())
|
||||
)
|
||||
"""Compétences (triées par nom, extraites des SxTag aggrégés)"""
|
||||
aff = pe_affichage.repr_comp_et_ues(self.acronymes_ues_to_competences)
|
||||
pe_affichage.pe_print(f"--> Compétences : {aff}")
|
||||
|
||||
# Les tags
|
||||
self.tags_sorted = self._do_taglist()
|
||||
"""Tags extraits de tous les SxTag aggrégés"""
|
||||
aff_tag = ["👜" + tag for tag in self.tags_sorted]
|
||||
pe_affichage.pe_print(f"--> Tags : {', '.join(aff_tag)}")
|
||||
|
||||
# Les moyennes
|
||||
self.moyennes_tags: dict[str, pe_moytag.MoyennesTag] = {}
|
||||
|
||||
"""Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)"""
|
||||
for tag in self.tags_sorted:
|
||||
pe_affichage.pe_print(f"--> Moyennes du tag 👜{tag}")
|
||||
|
||||
# Cubes d'inscription (etudids_sorted x compétences_sorted x sxstags),
|
||||
# de notes et de coeffs pour la moyenne générale
|
||||
# en "aggrégant" les données des sxstags, compétence par compétence
|
||||
(
|
||||
inscr_df,
|
||||
inscr_cube, # données d'inscriptions
|
||||
notes_df,
|
||||
notes_cube, # notes
|
||||
coeffs_df,
|
||||
coeffs_cube, # coeffs pour la moyenne générale (par UEs)
|
||||
coeffs_rcues_df,
|
||||
coeffs_rcues_cube, # coeffs pour la moyenne de regroupement d'UEs
|
||||
) = self.assemble_cubes(tag)
|
||||
|
||||
# Calcule les moyennes, et synthétise les coeffs
|
||||
(
|
||||
moys_competences,
|
||||
matrice_coeffs_moy_gen,
|
||||
) = self.compute_notes_et_coeffs_competences(
|
||||
notes_cube, coeffs_cube, coeffs_rcues_cube, inscr_cube
|
||||
)
|
||||
|
||||
# Affichage des coeffs
|
||||
aff = pe_affichage.repr_profil_coeffs(
|
||||
matrice_coeffs_moy_gen, with_index=True
|
||||
)
|
||||
pe_affichage.pe_print(f" > Moyenne calculée avec pour coeffs : {aff}")
|
||||
|
||||
# Mémorise les moyennes et les coeff associés
|
||||
infos = {"aggregat": self.rcs_id[0], "cohorte": pe_moytag.CHAMP_GROUPE}
|
||||
self.moyennes_tags[tag] = pe_moytag.MoyennesTag(
|
||||
tag,
|
||||
pe_moytag.CODE_MOY_COMPETENCES,
|
||||
moys_competences,
|
||||
matrice_coeffs_moy_gen,
|
||||
infos,
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Egalité de 2 RCS taggués sur la base de leur identifiant"""
|
||||
return self.rcs_id == other.sxtag_id
|
||||
|
||||
def get_repr(self, verbose=True) -> str:
|
||||
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
|
||||
est basée)"""
|
||||
if verbose:
|
||||
return f"{self.__class__.__name__} basé sur " + self.rcsemx.get_repr(
|
||||
verbose=verbose
|
||||
)
|
||||
else:
|
||||
return f"{self.__class__.__name__} {self.rcs_id}"
|
||||
|
||||
def assemble_cubes(self, tag):
|
||||
"""Pour un tag donné, construit les cubes :
|
||||
* d'inscriptions aux compétences (etudid x competences x SxTag)
|
||||
* de notes (etudid x competences x SxTag)
|
||||
* des coeffs pour le calcul des moyennes générales par UE (etudid x competences x SxTag)
|
||||
* des coeffs pour le calcul des regroupements cohérents d'UE/compétences
|
||||
|
||||
nécessaires au calcul des moyennes, en :
|
||||
|
||||
* transformant les données des UEs en données de compétences (changement de noms)
|
||||
* fusionnant les données d'un même semestre, lorsque plusieurs UEs traitent d'une même compétence (cas des RCSx = Sx)
|
||||
* aggrégeant les données de compétences sur plusieurs semestres (cas des RCSx = xA ou xS)
|
||||
|
||||
Args:
|
||||
tag: Le tag visé
|
||||
"""
|
||||
# etudids_sorted: list[int],
|
||||
# competences_sorted: list[str],
|
||||
# sxstags: dict[(str, int) : pe_sxtag.SxTag],
|
||||
|
||||
inscriptions_dfs = {}
|
||||
notes_dfs = {}
|
||||
coeffs_moy_gen_dfs = {}
|
||||
coeffs_rcue_dfs = {}
|
||||
|
||||
for sxtag_id, sxtag in self.sxstags_aggreges.items():
|
||||
# Partant de dataframes vierges
|
||||
inscription_df = pd.DataFrame(
|
||||
np.nan, index=self.etudids_sorted, columns=self.competences_sorted
|
||||
)
|
||||
notes_df = pd.DataFrame(
|
||||
np.nan, index=self.etudids_sorted, columns=self.competences_sorted
|
||||
)
|
||||
coeffs_moy_gen_df = pd.DataFrame(
|
||||
np.nan, index=self.etudids_sorted, columns=self.competences_sorted
|
||||
)
|
||||
coeffs_rcue_df = pd.DataFrame(
|
||||
np.nan, index=self.etudids_sorted, columns=self.competences_sorted
|
||||
)
|
||||
|
||||
# Charge les données du semestre tag (copie car changement de nom de colonnes à venir)
|
||||
if tag in sxtag.moyennes_tags: # si le tag est présent dans le semestre
|
||||
moys_tag = sxtag.moyennes_tags[tag]
|
||||
|
||||
# Les inscr, les notes, les coeffs
|
||||
acro_ues_inscr_parcours = sxtag.acro_ues_inscr_parcours
|
||||
notes = moys_tag.matrice_notes
|
||||
coeffs_moy_gen = moys_tag.matrice_coeffs # les coeffs
|
||||
coeffs_rcues = sxtag.coefs_rcue # dictionnaire UE -> coeff
|
||||
|
||||
# Traduction des acronymes d'UE en compétences
|
||||
# comp_to_ues = pe_comp.asso_comp_to_accronymes(self.acronymes_ues_to_competences)
|
||||
acronymes_ues_columns = notes.columns
|
||||
for acronyme in acronymes_ues_columns:
|
||||
# La compétence visée
|
||||
competence = self.acronymes_ues_to_competences[acronyme] # La comp
|
||||
|
||||
# Les étud inscrits à la comp reportés dans l'inscription au RCSemX
|
||||
comp_inscr = acro_ues_inscr_parcours[
|
||||
acro_ues_inscr_parcours.notnull()
|
||||
].index
|
||||
etudids_communs = list(
|
||||
inscription_df.index.intersection(comp_inscr)
|
||||
)
|
||||
inscription_df.loc[
|
||||
etudids_communs, competence
|
||||
] = acro_ues_inscr_parcours.loc[etudids_communs, acronyme]
|
||||
|
||||
# Les étud ayant une note à l'acronyme de la comp (donc à la comp)
|
||||
etuds_avec_notes = notes[notes[acronyme].notnull()].index
|
||||
etudids_communs = list(
|
||||
notes_df.index.intersection(etuds_avec_notes)
|
||||
)
|
||||
notes_df.loc[etudids_communs, competence] = notes.loc[
|
||||
etudids_communs, acronyme
|
||||
]
|
||||
|
||||
# Les coeffs pour la moyenne générale
|
||||
etuds_avec_coeffs = coeffs_moy_gen[
|
||||
coeffs_moy_gen[acronyme].notnull()
|
||||
].index
|
||||
etudids_communs = list(
|
||||
coeffs_moy_gen_df.index.intersection(etuds_avec_coeffs)
|
||||
)
|
||||
coeffs_moy_gen_df.loc[
|
||||
etudids_communs, competence
|
||||
] = coeffs_moy_gen.loc[etudids_communs, acronyme]
|
||||
|
||||
# Les coeffs des RCUE reportés là où les étudiants ont des notes
|
||||
etuds_avec_notes = notes[notes[acronyme].notnull()].index
|
||||
etudids_communs = list(
|
||||
notes_df.index.intersection(etuds_avec_notes)
|
||||
)
|
||||
coeffs_rcue_df.loc[etudids_communs, competence] = coeffs_rcues[
|
||||
acronyme
|
||||
]
|
||||
|
||||
# Supprime tout ce qui n'est pas numérique
|
||||
# for col in notes_df.columns:
|
||||
# notes_df[col] = pd.to_numeric(notes_df[col], errors="coerce")
|
||||
|
||||
# Stocke les dfs
|
||||
inscriptions_dfs[sxtag_id] = inscription_df
|
||||
notes_dfs[sxtag_id] = notes_df
|
||||
coeffs_moy_gen_dfs[sxtag_id] = coeffs_moy_gen_df
|
||||
coeffs_rcue_dfs[sxtag_id] = coeffs_rcue_df
|
||||
|
||||
# Réunit les inscriptions sous forme d'un cube etudids x competences x semestres
|
||||
sxtag_x_etudids_x_comps = [
|
||||
inscriptions_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges
|
||||
]
|
||||
inscriptions_etudids_x_comps_x_sxtag = np.stack(
|
||||
sxtag_x_etudids_x_comps, axis=-1
|
||||
)
|
||||
|
||||
# Réunit les notes sous forme d'un cube etudids x competences x semestres
|
||||
sxtag_x_etudids_x_comps = [
|
||||
notes_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges
|
||||
]
|
||||
notes_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1)
|
||||
|
||||
# Réunit les coeffs sous forme d'un cube etudids x competences x semestres
|
||||
sxtag_x_etudids_x_comps = [
|
||||
coeffs_moy_gen_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges
|
||||
]
|
||||
coeffs_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1)
|
||||
|
||||
# Normalise les coeffs de rcue par année (pour que le poids des années soit le
|
||||
# même)
|
||||
# Réunit les coeffs sous forme d'un cube etudids x competences x semestres
|
||||
sxtag_x_etudids_x_comps = [
|
||||
coeffs_rcue_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges
|
||||
]
|
||||
coeffs_rcues_etudids_x_comps_x_sxtag = np.stack(
|
||||
sxtag_x_etudids_x_comps, axis=-1
|
||||
)
|
||||
|
||||
return (
|
||||
inscriptions_dfs,
|
||||
inscriptions_etudids_x_comps_x_sxtag,
|
||||
notes_dfs,
|
||||
notes_etudids_x_comps_x_sxtag,
|
||||
coeffs_moy_gen_dfs,
|
||||
coeffs_etudids_x_comps_x_sxtag,
|
||||
coeffs_rcue_dfs,
|
||||
coeffs_rcues_etudids_x_comps_x_sxtag,
|
||||
)
|
||||
|
||||
def _do_taglist(self) -> list[str]:
|
||||
"""Synthétise les tags à partir des Sxtags aggrégés.
|
||||
|
||||
Returns:
|
||||
Liste de tags triés par ordre alphabétique
|
||||
"""
|
||||
tags = []
|
||||
for frmsem_id in self.sxstags_aggreges:
|
||||
tags.extend(self.sxstags_aggreges[frmsem_id].tags_sorted)
|
||||
return sorted(set(tags))
|
||||
|
||||
def _do_acronymes_to_competences(self) -> dict[str:str]:
|
||||
"""Synthétise l'association complète {acronyme_ue: competences}
|
||||
extraite de toutes les données/associations des SxTags
|
||||
aggrégés.
|
||||
|
||||
Returns:
|
||||
Un dictionnaire {'acronyme_ue' : 'compétences'}
|
||||
"""
|
||||
dict_competences = {}
|
||||
for sxtag_id, sxtag in self.sxstags_aggreges.items():
|
||||
dict_competences |= sxtag.acronymes_ues_to_competences
|
||||
return dict_competences
|
||||
|
||||
def compute_notes_et_coeffs_competences(
|
||||
self,
|
||||
notes_cube: np.array,
|
||||
coeffs_cube: np.array,
|
||||
coeffs_rcue: np.array,
|
||||
inscr_mask: np.array,
|
||||
):
|
||||
"""Calcule la moyenne par UEs|Compétences en moyennant sur les semestres et renvoie les résultats (notes
|
||||
et coeffs) sous la forme de DataFrame"""
|
||||
(etud_moy_tag, coeff_tag) = compute_moyennes_par_RCS(
|
||||
notes_cube, coeffs_cube, coeffs_rcue, inscr_mask
|
||||
)
|
||||
|
||||
etud_moy_tag_df = pd.DataFrame(
|
||||
etud_moy_tag,
|
||||
index=self.etudids_sorted, # les etudids
|
||||
columns=self.competences_sorted, # les competences
|
||||
)
|
||||
|
||||
coeffs_df = pd.DataFrame(
|
||||
coeff_tag, index=self.etudids_sorted, columns=self.competences_sorted
|
||||
)
|
||||
|
||||
return etud_moy_tag_df, coeffs_df
|
||||
|
||||
|
||||
def compute_moyennes_par_RCS(
|
||||
notes_cube: np.array,
|
||||
coeffs_cube: np.array,
|
||||
coeffs_rcue: np.array,
|
||||
inscr_mask: np.array,
|
||||
):
|
||||
"""Partant d'une série de notes (fournie sous forme d'un cube
|
||||
etudids_sorted x UEs|Compétences x semestres)
|
||||
chaque note étant pondérée par un coeff dépendant du semestre (fourni dans coeffs_rcue),
|
||||
et pondérée par un coeff dépendant de l'UE|Compétence pour calculer une moyenne générale
|
||||
(fourni dans coeffs_cube),
|
||||
calcule :
|
||||
* la moyenne par UEs|Compétences sur plusieurs semestres (partant du set_cube).
|
||||
* les coeffs "cumulés" à appliquer pour le calcul de la moyenne générale
|
||||
|
||||
La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles
|
||||
|
||||
Args:
|
||||
notes_cube: notes moyennes aux compétences ndarray
|
||||
(etuds_sorted x UEs|compétences x sxtags),
|
||||
des floats avec des NaN
|
||||
coeffs_cube: coeffs appliqués aux compétences dans le calcul des moyennes générales,
|
||||
(etuds_sorted x UEs|compétences x sxtags),
|
||||
des floats avec des NaN
|
||||
coeffs_rcue_cube: coeffs des RCUE appliqués dans les moyennes de RCS
|
||||
inscr_mask: inscriptions aux compétences ndarray
|
||||
(etuds_sorted x UEs|compétences x sxtags),
|
||||
des 1.0 (si inscrit) et des NaN (si non inscrit)
|
||||
Returns:
|
||||
Un DataFrame avec pour columns les moyennes par tags,
|
||||
et pour rows les etudid
|
||||
"""
|
||||
# Applique le masque d'inscriptions aux notes et aux coeffs
|
||||
notes_significatives = notes_cube * inscr_mask
|
||||
coeffs_moy_gen_significatifs = coeffs_cube * inscr_mask
|
||||
coeffs_rcues_significatifs = coeffs_rcue * inscr_mask
|
||||
|
||||
# Enlève les NaN des cubes pour les entrées manquantes
|
||||
notes_no_nan = np.nan_to_num(notes_significatives, nan=0.0)
|
||||
coeffs_no_nan = np.nan_to_num(coeffs_moy_gen_significatifs, nan=0.0)
|
||||
|
||||
# Les moyennes par tag
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
# Quelles entrées contiennent des notes et des coeffs?
|
||||
mask = ~np.isnan(notes_significatives) & ~np.isnan(coeffs_rcues_significatifs)
|
||||
|
||||
# La moyenne (pondérée) sur le regroupement cohérent de semestres
|
||||
coeffs_rcues_non_nan = np.nan_to_num(coeffs_rcues_significatifs * mask, nan=0.0)
|
||||
notes_ponderes = notes_no_nan * coeffs_rcues_non_nan
|
||||
etud_moy_tag = np.sum(notes_ponderes, axis=2) / np.sum(
|
||||
coeffs_rcues_non_nan, axis=2
|
||||
)
|
||||
|
||||
# Les coeffs pour la moyenne générale
|
||||
coeffs_pris_en_compte = coeffs_moy_gen_significatifs * mask
|
||||
coeffs_no_nan = np.nan_to_num(coeffs_pris_en_compte, nan=0.0)
|
||||
coeff_tag = np.sum(coeffs_no_nan, axis=2)
|
||||
|
||||
# Le masque des inscriptions prises en compte
|
||||
inscr_prise_en_compte = inscr_mask * mask
|
||||
inscr_prise_en_compte = np.nan_to_num(inscr_prise_en_compte, nan=-1.0)
|
||||
inscr_tag = np.max(inscr_prise_en_compte, axis=2)
|
||||
inscr_tag[inscr_tag < 0] = np.NaN # fix les max non calculés (-1) -> Na?
|
||||
|
||||
# Le dataFrame des notes moyennes, en réappliquant le masque des inscriptions
|
||||
etud_moy_tag = etud_moy_tag * inscr_tag
|
||||
# Le dataFrame des coeffs pour la moyenne générale, en réappliquant le masque des inscriptions
|
||||
coeff_tag = coeff_tag * inscr_tag # Réapplique le masque des inscriptions
|
||||
|
||||
return (etud_moy_tag, coeff_tag)
|
|
@ -0,0 +1,538 @@
|
|||
# -*- pole: 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 Generfal 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
|
||||
|
||||
from app import ScoValueError
|
||||
from app import comp
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import FormSemestre, UniteEns
|
||||
import app.pe.pe_affichage as pe_affichage
|
||||
import app.pe.pe_etudiant as pe_etudiant
|
||||
from app.pe.moys import pe_tabletags, pe_moytag
|
||||
from app.scodoc import sco_tag_module
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
from app.scodoc.sco_utils import *
|
||||
|
||||
|
||||
class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
|
||||
"""
|
||||
Un ResSemBUTTag représente les résultats des étudiants à un semestre, en donnant
|
||||
accès aux moyennes par tag.
|
||||
Il s'appuie principalement sur un ResultatsSemestreBUT.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
formsemestre: FormSemestre,
|
||||
options={"moyennes_tags": True, "moyennes_ue_res_sae": False},
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
formsemestre: le ``FormSemestre`` sur lequel il se base
|
||||
options: Un dictionnaire d'options
|
||||
"""
|
||||
ResultatsSemestreBUT.__init__(self, formsemestre)
|
||||
pe_tabletags.TableTag.__init__(self)
|
||||
|
||||
# Le nom du res_semestre taggué
|
||||
self.nom = self.get_repr(verbose=True)
|
||||
|
||||
# Le rang du ResultatsSemestreBUT
|
||||
self.semestre_id = self.formsemestre.semestre_id
|
||||
"""Rang du ResultatsSemestreBUT"""
|
||||
self.annee_id = (self.semestre_id - 1) // 2 + 1
|
||||
"""Année du ResultatsSemestreBUT"""
|
||||
|
||||
# Les étudiants (etuds, états civils & etudis) ajouté
|
||||
self.add_etuds(self.etuds)
|
||||
self.etudids_sorted = sorted(self.etudids)
|
||||
"""Les etudids des étudiants du ResultatsSemestreBUT triés"""
|
||||
|
||||
pe_affichage.pe_print(
|
||||
f"*** ResSemBUTTag du {self.nom} => {len(self.etudids_sorted)} étudiants"
|
||||
)
|
||||
|
||||
# Les UEs (et les dispenses d'UE)
|
||||
self.ues_standards: list[UniteEns] = [
|
||||
ue for ue in self.ues if ue.type == sco_codes.UE_STANDARD
|
||||
]
|
||||
"""Liste des UEs standards du ResultatsSemestreBUT"""
|
||||
|
||||
# Les parcours des étudiants à ce semestre (None si pas de parcours)
|
||||
self.parcours = []
|
||||
"""Parcours auxquels sont inscrits les étudiants"""
|
||||
for etudid in self.etudids_sorted:
|
||||
parcour = self.formsemestre.etuds_inscriptions[etudid].parcour
|
||||
if parcour:
|
||||
self.parcours += [parcour.libelle]
|
||||
else:
|
||||
self.parcours += [None]
|
||||
|
||||
# Les UEs en fonction des parcours
|
||||
self.ues_inscr_parcours_df = self.load_ues_inscr_parcours()
|
||||
"""Inscription des étudiants aux UEs des parcours (etudids x ue_ids)"""
|
||||
|
||||
# Les acronymes des UEs
|
||||
self.ues_to_acronymes = {ue.id: ue.acronyme for ue in self.ues_standards}
|
||||
self.acronymes_sorted = sorted(self.ues_to_acronymes.values())
|
||||
"""Liste des acronymes des UE triés par ordre alphabétique"""
|
||||
|
||||
# Les compétences associées aux UEs (définies par les acronymes)
|
||||
self.acronymes_ues_to_competences = {}
|
||||
"""Association {acronyme d'UEs -> compétence}"""
|
||||
for ue in self.ues_standards:
|
||||
assert ue.niveau_competence, ScoValueError(
|
||||
"Des UEs ne sont pas rattachées à des compétences"
|
||||
)
|
||||
nom = ue.niveau_competence.competence.titre
|
||||
self.acronymes_ues_to_competences[ue.acronyme] = nom
|
||||
self.competences_sorted = sorted(
|
||||
list(set(self.acronymes_ues_to_competences.values()))
|
||||
)
|
||||
"""Liste de compétences triées par nom"""
|
||||
aff = pe_affichage.repr_asso_ue_comp(self.acronymes_ues_to_competences)
|
||||
pe_affichage.pe_print(f"--> UEs/Compétences : {aff}")
|
||||
|
||||
# Les tags personnalisés et auto:
|
||||
if "moyennes_tags" in options:
|
||||
self.tags_dict = self._get_tags_dict(
|
||||
avec_moyennes_tags=options["moyennes_tags"]
|
||||
)
|
||||
else:
|
||||
self.tags_dict = self._get_tags_dict()
|
||||
|
||||
pe_affichage.pe_print(
|
||||
f"""--> {pe_affichage.aff_tags_par_categories(self.tags_dict)}"""
|
||||
)
|
||||
self._check_tags(self.tags_dict)
|
||||
|
||||
# Les coefficients pour le calcul de la moyenne générale, donnés par
|
||||
# acronymes d'UE
|
||||
self.matrice_coeffs_moy_gen = self._get_matrice_coeffs(
|
||||
self.ues_inscr_parcours_df, self.ues_standards
|
||||
)
|
||||
"""DataFrame indiquant les coeffs des UEs par ordre alphabétique d'acronyme"""
|
||||
profils_aff = pe_affichage.repr_profil_coeffs(self.matrice_coeffs_moy_gen)
|
||||
pe_affichage.pe_print(
|
||||
f"--> Moyenne générale calculée avec pour coeffs d'UEs : {profils_aff}"
|
||||
)
|
||||
|
||||
# Les inscriptions aux acronymes d'ues
|
||||
self.acro_ues_inscr_parcours = self._get_acro_ues_inscr_parcours(
|
||||
self.ues_inscr_parcours_df, self.ues_standards
|
||||
)
|
||||
"""DataFrame indiquant à quelles UEs (données par leurs acronymes) sont inscrits les étudiants)"""
|
||||
|
||||
# Les capitalisations (mask etuids x acronyme_ue valant True si capitalisée, False sinon)
|
||||
self.capitalisations = self._get_capitalisations(self.ues_standards)
|
||||
"""DataFrame indiquant les UEs capitalisables d'un étudiant (etudids x )"""
|
||||
|
||||
# Les coeffs des RCUE
|
||||
self.coefs_rcue = {}
|
||||
"""Coefs de RCUE par acronyme d'UEs"""
|
||||
for ue in self.ues_standards:
|
||||
self.coefs_rcue[ue.acronyme] = ue.coef_rcue
|
||||
|
||||
# Calcul des moyennes & les classements de chaque étudiant à chaque tag
|
||||
self.moyennes_tags = {}
|
||||
"""Moyennes par tags (personnalisés ou 'but')"""
|
||||
for tag in self.tags_dict["personnalises"]:
|
||||
# pe_affichage.pe_print(f" -> Traitement du tag {tag}")
|
||||
info_tag = self.tags_dict["personnalises"][tag]
|
||||
# Les moyennes générales par UEs
|
||||
moy_ues_tag = self.compute_moy_ues_tag(
|
||||
self.ues_inscr_parcours_df, info_tag=info_tag, pole=None
|
||||
)
|
||||
# Mémorise les moyennes
|
||||
infos = {"aggregat": None, "cohorte": pe_moytag.CHAMP_GROUPE}
|
||||
self.moyennes_tags[tag] = pe_moytag.MoyennesTag(
|
||||
tag,
|
||||
pe_moytag.CODE_MOY_UE,
|
||||
moy_ues_tag,
|
||||
self.matrice_coeffs_moy_gen,
|
||||
infos,
|
||||
)
|
||||
|
||||
# Ajoute les moyennes par UEs + la moyenne générale (but)
|
||||
moy_gen = self.compute_moy_gen(self.acro_ues_inscr_parcours)
|
||||
infos = {"aggregat": None, "cohorte": pe_moytag.CHAMP_GROUPE}
|
||||
self.moyennes_tags["but"] = pe_moytag.MoyennesTag(
|
||||
"but", pe_moytag.CODE_MOY_UE, moy_gen, self.matrice_coeffs_moy_gen, infos
|
||||
)
|
||||
|
||||
# Ajoute la moyenne générale par ressources
|
||||
if "moyennes_ue_res_sae" in options and options["moyennes_ue_res_sae"]:
|
||||
moy_res_gen = self.compute_moy_ues_tag(
|
||||
self.ues_inscr_parcours_df, info_tag=None, pole=ModuleType.RESSOURCE
|
||||
)
|
||||
infos = {"aggregat": None, "cohorte": pe_moytag.CHAMP_GROUPE}
|
||||
self.moyennes_tags["ressources"] = pe_moytag.MoyennesTag(
|
||||
"ressources",
|
||||
pe_moytag.CODE_MOY_UE,
|
||||
moy_res_gen,
|
||||
self.matrice_coeffs_moy_gen,
|
||||
infos,
|
||||
)
|
||||
|
||||
# Ajoute la moyenne générale par saes
|
||||
if "moyennes_ue_res_sae" in options and options["moyennes_ue_res_sae"]:
|
||||
moy_saes_gen = self.compute_moy_ues_tag(
|
||||
self.ues_inscr_parcours_df, info_tag=None, pole=ModuleType.SAE
|
||||
)
|
||||
infos = {"aggregat": None, "cohorte": pe_moytag.CHAMP_GROUPE}
|
||||
self.moyennes_tags["saes"] = pe_moytag.MoyennesTag(
|
||||
"saes",
|
||||
pe_moytag.CODE_MOY_UE,
|
||||
moy_saes_gen,
|
||||
self.matrice_coeffs_moy_gen,
|
||||
infos,
|
||||
)
|
||||
|
||||
# Tous les tags
|
||||
self.tags_sorted = self.get_all_significant_tags()
|
||||
"""Tags (personnalisés+compétences) par ordre alphabétique"""
|
||||
|
||||
def get_repr(self, verbose=False) -> str:
|
||||
"""Nom affiché pour le semestre taggué, de la forme (par ex.):
|
||||
|
||||
* S1#69 si verbose est False
|
||||
* S1 FI 2023 si verbose est True
|
||||
"""
|
||||
if not verbose:
|
||||
return f"{self.formsemestre}#{self.formsemestre.formsemestre_id}"
|
||||
else:
|
||||
return pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True)
|
||||
|
||||
def _get_matrice_coeffs(
|
||||
self, ues_inscr_parcours_df: pd.DataFrame, ues_standards: list[UniteEns]
|
||||
) -> pd.DataFrame:
|
||||
"""Renvoie un dataFrame donnant les coefficients à appliquer aux UEs
|
||||
dans le calcul de la moyenne générale (toutes UEs confondues).
|
||||
Prend en compte l'inscription des étudiants aux UEs en fonction de leur parcours
|
||||
(cf. ues_inscr_parcours_df).
|
||||
|
||||
Args:
|
||||
ues_inscr_parcours_df: Les inscriptions des étudiants aux UEs
|
||||
ues_standards: Les UEs standards à prendre en compte
|
||||
|
||||
Returns:
|
||||
Un dataFrame etudids x acronymes_UEs avec les coeffs des UEs
|
||||
"""
|
||||
matrice_coeffs_moy_gen = ues_inscr_parcours_df * [
|
||||
ue.ects for ue in ues_standards # if ue.type != UE_SPORT <= déjà supprimé
|
||||
]
|
||||
matrice_coeffs_moy_gen.columns = [
|
||||
self.ues_to_acronymes[ue.id] for ue in ues_standards
|
||||
]
|
||||
# Tri par etudids (dim 0) et par acronymes (dim 1)
|
||||
matrice_coeffs_moy_gen = matrice_coeffs_moy_gen.sort_index()
|
||||
matrice_coeffs_moy_gen = matrice_coeffs_moy_gen.sort_index(axis=1)
|
||||
return matrice_coeffs_moy_gen
|
||||
|
||||
def _get_acro_ues_inscr_parcours(
|
||||
self, ues_inscr_parcours_df: pd.DataFrame, ues_standards: list[UniteEns]
|
||||
) -> pd.DataFrame:
|
||||
"""Renvoie un dataFrame donnant les inscriptions (Nan ou 1) des
|
||||
étudiants aux UEs définies par leur acronyme, en fonction de leur parcours
|
||||
(cf. ues_inscr_parcours_df) et en limitant les données aux UEs standards (hors sport=
|
||||
|
||||
Args:
|
||||
ues_inscr_parcours_df: Les inscriptions des étudiants aux UEs
|
||||
ues_standards: Les UEs standards à prendre en compte
|
||||
|
||||
Returns:
|
||||
Un dataFrame etudids x acronymes_UEs avec les coeffs des UEs
|
||||
"""
|
||||
matrice_inscription = ues_inscr_parcours_df * [
|
||||
1 for ue in ues_standards # if ue.type != UE_SPORT <= déjà supprimé
|
||||
]
|
||||
matrice_inscription.columns = [
|
||||
self.ues_to_acronymes[ue.id] for ue in ues_standards
|
||||
]
|
||||
# Tri par etudids (dim 0) et par acronymes (dim 1)
|
||||
matrice_inscription = matrice_inscription.sort_index()
|
||||
matrice_inscription = matrice_inscription.sort_index(axis=1)
|
||||
return matrice_inscription
|
||||
|
||||
def _get_capitalisations(self, ues_standards) -> pd.DataFrame:
|
||||
"""Renvoie un dataFrame résumant les UEs capitalisables par les
|
||||
étudiants, d'après les décisions de jury (sous réserve qu'elles existent).
|
||||
|
||||
Args:
|
||||
ues_standards: Liste des UEs standards (notamment autres que le sport)
|
||||
Returns:
|
||||
Un dataFrame etudids x acronymes_UEs dont les valeurs sont ``True`` si l'UE
|
||||
est capitalisable, ``False`` sinon
|
||||
"""
|
||||
capitalisations = pd.DataFrame(
|
||||
False, index=self.etudids_sorted, columns=self.acronymes_sorted
|
||||
)
|
||||
self.get_formsemestre_validations() # charge les validations
|
||||
res_jury = self.validations
|
||||
if res_jury:
|
||||
for etud in self.etuds:
|
||||
etudid = etud.etudid
|
||||
decisions = res_jury.decisions_jury_ues.get(etudid, {})
|
||||
for ue in ues_standards:
|
||||
if ue.id in decisions and decisions[ue.id]["code"] == sco_codes.ADM:
|
||||
capitalisations.loc[etudid, ue.acronyme] = True
|
||||
# Tri par etudis et par accronyme d'UE
|
||||
capitalisations = capitalisations.sort_index()
|
||||
capitalisations = capitalisations.sort_index(axis=1)
|
||||
return capitalisations
|
||||
|
||||
def compute_moy_ues_tag(
|
||||
self,
|
||||
ues_inscr_parcours_df: pd.DataFrame,
|
||||
info_tag: dict[int, dict] = None,
|
||||
pole=None,
|
||||
) -> pd.DataFrame:
|
||||
"""Calcule la moyenne par UE des étudiants pour un tag donné,
|
||||
en ayant connaissance des informations sur le tag et des inscriptions des étudiants aux différentes UEs.
|
||||
|
||||
info_tag détermine les modules pris en compte :
|
||||
* si non `None`, seuls les modules rattachés au tag sont pris en compte
|
||||
* si `None`, tous les modules (quelque soit leur rattachement au tag) sont pris
|
||||
en compte (sert au calcul de la moyenne générale par ressource ou SAE)
|
||||
|
||||
ues_inscr_parcours_df détermine les UEs pour lesquels le calcul d'une moyenne à un sens.
|
||||
|
||||
`pole` détermine les modules pris en compte :
|
||||
|
||||
* si `pole` vaut `ModuleType.RESSOURCE`, seules les ressources sont prises
|
||||
en compte (moyenne de ressources par UEs)
|
||||
* si `pole` vaut `ModuleType.SAE`, seules les SAEs sont prises en compte
|
||||
* si `pole` vaut `None` (ou toute autre valeur),
|
||||
tous les modules sont pris en compte (moyenne d'UEs)
|
||||
|
||||
Les informations sur le tag sont un dictionnaire listant les modimpl_id rattachés au tag,
|
||||
et pour chacun leur éventuel coefficient de **repondération**.
|
||||
|
||||
Args:
|
||||
ues_inscr_parcours_df: L'inscription aux UEs
|
||||
Returns:
|
||||
Le dataframe des moyennes du tag par UE
|
||||
"""
|
||||
modimpls_sorted = self.formsemestre.modimpls_sorted
|
||||
|
||||
# Adaptation du mask de calcul des moyennes au tag visé
|
||||
modimpls_mask = []
|
||||
for modimpl in modimpls_sorted:
|
||||
module = modimpl.module # Le module
|
||||
mask = module.ue.type == sco_codes.UE_STANDARD # Est-ce une UE stantard ?
|
||||
if pole == ModuleType.RESSOURCE:
|
||||
mask &= module.module_type == ModuleType.RESSOURCE
|
||||
elif pole == ModuleType.SAE:
|
||||
mask &= module.module_type == ModuleType.SAE
|
||||
modimpls_mask += [mask]
|
||||
|
||||
# Prise en compte du tag
|
||||
if info_tag:
|
||||
# Désactive tous les modules qui ne sont pas pris en compte pour ce tag
|
||||
for i, modimpl in enumerate(modimpls_sorted):
|
||||
if modimpl.moduleimpl_id not in info_tag:
|
||||
modimpls_mask[i] = False
|
||||
|
||||
# Applique la pondération des coefficients
|
||||
modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy()
|
||||
if info_tag:
|
||||
for modimpl_id in info_tag:
|
||||
ponderation = info_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, # matrice etudids x modimpls x UEs
|
||||
self.etuds,
|
||||
self.formsemestre.modimpls_sorted,
|
||||
self.modimpl_inscr_df,
|
||||
modimpl_coefs_ponderes_df,
|
||||
modimpls_mask,
|
||||
self.dispense_ues,
|
||||
block=False, # self.formsemestre.block_moyennes, calcul même si sem bloqué
|
||||
)
|
||||
|
||||
# Ne conserve que les UEs standards
|
||||
colonnes = [ue.id for ue in self.ues_standards]
|
||||
moyennes_ues_tag = moyennes_ues_tag[colonnes]
|
||||
|
||||
# Met à zéro les moyennes non calculées/calculables pour les UEs ayant des notes
|
||||
for col in colonnes:
|
||||
if moyennes_ues_tag[col].isna().sum() != len(moyennes_ues_tag[col]):
|
||||
moyennes_ues_tag[col].fillna(0.0, inplace=True)
|
||||
|
||||
# Applique le masque d'inscription aux UE pour ne conserver que les UE dans lequel l'étudiant est inscrit
|
||||
moyennes_ues_tag = moyennes_ues_tag[colonnes] * ues_inscr_parcours_df[colonnes]
|
||||
|
||||
# Transforme les UEs en acronyme
|
||||
acronymes = [self.ues_to_acronymes[ue.id] for ue in self.ues_standards]
|
||||
moyennes_ues_tag.columns = acronymes
|
||||
|
||||
# Tri par etudids et par ordre alphabétique d'acronyme
|
||||
moyennes_ues_tag = moyennes_ues_tag.sort_index()
|
||||
moyennes_ues_tag = moyennes_ues_tag.sort_index(axis=1)
|
||||
|
||||
return moyennes_ues_tag
|
||||
|
||||
def compute_moy_gen(self, acro_ues_inscr_parcours):
|
||||
"""Récupère les moyennes des UEs pour le calcul de la moyenne générale,
|
||||
en associant à chaque UE.id son acronyme (toutes UEs confondues)
|
||||
"""
|
||||
df_ues = pd.DataFrame(
|
||||
{ue.id: self.etud_moy_ue[ue.id] for ue in self.ues_standards},
|
||||
index=self.etudids,
|
||||
)
|
||||
# Transforme les UEs en acronyme
|
||||
colonnes = df_ues.columns
|
||||
acronymes = [self.ues_to_acronymes[col] for col in colonnes]
|
||||
df_ues.columns = acronymes
|
||||
|
||||
# Met à zéro les moyennes non calculées/calculables des UEs ayant des notes
|
||||
for col in df_ues.columns:
|
||||
if df_ues[col].isna().sum() != len(df_ues[col]):
|
||||
df_ues[col].fillna(0.0, inplace=True)
|
||||
|
||||
# Réapplique le mask d'inscription
|
||||
df_ues = df_ues * acro_ues_inscr_parcours
|
||||
|
||||
# Tri par ordre aphabétique de colonnes
|
||||
df_ues.sort_index(axis=1)
|
||||
|
||||
return df_ues
|
||||
|
||||
def _get_tags_dict(self, avec_moyennes_tags=True):
|
||||
"""Renvoie les tags personnalisés (déduits des modules du semestre)
|
||||
et les tags automatiques ('but'), et toutes leurs informations,
|
||||
dans un dictionnaire de la forme :
|
||||
|
||||
``{"personnalises": {tag: info_sur_le_tag},
|
||||
"auto": {tag: {}}``
|
||||
|
||||
Returns:
|
||||
Le dictionnaire structuré des tags ("personnalises" vs. "auto")
|
||||
"""
|
||||
dict_tags = {"personnalises": dict(), "auto": dict()}
|
||||
|
||||
if avec_moyennes_tags:
|
||||
# Les tags perso (seulement si l'option d'utiliser les tags perso est choisie)
|
||||
dict_tags["personnalises"] = get_synthese_tags_personnalises_semestre(
|
||||
self.formsemestre
|
||||
)
|
||||
|
||||
# Les tags automatiques
|
||||
# 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()))
|
||||
|
||||
# BUT
|
||||
dict_tags["auto"] = {"but": {}, "ressources": {}, "saes": {}}
|
||||
return dict_tags
|
||||
|
||||
def _check_tags(self, dict_tags):
|
||||
"""Vérifie l'unicité des tags"""
|
||||
noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys())))
|
||||
noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp
|
||||
noms_tags = noms_tags_perso + noms_tags_auto
|
||||
|
||||
intersection = list(set(noms_tags_perso) & set(noms_tags_auto))
|
||||
|
||||
if intersection:
|
||||
liste_intersection = "\n".join(
|
||||
[f"<li><code>{tag}</code></li>" for tag in intersection]
|
||||
)
|
||||
s = "s" if len(intersection) > 1 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)
|
||||
|
||||
|
||||
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
|
||||
au coeff de repondération fournie avec le tag (par défaut 1 si non indiquée)).
|
||||
|
||||
Le dictionnaire fournit est de la forme :
|
||||
|
||||
``{ tag : { modimplid: {"modimpl": ModImpl,
|
||||
"ponderation": coeff_de_reponderation}
|
||||
} }``
|
||||
|
||||
Args:
|
||||
formsemestre: Le formsemestre à la base de la recherche des tags
|
||||
|
||||
Return:
|
||||
Un dictionnaire décrivant les 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
|
||||
"ponderation": ponderation, # la pondération demandée pour le tag sur le module
|
||||
}
|
||||
|
||||
return synthese_tags
|
|
@ -0,0 +1,421 @@
|
|||
# -*- 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 import pe_affichage, pe_comp
|
||||
import app.pe.moys.pe_ressemtag as pe_ressemtag
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
from app.pe.moys import pe_moytag, pe_tabletags
|
||||
import app.pe.rcss.pe_trajectoires as pe_trajectoires
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
class SxTag(pe_tabletags.TableTag):
|
||||
def __init__(
|
||||
self,
|
||||
sxtag_id: (str, int),
|
||||
semx: pe_trajectoires.SemX,
|
||||
ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag],
|
||||
):
|
||||
"""Calcule les moyennes/classements par tag d'un semestre de type 'Sx'
|
||||
(par ex. 'S1', 'S2', ...) représentés par acronyme d'UE.
|
||||
|
||||
Il représente :
|
||||
|
||||
* pour les étudiants *non redoublants* : moyennes/classements
|
||||
du semestre suivi
|
||||
* pour les étudiants *redoublants* : une fusion des moyennes/classements
|
||||
dans les (2) 'Sx' qu'il a suivi, en exploitant les informations de capitalisation :
|
||||
meilleure moyenne entre l'UE capitalisée et l'UE refaite (la notion de meilleure
|
||||
s'appliquant à la moyenne d'UE)
|
||||
|
||||
Un SxTag (regroupant potentiellement plusieurs semestres) est identifié
|
||||
par un tuple ``(Sx, fid)`` où :
|
||||
|
||||
* ``x`` est le rang (semestre_id) du semestre
|
||||
* ``fid`` le formsemestre_id du semestre final (le plus récent) du regroupement.
|
||||
|
||||
Les **tags**, les **UE** et les inscriptions aux UEs (pour les étudiants)
|
||||
considérés sont uniquement ceux du semestre final.
|
||||
|
||||
Args:
|
||||
sxtag_id: L'identifiant de SxTag
|
||||
ressembuttags: Un dictionnaire de la forme `{fid: ResSemBUTTag(fid)}` donnant
|
||||
les semestres à regrouper et les résultats/moyennes par tag des
|
||||
semestres
|
||||
"""
|
||||
pe_tabletags.TableTag.__init__(self)
|
||||
|
||||
assert sxtag_id and len(sxtag_id) == 2 and sxtag_id[1] in ressembuttags
|
||||
|
||||
self.sxtag_id: (str, int) = sxtag_id
|
||||
"""Identifiant du SxTag de la forme (nom_Sx, fid_semestre_final)"""
|
||||
assert (
|
||||
len(self.sxtag_id) == 2
|
||||
and isinstance(self.sxtag_id[0], str)
|
||||
and isinstance(self.sxtag_id[1], int)
|
||||
), "Format de l'identifiant du SxTag non respecté"
|
||||
|
||||
self.agregat = sxtag_id[0]
|
||||
"""Nom de l'aggrégat du RCS"""
|
||||
|
||||
self.semx = semx
|
||||
"""Le SemX sur lequel il s'appuie"""
|
||||
assert semx.rcs_id == sxtag_id, "Problème de correspondance SxTag/SemX"
|
||||
|
||||
# Les resultats des semestres taggués à prendre en compte dans le SemX
|
||||
self.ressembuttags = {
|
||||
fid: ressembuttags[fid] for fid in semx.semestres_aggreges
|
||||
}
|
||||
"""Les ResSemBUTTags à regrouper dans le SxTag"""
|
||||
|
||||
# Les données du semestre final
|
||||
self.fid_final = sxtag_id[1]
|
||||
self.ressembuttag_final = ressembuttags[self.fid_final]
|
||||
"""Le ResSemBUTTag final"""
|
||||
self.semestre_id = self.ressembuttag_final.semestre_id
|
||||
"""Rang du SxTag"""
|
||||
self.annee_id = self.ressembuttag_final.annee_id
|
||||
"""Année du SxTag"""
|
||||
|
||||
# Ajoute les etudids et les états civils
|
||||
self.etuds = self.ressembuttag_final.etuds
|
||||
"""Les étudiants (extraits du ReSemBUTTag final)"""
|
||||
self.add_etuds(self.etuds)
|
||||
self.etudids_sorted = sorted(self.etudids)
|
||||
"""Les etudids triés"""
|
||||
|
||||
# Affichage
|
||||
pe_affichage.pe_print(f"*** {self.get_repr(verbose=True)}")
|
||||
|
||||
# Les tags
|
||||
self.tags_sorted = self.ressembuttag_final.tags_sorted
|
||||
"""Tags (extraits du ReSemBUTTag final)"""
|
||||
aff_tag = pe_affichage.repr_tags(self.tags_sorted)
|
||||
pe_affichage.pe_print(f"--> Tags : {aff_tag}")
|
||||
|
||||
# Les UE données par leur acronyme
|
||||
self.acronymes_sorted = self.ressembuttag_final.acronymes_sorted
|
||||
"""Les acronymes des UEs (extraits du ResSemBUTTag final)"""
|
||||
|
||||
# L'association UE-compétences extraites du dernier semestre
|
||||
self.acronymes_ues_to_competences = (
|
||||
self.ressembuttag_final.acronymes_ues_to_competences
|
||||
)
|
||||
"""L'association acronyme d'UEs -> compétence"""
|
||||
self.competences_sorted = sorted(self.acronymes_ues_to_competences.values())
|
||||
"""Les compétences triées par nom"""
|
||||
aff = pe_affichage.repr_asso_ue_comp(self.acronymes_ues_to_competences)
|
||||
pe_affichage.pe_print(f"--> UEs/Compétences : {aff}")
|
||||
|
||||
# Les coefs de RCUE issus du dernier semestre
|
||||
self.coefs_rcue = self.ressembuttag_final.coefs_rcue
|
||||
"""Coefs de RCUE par acronyme d'UEs"""
|
||||
|
||||
# Les inscriptions des étudiants aux UEs (données par leur acronyme)
|
||||
# par report de celle du ressemfinal
|
||||
self.acro_ues_inscr_parcours = self.ressembuttag_final.acro_ues_inscr_parcours
|
||||
|
||||
# Les coeffs pour la moyenne générale (traduisant également l'inscription
|
||||
# des étudiants aux UEs) (etudids_sorted x acronymes_ues_sorted)
|
||||
self.matrice_coeffs_moy_gen = self.ressembuttag_final.matrice_coeffs_moy_gen
|
||||
"""La matrice des coeffs pour la moyenne générale"""
|
||||
aff = pe_affichage.repr_profil_coeffs(self.matrice_coeffs_moy_gen)
|
||||
pe_affichage.pe_print(
|
||||
f"--> Moyenne générale calculée avec pour coeffs d'UEs : {aff}"
|
||||
)
|
||||
|
||||
# Masque des inscriptions et des capitalisations
|
||||
self.masque_df = None
|
||||
"""Le DataFrame traduisant les capitalisations des différents semestres"""
|
||||
self.masque_df, masque_cube = compute_masques_capitalisation_cube(
|
||||
self.etudids_sorted,
|
||||
self.acronymes_sorted,
|
||||
self.ressembuttags,
|
||||
self.fid_final,
|
||||
)
|
||||
pe_affichage.aff_capitalisations(
|
||||
self.etuds,
|
||||
self.ressembuttags,
|
||||
self.fid_final,
|
||||
self.acronymes_sorted,
|
||||
self.masque_df,
|
||||
)
|
||||
|
||||
# Les moyennes par tag
|
||||
self.moyennes_tags: dict[str, pd.DataFrame] = {}
|
||||
"""Moyennes aux UEs (identifiées par leur acronyme) des différents tags"""
|
||||
|
||||
if self.tags_sorted:
|
||||
pe_affichage.pe_print("--> Calcul des moyennes par tags :")
|
||||
|
||||
for tag in self.tags_sorted:
|
||||
pe_affichage.pe_print(f" > MoyTag 👜{tag}")
|
||||
|
||||
# Masque des inscriptions aux UEs (extraits de la matrice de coefficients)
|
||||
# inscr_mask: np.array = ~np.isnan(self.matrice_coeffs_moy_gen.to_numpy())
|
||||
|
||||
# Moyennes (tous modules confondus)
|
||||
if not self.has_notes_tag(tag):
|
||||
pe_affichage.pe_print(
|
||||
f" --> Semestre (final) actuellement sans notes"
|
||||
)
|
||||
matrice_moys_ues = pd.DataFrame(
|
||||
np.nan, index=self.etudids_sorted, columns=self.acronymes_sorted
|
||||
)
|
||||
else:
|
||||
# Moyennes tous modules confondus
|
||||
### Cube de note etudids x UEs tous modules confondus
|
||||
notes_df_gen, notes_cube_gen = self.compute_notes_ues_cube(tag)
|
||||
|
||||
# DataFrame des moyennes (tous modules confondus)
|
||||
inscr_mask = self.acro_ues_inscr_parcours.to_numpy()
|
||||
matrice_moys_ues = self.compute_notes_ues(
|
||||
notes_cube_gen, masque_cube, inscr_mask
|
||||
)
|
||||
|
||||
# Mémorise les infos pour la moyenne au tag
|
||||
infos = {"aggregat": self.sxtag_id[0], "cohorte": pe_moytag.CHAMP_GROUPE}
|
||||
self.moyennes_tags[tag] = pe_moytag.MoyennesTag(
|
||||
tag,
|
||||
pe_moytag.CODE_MOY_UE,
|
||||
matrice_moys_ues,
|
||||
self.matrice_coeffs_moy_gen,
|
||||
infos,
|
||||
)
|
||||
|
||||
# Affichage de debug
|
||||
aff = pe_affichage.repr_profil_coeffs(
|
||||
self.matrice_coeffs_moy_gen, with_index=True
|
||||
)
|
||||
pe_affichage.pe_print(f" > Moyenne générale calculée avec : {aff}")
|
||||
|
||||
def has_notes_tag(self, tag):
|
||||
"""Détermine si le SxTag, pour un tag donné, est en cours d'évaluation.
|
||||
Si oui, n'a pas (encore) de notes dans le resformsemestre final.
|
||||
|
||||
Args:
|
||||
tag: Le tag visé
|
||||
|
||||
Returns:
|
||||
True si a des notes, False sinon
|
||||
"""
|
||||
moy_tag_dernier_sem = self.ressembuttag_final.moyennes_tags[tag]
|
||||
return moy_tag_dernier_sem.has_notes()
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Egalité de 2 SxTag sur la base de leur identifiant"""
|
||||
return self.sxtag_id == other.sxtag_id
|
||||
|
||||
def get_repr(self, verbose=False) -> str:
|
||||
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
|
||||
est basée)"""
|
||||
if verbose:
|
||||
return f"SXTag basé sur {self.semx.get_repr()}"
|
||||
else:
|
||||
# affichage = [str(fid) for fid in self.ressembuttags]
|
||||
return f"SXTag {self.agregat}#{self.fid_final}"
|
||||
|
||||
def compute_notes_ues_cube(self, tag) -> (pd.DataFrame, np.array):
|
||||
"""Construit le cube de notes des UEs (etudid x accronyme_ue x semestre_aggregé)
|
||||
nécessaire au calcul des moyennes du tag pour le RCS Sx.
|
||||
(Renvoie également le dataframe associé pour debug).
|
||||
|
||||
Args:
|
||||
tag: Le tag considéré (personalisé ou "but")
|
||||
"""
|
||||
# Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2)
|
||||
# etudids_sorted = etudids_sorted
|
||||
# acronymes_ues = sorted([ue.acronyme for ue in selMf.ues.values()])
|
||||
semestres_id = list(self.ressembuttags.keys())
|
||||
|
||||
dfs = {}
|
||||
|
||||
for frmsem_id in semestres_id:
|
||||
# Partant d'un dataframe vierge
|
||||
df = pd.DataFrame(
|
||||
np.nan, index=self.etudids_sorted, columns=self.acronymes_sorted
|
||||
)
|
||||
|
||||
# Charge les notes du semestre tag
|
||||
sem_tag = self.ressembuttags[frmsem_id]
|
||||
moys_tag = sem_tag.moyennes_tags[tag]
|
||||
notes = moys_tag.matrice_notes # dataframe etudids x ues
|
||||
|
||||
# les étudiants et les acronymes communs
|
||||
etudids_communs, acronymes_communs = pe_comp.find_index_and_columns_communs(
|
||||
df, notes
|
||||
)
|
||||
|
||||
# Recopie
|
||||
df.loc[etudids_communs, acronymes_communs] = notes.loc[
|
||||
etudids_communs, acronymes_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 etudids x ues x semestres"""
|
||||
semestres_x_etudids_x_ues = [dfs[fid].values for fid in dfs]
|
||||
etudids_x_ues_x_semestres = np.stack(semestres_x_etudids_x_ues, axis=-1)
|
||||
return dfs, etudids_x_ues_x_semestres
|
||||
|
||||
def compute_notes_ues(
|
||||
self,
|
||||
set_cube: np.array,
|
||||
cap_mask_3D: np.array,
|
||||
inscr_mask: np.array,
|
||||
) -> pd.DataFrame:
|
||||
"""Calcule la moyenne par UEs à un tag donné en prenant la note maximum (UE
|
||||
par UE) obtenue par un étudiant à un semestre.
|
||||
|
||||
Args:
|
||||
set_cube: notes moyennes aux modules ndarray
|
||||
(semestre_ids x etudids x UEs), des floats avec des NaN
|
||||
cap_mask_3D
|
||||
: masque indiquant si la note doit être prise en compte ndarray
|
||||
(semestre_ids x etudids x UEs), des 1.0 ou des 0.0
|
||||
inscr_mask: masque etudids x UE traduisant les inscriptions des
|
||||
étudiants aux UE (du semestre terminal)
|
||||
Returns:
|
||||
Un DataFrame avec pour columns les moyennes par ues,
|
||||
et pour rows les etudid
|
||||
"""
|
||||
# etudids_sorted: liste des étudiants (dim. 0 du cube) trié par etudid
|
||||
# acronymes_sorted: liste des acronymes des ues (dim. 1 du cube) trié par acronyme
|
||||
nb_etuds, nb_ues, nb_semestres = set_cube.shape
|
||||
nb_etuds_mask, nb_ues_mask = inscr_mask.shape
|
||||
# assert nb_etuds == len(self.etudids_sorted)
|
||||
# assert nb_ues == len(self.acronymes_sorted)
|
||||
# assert nb_etuds == nb_etuds_mask
|
||||
# assert nb_ues == nb_ues_mask
|
||||
|
||||
# Entrées à garder dans le cube en fonction du masque d'inscription aux UEs du parcours
|
||||
inscr_mask_3D = np.stack([inscr_mask] * nb_semestres, axis=-1)
|
||||
set_cube = set_cube * inscr_mask_3D
|
||||
|
||||
# Entrées à garder en fonction des UEs capitalisées ou non
|
||||
set_cube = set_cube * cap_mask_3D
|
||||
|
||||
# Enlève les NaN du cube pour les entrées manquantes : NaN -> -1.0
|
||||
set_cube_no_nan = np.nan_to_num(set_cube, nan=-1.0)
|
||||
|
||||
# Les moyennes par ues
|
||||
# TODO: Pour l'instant un max sans prise en compte des UE capitalisées
|
||||
etud_moy = np.max(set_cube_no_nan, axis=2)
|
||||
|
||||
# Fix les max non calculés (-1) -> NaN
|
||||
etud_moy[etud_moy < 0] = np.NaN
|
||||
|
||||
# Réapplique le masque d'inscription (dans le doute)
|
||||
etud_moy = etud_moy * inscr_mask
|
||||
|
||||
# Le dataFrame
|
||||
etud_moy_tag_df = pd.DataFrame(
|
||||
etud_moy,
|
||||
index=self.etudids_sorted, # les etudids
|
||||
columns=self.acronymes_sorted, # les acronymes d'UEs
|
||||
)
|
||||
|
||||
etud_moy_tag_df = etud_moy_tag_df.fillna(np.nan)
|
||||
|
||||
return etud_moy_tag_df
|
||||
|
||||
|
||||
def compute_masques_capitalisation_cube(
|
||||
etudids_sorted: list[int],
|
||||
acronymes_sorted: list[str],
|
||||
ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag],
|
||||
formsemestre_id_final: int,
|
||||
) -> (pd.DataFrame, np.array):
|
||||
"""Construit le cube traduisant les masques des UEs à prendre en compte dans le calcul
|
||||
des moyennes, en utilisant le dataFrame de capitalisations de chaque ResSemBUTTag
|
||||
|
||||
Ces masques contiennent : 1 si la note doit être prise en compte, 0 sinon
|
||||
|
||||
Le masque des UEs à prendre en compte correspondant au semestre final (identifié par
|
||||
son formsemestre_id_final) est systématiquement à 1 (puisque les résultats
|
||||
de ce semestre doivent systématiquement
|
||||
être pris en compte notamment pour les étudiants non redoublant).
|
||||
|
||||
Args:
|
||||
etudids_sorted: La liste des etudids triés par ordre croissant (dim 0)
|
||||
acronymes_sorted: La liste des acronymes de UEs triés par acronyme croissant (dim 1)
|
||||
ressembuttags: Le dictionnaire des résultats de semestres BUT (tous tags confondus)
|
||||
formsemestre_id_final: L'identifiant du formsemestre_id_final
|
||||
"""
|
||||
# Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2)
|
||||
# etudids_sorted = etudids_sorted
|
||||
# acronymes_ues = sorted([ue.acronyme for ue in selMf.ues.values()])
|
||||
semestres_id = list(ressembuttags.keys())
|
||||
|
||||
dfs = {}
|
||||
|
||||
for frmsem_id in semestres_id:
|
||||
# Partant d'un dataframe contenant des 1.0
|
||||
if frmsem_id == formsemestre_id_final:
|
||||
df = pd.DataFrame(1.0, index=etudids_sorted, columns=acronymes_sorted)
|
||||
else: # semestres redoublés
|
||||
df = pd.DataFrame(0.0, index=etudids_sorted, columns=acronymes_sorted)
|
||||
|
||||
# Traitement des capitalisations : remplace les infos de capitalisations par les coeff 1 ou 0
|
||||
capitalisations = ressembuttags[frmsem_id].capitalisations
|
||||
capitalisations = capitalisations.replace(True, 1.0).replace(False, 0.0)
|
||||
|
||||
# Met à 0 les coeffs des UEs non capitalisées pour les étudiants
|
||||
# inscrits dans les 2 semestres: 1.0*False => 0.0
|
||||
etudids_communs, acronymes_communs = pe_comp.find_index_and_columns_communs(
|
||||
df, capitalisations
|
||||
)
|
||||
|
||||
df.loc[etudids_communs, acronymes_communs] = capitalisations.loc[
|
||||
etudids_communs, acronymes_communs
|
||||
]
|
||||
|
||||
# Stocke le df
|
||||
dfs[frmsem_id] = df
|
||||
|
||||
"""Réunit les notes sous forme d'un cube etudids x ues x semestres"""
|
||||
semestres_x_etudids_x_ues = [dfs[fid].values for fid in dfs]
|
||||
etudids_x_ues_x_semestres = np.stack(semestres_x_etudids_x_ues, axis=-1)
|
||||
return dfs, etudids_x_ues_x_semestres
|
|
@ -0,0 +1,197 @@
|
|||
# -*- pole: 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 pandas as pd
|
||||
|
||||
from app.models import Identite
|
||||
from app.pe.moys import pe_moytag
|
||||
|
||||
TAGS_RESERVES = ["but"]
|
||||
|
||||
CHAMPS_ADMINISTRATIFS = ["Civilité", "Nom", "Prénom"]
|
||||
|
||||
|
||||
class TableTag(object):
|
||||
def __init__(self):
|
||||
"""Classe centralisant différentes méthodes communes aux
|
||||
SemestreTag, TrajectoireTag, AggregatInterclassTag
|
||||
"""
|
||||
# Les étudiants
|
||||
# self.etuds: list[Identite] = None # A venir
|
||||
"""Les étudiants"""
|
||||
# self.etudids: list[int] = {}
|
||||
"""Les etudids"""
|
||||
|
||||
def add_etuds(self, etuds: list[Identite]):
|
||||
"""Mémorise les informations sur les étudiants
|
||||
|
||||
Args:
|
||||
etuds: la liste des identités de l'étudiant
|
||||
"""
|
||||
# self.etuds = etuds
|
||||
self.etudids = list({etud.etudid for etud in etuds})
|
||||
|
||||
def get_all_significant_tags(self):
|
||||
"""Liste des tags de la table, triée par ordre alphabétique,
|
||||
extraite des clés du dictionnaire ``moyennes_tags``, en ne
|
||||
considérant que les moyennes ayant des notes.
|
||||
|
||||
Returns:
|
||||
Liste de tags triés par ordre alphabétique
|
||||
"""
|
||||
tags = []
|
||||
for tag, moytag in self.moyennes_tags.items():
|
||||
if moytag.has_notes():
|
||||
tags.append(tag)
|
||||
return sorted(tags)
|
||||
|
||||
def to_df(
|
||||
self,
|
||||
administratif=True,
|
||||
aggregat=None,
|
||||
tags_cibles=None,
|
||||
cohorte=None,
|
||||
options={"min_max_moy": True},
|
||||
) -> pd.DataFrame:
|
||||
"""Renvoie un dataframe listant toutes les données
|
||||
des moyennes/classements/min/max/moy
|
||||
des étudiants aux différents tags.
|
||||
|
||||
tags_cibles limitent le dataframe aux tags indiqués
|
||||
type_colonnes indiquent si les colonnes doivent être passées en multiindex
|
||||
|
||||
Args:
|
||||
administratif: Indique si les données administratives sont incluses
|
||||
aggregat: l'aggrégat représenté (éventuellement `None` si non connu)
|
||||
tags_cibles: la liste des tags ciblés
|
||||
cohorte: la cohorte représentée (éventuellement `None` si non connue)
|
||||
Returns:
|
||||
Le dataframe complet de synthèse
|
||||
"""
|
||||
# Les tags visés
|
||||
tags_tries = self.get_all_significant_tags()
|
||||
if not tags_cibles:
|
||||
tags_cibles = tags_tries
|
||||
tags_cibles = sorted(tags_cibles)
|
||||
|
||||
# Les étudiants visés
|
||||
if administratif:
|
||||
df = df_administratif(self.etuds, aggregat=aggregat, cohorte=cohorte)
|
||||
else:
|
||||
df = pd.DataFrame(index=self.etudids)
|
||||
|
||||
if not self.is_significatif():
|
||||
return df
|
||||
|
||||
# Ajout des données par tags
|
||||
for tag in tags_cibles:
|
||||
if tag in self.moyennes_tags:
|
||||
moy_tag_df = self.moyennes_tags[tag].to_df(options=options)
|
||||
df = df.join(moy_tag_df)
|
||||
|
||||
# Tri par nom, prénom
|
||||
if administratif:
|
||||
colonnes_tries = [
|
||||
_get_champ_administratif(champ, aggregat=aggregat, cohorte=cohorte)
|
||||
for champ in CHAMPS_ADMINISTRATIFS[1:]
|
||||
] # Nom + Prénom
|
||||
df = df.sort_values(by=colonnes_tries)
|
||||
return df
|
||||
|
||||
def has_etuds(self):
|
||||
"""Indique si un tabletag contient des étudiants"""
|
||||
return len(self.etuds) > 0
|
||||
|
||||
def is_significatif(self):
|
||||
"""Indique si une tabletag a des données"""
|
||||
# A des étudiants
|
||||
if not self.etuds:
|
||||
return False
|
||||
# A des tags avec des notes
|
||||
tags_tries = self.get_all_significant_tags()
|
||||
if not tags_tries:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _get_champ_administratif(champ, aggregat=None, cohorte=None):
|
||||
"""Pour un champ donné, renvoie l'index (ou le multindex)
|
||||
à intégrer au dataframe"""
|
||||
liste = []
|
||||
if aggregat != None:
|
||||
liste += [aggregat]
|
||||
liste += ["Administratif", "Identité"]
|
||||
if cohorte != None:
|
||||
liste += [champ]
|
||||
liste += [champ]
|
||||
return "|".join(liste)
|
||||
|
||||
|
||||
def df_administratif(
|
||||
etuds: list[Identite], aggregat=None, cohorte=None
|
||||
) -> pd.DataFrame:
|
||||
"""Renvoie un dataframe donnant les données administratives
|
||||
des étudiants du TableTag
|
||||
|
||||
Args:
|
||||
etuds: Identité des étudiants générant les données administratives
|
||||
"""
|
||||
identites = {etud.etudid: etud for etud in etuds}
|
||||
|
||||
donnees = {}
|
||||
etud: Identite = None
|
||||
for etudid, etud in identites.items():
|
||||
data = {
|
||||
CHAMPS_ADMINISTRATIFS[0]: etud.civilite_str,
|
||||
CHAMPS_ADMINISTRATIFS[1]: etud.nom,
|
||||
CHAMPS_ADMINISTRATIFS[2]: etud.prenom_str,
|
||||
}
|
||||
donnees[etudid] = {
|
||||
_get_champ_administratif(champ, aggregat, cohorte): data[champ]
|
||||
for champ in CHAMPS_ADMINISTRATIFS
|
||||
}
|
||||
|
||||
colonnes = [
|
||||
_get_champ_administratif(champ, aggregat, cohorte)
|
||||
for champ in CHAMPS_ADMINISTRATIFS
|
||||
]
|
||||
|
||||
df = pd.DataFrame.from_dict(donnees, orient="index", columns=colonnes)
|
||||
df = df.sort_values(by=colonnes[1:])
|
||||
return df
|
|
@ -8,8 +8,10 @@
|
|||
|
||||
from flask import g
|
||||
from app import log
|
||||
from app.pe.rcss import pe_rcs
|
||||
import app.pe.pe_comp as pe_comp
|
||||
|
||||
PE_DEBUG = False
|
||||
PE_DEBUG = True
|
||||
|
||||
|
||||
# On stocke les logs PE dans g.scodoc_pe_log
|
||||
|
@ -20,17 +22,19 @@ def pe_start_log() -> list[str]:
|
|||
return g.scodoc_pe_log
|
||||
|
||||
|
||||
def pe_print(*a):
|
||||
def pe_print(*a, **cles):
|
||||
"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:
|
||||
msg = " ".join(a)
|
||||
print(msg)
|
||||
else:
|
||||
log(msg)
|
||||
lines = getattr(g, "scodoc_pe_log")
|
||||
if lines is None:
|
||||
lines = pe_start_log()
|
||||
msg = " ".join(a)
|
||||
lines.append(msg)
|
||||
if "info" in cles:
|
||||
log(msg)
|
||||
|
||||
|
||||
def pe_get_log() -> str:
|
||||
|
@ -40,5 +44,212 @@ def pe_get_log() -> str:
|
|||
|
||||
# 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"
|
||||
|
||||
|
||||
def repr_profil_coeffs(matrice_coeffs_moy_gen, with_index=False):
|
||||
"""Affiche les différents types de coefficients (appelés profil)
|
||||
d'une matrice_coeffs_moy_gen (pour debug)
|
||||
"""
|
||||
|
||||
# Les profils des coeffs d'UE (pour debug)
|
||||
profils = []
|
||||
index_a_profils = {}
|
||||
for i in matrice_coeffs_moy_gen.index:
|
||||
val = matrice_coeffs_moy_gen.loc[i].fillna("-")
|
||||
val = " | ".join([str(v) for v in val])
|
||||
if val not in profils:
|
||||
profils += [val]
|
||||
index_a_profils[val] = [str(i)]
|
||||
else:
|
||||
index_a_profils[val] += [str(i)]
|
||||
|
||||
# L'affichage
|
||||
if len(profils) > 1:
|
||||
if with_index:
|
||||
elmts = [
|
||||
" " * 10
|
||||
+ prof
|
||||
+ " (par ex. "
|
||||
+ ", ".join(index_a_profils[prof][:10])
|
||||
+ ")"
|
||||
for prof in profils
|
||||
]
|
||||
else:
|
||||
elmts = [" " * 10 + prof for prof in profils]
|
||||
profils_aff = "\n" + "\n".join(elmts)
|
||||
else:
|
||||
profils_aff = "\n".join(profils)
|
||||
return profils_aff
|
||||
|
||||
|
||||
def repr_asso_ue_comp(acronymes_ues_to_competences):
|
||||
"""Représentation textuelle de l'association UE -> Compétences
|
||||
fournies dans acronymes_ues_to_competences
|
||||
"""
|
||||
champs = acronymes_ues_to_competences.keys()
|
||||
champs = sorted(champs)
|
||||
aff_comp = []
|
||||
for acro in champs:
|
||||
aff_comp += [f"📍{acro} (∈ 💡{acronymes_ues_to_competences[acro]})"]
|
||||
return ", ".join(aff_comp)
|
||||
|
||||
|
||||
def aff_UEs(champs):
|
||||
"""Représentation textuelle des UEs fournies dans `champs`"""
|
||||
champs_tries = sorted(champs)
|
||||
aff_comp = []
|
||||
|
||||
for comp in champs_tries:
|
||||
aff_comp += ["📍" + comp]
|
||||
return ", ".join(aff_comp)
|
||||
|
||||
|
||||
def aff_competences(champs):
|
||||
"""Affiche les compétences"""
|
||||
champs_tries = sorted(champs)
|
||||
aff_comp = []
|
||||
|
||||
for comp in champs_tries:
|
||||
aff_comp += ["💡" + comp]
|
||||
return ", ".join(aff_comp)
|
||||
|
||||
|
||||
def repr_tags(tags):
|
||||
"""Affiche les tags"""
|
||||
tags_tries = sorted(tags)
|
||||
aff_tag = ["👜" + tag for tag in tags_tries]
|
||||
return ", ".join(aff_tag)
|
||||
|
||||
|
||||
def aff_tags_par_categories(dict_tags):
|
||||
"""Etant donné un dictionnaire de tags, triés
|
||||
par catégorie (ici "personnalisés" ou "auto")
|
||||
représentation textuelle des tags
|
||||
"""
|
||||
noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys())))
|
||||
noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp
|
||||
if noms_tags_perso:
|
||||
aff_tags_perso = ", ".join([f"👜{nom}" for nom in noms_tags_perso])
|
||||
aff_tags_auto = ", ".join([f"👜{nom}" for nom in noms_tags_auto])
|
||||
return f"Tags du programme de formation : {aff_tags_perso} + Automatiques : {aff_tags_auto}"
|
||||
else:
|
||||
aff_tags_auto = ", ".join([f"👜{nom}" for nom in noms_tags_auto])
|
||||
return f"Tags automatiques {aff_tags_auto} (aucun tag personnalisé)"
|
||||
|
||||
|
||||
def repr_jeune(etudid, etudiants):
|
||||
"""Renvoie la représentation d'un étudiant"""
|
||||
etat = "⛔" if etudid in etudiants.abandons_ids else "✅"
|
||||
jeune = f"{etat} {etudiants.identites[etudid].nomprenom} (#{etudid})"
|
||||
return jeune
|
||||
|
||||
|
||||
def aff_trajectoires_suivies_par_etudiants(etudiants):
|
||||
"""Affiche les trajectoires (regroupement de (form)semestres)
|
||||
amenant un étudiant du S1 à un semestre final,
|
||||
en regroupant les étudiants par profil de trajectoires"""
|
||||
|
||||
# Affichage pour debug
|
||||
etudiants_ids = etudiants.etudiants_ids
|
||||
jeunes = list(enumerate(etudiants_ids))
|
||||
|
||||
profils_traj = {}
|
||||
|
||||
for no_etud, etudid in jeunes:
|
||||
jeune = repr_jeune(etudid, etudiants)
|
||||
|
||||
# La trajectoire du jeune
|
||||
trajectoires = etudiants.trajectoires[etudid]
|
||||
profil_traj = []
|
||||
for nom_rcs, rcs in trajectoires.items():
|
||||
if rcs:
|
||||
profil_traj += [f" > RCS ⏯️{nom_rcs}: {rcs.get_repr()}"]
|
||||
aff_profil_traj = "\n".join(profil_traj)
|
||||
if aff_profil_traj not in profils_traj:
|
||||
profils_traj[aff_profil_traj] = []
|
||||
|
||||
profils_traj[aff_profil_traj] += [jeune]
|
||||
|
||||
# Affichage final
|
||||
for profil, jeunes in profils_traj.items():
|
||||
pe_print(f"--> Trajectoire suivie par : ")
|
||||
pe_print("\n".join([" " + jeune for jeune in jeunes]))
|
||||
pe_print(profil)
|
||||
|
||||
|
||||
def aff_semXs_suivis_par_etudiants(etudiants):
|
||||
"""Affiche les SemX (regroupement de semestres de type Sx)
|
||||
amenant un étudiant à valider un Sx"""
|
||||
etudiants_ids = etudiants.etudiants_ids
|
||||
jeunes = list(enumerate(etudiants_ids))
|
||||
|
||||
for no_etud, etudid in jeunes:
|
||||
etat = "⛔" if etudid in etudiants.abandons_ids else "✅"
|
||||
pe_print(f"--> {etat} {etudiants.identites[etudid].nomprenom} :")
|
||||
for nom_rcs, rcs in etudiants.semXs[etudid].items():
|
||||
if rcs:
|
||||
pe_print(f" > SemX ⏯️{nom_rcs}: {rcs.get_repr()}")
|
||||
|
||||
vides = []
|
||||
for nom_rcs in pe_rcs.TOUS_LES_SEMESTRES:
|
||||
les_semX_suivis = []
|
||||
for no_etud, etudid in jeunes:
|
||||
if etudiants.semXs[etudid][nom_rcs]:
|
||||
les_semX_suivis.append(etudiants.semXs[etudid][nom_rcs])
|
||||
if not les_semX_suivis:
|
||||
vides += [nom_rcs]
|
||||
vides = sorted(list(set(vides)))
|
||||
pe_print(f"⚠️ SemX sans données : {', '.join(vides)}")
|
||||
|
||||
|
||||
def aff_capitalisations(etuds, ressembuttags, fid_final, acronymes_sorted, masque_df):
|
||||
"""Affichage des capitalisations du sxtag pour debug"""
|
||||
aff_cap = []
|
||||
for etud in etuds:
|
||||
cap = []
|
||||
for frmsem_id in ressembuttags:
|
||||
if frmsem_id != fid_final:
|
||||
for accr in acronymes_sorted:
|
||||
if masque_df[frmsem_id].loc[etud.etudid, accr] > 0.0:
|
||||
cap += [accr]
|
||||
if cap:
|
||||
aff_cap += [f" > {etud.nomprenom} : {', '.join(cap)}"]
|
||||
if aff_cap:
|
||||
pe_print(f"--> ⚠️ Capitalisations :")
|
||||
pe_print("\n".join(aff_cap))
|
||||
|
||||
|
||||
def repr_comp_et_ues(acronymes_ues_to_competences):
|
||||
"""Affichage pour debug"""
|
||||
asso_comp_to_ues = pe_comp.asso_comp_to_accronymes(acronymes_ues_to_competences)
|
||||
aff_comp = []
|
||||
competences_sorted = sorted(asso_comp_to_ues.keys())
|
||||
for comp in competences_sorted:
|
||||
liste = ["📍" + accro for accro in asso_comp_to_ues[comp]]
|
||||
aff_comp += [f" 💡{comp} (⇔ {', '.join(liste)})"]
|
||||
return "\n".join(aff_comp)
|
||||
|
||||
|
||||
def aff_rcsemxs_suivis_par_etudiants(etudiants):
|
||||
"""Affiche les RCSemX (regroupement de SemX)
|
||||
amenant un étudiant du S1 à un Sx"""
|
||||
etudiants_ids = etudiants.etudiants_ids
|
||||
jeunes = list(enumerate(etudiants_ids))
|
||||
|
||||
for no_etud, etudid in jeunes:
|
||||
etat = "⛔" if etudid in etudiants.abandons_ids else "✅"
|
||||
pe_print(f"-> {etat} {etudiants.identites[etudid].nomprenom} :")
|
||||
for nom_rcs, rcs in etudiants.rcsemXs[etudid].items():
|
||||
if rcs:
|
||||
pe_print(f" > RCSemX ⏯️{nom_rcs}: {rcs.get_repr()}")
|
||||
|
||||
vides = []
|
||||
for nom_rcs in pe_rcs.TOUS_LES_RCS:
|
||||
les_rcssemX_suivis = []
|
||||
for no_etud, etudid in jeunes:
|
||||
if etudiants.rcsemXs[etudid][nom_rcs]:
|
||||
les_rcssemX_suivis.append(etudiants.rcsemXs[etudid][nom_rcs])
|
||||
if not les_rcssemX_suivis:
|
||||
vides += [nom_rcs]
|
||||
vides = sorted(list(set(vides)))
|
||||
pe_print(f"⚠️ RCSemX vides : {', '.join(vides)}")
|
||||
|
|
|
@ -41,13 +41,13 @@ import datetime
|
|||
import re
|
||||
import unicodedata
|
||||
|
||||
|
||||
import pandas as pd
|
||||
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.pe.rcss.pe_rcs import TYPES_RCS
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
|
||||
|
@ -284,3 +284,74 @@ def get_cosemestres_diplomants(annee_diplome: int) -> dict[int, FormSemestre]:
|
|||
cosemestres[fid] = cosem
|
||||
|
||||
return cosemestres
|
||||
|
||||
|
||||
def tri_semestres_par_rang(cosemestres: dict[int, FormSemestre]):
|
||||
"""Partant d'un dictionnaire de cosemestres, les tri par rang (semestre_id) dans un
|
||||
dictionnaire {rang: [liste des semestres du dit rang]}"""
|
||||
cosemestres_tries = {}
|
||||
for sem in cosemestres.values():
|
||||
cosemestres_tries[sem.semestre_id] = cosemestres_tries.get(
|
||||
sem.semestre_id, []
|
||||
) + [sem]
|
||||
return cosemestres_tries
|
||||
|
||||
|
||||
def find_index_and_columns_communs(
|
||||
df1: pd.DataFrame, df2: pd.DataFrame
|
||||
) -> (list, list):
|
||||
"""Partant de 2 DataFrames ``df1`` et ``df2``, renvoie les indices de lignes
|
||||
et de colonnes, communes aux 2 dataframes
|
||||
|
||||
Args:
|
||||
df1: Un dataFrame
|
||||
df2: Un dataFrame
|
||||
Returns:
|
||||
Le tuple formé par la liste des indices de lignes communs et la liste des indices
|
||||
de colonnes communes entre les 2 dataFrames
|
||||
"""
|
||||
indices1 = df1.index
|
||||
indices2 = df2.index
|
||||
indices_communs = list(df1.index.intersection(df2.index))
|
||||
colonnes1 = df1.columns
|
||||
colonnes2 = df2.columns
|
||||
colonnes_communes = list(set(colonnes1) & set(colonnes2))
|
||||
return indices_communs, colonnes_communes
|
||||
|
||||
|
||||
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 asso_comp_to_accronymes(accro_ues_to_competences):
|
||||
"""Partant d'un dictionnaire ``{nom_ue: compétence}`` associant des
|
||||
accronymes d'UEs à des compétences, renvoie l'association d'une compétence
|
||||
à ou aux UEs qui l'adresse : ``{competence: [liste_nom_ue]}``
|
||||
|
||||
Args:
|
||||
accro_ues_to_competences: Dictionnaire ``{nom_ue: compétence}``
|
||||
Return:
|
||||
Le dictionnaire ``{competence: [liste_nom_ue]}``
|
||||
"""
|
||||
asso = {}
|
||||
for accro, comp in accro_ues_to_competences.items():
|
||||
if comp not in asso:
|
||||
asso[comp] = []
|
||||
asso[comp].append(accro)
|
||||
return asso
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. c 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
|
||||
|
@ -37,8 +37,10 @@ Created on 17/01/2024
|
|||
"""
|
||||
import pandas as pd
|
||||
|
||||
from app import ScoValueError
|
||||
from app.models import FormSemestre, Identite, Formation
|
||||
from app.pe import pe_comp, pe_affichage
|
||||
from app.pe.rcss import pe_rcs
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.comp.res_sem import load_formsemestre_results
|
||||
|
@ -55,16 +57,20 @@ class EtudiantsJuryPE:
|
|||
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.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.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.trajectoires: dict[int:dict] = {}
|
||||
"""Les trajectoires (regroupement cohérents de semestres) suivis par les étudiants"""
|
||||
|
||||
self.semXs: dict[int:dict] = {}
|
||||
"""Les semXs (RCS de type Sx) suivis par chaque étudiant"""
|
||||
|
||||
self.rcsemXs: dict[int:dict] = {}
|
||||
"""Les RC de SemXs (RCS de type Sx, xA, xS) suivis par chaque étudiant"""
|
||||
|
||||
self.etudiants_diplomes = {}
|
||||
"""Les identités des étudiants à considérer au jury (ceux qui seront effectivement
|
||||
|
@ -99,27 +105,26 @@ class EtudiantsJuryPE:
|
|||
self.cosemestres = cosemestres
|
||||
|
||||
pe_affichage.pe_print(
|
||||
f"1) Recherche des coSemestres -> {len(cosemestres)} trouvés"
|
||||
f"1) Recherche des cosemestres -> {len(cosemestres)} trouvés", info=True
|
||||
)
|
||||
|
||||
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"
|
||||
"2) Liste des étudiants dans les différents cosemestres", info=True
|
||||
)
|
||||
etudiants_ids = get_etudiants_dans_semestres(cosemestres)
|
||||
pe_affichage.pe_print(
|
||||
f" => {len(etudiants_ids)} étudiants trouvés dans les cosemestres",
|
||||
info=True,
|
||||
)
|
||||
|
||||
# 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")
|
||||
pe_affichage.pe_print(
|
||||
"3) Analyse des parcours individuels des étudiants", info=True
|
||||
)
|
||||
|
||||
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)
|
||||
# Ajoute une liste d'étudiants
|
||||
self.add_etudiants(etudiants_ids)
|
||||
|
||||
# Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris
|
||||
self.etudiants_diplomes = self.get_etudiants_diplomes()
|
||||
|
@ -134,23 +139,35 @@ class EtudiantsJuryPE:
|
|||
# Les identifiants des étudiants ayant redoublés ou ayant abandonnés
|
||||
|
||||
# Synthèse
|
||||
pe_affichage.pe_print(f"4) Bilan", info=True)
|
||||
pe_affichage.pe_print(
|
||||
f" => {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}"
|
||||
f"--> {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}",
|
||||
info=True,
|
||||
)
|
||||
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"
|
||||
f"--> {nbre_abandons} étudiants traités mais non diplômé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 add_etudiants(self, etudiants_ids):
|
||||
"""Ajoute une liste d'étudiants aux données du jury"""
|
||||
nbre_etudiants_ajoutes = 0
|
||||
for etudid in etudiants_ids:
|
||||
if etudid not in self.identites:
|
||||
nbre_etudiants_ajoutes += 1
|
||||
|
||||
# L'identité de l'étudiant
|
||||
self.identites[etudid] = Identite.get_etud(etudid)
|
||||
|
||||
# Analyse son cursus
|
||||
self.analyse_etat_etudiant(etudid, self.cosemestres)
|
||||
|
||||
# Analyse son parcours pour atteindre chaque semestre de la formation
|
||||
self.structure_cursus_etudiant(etudid)
|
||||
self.etudiants_ids = set(self.identites.keys())
|
||||
return nbre_etudiants_ajoutes
|
||||
|
||||
def get_etudiants_diplomes(self) -> dict[int, Identite]:
|
||||
"""Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}`
|
||||
|
@ -198,8 +215,11 @@ class EtudiantsJuryPE:
|
|||
|
||||
* à 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)
|
||||
* à analyser son parcours, pour déterminer s'il a démissionné, redoublé (autre année de diplôme)
|
||||
ou a abandonné l'IUT en cours de route (cf. clé abandon). Un étudiant est considéré
|
||||
en abandon si connaissant son dernier semestre (par ex. un S3) il n'est pas systématiquement
|
||||
inscrit à l'un des S4, S5 ou S6 existants dans les cosemestres.
|
||||
|
||||
|
||||
Args:
|
||||
etudid: L'etudid d'un étudiant, à ajouter à ceux traiter par le jury
|
||||
|
@ -217,11 +237,19 @@ class EtudiantsJuryPE:
|
|||
if formsemestre.formation.is_apc()
|
||||
}
|
||||
|
||||
# Le parcours final
|
||||
parcour = formsemestres[0].etuds_inscriptions[etudid].parcour
|
||||
if parcour:
|
||||
libelle = parcour.libelle
|
||||
else:
|
||||
libelle = None
|
||||
|
||||
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
|
||||
"parcours": libelle, # Le parcours final
|
||||
"diplome": get_annee_diplome(
|
||||
identite
|
||||
), # Le date prévisionnelle de son diplôme
|
||||
|
@ -232,35 +260,24 @@ class EtudiantsJuryPE:
|
|||
"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)
|
||||
# Si l'étudiant est succeptible d'être diplomé
|
||||
if self.cursus[etudid]["diplome"] == self.annee_diplome:
|
||||
# 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
|
||||
# Initialise ses trajectoires/SemX/RCSemX
|
||||
self.trajectoires[etudid] = {aggregat: None for aggregat in pe_rcs.TOUS_LES_RCS}
|
||||
self.semXs[etudid] = {aggregat: None for aggregat in pe_rcs.TOUS_LES_SEMESTRES}
|
||||
self.rcsemXs[etudid] = {aggregat: None for aggregat in pe_rcs.TOUS_LES_RCS}
|
||||
|
||||
def structure_cursus_etudiant(self, etudid: int):
|
||||
"""Structure les informations sur les semestres suivis par un
|
||||
|
@ -269,9 +286,11 @@ class EtudiantsJuryPE:
|
|||
|
||||
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.
|
||||
Ce semestre influera les interclassements par semestre dans la promo.
|
||||
"""
|
||||
semestres_significatifs = self.get_semestres_significatifs(etudid)
|
||||
semestres_significatifs = get_semestres_significatifs(
|
||||
self.cursus[etudid]["formsemestres"], self.annee_diplome
|
||||
)
|
||||
|
||||
# Tri des semestres par numéro de semestre
|
||||
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1):
|
||||
|
@ -283,12 +302,10 @@ class EtudiantsJuryPE:
|
|||
}
|
||||
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 :
|
||||
def get_formsemestres_finals_des_rcs(self, nom_rcs: str) -> dict[int, FormSemestre]:
|
||||
"""Pour un nom de RCS donné, ensemble des formsemestres finals possibles
|
||||
pour les RCS. Par ex. un RCS '3S' incluant S1+S2+S3 a pour semestre final un S3.
|
||||
Les formsemestres finals obtenus 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
|
||||
|
@ -299,14 +316,14 @@ class EtudiantsJuryPE:
|
|||
renvoyant les formsemestre_id du S3 (1ère session) et du S3 (2ème session)
|
||||
|
||||
Args:
|
||||
aggregat: L'aggrégat
|
||||
nom_rcs: Le nom du RCS (parmi Sx, xA, xS)
|
||||
|
||||
Returns:
|
||||
Un dictionnaire ``{fid: FormSemestre(fid)}``
|
||||
"""
|
||||
formsemestres_terminaux = {}
|
||||
for trajectoire_aggr in self.trajectoires.values():
|
||||
trajectoire = trajectoire_aggr[aggregat]
|
||||
for trajectoire_aggr in self.cursus.values():
|
||||
trajectoire = trajectoire_aggr[nom_rcs]
|
||||
if trajectoire:
|
||||
# Le semestre terminal de l'étudiant de l'aggrégat
|
||||
fid = trajectoire.formsemestre_final.formsemestre_id
|
||||
|
@ -345,7 +362,9 @@ class EtudiantsJuryPE:
|
|||
etudiant = self.identites[etudid]
|
||||
cursus = self.cursus[etudid]
|
||||
formsemestres = cursus["formsemestres"]
|
||||
|
||||
parcours = cursus["parcours"]
|
||||
if not parcours:
|
||||
parcours = ""
|
||||
if cursus["diplome"]:
|
||||
diplome = cursus["diplome"]
|
||||
else:
|
||||
|
@ -359,6 +378,7 @@ class EtudiantsJuryPE:
|
|||
"Prenom": etudiant.prenom,
|
||||
"Civilite": etudiant.civilite_str,
|
||||
"Age": pe_comp.calcul_age(etudiant.date_naissance),
|
||||
"Parcours": parcours,
|
||||
"Date entree": cursus["entree"],
|
||||
"Date diplome": diplome,
|
||||
"Nb semestres": len(formsemestres),
|
||||
|
@ -376,13 +396,35 @@ class EtudiantsJuryPE:
|
|||
return df
|
||||
|
||||
|
||||
def get_semestres_significatifs(formsemestres, annee_diplome):
|
||||
"""Partant d'un ensemble de semestre, renvoie les semestres qui amèneraient les étudiants
|
||||
à être diplômé à l'année visée, y compris s'ils n'avaient pas redoublé et seraient donc
|
||||
diplômé plus tard.
|
||||
|
||||
De fait, supprime les semestres qui conduisent à une diplomation postérieure
|
||||
à celle visée.
|
||||
|
||||
Args:
|
||||
formsemestres: une liste de formsemestres
|
||||
annee_diplome: l'année du diplôme visée
|
||||
|
||||
Returns:
|
||||
Un dictionnaire ``{fid: FormSemestre(fid)}`` dans lequel les semestres
|
||||
amènent à une diplômation antérieur à celle de la diplômation visée par le jury
|
||||
"""
|
||||
# semestres_etudiant = self.cursus[etudid]["formsemestres"]
|
||||
semestres_significatifs = {}
|
||||
for fid in formsemestres:
|
||||
semestre = formsemestres[fid]
|
||||
if pe_comp.get_annee_diplome_semestre(semestre) <= annee_diplome:
|
||||
semestres_significatifs[fid] = semestre
|
||||
return semestres_significatifs
|
||||
|
||||
|
||||
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
|
||||
|
@ -430,7 +472,7 @@ def get_annee_diplome(etud: Identite) -> int | None:
|
|||
|
||||
|
||||
def get_semestres_apc(identite: Identite) -> list:
|
||||
"""Liste des semestres d'un étudiant qui corresponde à une formation APC.
|
||||
"""Liste des semestres d'un étudiant qui correspondent à une formation APC.
|
||||
|
||||
Args:
|
||||
identite: L'identité d'un étudiant
|
||||
|
@ -446,8 +488,8 @@ def get_semestres_apc(identite: Identite) -> list:
|
|||
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 :
|
||||
def arret_de_formation(etud: Identite, cosemestres: dict[int, FormSemestre]) -> bool:
|
||||
"""Détermine si un étudiant a arrêté sa formation (volontairement ou non). 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
|
||||
|
@ -458,7 +500,8 @@ def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool:
|
|||
|
||||
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.
|
||||
connu dans Scodoc. Par "derniers" cosemestres, est fait le choix d'analyser tous les cosemestres
|
||||
de rang/semestre_id supérieur (et donc de dates) au dernier semestre dans lequel il a été inscrit.
|
||||
|
||||
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
|
||||
|
@ -485,7 +528,6 @@ def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool:
|
|||
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)
|
||||
|
@ -493,61 +535,54 @@ def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool:
|
|||
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
|
||||
# Le dernier semestre de l'étudiant
|
||||
dernier_formsemestre = semestres[0]
|
||||
rang_dernier_semestre = 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,
|
||||
)
|
||||
# Les cosemestres de rang supérieur ou égal à celui de formsemestre, triés par rang,
|
||||
# sous la forme ``{semestre_id: [liste des comestres associé à ce semestre_id]}``
|
||||
cosemestres_tries_par_rang = pe_comp.tri_semestres_par_rang(cosemestres)
|
||||
|
||||
cosemestres_superieurs = {}
|
||||
for rang in cosemestres_tries_par_rang:
|
||||
if rang > rang_dernier_semestre:
|
||||
cosemestres_superieurs[rang] = cosemestres_tries_par_rang[rang]
|
||||
|
||||
# Si pas d'autres cosemestres postérieurs
|
||||
if not cosemestres_superieurs:
|
||||
return False
|
||||
|
||||
# Pour chaque rang de (co)semestres, y-a-il un dans lequel il est inscrit ?
|
||||
etat_inscriptions = {rang: False for rang in cosemestres_superieurs}
|
||||
for rang in etat_inscriptions:
|
||||
for sem in cosemestres_superieurs[rang]:
|
||||
etudiants_du_sem = {ins.etudid for ins in sem.inscriptions}
|
||||
if etud.etudid in etudiants_du_sem:
|
||||
etat_inscriptions[rang] = True
|
||||
|
||||
# Vérifie qu'il n'y a pas de "trous" dans les rangs des cosemestres
|
||||
rangs = sorted(etat_inscriptions.keys())
|
||||
if list(rangs) != list(range(min(rangs), max(rangs) + 1)):
|
||||
difference = set(range(min(rangs), max(rangs) + 1)) - set(rangs)
|
||||
affichage = ",".join([f"S{val}" for val in difference])
|
||||
raise ScoValueError(
|
||||
f"Il manque le(s) semestre(s) {affichage} au cursus de {etud.etat_civil} ({etud.etudid})."
|
||||
)
|
||||
|
||||
# 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)
|
||||
# Est-il inscrit à tous les semestres de rang supérieur ? Si non, est démissionnaire
|
||||
est_demissionnaire = sum(etat_inscriptions.values()) != len(rangs)
|
||||
if est_demissionnaire:
|
||||
non_inscrit_a = [
|
||||
rang for rang in etat_inscriptions if not etat_inscriptions[rang]
|
||||
]
|
||||
affichage = ", ".join([f"S{val}" for val in non_inscrit_a])
|
||||
pe_affichage.pe_print(
|
||||
f"--> ⛔ {etud.etat_civil} ({etud.etudid}), non inscrit dans {affichage} amenant à diplômation"
|
||||
)
|
||||
else:
|
||||
pe_affichage.pe_print(f"--> ✅ {etud.etat_civil} ({etud.etudid})")
|
||||
|
||||
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
|
||||
return est_demissionnaire
|
||||
|
||||
|
||||
def etapes_du_cursus(
|
||||
|
@ -613,6 +648,6 @@ def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str:
|
|||
f"{semestre.date_debut.year}-{semestre.date_fin.year}",
|
||||
]
|
||||
if avec_fid:
|
||||
description.append(f"({semestre.formsemestre_id})")
|
||||
description.append(f"(#{semestre.formsemestre_id})")
|
||||
|
||||
return " ".join(description)
|
||||
|
|
|
@ -1,160 +0,0 @@
|
|||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
##############################################################################
|
||||
# Module "Avis de poursuite d'étude"
|
||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Created on Thu Sep 8 09:36:33 2016
|
||||
|
||||
@author: barasc
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
from app.pe.pe_tabletags import TableTag, MoyenneTag
|
||||
from app.pe.pe_etudiant import EtudiantsJuryPE
|
||||
from app.pe.pe_rcs import RCS, RCSsJuryPE
|
||||
from app.pe.pe_rcstag import RCSTag
|
||||
|
||||
|
||||
class RCSInterclasseTag(TableTag):
|
||||
"""
|
||||
Interclasse l'ensemble des étudiants diplômés à une année
|
||||
donnée (celle du jury), pour un RCS donné (par ex: 'S2', '3S')
|
||||
en reportant :
|
||||
|
||||
* les moyennes obtenues sur la trajectoire qu'il ont suivi pour atteindre
|
||||
le numéro de semestre de fin de l'aggrégat (indépendamment de son
|
||||
formsemestre)
|
||||
* calculant le classement sur les étudiants diplômes
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
nom_rcs: str,
|
||||
etudiants: EtudiantsJuryPE,
|
||||
rcss_jury_pe: RCSsJuryPE,
|
||||
rcss_tags: dict[tuple, RCSTag],
|
||||
):
|
||||
TableTag.__init__(self)
|
||||
|
||||
self.nom_rcs = nom_rcs
|
||||
"""Le nom du RCS interclassé"""
|
||||
|
||||
self.nom = self.get_repr()
|
||||
|
||||
"""Les étudiants diplômés et leurs rcss""" # TODO
|
||||
self.diplomes_ids = etudiants.etudiants_diplomes
|
||||
self.etudiants_diplomes = {etudid for etudid in self.diplomes_ids}
|
||||
# pour les exports sous forme de dataFrame
|
||||
self.etudiants = {
|
||||
etudid: etudiants.identites[etudid].etat_civil
|
||||
for etudid in self.diplomes_ids
|
||||
}
|
||||
|
||||
# Les trajectoires (et leur version tagguées), en ne gardant que
|
||||
# celles associées à l'aggrégat
|
||||
self.rcss: dict[int, RCS] = {}
|
||||
"""Ensemble des trajectoires associées à l'aggrégat"""
|
||||
for trajectoire_id in rcss_jury_pe.rcss:
|
||||
trajectoire = rcss_jury_pe.rcss[trajectoire_id]
|
||||
if trajectoire_id[0] == nom_rcs:
|
||||
self.rcss[trajectoire_id] = trajectoire
|
||||
|
||||
self.trajectoires_taggues: dict[int, RCS] = {}
|
||||
"""Ensemble des trajectoires tagguées associées à l'aggrégat"""
|
||||
for trajectoire_id in self.rcss:
|
||||
self.trajectoires_taggues[trajectoire_id] = rcss_tags[trajectoire_id]
|
||||
|
||||
# Les trajectoires suivies par les étudiants du jury, en ne gardant que
|
||||
# celles associées aux diplomés
|
||||
self.suivi: dict[int, RCS] = {}
|
||||
"""Association entre chaque étudiant et la trajectoire tagguée à prendre en
|
||||
compte pour l'aggrégat"""
|
||||
for etudid in self.diplomes_ids:
|
||||
self.suivi[etudid] = rcss_jury_pe.suivi[etudid][nom_rcs]
|
||||
|
||||
self.tags_sorted = self.do_taglist()
|
||||
"""Liste des tags (triés par ordre alphabétique)"""
|
||||
|
||||
# Construit la matrice de notes
|
||||
self.notes = self.compute_notes_matrice()
|
||||
"""Matrice des notes de l'aggrégat"""
|
||||
|
||||
# Synthétise les moyennes/classements par tag
|
||||
self.moyennes_tags: dict[str, MoyenneTag] = {}
|
||||
for tag in self.tags_sorted:
|
||||
moy_gen_tag = self.notes[tag]
|
||||
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag)
|
||||
|
||||
# Est significatif ? (aka a-t-il des tags et des notes)
|
||||
self.significatif = len(self.tags_sorted) > 0
|
||||
|
||||
def get_repr(self) -> str:
|
||||
"""Une représentation textuelle"""
|
||||
return f"Aggrégat {self.nom_rcs}"
|
||||
|
||||
def do_taglist(self):
|
||||
"""Synthétise les tags à partir des trajectoires_tagguées
|
||||
|
||||
Returns:
|
||||
Une liste de tags triés par ordre alphabétique
|
||||
"""
|
||||
tags = []
|
||||
for trajectoire in self.trajectoires_taggues.values():
|
||||
tags.extend(trajectoire.tags_sorted)
|
||||
return sorted(set(tags))
|
||||
|
||||
def compute_notes_matrice(self):
|
||||
"""Construit la matrice de notes (etudid x tags)
|
||||
retraçant les moyennes obtenues par les étudiants dans les semestres associés à
|
||||
l'aggrégat (une trajectoire ayant pour numéro de semestre final, celui de l'aggrégat).
|
||||
"""
|
||||
# nb_tags = len(self.tags_sorted) unused ?
|
||||
# nb_etudiants = len(self.diplomes_ids)
|
||||
|
||||
# Index de la matrice (etudids -> dim 0, tags -> dim 1)
|
||||
etudids = list(self.diplomes_ids)
|
||||
tags = self.tags_sorted
|
||||
|
||||
# Partant d'un dataframe vierge
|
||||
df = pd.DataFrame(np.nan, index=etudids, columns=tags)
|
||||
|
||||
for trajectoire in self.trajectoires_taggues.values():
|
||||
# Charge les moyennes par tag de la trajectoire tagguée
|
||||
notes = trajectoire.notes
|
||||
# Etudiants/Tags communs entre la trajectoire_tagguée et les données interclassées
|
||||
etudids_communs = df.index.intersection(notes.index)
|
||||
tags_communs = df.columns.intersection(notes.columns)
|
||||
|
||||
# Injecte les notes par tag
|
||||
df.loc[etudids_communs, tags_communs] = notes.loc[
|
||||
etudids_communs, tags_communs
|
||||
]
|
||||
|
||||
return df
|
1183
app/pe/pe_jury.py
1183
app/pe/pe_jury.py
File diff suppressed because it is too large
Load Diff
269
app/pe/pe_rcs.py
269
app/pe/pe_rcs.py
|
@ -1,269 +0,0 @@
|
|||
##############################################################################
|
||||
# Module "Avis de poursuite d'étude"
|
||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Created on 01-2024
|
||||
|
||||
@author: barasc
|
||||
"""
|
||||
|
||||
import app.pe.pe_comp as pe_comp
|
||||
|
||||
from app.models import FormSemestre
|
||||
from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date
|
||||
|
||||
|
||||
TYPES_RCS = {
|
||||
"S1": {
|
||||
"aggregat": ["S1"],
|
||||
"descr": "Semestre 1 (S1)",
|
||||
},
|
||||
"S2": {
|
||||
"aggregat": ["S2"],
|
||||
"descr": "Semestre 2 (S2)",
|
||||
},
|
||||
"1A": {
|
||||
"aggregat": ["S1", "S2"],
|
||||
"descr": "BUT1 (S1+S2)",
|
||||
},
|
||||
"S3": {
|
||||
"aggregat": ["S3"],
|
||||
"descr": "Semestre 3 (S3)",
|
||||
},
|
||||
"S4": {
|
||||
"aggregat": ["S4"],
|
||||
"descr": "Semestre 4 (S4)",
|
||||
},
|
||||
"2A": {
|
||||
"aggregat": ["S3", "S4"],
|
||||
"descr": "BUT2 (S3+S4)",
|
||||
},
|
||||
"3S": {
|
||||
"aggregat": ["S1", "S2", "S3"],
|
||||
"descr": "Moyenne du semestre 1 au semestre 3 (S1+S2+S3)",
|
||||
},
|
||||
"4S": {
|
||||
"aggregat": ["S1", "S2", "S3", "S4"],
|
||||
"descr": "Moyenne du semestre 1 au semestre 4 (S1+S2+S3+S4)",
|
||||
},
|
||||
"S5": {
|
||||
"aggregat": ["S5"],
|
||||
"descr": "Semestre 5 (S5)",
|
||||
},
|
||||
"S6": {
|
||||
"aggregat": ["S6"],
|
||||
"descr": "Semestre 6 (S6)",
|
||||
},
|
||||
"3A": {
|
||||
"aggregat": ["S5", "S6"],
|
||||
"descr": "3ème année (S5+S6)",
|
||||
},
|
||||
"5S": {
|
||||
"aggregat": ["S1", "S2", "S3", "S4", "S5"],
|
||||
"descr": "Moyenne du semestre 1 au semestre 5 (S1+S2+S3+S4+S5)",
|
||||
},
|
||||
"6S": {
|
||||
"aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"],
|
||||
"descr": "Moyenne globale (S1+S2+S3+S4+S5+S6)",
|
||||
},
|
||||
}
|
||||
"""Dictionnaire détaillant les différents regroupements cohérents
|
||||
de semestres (RCS), en leur attribuant un nom et en détaillant
|
||||
le nom des semestres qu'ils regroupent et l'affichage qui en sera fait
|
||||
dans les tableurs de synthèse.
|
||||
"""
|
||||
|
||||
TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS if not cle.startswith("S")]
|
||||
TOUS_LES_RCS = list(TYPES_RCS.keys())
|
||||
TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")]
|
||||
|
||||
|
||||
class RCS:
|
||||
"""Modélise un ensemble de semestres d'étudiants
|
||||
associé à un type de regroupement cohérent de semestres
|
||||
donné (par ex: 'S2', '3S', '2A').
|
||||
|
||||
Si le RCS est un semestre de type Si, stocke le (ou les)
|
||||
formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si
|
||||
(en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants)
|
||||
|
||||
Pour le RCS de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie
|
||||
les semestres que les étudiants ont suivis pour les amener jusqu'au semestre
|
||||
terminal de la trajectoire (par ex: ici un S3).
|
||||
|
||||
Ces semestres peuvent être :
|
||||
|
||||
* des S1+S2+S1+S2+S3 si redoublement de la 1ère année
|
||||
* des S1+S2+(année de césure)+S3 si césure, ...
|
||||
|
||||
Args:
|
||||
nom_rcs: Un nom du RCS (par ex: '5S')
|
||||
semestre_final: Le semestre final du RCS
|
||||
"""
|
||||
|
||||
def __init__(self, nom_rcs: str, semestre_final: FormSemestre):
|
||||
self.nom = nom_rcs
|
||||
"""Nom du RCS"""
|
||||
|
||||
self.formsemestre_final = semestre_final
|
||||
"""FormSemestre terminal du RCS"""
|
||||
|
||||
self.rcs_id = (nom_rcs, semestre_final.formsemestre_id)
|
||||
"""Identifiant du RCS sous forme (nom_rcs, id du semestre_terminal)"""
|
||||
|
||||
self.semestres_aggreges = {}
|
||||
"""Semestres regroupés dans le RCS"""
|
||||
|
||||
def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]):
|
||||
"""Ajout de semestres aux semestres à regrouper
|
||||
|
||||
Args:
|
||||
semestres: Dictionnaire ``{fid: FormSemestre(fid)}`` à ajouter
|
||||
"""
|
||||
self.semestres_aggreges = self.semestres_aggreges | semestres
|
||||
|
||||
def get_repr(self, verbose=True) -> str:
|
||||
"""Représentation textuelle d'un RCS
|
||||
basé sur ses semestres aggrégés"""
|
||||
|
||||
noms = []
|
||||
for fid in self.semestres_aggreges:
|
||||
semestre = self.semestres_aggreges[fid]
|
||||
noms.append(f"S{semestre.semestre_id}({fid})")
|
||||
noms = sorted(noms)
|
||||
title = f"""{self.nom} ({
|
||||
self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}"""
|
||||
if verbose and noms:
|
||||
title += " - " + "+".join(noms)
|
||||
return title
|
||||
|
||||
|
||||
class RCSsJuryPE:
|
||||
"""Classe centralisant toutes les regroupements cohérents de
|
||||
semestres (RCS) des étudiants à prendre en compte dans un jury PE
|
||||
|
||||
Args:
|
||||
annee_diplome: L'année de diplomation
|
||||
"""
|
||||
|
||||
def __init__(self, annee_diplome: int):
|
||||
self.annee_diplome = annee_diplome
|
||||
"""Année de diplômation"""
|
||||
|
||||
self.rcss: dict[tuple:RCS] = {}
|
||||
"""Ensemble des RCS recensés : {(nom_RCS, fid_terminal): RCS}"""
|
||||
|
||||
self.suivi: dict[int:str] = {}
|
||||
"""Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS,
|
||||
son RCS : {etudid: {nom_RCS: RCS}}"""
|
||||
|
||||
def cree_rcss(self, etudiants: EtudiantsJuryPE):
|
||||
"""Créé tous les RCS, au regard du cursus des étudiants
|
||||
analysés + les mémorise dans les données de l'étudiant
|
||||
|
||||
Args:
|
||||
etudiants: Les étudiants à prendre en compte dans le Jury PE
|
||||
"""
|
||||
|
||||
for nom_rcs in pe_comp.TOUS_LES_SEMESTRES + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM:
|
||||
# L'aggrégat considéré (par ex: 3S=S1+S2+S3), son nom de son semestre
|
||||
# terminal (par ex: S3) et son numéro (par ex: 3)
|
||||
noms_semestre_de_aggregat = TYPES_RCS[nom_rcs]["aggregat"]
|
||||
nom_semestre_terminal = noms_semestre_de_aggregat[-1]
|
||||
|
||||
for etudid in etudiants.cursus:
|
||||
if etudid not in self.suivi:
|
||||
self.suivi[etudid] = {
|
||||
aggregat: None
|
||||
for aggregat in pe_comp.TOUS_LES_SEMESTRES
|
||||
+ TOUS_LES_RCS_AVEC_PLUSIEURS_SEM
|
||||
}
|
||||
|
||||
# Le formsemestre terminal (dernier en date) associé au
|
||||
# semestre marquant la fin de l'aggrégat
|
||||
# (par ex: son dernier S3 en date)
|
||||
semestres = etudiants.cursus[etudid][nom_semestre_terminal]
|
||||
if semestres:
|
||||
formsemestre_final = get_dernier_semestre_en_date(semestres)
|
||||
|
||||
# Ajout ou récupération de la trajectoire
|
||||
trajectoire_id = (nom_rcs, formsemestre_final.formsemestre_id)
|
||||
if trajectoire_id not in self.rcss:
|
||||
trajectoire = RCS(nom_rcs, formsemestre_final)
|
||||
self.rcss[trajectoire_id] = trajectoire
|
||||
else:
|
||||
trajectoire = self.rcss[trajectoire_id]
|
||||
|
||||
# La liste des semestres de l'étudiant à prendre en compte
|
||||
# pour cette trajectoire
|
||||
semestres_a_aggreger = get_rcs_etudiant(
|
||||
etudiants.cursus[etudid], formsemestre_final, nom_rcs
|
||||
)
|
||||
|
||||
# Ajout des semestres à la trajectoire
|
||||
trajectoire.add_semestres_a_aggreger(semestres_a_aggreger)
|
||||
|
||||
# Mémoire la trajectoire suivie par l'étudiant
|
||||
self.suivi[etudid][nom_rcs] = trajectoire
|
||||
|
||||
|
||||
def get_rcs_etudiant(
|
||||
semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str
|
||||
) -> dict[int, FormSemestre]:
|
||||
"""Ensemble des semestres parcourus par un étudiant, connaissant
|
||||
les semestres de son cursus,
|
||||
dans le cadre du RCS visé et ayant pour semestre terminal `formsemestre_final`.
|
||||
|
||||
Si le RCS est de type "Si", limite les semestres à ceux de numéro i.
|
||||
Par ex: si formsemestre_terminal est un S3 et nom_agrregat "S3", ne prend en compte que les
|
||||
semestres 3.
|
||||
|
||||
Si le RCS est de type "iA" ou "iS" (incluant plusieurs numéros de semestres), prend en
|
||||
compte les dit numéros de semestres.
|
||||
|
||||
Par ex: si formsemestre_terminal est un S3, ensemble des S1,
|
||||
S2, S3 suivi pour l'amener au S3 (il peut y avoir plusieurs S1,
|
||||
ou S2, ou S3 s'il a redoublé).
|
||||
|
||||
Les semestres parcourus sont antérieurs (en terme de date de fin)
|
||||
au formsemestre_terminal.
|
||||
|
||||
Args:
|
||||
cursus: Dictionnaire {fid: FormSemestre(fid)} donnant l'ensemble des semestres
|
||||
dans lesquels l'étudiant a été inscrit
|
||||
formsemestre_final: le semestre final visé
|
||||
nom_rcs: Nom du RCS visé
|
||||
"""
|
||||
numero_semestre_terminal = formsemestre_final.semestre_id
|
||||
# semestres_significatifs = self.get_semestres_significatifs(etudid)
|
||||
semestres_significatifs = {}
|
||||
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1):
|
||||
semestres_significatifs = semestres_significatifs | semestres[f"S{i}"]
|
||||
|
||||
if nom_rcs.startswith("S"): # les semestres
|
||||
numero_semestres_possibles = [numero_semestre_terminal]
|
||||
elif nom_rcs.endswith("A"): # les années
|
||||
numero_semestres_possibles = [
|
||||
int(sem[-1]) for sem in TYPES_RCS[nom_rcs]["aggregat"]
|
||||
]
|
||||
assert numero_semestre_terminal in numero_semestres_possibles
|
||||
else: # les xS = tous les semestres jusqu'à Sx (eg S1, S2, S3 pour un S3 terminal)
|
||||
numero_semestres_possibles = list(range(1, numero_semestre_terminal + 1))
|
||||
|
||||
semestres_aggreges = {}
|
||||
for fid, semestre in semestres_significatifs.items():
|
||||
# Semestres parmi ceux de n° possibles & qui lui sont antérieurs
|
||||
if (
|
||||
semestre.semestre_id in numero_semestres_possibles
|
||||
and semestre.date_fin <= formsemestre_final.date_fin
|
||||
):
|
||||
semestres_aggreges[fid] = semestre
|
||||
return semestres_aggreges
|
||||
|
||||
|
||||
def get_descr_rcs(nom_rcs: str) -> str:
|
||||
"""Renvoie la description pour les tableurs de synthèse
|
||||
Excel d'un nom de RCS"""
|
||||
return TYPES_RCS[nom_rcs]["descr"]
|
|
@ -0,0 +1,289 @@
|
|||
import app.pe.pe_comp
|
||||
from app.pe.rcss import pe_rcs, pe_trajectoires, pe_rcsemx
|
||||
import app.pe.pe_etudiant as pe_etudiant
|
||||
import app.pe.pe_comp as pe_comp
|
||||
from app.models import FormSemestre
|
||||
from app.pe import pe_affichage
|
||||
|
||||
|
||||
class RCSsJuryPE:
|
||||
"""Classe centralisant tous 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, etudiants: pe_etudiant.EtudiantsJuryPE):
|
||||
self.annee_diplome = annee_diplome
|
||||
"""Année de diplômation"""
|
||||
|
||||
self.etudiants = etudiants
|
||||
"""Les étudiants recensés"""
|
||||
|
||||
self.trajectoires: dict[tuple(int, str) : pe_trajectoires.Trajectoire] = {}
|
||||
"""Ensemble des trajectoires recensées (regroupement de (form)semestres BUT)"""
|
||||
|
||||
self.trajectoires_suivies: dict[int:dict] = {}
|
||||
"""Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS,
|
||||
sa Trajectoire : {etudid: {nom_RCS: Trajectoire}}"""
|
||||
|
||||
self.semXs: dict[tuple(int, str) : pe_trajectoires.SemX] = {}
|
||||
"""Ensemble des SemX recensés (regroupement de (form)semestre BUT de rang x) :
|
||||
{(nom_RCS, fid_terminal): SemX}"""
|
||||
|
||||
self.semXs_suivis: dict[int:dict] = {}
|
||||
"""Dictionnaire associant, pour chaque étudiant et pour chaque RCS de type Sx,
|
||||
son SemX : {etudid: {nom_RCS_de_type_Sx: SemX}}"""
|
||||
|
||||
self.rcsemxs: dict[tuple(int, str) : pe_rcsemx.RCSemX] = {}
|
||||
"""Ensemble des RCSemX (regroupement de SemX donnant les résultats aux sems de rang x)
|
||||
recensés : {(nom_RCS, fid_terminal): RCSemX}"""
|
||||
|
||||
self.rcsemxs_suivis: dict[int:str] = {}
|
||||
"""Dictionnaire associant, pour chaque étudiant et pour chaque type de RCS,
|
||||
son RCSemX : {etudid: {nom_RCS: RCSemX}}"""
|
||||
|
||||
def cree_trajectoires(self):
|
||||
"""Créé toutes les trajectoires, 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
|
||||
"""
|
||||
|
||||
tous_les_aggregats = pe_rcs.TOUS_LES_RCS
|
||||
|
||||
for etudid in self.etudiants.cursus:
|
||||
self.trajectoires_suivies[etudid] = self.etudiants.trajectoires[etudid]
|
||||
|
||||
for nom_rcs in tous_les_aggregats:
|
||||
# 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_semestres = pe_rcs.TYPES_RCS[nom_rcs]["aggregat"]
|
||||
nom_semestre_final = noms_semestres[-1]
|
||||
|
||||
for etudid in self.etudiants.cursus:
|
||||
# Le (ou les) semestre(s) marquant la fin du cursus de l'étudiant
|
||||
sems_final = self.etudiants.cursus[etudid][nom_semestre_final]
|
||||
if sems_final:
|
||||
# Le formsemestre final (dernier en date) de l'étudiant,
|
||||
# marquant la fin de son aggrégat (par ex: son dernier S3 en date)
|
||||
formsemestre_final = app.pe.pe_comp.get_dernier_semestre_en_date(
|
||||
sems_final
|
||||
)
|
||||
|
||||
# Ajout (si nécessaire) et récupération du RCS associé
|
||||
rcs_id = (nom_rcs, formsemestre_final.formsemestre_id)
|
||||
if rcs_id not in self.trajectoires:
|
||||
self.trajectoires[rcs_id] = pe_trajectoires.Trajectoire(
|
||||
nom_rcs, formsemestre_final
|
||||
)
|
||||
rcs = self.trajectoires[rcs_id]
|
||||
|
||||
# La liste des semestres de l'étudiant à prendre en compte
|
||||
# pour cette trajectoire
|
||||
semestres_a_aggreger = get_rcs_etudiant(
|
||||
self.etudiants.cursus[etudid], formsemestre_final, nom_rcs
|
||||
)
|
||||
|
||||
# Ajout des semestres au RCS
|
||||
rcs.add_semestres(semestres_a_aggreger)
|
||||
|
||||
# Mémorise le RCS suivi par l'étudiant
|
||||
self.trajectoires_suivies[etudid][nom_rcs] = rcs
|
||||
self.etudiants.trajectoires[etudid][nom_rcs] = rcs
|
||||
|
||||
def cree_semxs(self):
|
||||
"""Créé les SemXs (trajectoires/combinaisons de semestre de même rang x),
|
||||
en ne conservant dans les trajectoires que les regroupements
|
||||
de type Sx"""
|
||||
self.semXs = {}
|
||||
for rcs_id, trajectoire in self.trajectoires.items():
|
||||
if trajectoire.nom in pe_rcs.TOUS_LES_SEMESTRES:
|
||||
self.semXs[rcs_id] = pe_trajectoires.SemX(trajectoire)
|
||||
|
||||
# L'association (pour chaque étudiant entre chaque Sx et le SemX associé)
|
||||
self.semXs_suivis = {}
|
||||
for etudid in self.etudiants.trajectoires:
|
||||
self.semXs_suivis[etudid] = {
|
||||
agregat: None for agregat in pe_rcs.TOUS_LES_SEMESTRES
|
||||
}
|
||||
for agregat in pe_rcs.TOUS_LES_SEMESTRES:
|
||||
trajectoire = self.etudiants.trajectoires[etudid][agregat]
|
||||
if trajectoire:
|
||||
rcs_id = trajectoire.rcs_id
|
||||
semX = self.semXs[rcs_id]
|
||||
self.semXs_suivis[etudid][agregat] = semX
|
||||
self.etudiants.semXs[etudid][agregat] = semX
|
||||
|
||||
def cree_rcsemxs(self, options={"moyennes_ues_rcues": True}):
|
||||
"""Créé tous les RCSemXs, au regard du cursus des étudiants
|
||||
analysés (trajectoires traduisant son parcours dans les
|
||||
différents semestres) + les mémorise dans les données de l'étudiant
|
||||
"""
|
||||
self.rcsemxs_suivis = {}
|
||||
self.rcsemxs = {}
|
||||
|
||||
if "moyennes_ues_rcues" in options and options["moyennes_ues_rcues"] == False:
|
||||
# Pas de RCSemX généré
|
||||
pe_affichage.pe_print("⚠️ Pas de RCSemX générés")
|
||||
return
|
||||
|
||||
# Pour tous les étudiants du jury
|
||||
pas_de_semestres = []
|
||||
for etudid in self.trajectoires_suivies:
|
||||
self.rcsemxs_suivis[etudid] = {
|
||||
nom_rcs: None for nom_rcs in pe_rcs.TOUS_LES_RCS_AVEC_PLUSIEURS_SEM
|
||||
}
|
||||
|
||||
# Pour chaque aggréggat de type xA ou Sx ou xS
|
||||
for agregat in pe_rcs.TOUS_LES_RCS:
|
||||
trajectoire = self.trajectoires_suivies[etudid][agregat]
|
||||
if not trajectoire:
|
||||
self.rcsemxs_suivis[etudid][agregat] = None
|
||||
else:
|
||||
# Identifiant de la trajectoire => donnera ceux du RCSemX
|
||||
tid = trajectoire.rcs_id
|
||||
# Ajout du RCSemX
|
||||
if tid not in self.rcsemxs:
|
||||
self.rcsemxs[tid] = pe_rcsemx.RCSemX(
|
||||
trajectoire.nom, trajectoire.formsemestre_final
|
||||
)
|
||||
|
||||
# Récupére les SemX (RC de type Sx) associés aux semestres de son cursus
|
||||
# Par ex: dans S1+S2+S1+S2+S3 => les 2 S1 devient le SemX('S1'), les 2 S2 le SemX('S2'), etc..
|
||||
|
||||
# Les Sx pris en compte dans l'aggrégat
|
||||
noms_sems_aggregat = pe_rcs.TYPES_RCS[agregat]["aggregat"]
|
||||
|
||||
semxs_a_aggreger = {}
|
||||
for Sx in noms_sems_aggregat:
|
||||
semestres_etudiants = self.etudiants.cursus[etudid][Sx]
|
||||
if not semestres_etudiants:
|
||||
pas_de_semestres += [
|
||||
f"{Sx} pour {self.etudiants.identites[etudid].nomprenom}"
|
||||
]
|
||||
else:
|
||||
semx_id = get_semx_from_semestres_aggreges(
|
||||
self.semXs, semestres_etudiants
|
||||
)
|
||||
if not semx_id:
|
||||
raise (
|
||||
"Il manque un SemX pour créer les RCSemX dans cree_rcsemxs"
|
||||
)
|
||||
# Les SemX à ajouter au RCSemX
|
||||
semxs_a_aggreger[semx_id] = self.semXs[semx_id]
|
||||
|
||||
# Ajout des SemX à ceux à aggréger dans le RCSemX
|
||||
rcsemx = self.rcsemxs[tid]
|
||||
rcsemx.add_semXs(semxs_a_aggreger)
|
||||
|
||||
# Mémoire du RCSemX aux informations de suivi de l'étudiant
|
||||
self.rcsemxs_suivis[etudid][agregat] = rcsemx
|
||||
self.etudiants.rcsemXs[etudid][agregat] = rcsemx
|
||||
|
||||
# Affichage des étudiants pour lesquels il manque un semestre
|
||||
pas_de_semestres = sorted(set(pas_de_semestres))
|
||||
if pas_de_semestres:
|
||||
pe_affichage.pe_print("⚠️ Semestres manquants :")
|
||||
pe_affichage.pe_print(
|
||||
"\n".join([" " * 10 + psd for psd in pas_de_semestres])
|
||||
)
|
||||
|
||||
|
||||
def get_rcs_etudiant(
|
||||
semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str
|
||||
) -> dict[int, FormSemestre]:
|
||||
"""Ensemble des semestres parcourus (trajectoire)
|
||||
par un étudiant dans le cadre
|
||||
d'un RCS de type Sx, iA ou iS et ayant pour semestre terminal `formsemestre_final`.
|
||||
|
||||
Par ex: pour un RCS "3S", dont le formsemestre_terminal est un S3, regroupe
|
||||
le ou les S1 qu'il a suivi (1 ou 2 si redoublement) + le ou les S2 + le ou les S3.
|
||||
|
||||
Les semestres parcourus sont antérieurs (en terme de date de fin)
|
||||
au formsemestre_terminal.
|
||||
|
||||
Args:
|
||||
cursus: Dictionnaire {fid: Formsemestre} 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 pe_rcs.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_semx_from_semestres_aggreges(
|
||||
semXs: dict[(str, int) : pe_trajectoires.SemX],
|
||||
semestres_a_aggreger: dict[(str, int):FormSemestre],
|
||||
) -> (str, int):
|
||||
"""Partant d'un dictionnaire de SemX (de la forme
|
||||
``{ (nom_rcs, fid): SemX }, et connaissant une liste
|
||||
de (form)semestres suivis, renvoie l'identifiant
|
||||
(nom_rcs, fid) du SemX qui lui correspond.
|
||||
|
||||
Le SemX qui correspond est tel que :
|
||||
|
||||
* le semestre final du SemX correspond au dernier semestre en date des
|
||||
semestres_a_aggreger
|
||||
* le rang du SemX est le même que celui des semestres_aggreges
|
||||
* les semestres_a_aggreger (plus large, car contenant plusieurs
|
||||
parcours), matchent avec les semestres aggrégés
|
||||
par le SemX
|
||||
|
||||
|
||||
Returns:
|
||||
rcf_id: L'identifiant du RCF trouvé
|
||||
"""
|
||||
assert semestres_a_aggreger, "Pas de semestres à aggréger"
|
||||
rangs_a_aggreger = [sem.semestre_id for fid, sem in semestres_a_aggreger.items()]
|
||||
assert (
|
||||
len(set(rangs_a_aggreger)) == 1
|
||||
), "Tous les sem à aggréger doivent être de même rang"
|
||||
|
||||
# Le dernier semestre des semestres à regrouper
|
||||
dernier_sem_a_aggreger = pe_comp.get_dernier_semestre_en_date(semestres_a_aggreger)
|
||||
|
||||
semxs_ids = [] # Au cas où il y ait plusieurs solutions
|
||||
for semx_id, semx in semXs.items():
|
||||
# Même semestre final ?
|
||||
if semx.get_formsemestre_id_final() == dernier_sem_a_aggreger.formsemestre_id:
|
||||
# Les fids
|
||||
fids_a_aggreger = set(semestres_a_aggreger.keys())
|
||||
# Ceux du semx
|
||||
fids_semx = set(semx.semestres_aggreges.keys())
|
||||
if fids_a_aggreger.issubset(
|
||||
fids_semx
|
||||
): # tous les semestres du semx correspond à des sems de la trajectoire
|
||||
semxs_ids += [semx_id]
|
||||
if len(semxs_ids) == 0:
|
||||
return None # rien trouvé
|
||||
elif len(semxs_ids) == 1:
|
||||
return semxs_ids[0]
|
||||
else:
|
||||
raise "Plusieurs solutions :)"
|
|
@ -1,217 +0,0 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
##############################################################################
|
||||
# Module "Avis de poursuite d'étude"
|
||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Created on Fri Sep 9 09:15:05 2016
|
||||
|
||||
@author: barasc
|
||||
"""
|
||||
|
||||
from app.comp.res_sem import load_formsemestre_results
|
||||
from app.pe.pe_semtag import SemestreTag
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from app.pe.pe_rcs import RCS
|
||||
|
||||
from app.pe.pe_tabletags import TableTag, MoyenneTag
|
||||
|
||||
|
||||
class RCSTag(TableTag):
|
||||
def __init__(
|
||||
self, rcs: RCS, semestres_taggues: dict[int, SemestreTag]
|
||||
):
|
||||
"""Calcule les moyennes par tag d'une combinaison de semestres
|
||||
(RCS), pour extraire les classements par tag pour un
|
||||
groupe d'étudiants donnés. Le groupe d'étudiants est formé par ceux ayant tous
|
||||
participé au semestre terminal.
|
||||
|
||||
|
||||
Args:
|
||||
rcs: Un RCS (identifié par un nom et l'id de son semestre terminal)
|
||||
semestres_taggues: Les données sur les semestres taggués
|
||||
"""
|
||||
TableTag.__init__(self)
|
||||
|
||||
|
||||
self.rcs_id = rcs.rcs_id
|
||||
"""Identifiant du RCS taggué (identique au RCS sur lequel il s'appuie)"""
|
||||
|
||||
self.rcs = rcs
|
||||
"""RCS associé au RCS taggué"""
|
||||
|
||||
self.nom = self.get_repr()
|
||||
"""Représentation textuelle du RCS taggué"""
|
||||
|
||||
self.formsemestre_terminal = rcs.formsemestre_final
|
||||
"""Le formsemestre terminal"""
|
||||
|
||||
# Les résultats du formsemestre terminal
|
||||
nt = load_formsemestre_results(self.formsemestre_terminal)
|
||||
|
||||
self.semestres_aggreges = rcs.semestres_aggreges
|
||||
"""Les semestres aggrégés"""
|
||||
|
||||
self.semestres_tags_aggreges = {}
|
||||
"""Les semestres tags associés aux semestres aggrégés"""
|
||||
for frmsem_id in self.semestres_aggreges:
|
||||
try:
|
||||
self.semestres_tags_aggreges[frmsem_id] = semestres_taggues[frmsem_id]
|
||||
except:
|
||||
raise ValueError("Semestres taggués manquants")
|
||||
|
||||
"""Les étudiants (état civil + cursus connu)"""
|
||||
self.etuds = nt.etuds
|
||||
|
||||
# assert self.etuds == trajectoire.suivi # manque-t-il des étudiants ?
|
||||
self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds}
|
||||
|
||||
self.tags_sorted = self.do_taglist()
|
||||
"""Tags extraits de tous les semestres"""
|
||||
|
||||
self.notes_cube = self.compute_notes_cube()
|
||||
"""Cube de notes"""
|
||||
|
||||
etudids = list(self.etudiants.keys())
|
||||
self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted)
|
||||
"""Calcul les moyennes par tag sous forme d'un dataframe"""
|
||||
|
||||
self.moyennes_tags: dict[str, MoyenneTag] = {}
|
||||
"""Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)"""
|
||||
for tag in self.tags_sorted:
|
||||
moy_gen_tag = self.notes[tag]
|
||||
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag)
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Egalité de 2 RCS taggués sur la base de leur identifiant"""
|
||||
return self.rcs_id == other.rcs_id
|
||||
|
||||
def get_repr(self, verbose=False) -> str:
|
||||
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
|
||||
est basée)"""
|
||||
return self.rcs.get_repr(verbose=verbose)
|
||||
|
||||
def compute_notes_cube(self):
|
||||
"""Construit le cube de notes (etudid x tags x semestre_aggregé)
|
||||
nécessaire au calcul des moyennes de l'aggrégat
|
||||
"""
|
||||
# nb_tags = len(self.tags_sorted)
|
||||
# nb_etudiants = len(self.etuds)
|
||||
# nb_semestres = len(self.semestres_tags_aggreges)
|
||||
|
||||
# Index du cube (etudids -> dim 0, tags -> dim 1)
|
||||
etudids = [etud.etudid for etud in self.etuds]
|
||||
tags = self.tags_sorted
|
||||
semestres_id = list(self.semestres_tags_aggreges.keys())
|
||||
|
||||
dfs = {}
|
||||
|
||||
for frmsem_id in semestres_id:
|
||||
# Partant d'un dataframe vierge
|
||||
df = pd.DataFrame(np.nan, index=etudids, columns=tags)
|
||||
|
||||
# Charge les notes du semestre tag
|
||||
notes = self.semestres_tags_aggreges[frmsem_id].notes
|
||||
|
||||
# Les étudiants & les tags commun au dataframe final et aux notes du semestre)
|
||||
etudids_communs = df.index.intersection(notes.index)
|
||||
tags_communs = df.columns.intersection(notes.columns)
|
||||
|
||||
# Injecte les notes par tag
|
||||
df.loc[etudids_communs, tags_communs] = notes.loc[
|
||||
etudids_communs, tags_communs
|
||||
]
|
||||
|
||||
# Supprime tout ce qui n'est pas numérique
|
||||
for col in df.columns:
|
||||
df[col] = pd.to_numeric(df[col], errors="coerce")
|
||||
|
||||
# Stocke le df
|
||||
dfs[frmsem_id] = df
|
||||
|
||||
"""Réunit les notes sous forme d'un cube etdids x tags x semestres"""
|
||||
semestres_x_etudids_x_tags = [dfs[fid].values for fid in dfs]
|
||||
etudids_x_tags_x_semestres = np.stack(semestres_x_etudids_x_tags, axis=-1)
|
||||
|
||||
return etudids_x_tags_x_semestres
|
||||
|
||||
def do_taglist(self):
|
||||
"""Synthétise les tags à partir des semestres (taggués) aggrégés
|
||||
|
||||
Returns:
|
||||
Une liste de tags triés par ordre alphabétique
|
||||
"""
|
||||
tags = []
|
||||
for frmsem_id in self.semestres_tags_aggreges:
|
||||
tags.extend(self.semestres_tags_aggreges[frmsem_id].tags_sorted)
|
||||
return sorted(set(tags))
|
||||
|
||||
|
||||
def compute_tag_moy(set_cube: np.array, etudids: list, tags: list):
|
||||
"""Calcul de la moyenne par tag sur plusieurs semestres.
|
||||
La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles
|
||||
|
||||
*Remarque* : Adaptation de moy_ue.compute_ue_moys_apc au cas des moyennes de tag
|
||||
par aggrégat de plusieurs semestres.
|
||||
|
||||
Args:
|
||||
set_cube: notes moyennes aux modules ndarray
|
||||
(etuds x modimpls x UEs), des floats avec des NaN
|
||||
etudids: liste des étudiants (dim. 0 du cube)
|
||||
tags: liste des tags (dim. 1 du cube)
|
||||
Returns:
|
||||
Un DataFrame avec pour columns les moyennes par tags,
|
||||
et pour rows les etudid
|
||||
"""
|
||||
nb_etuds, nb_tags, nb_semestres = set_cube.shape
|
||||
assert nb_etuds == len(etudids)
|
||||
assert nb_tags == len(tags)
|
||||
|
||||
# Quelles entrées du cube contiennent des notes ?
|
||||
mask = ~np.isnan(set_cube)
|
||||
|
||||
# Enlève les NaN du cube pour les entrées manquantes
|
||||
set_cube_no_nan = np.nan_to_num(set_cube, nan=0.0)
|
||||
|
||||
# Les moyennes par tag
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etud_moy_tag = np.sum(set_cube_no_nan, axis=2) / np.sum(mask, axis=2)
|
||||
|
||||
# Le dataFrame
|
||||
etud_moy_tag_df = pd.DataFrame(
|
||||
etud_moy_tag,
|
||||
index=etudids, # les etudids
|
||||
columns=tags, # les tags
|
||||
)
|
||||
|
||||
etud_moy_tag_df.fillna(np.nan)
|
||||
|
||||
return etud_moy_tag_df
|
|
@ -1,310 +0,0 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
##############################################################################
|
||||
# Module "Avis de poursuite d'étude"
|
||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Created on Fri Sep 9 09:15:05 2016
|
||||
|
||||
@author: barasc
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
import app.pe.pe_etudiant
|
||||
from app import db, ScoValueError
|
||||
from app import comp
|
||||
from app.comp.res_sem import load_formsemestre_results
|
||||
from app.models import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
import app.pe.pe_affichage as pe_affichage
|
||||
from app.pe.pe_tabletags import TableTag, MoyenneTag
|
||||
from app.scodoc import sco_tag_module
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
|
||||
|
||||
class SemestreTag(TableTag):
|
||||
"""
|
||||
Un SemestreTag représente les résultats des étudiants à un semestre, en donnant
|
||||
accès aux moyennes par tag.
|
||||
Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT.
|
||||
"""
|
||||
|
||||
def __init__(self, formsemestre_id: int):
|
||||
"""
|
||||
Args:
|
||||
formsemestre_id: Identifiant du ``FormSemestre`` sur lequel il se base
|
||||
"""
|
||||
TableTag.__init__(self)
|
||||
|
||||
# Le semestre
|
||||
self.formsemestre_id = formsemestre_id
|
||||
self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
|
||||
# Le nom du semestre taggué
|
||||
self.nom = self.get_repr()
|
||||
|
||||
# Les résultats du semestre
|
||||
self.nt = load_formsemestre_results(self.formsemestre)
|
||||
|
||||
# Les étudiants
|
||||
self.etuds = self.nt.etuds
|
||||
self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds}
|
||||
|
||||
# Les notes, les modules implémentés triés, les étudiants, les coeffs,
|
||||
# récupérés notamment de py:mod:`res_but`
|
||||
self.sem_cube = self.nt.sem_cube
|
||||
self.modimpls_sorted = self.nt.formsemestre.modimpls_sorted
|
||||
self.modimpl_coefs_df = self.nt.modimpl_coefs_df
|
||||
|
||||
# Les inscriptions au module et les dispenses d'UE
|
||||
self.modimpl_inscr_df = self.nt.modimpl_inscr_df
|
||||
self.ues = self.nt.ues
|
||||
self.ues_inscr_parcours_df = self.nt.load_ues_inscr_parcours()
|
||||
self.dispense_ues = self.nt.dispense_ues
|
||||
|
||||
# Les tags :
|
||||
## Saisis par l'utilisateur
|
||||
tags_personnalises = get_synthese_tags_personnalises_semestre(
|
||||
self.nt.formsemestre
|
||||
)
|
||||
noms_tags_perso = list(set(tags_personnalises.keys()))
|
||||
|
||||
## Déduit des compétences
|
||||
dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre)
|
||||
noms_tags_comp = list(set(dict_ues_competences.values()))
|
||||
noms_tags_auto = ["but"] + noms_tags_comp
|
||||
self.tags = noms_tags_perso + noms_tags_auto
|
||||
"""Tags du semestre taggué"""
|
||||
|
||||
## Vérifie l'unicité des tags
|
||||
if len(set(self.tags)) != len(self.tags):
|
||||
intersection = list(set(noms_tags_perso) & set(noms_tags_auto))
|
||||
liste_intersection = "\n".join(
|
||||
[f"<li><code>{tag}</code></li>" for tag in intersection]
|
||||
)
|
||||
s = "s" if len(intersection) > 0 else ""
|
||||
message = f"""Erreur dans le module PE : Un des tags saisis dans votre
|
||||
programme de formation fait parti des tags réservés. En particulier,
|
||||
votre semestre <em>{self.formsemestre.titre_annee()}</em>
|
||||
contient le{s} tag{s} réservé{s} suivant :
|
||||
<ul>
|
||||
{liste_intersection}
|
||||
</ul>
|
||||
Modifiez votre programme de formation pour le{s} supprimer.
|
||||
Il{s} ser{'ont' if s else 'a'} automatiquement à vos documents de poursuites d'études.
|
||||
"""
|
||||
raise ScoValueError(message)
|
||||
|
||||
# Calcul des moyennes & les classements de chaque étudiant à chaque tag
|
||||
self.moyennes_tags = {}
|
||||
|
||||
for tag in tags_personnalises:
|
||||
# pe_affichage.pe_print(f" -> Traitement du tag {tag}")
|
||||
moy_gen_tag = self.compute_moyenne_tag(tag, tags_personnalises)
|
||||
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag)
|
||||
|
||||
# Ajoute les moyennes générales de BUT pour le semestre considéré
|
||||
moy_gen_but = self.nt.etud_moy_gen
|
||||
self.moyennes_tags["but"] = MoyenneTag("but", moy_gen_but)
|
||||
|
||||
# Ajoute les moyennes par compétence
|
||||
for ue_id, competence in dict_ues_competences.items():
|
||||
if competence not in self.moyennes_tags:
|
||||
moy_ue = self.nt.etud_moy_ue[ue_id]
|
||||
self.moyennes_tags[competence] = MoyenneTag(competence, moy_ue)
|
||||
|
||||
self.tags_sorted = self.get_all_tags()
|
||||
"""Tags (personnalisés+compétences) par ordre alphabétique"""
|
||||
|
||||
# Synthétise l'ensemble des moyennes dans un dataframe
|
||||
|
||||
self.notes = self.df_notes()
|
||||
"""Dataframe synthétique des notes par tag"""
|
||||
|
||||
pe_affichage.pe_print(
|
||||
f" => Traitement des tags {', '.join(self.tags_sorted)}"
|
||||
)
|
||||
|
||||
def get_repr(self):
|
||||
"""Nom affiché pour le semestre taggué"""
|
||||
return app.pe.pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True)
|
||||
|
||||
def compute_moyenne_tag(self, tag: str, tags_infos: dict) -> pd.Series:
|
||||
"""Calcule la moyenne des étudiants pour le tag indiqué,
|
||||
pour ce SemestreTag, en ayant connaissance des informations sur
|
||||
les tags (dictionnaire donnant les coeff de repondération)
|
||||
|
||||
Sont pris en compte les modules implémentés associés au tag,
|
||||
avec leur éventuel coefficient de **repondération**, en utilisant les notes
|
||||
chargées pour ce SemestreTag.
|
||||
|
||||
Force ou non le calcul de la moyenne lorsque des notes sont manquantes.
|
||||
|
||||
Returns:
|
||||
La série des moyennes
|
||||
"""
|
||||
|
||||
# Adaptation du mask de calcul des moyennes au tag visé
|
||||
modimpls_mask = [
|
||||
modimpl.module.ue.type != UE_SPORT
|
||||
for modimpl in self.formsemestre.modimpls_sorted
|
||||
]
|
||||
|
||||
# Désactive tous les modules qui ne sont pas pris en compte pour ce tag
|
||||
for i, modimpl in enumerate(self.formsemestre.modimpls_sorted):
|
||||
if modimpl.moduleimpl_id not in tags_infos[tag]:
|
||||
modimpls_mask[i] = False
|
||||
|
||||
# Applique la pondération des coefficients
|
||||
modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy()
|
||||
for modimpl_id in tags_infos[tag]:
|
||||
ponderation = tags_infos[tag][modimpl_id]["ponderation"]
|
||||
modimpl_coefs_ponderes_df[modimpl_id] *= ponderation
|
||||
|
||||
# Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)#
|
||||
moyennes_ues_tag = comp.moy_ue.compute_ue_moys_apc(
|
||||
self.sem_cube,
|
||||
self.etuds,
|
||||
self.formsemestre.modimpls_sorted,
|
||||
self.modimpl_inscr_df,
|
||||
modimpl_coefs_ponderes_df,
|
||||
modimpls_mask,
|
||||
self.dispense_ues,
|
||||
block=self.formsemestre.block_moyennes,
|
||||
)
|
||||
|
||||
# Les ects
|
||||
ects = self.ues_inscr_parcours_df.fillna(0.0) * [
|
||||
ue.ects for ue in self.ues if ue.type != UE_SPORT
|
||||
]
|
||||
|
||||
# Calcule la moyenne générale dans le semestre (pondérée par le ECTS)
|
||||
moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects(
|
||||
moyennes_ues_tag,
|
||||
ects,
|
||||
formation_id=self.formsemestre.formation_id,
|
||||
skip_empty_ues=True,
|
||||
)
|
||||
|
||||
return moy_gen_tag
|
||||
|
||||
|
||||
def get_moduleimpl(modimpl_id) -> dict:
|
||||
"""Renvoie l'objet modimpl dont l'id est modimpl_id"""
|
||||
modimpl = db.session.get(ModuleImpl, modimpl_id)
|
||||
if modimpl:
|
||||
return modimpl
|
||||
return None
|
||||
|
||||
|
||||
def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float:
|
||||
"""Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve
|
||||
le module de modimpl_id
|
||||
"""
|
||||
# ré-écrit
|
||||
modimpl = get_moduleimpl(modimpl_id) # le module
|
||||
ue_status = nt.get_etud_ue_status(etudid, modimpl.module.ue.id)
|
||||
if ue_status is None:
|
||||
return None
|
||||
return ue_status["moy"]
|
||||
|
||||
|
||||
def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre):
|
||||
"""Etant données les implémentations des modules du semestre (modimpls),
|
||||
synthétise les tags renseignés dans le programme pédagogique &
|
||||
associés aux modules du semestre,
|
||||
en les associant aux modimpls qui les concernent (modimpl_id) et
|
||||
aucoeff et pondération fournie avec le tag (par défaut 1 si non indiquée)).
|
||||
|
||||
|
||||
Args:
|
||||
formsemestre: Le formsemestre à la base de la recherche des tags
|
||||
|
||||
Return:
|
||||
Un dictionnaire de tags
|
||||
"""
|
||||
synthese_tags = {}
|
||||
|
||||
# Instance des modules du semestre
|
||||
modimpls = formsemestre.modimpls_sorted
|
||||
|
||||
for modimpl in modimpls:
|
||||
modimpl_id = modimpl.id
|
||||
|
||||
# Liste des tags pour le module concerné
|
||||
tags = sco_tag_module.module_tag_list(modimpl.module.id)
|
||||
|
||||
# Traitement des tags recensés, chacun pouvant étant de la forme
|
||||
# "mathématiques", "théorie", "pe:0", "maths:2"
|
||||
for tag in tags:
|
||||
# Extraction du nom du tag et du coeff de pondération
|
||||
(tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag)
|
||||
|
||||
# Ajout d'une clé pour le tag
|
||||
if tagname not in synthese_tags:
|
||||
synthese_tags[tagname] = {}
|
||||
|
||||
# Ajout du module (modimpl) au tagname considéré
|
||||
synthese_tags[tagname][modimpl_id] = {
|
||||
"modimpl": modimpl, # les données sur le module
|
||||
# "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre
|
||||
"ponderation": ponderation, # la pondération demandée pour le tag sur le module
|
||||
# "module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee
|
||||
# "ue_id": modimpl.module.ue.id, # les données sur l'ue
|
||||
# "ue_code": modimpl.module.ue.ue_code,
|
||||
# "ue_acronyme": modimpl.module.ue.acronyme,
|
||||
}
|
||||
|
||||
return synthese_tags
|
||||
|
||||
|
||||
def get_noms_competences_from_ues(formsemestre: FormSemestre) -> dict[int, str]:
|
||||
"""Partant d'un formsemestre, extrait le nom des compétences associés
|
||||
à (ou aux) parcours des étudiants du formsemestre.
|
||||
|
||||
Ignore les UEs non associées à un niveau de compétence.
|
||||
|
||||
Args:
|
||||
formsemestre: Un FormSemestre
|
||||
|
||||
Returns:
|
||||
Dictionnaire {ue_id: nom_competence} lisant tous les noms des compétences
|
||||
en les raccrochant à leur ue
|
||||
"""
|
||||
# Les résultats du semestre
|
||||
nt = load_formsemestre_results(formsemestre)
|
||||
|
||||
noms_competences = {}
|
||||
for ue in nt.ues:
|
||||
if ue.niveau_competence and ue.type != UE_SPORT:
|
||||
# ?? inutilisé ordre = ue.niveau_competence.ordre
|
||||
nom = ue.niveau_competence.competence.titre
|
||||
noms_competences[ue.ue_id] = f"comp. {nom}"
|
||||
return noms_competences
|
|
@ -1,263 +0,0 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
##############################################################################
|
||||
# Module "Avis de poursuite d'étude"
|
||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||
##############################################################################
|
||||
|
||||
|
||||
"""
|
||||
Created on Thu Sep 8 09:36:33 2016
|
||||
|
||||
@author: barasc
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import numpy as np
|
||||
|
||||
from app import ScoValueError
|
||||
from app.comp.moy_sem import comp_ranks_series
|
||||
from app.pe import pe_affichage
|
||||
from app.pe.pe_affichage import SANS_NOTE
|
||||
from app.scodoc import sco_utils as scu
|
||||
import pandas as pd
|
||||
|
||||
|
||||
TAGS_RESERVES = ["but"]
|
||||
|
||||
|
||||
class MoyenneTag:
|
||||
def __init__(self, tag: str, notes: pd.Series):
|
||||
"""Classe centralisant la synthèse des moyennes/classements d'une série
|
||||
d'étudiants à un tag donné, en stockant un dictionnaire :
|
||||
|
||||
``
|
||||
{
|
||||
"notes": la Serie pandas des notes (float),
|
||||
"classements": la Serie pandas des classements (float),
|
||||
"min": la note minimum,
|
||||
"max": la note maximum,
|
||||
"moy": la moyenne,
|
||||
"nb_inscrits": le nombre d'étudiants ayant une note,
|
||||
}
|
||||
``
|
||||
|
||||
Args:
|
||||
tag: Un tag
|
||||
note: Une série de notes (moyenne) sous forme d'un pd.Series()
|
||||
"""
|
||||
self.tag = tag
|
||||
"""Le tag associé à la moyenne"""
|
||||
self.etudids = list(notes.index) # calcul à venir
|
||||
"""Les id des étudiants"""
|
||||
self.inscrits_ids = notes[notes.notnull()].index.to_list()
|
||||
"""Les id des étudiants dont la moyenne est non nulle"""
|
||||
self.df: pd.DataFrame = self.comp_moy_et_stat(notes)
|
||||
"""Le dataframe retraçant les moyennes/classements/statistiques"""
|
||||
self.synthese = self.to_dict()
|
||||
"""La synthèse (dictionnaire) des notes/classements/statistiques"""
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Egalité de deux MoyenneTag lorsque leur tag sont identiques"""
|
||||
return self.tag == other.tag
|
||||
|
||||
def comp_moy_et_stat(self, notes: pd.Series) -> dict:
|
||||
"""Calcule et structure les données nécessaires au PE pour une série
|
||||
de notes (souvent une moyenne par tag) dans un dictionnaire spécifique.
|
||||
|
||||
Partant des notes, sont calculés les classements (en ne tenant compte
|
||||
que des notes non nulles).
|
||||
|
||||
Args:
|
||||
notes: Une série de notes (avec des éventuels NaN)
|
||||
|
||||
Returns:
|
||||
Un dictionnaire stockant les notes, les classements, le min,
|
||||
le max, la moyenne, le nb de notes (donc d'inscrits)
|
||||
"""
|
||||
df = pd.DataFrame(
|
||||
np.nan,
|
||||
index=self.etudids,
|
||||
columns=[
|
||||
"note",
|
||||
"classement",
|
||||
"rang",
|
||||
"min",
|
||||
"max",
|
||||
"moy",
|
||||
"nb_etuds",
|
||||
"nb_inscrits",
|
||||
],
|
||||
)
|
||||
|
||||
# Supprime d'éventuelles chaines de caractères dans les notes
|
||||
notes = pd.to_numeric(notes, errors="coerce")
|
||||
df["note"] = notes
|
||||
|
||||
# Les nb d'étudiants & nb d'inscrits
|
||||
df["nb_etuds"] = len(self.etudids)
|
||||
df.loc[self.inscrits_ids, "nb_inscrits"] = len(self.inscrits_ids)
|
||||
|
||||
# Le classement des inscrits
|
||||
notes_non_nulles = notes[self.inscrits_ids]
|
||||
(class_str, class_int) = comp_ranks_series(notes_non_nulles)
|
||||
df.loc[self.inscrits_ids, "classement"] = class_int
|
||||
|
||||
# Le rang (classement/nb_inscrit)
|
||||
df["rang"] = df["rang"].astype(str)
|
||||
df.loc[self.inscrits_ids, "rang"] = (
|
||||
df.loc[self.inscrits_ids, "classement"].astype(int).astype(str)
|
||||
+ "/"
|
||||
+ df.loc[self.inscrits_ids, "nb_inscrits"].astype(int).astype(str)
|
||||
)
|
||||
|
||||
# Les stat (des inscrits)
|
||||
df.loc[self.inscrits_ids, "min"] = notes.min()
|
||||
df.loc[self.inscrits_ids, "max"] = notes.max()
|
||||
df.loc[self.inscrits_ids, "moy"] = notes.mean()
|
||||
|
||||
return df
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Renvoie un dictionnaire de synthèse des moyennes/classements/statistiques"""
|
||||
synthese = {
|
||||
"notes": self.df["note"],
|
||||
"classements": self.df["classement"],
|
||||
"min": self.df["min"].mean(),
|
||||
"max": self.df["max"].mean(),
|
||||
"moy": self.df["moy"].mean(),
|
||||
"nb_inscrits": self.df["nb_inscrits"].mean(),
|
||||
}
|
||||
return synthese
|
||||
|
||||
def get_notes(self):
|
||||
"""Série des notes, arrondies à 2 chiffres après la virgule"""
|
||||
return self.df["note"].round(2)
|
||||
|
||||
def get_rangs_inscrits(self) -> pd.Series:
|
||||
"""Série des rangs classement/nbre_inscrit"""
|
||||
return self.df["rang"]
|
||||
|
||||
def get_min(self) -> pd.Series:
|
||||
"""Série des min"""
|
||||
return self.df["min"].round(2)
|
||||
|
||||
def get_max(self) -> pd.Series:
|
||||
"""Série des max"""
|
||||
return self.df["max"].round(2)
|
||||
|
||||
def get_moy(self) -> pd.Series:
|
||||
"""Série des moy"""
|
||||
return self.df["moy"].round(2)
|
||||
|
||||
|
||||
def get_note_for_df(self, etudid: int):
|
||||
"""Note d'un étudiant donné par son etudid"""
|
||||
return round(self.df["note"].loc[etudid], 2)
|
||||
|
||||
def get_min_for_df(self) -> float:
|
||||
"""Min renseigné pour affichage dans un df"""
|
||||
return round(self.synthese["min"], 2)
|
||||
|
||||
def get_max_for_df(self) -> float:
|
||||
"""Max renseigné pour affichage dans un df"""
|
||||
return round(self.synthese["max"], 2)
|
||||
|
||||
def get_moy_for_df(self) -> float:
|
||||
"""Moyenne renseignée pour affichage dans un df"""
|
||||
return round(self.synthese["moy"], 2)
|
||||
|
||||
def get_class_for_df(self, etudid: int) -> str:
|
||||
"""Classement ramené au nombre d'inscrits,
|
||||
pour un étudiant donné par son etudid"""
|
||||
classement = self.df["rang"].loc[etudid]
|
||||
if not pd.isna(classement):
|
||||
return classement
|
||||
else:
|
||||
return pe_affichage.SANS_NOTE
|
||||
|
||||
def is_significatif(self) -> bool:
|
||||
"""Indique si la moyenne est significative (c'est-à-dire à des notes)"""
|
||||
return self.synthese["nb_inscrits"] > 0
|
||||
|
||||
|
||||
class TableTag(object):
|
||||
def __init__(self):
|
||||
"""Classe centralisant différentes méthodes communes aux
|
||||
SemestreTag, TrajectoireTag, AggregatInterclassTag
|
||||
"""
|
||||
pass
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------
|
||||
def get_all_tags(self):
|
||||
"""Liste des tags de la table, triée par ordre alphabétique,
|
||||
extraite des clés du dictionnaire ``moyennes_tags`` connues (tags en doublon
|
||||
possible).
|
||||
|
||||
Returns:
|
||||
Liste de tags triés par ordre alphabétique
|
||||
"""
|
||||
return sorted(list(self.moyennes_tags.keys()))
|
||||
|
||||
def df_moyennes_et_classements(self) -> pd.DataFrame:
|
||||
"""Renvoie un dataframe listant toutes les moyennes,
|
||||
et les classements des étudiants pour tous les tags.
|
||||
|
||||
Est utilisé pour afficher le détail d'un tableau taggué
|
||||
(semestres, trajectoires ou aggrégat)
|
||||
|
||||
Returns:
|
||||
Le dataframe des notes et des classements
|
||||
"""
|
||||
|
||||
etudiants = self.etudiants
|
||||
df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"])
|
||||
|
||||
tags_tries = self.get_all_tags()
|
||||
for tag in tags_tries:
|
||||
moy_tag = self.moyennes_tags[tag]
|
||||
df = df.join(moy_tag.synthese["notes"].rename(f"Moy {tag}"))
|
||||
df = df.join(moy_tag.synthese["classements"].rename(f"Class {tag}"))
|
||||
|
||||
return df
|
||||
|
||||
def df_notes(self) -> pd.DataFrame | None:
|
||||
"""Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags
|
||||
|
||||
Returns:
|
||||
Un dataframe etudids x tag (avec tag par ordre alphabétique)
|
||||
"""
|
||||
tags_tries = self.get_all_tags()
|
||||
if tags_tries:
|
||||
dict_series = {}
|
||||
for tag in tags_tries:
|
||||
# Les moyennes associés au tag
|
||||
moy_tag = self.moyennes_tags[tag]
|
||||
dict_series[tag] = moy_tag.synthese["notes"]
|
||||
df = pd.DataFrame(dict_series)
|
||||
return df
|
|
@ -38,6 +38,7 @@
|
|||
from flask import flash, g, redirect, render_template, request, send_file, url_for
|
||||
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.forms.pe.pe_sem_recap import ParametrageClasseurPE
|
||||
from app.models import FormSemestre
|
||||
from app.pe import pe_comp
|
||||
from app.pe import pe_jury
|
||||
|
@ -73,32 +74,50 @@ def pe_view_sem_recap(formsemestre_id: int):
|
|||
# Cosemestres diplomants
|
||||
cosemestres = pe_comp.get_cosemestres_diplomants(annee_diplome)
|
||||
|
||||
form = ParametrageClasseurPE()
|
||||
|
||||
cosemestres_tries = pe_comp.tri_semestres_par_rang(cosemestres)
|
||||
affichage_cosemestres_tries = {
|
||||
rang: ", ".join([sem.titre_annee() for sem in cosemestres_tries[rang]])
|
||||
for rang in cosemestres_tries
|
||||
}
|
||||
if request.method == "GET":
|
||||
return render_template(
|
||||
"pe/pe_view_sem_recap.j2",
|
||||
annee_diplome=annee_diplome,
|
||||
form=form,
|
||||
formsemestre=formsemestre,
|
||||
sco=ScoData(formsemestre=formsemestre),
|
||||
cosemestres=cosemestres,
|
||||
cosemestres=affichage_cosemestres_tries,
|
||||
rangs_tries=sorted(affichage_cosemestres_tries.keys()),
|
||||
)
|
||||
|
||||
# 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,
|
||||
if form.validate_on_submit():
|
||||
jury = pe_jury.JuryPE(annee_diplome, formsemestre_id, options=form.data)
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
data = jury.get_zipped_data()
|
||||
|
||||
return send_file(
|
||||
data,
|
||||
mimetype="application/zip",
|
||||
download_name=scu.sanitize_filename(jury.nom_export_zip + ".zip"),
|
||||
as_attachment=True,
|
||||
)
|
||||
|
||||
data = jury.get_zipped_data()
|
||||
|
||||
return send_file(
|
||||
data,
|
||||
mimetype="application/zip",
|
||||
download_name=scu.sanitize_filename(jury.nom_export_zip + ".zip"),
|
||||
as_attachment=True,
|
||||
return redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
)
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
##############################################################################
|
||||
# Module "Avis de poursuite d'étude"
|
||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Created on 01-2024
|
||||
|
||||
@author: barasc
|
||||
"""
|
||||
|
||||
from app.models import FormSemestre
|
||||
|
||||
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 S1 au S3 (S1+S2+S3)",
|
||||
},
|
||||
"4S": {
|
||||
"aggregat": ["S1", "S2", "S3", "S4"],
|
||||
"descr": "Moyenne du S1 au S4 (S1+S2+S3+S4)",
|
||||
},
|
||||
"S5": {
|
||||
"aggregat": ["S5"],
|
||||
"descr": "Semestre 5 (S5)",
|
||||
},
|
||||
"S6": {
|
||||
"aggregat": ["S6"],
|
||||
"descr": "Semestre 6 (S6)",
|
||||
},
|
||||
"3A": {
|
||||
"aggregat": ["S5", "S6"],
|
||||
"descr": "BUT3 (S5+S6)",
|
||||
},
|
||||
"5S": {
|
||||
"aggregat": ["S1", "S2", "S3", "S4", "S5"],
|
||||
"descr": "Moyenne du S1 au S5 (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")]
|
||||
|
||||
|
||||
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"]
|
||||
|
||||
|
||||
class RCS:
|
||||
"""Modélise un regroupement cohérent de semestres,
|
||||
tous se terminant par un (form)semestre final.
|
||||
"""
|
||||
|
||||
def __init__(self, nom: str, semestre_final: FormSemestre):
|
||||
self.nom: str = nom
|
||||
"""Nom du RCS"""
|
||||
assert self.nom in TOUS_LES_RCS, "Le nom d'un RCS doit être un aggrégat"
|
||||
|
||||
self.noms_semestres_aggreges: list[str] = TYPES_RCS[nom]["aggregat"]
|
||||
"""Noms des semestres aggrégés)"""
|
||||
|
||||
self.formsemestre_final: FormSemestre = semestre_final
|
||||
"""(Form)Semestre final du RCS"""
|
||||
|
||||
self.rang_final = self.formsemestre_final.semestre_id
|
||||
"""Rang du formsemestre final"""
|
||||
|
||||
self.rcs_id: (str, int) = (nom, semestre_final.formsemestre_id)
|
||||
"""Identifiant du RCS sous forme (nom_rcs, id du semestre_terminal)"""
|
||||
|
||||
self.fid_final: int = self.formsemestre_final.formsemestre_id
|
||||
"""Identifiant du (Form)Semestre final"""
|
||||
|
||||
def get_formsemestre_id_final(self) -> int:
|
||||
"""Renvoie l'identifiant du formsemestre final du RCS
|
||||
|
||||
Returns:
|
||||
L'id du formsemestre final (marquant la fin) du RCS
|
||||
"""
|
||||
return self.formsemestre_final.formsemestre_id
|
||||
|
||||
def __str__(self):
|
||||
"""Représentation textuelle d'un RCS"""
|
||||
return f"{self.nom}[#{self.formsemestre_final.formsemestre_id}✟{self.formsemestre_final.date_fin.year}]"
|
||||
|
||||
def get_repr(self, verbose=True):
|
||||
"""Représentation textuelle d'un RCS"""
|
||||
return self.__str__()
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Egalité de RCS"""
|
||||
return (
|
||||
self.nom == other.nom
|
||||
and self.formsemestre_final == other.formsemestre_final
|
||||
)
|
|
@ -0,0 +1,59 @@
|
|||
##############################################################################
|
||||
# Module "Avis de poursuite d'étude"
|
||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Created on 01-2024
|
||||
|
||||
@author: barasc
|
||||
"""
|
||||
|
||||
from app.models import FormSemestre
|
||||
from app.pe.moys import pe_sxtag
|
||||
from app.pe.rcss import pe_rcs, pe_trajectoires
|
||||
|
||||
|
||||
class RCSemX(pe_rcs.RCS):
|
||||
"""Modélise un regroupement cohérent de SemX (en regroupant
|
||||
des semestres Sx combinés pour former les résultats des étudiants
|
||||
au semestre de rang x) dans le but de synthétiser les résultats
|
||||
du S1 jusqu'au semestre final ciblé par le RCSemX (dépendant de l'aggrégat
|
||||
visé).
|
||||
|
||||
Par ex: Si l'aggrégat du RCSemX est '3S' (=S1+S2+S3),
|
||||
regroupement le SemX du S1 + le SemX du S2 + le SemX du S3 (chacun
|
||||
incluant des infos sur les redoublements).
|
||||
|
||||
Args:
|
||||
nom: Un nom du RCS (par ex: '5S')
|
||||
semestre_final: Le semestre final du RCS
|
||||
"""
|
||||
|
||||
def __init__(self, nom: str, semestre_final: FormSemestre):
|
||||
pe_rcs.RCS.__init__(self, nom, semestre_final)
|
||||
|
||||
self.semXs_aggreges: dict[(str, int) : pe_sxtag.SxTag] = {}
|
||||
"""Les semX à aggréger"""
|
||||
|
||||
def add_semXs(self, semXs: dict[(str, int) : pe_trajectoires.SemX]):
|
||||
"""Ajoute des semXs aux semXs à regrouper dans le RCSemX
|
||||
|
||||
Args:
|
||||
semXs: Dictionnaire ``{(str,fid): RCF}`` à ajouter
|
||||
"""
|
||||
self.semXs_aggreges = self.semXs_aggreges | semXs
|
||||
|
||||
def get_repr(self, verbose=True) -> str:
|
||||
"""Représentation textuelle d'un RCSF
|
||||
basé sur ses RCF aggrégés"""
|
||||
title = f"""{self.__class__.__name__} {pe_rcs.RCS.__str__(self)}"""
|
||||
if verbose:
|
||||
noms = []
|
||||
for semx_id, semx in self.semXs_aggreges.items():
|
||||
noms.append(semx.get_repr(verbose=False))
|
||||
if noms:
|
||||
title += " <<" + "+".join(noms) + ">>"
|
||||
else:
|
||||
title += " <<vide>>"
|
||||
return title
|
|
@ -0,0 +1,87 @@
|
|||
from app.models import FormSemestre
|
||||
import app.pe.rcss.pe_rcs as pe_rcs
|
||||
|
||||
|
||||
class Trajectoire(pe_rcs.RCS):
|
||||
"""Regroupement Cohérent de Semestres ciblant un type d'aggrégat (par ex.
|
||||
'S2', '3S', '1A') et un semestre final, et dont les données regroupées
|
||||
sont des **FormSemestres** suivis par les étudiants.
|
||||
|
||||
Une *Trajectoire* traduit la succession de semestres
|
||||
qu'ont pu suivre des étudiants pour aller d'un semestre S1 jusqu'au semestre final
|
||||
de l'aggrégat.
|
||||
|
||||
Une *Trajectoire* peut être :
|
||||
|
||||
* un RCS de semestre de type "Sx" (cf. classe "SemX"), qui stocke les
|
||||
formsemestres de rang x qu'ont suivi l'étudiant pour valider le Sx
|
||||
(en général 1 formsemestre pour les non-redoublants et 2 pour les redoublants)
|
||||
|
||||
* un RCS de type iS ou iA (par ex, 3A=S1+S2+S3), qui identifie
|
||||
les formsemestres que des étudiants ont suivis pour les amener jusqu'au semestre
|
||||
terminal du RCS. Par ex: si le RCS est un 3S:
|
||||
|
||||
* 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: Un nom du RCS (par ex: '5S')
|
||||
semestre_final: Le formsemestre final du RCS
|
||||
"""
|
||||
|
||||
def __init__(self, nom: str, semestre_final: FormSemestre):
|
||||
pe_rcs.RCS.__init__(self, nom, semestre_final)
|
||||
|
||||
self.semestres_aggreges: dict[int:FormSemestre] = {}
|
||||
"""Formsemestres regroupés dans le RCS"""
|
||||
|
||||
def add_semestres(self, semestres: dict[int:FormSemestre]):
|
||||
"""Ajout de semestres aux semestres à regrouper
|
||||
|
||||
Args:
|
||||
semestres: Dictionnaire ``{fid: Formsemestre)``
|
||||
"""
|
||||
for sem in semestres.values():
|
||||
assert isinstance(
|
||||
sem, FormSemestre
|
||||
), "Les données aggrégées d'une Trajectoire doivent être des FormSemestres"
|
||||
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"""
|
||||
title = f"""{self.__class__.__name__} {pe_rcs.RCS.__str__(self)}"""
|
||||
if verbose:
|
||||
noms = []
|
||||
for fid in self.semestres_aggreges:
|
||||
semestre = self.semestres_aggreges[fid]
|
||||
noms.append(f"S{semestre.semestre_id}#{fid}")
|
||||
noms = sorted(noms)
|
||||
if noms:
|
||||
title += " <" + "+".join(noms) + ">"
|
||||
else:
|
||||
title += " <vide>"
|
||||
return title
|
||||
|
||||
|
||||
class SemX(Trajectoire):
|
||||
"""Trajectoire (regroupement cohérent de (form)semestres
|
||||
dans laquelle tous les semestres regroupés sont de même rang `x`.
|
||||
|
||||
Les SemX stocke les
|
||||
formsemestres de rang x qu'ont suivi l'étudiant pour valider le Sx
|
||||
(en général 1 formsemestre pour les non-redoublants et 2 pour les redoublants).
|
||||
|
||||
Ils servent à calculer les SemXTag (moyennes par tag des RCS de type `Sx`).
|
||||
"""
|
||||
|
||||
def __init__(self, trajectoire: Trajectoire):
|
||||
Trajectoire.__init__(self, trajectoire.nom, trajectoire.formsemestre_final)
|
||||
|
||||
semestres_aggreges = trajectoire.semestres_aggreges
|
||||
for sem in semestres_aggreges.values():
|
||||
assert (
|
||||
sem.semestre_id == trajectoire.rang_final
|
||||
), "Tous les semestres aggrégés d'un SemX doivent être de même rang"
|
||||
|
||||
self.semestres_aggreges = trajectoire.semestres_aggreges
|
|
@ -85,17 +85,6 @@ UE_ELECTIVE = 4 # UE "élective" dans certains cursus (UCAC?, ISCID)
|
|||
UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...)
|
||||
UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...)
|
||||
|
||||
|
||||
def ue_is_fondamentale(ue_type):
|
||||
return ue_type in (UE_STANDARD, UE_STAGE_LP, UE_PROFESSIONNELLE)
|
||||
|
||||
|
||||
def ue_is_professionnelle(ue_type):
|
||||
return (
|
||||
ue_type == UE_PROFESSIONNELLE
|
||||
) # NB: les UE_PROFESSIONNELLE sont à la fois fondamentales et pro
|
||||
|
||||
|
||||
UE_TYPE_NAME = {
|
||||
UE_STANDARD: "Standard",
|
||||
UE_SPORT: "Sport/Culture (points bonus)",
|
||||
|
@ -104,8 +93,6 @@ UE_TYPE_NAME = {
|
|||
UE_ELECTIVE: "Elective (ISCID)",
|
||||
UE_PROFESSIONNELLE: "Professionnelle (ISCID)",
|
||||
UE_OPTIONNELLE: "Optionnelle",
|
||||
# UE_FONDAMENTALE : '"Fondamentale" (eg UCAC)',
|
||||
# UE_OPTIONNELLE : '"Optionnelle" (UCAC)'
|
||||
}
|
||||
|
||||
# Couleurs RGB (dans [0.,1.]) des UE pour les bulletins:
|
||||
|
@ -409,6 +396,7 @@ class CursusBUT(TypeCursus):
|
|||
APC_SAE = True
|
||||
USE_REFERENTIEL_COMPETENCES = True
|
||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
|
||||
ECTS_DIPLOME = 180
|
||||
|
||||
|
||||
register_cursus(CursusBUT())
|
||||
|
|
|
@ -44,13 +44,15 @@ import random
|
|||
from collections import OrderedDict
|
||||
from xml.etree import ElementTree
|
||||
import json
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse, urlencode, parse_qs, urlunparse
|
||||
|
||||
from openpyxl.utils import get_column_letter
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
|
||||
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
|
||||
from reportlab.platypus import Paragraph, Spacer
|
||||
from reportlab.platypus import Table, KeepInFrame
|
||||
from reportlab.lib.colors import Color
|
||||
from reportlab.lib import styles
|
||||
from reportlab.lib.units import inch, cm, mm
|
||||
from reportlab.rl_config import defaultPageSize # pylint: disable=no-name-in-module
|
||||
from reportlab.lib.units import cm
|
||||
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
@ -62,16 +64,32 @@ from app.scodoc.sco_pdf import SU
|
|||
from app import log, ScoDocJSONEncoder
|
||||
|
||||
|
||||
def mark_paras(L, tags) -> list[str]:
|
||||
"""Put each (string) element of L between <tag>...</tag>,
|
||||
def mark_paras(items: list[Any], tags: list[str]) -> list[str]:
|
||||
"""Put each string element of items between <tag>...</tag>,
|
||||
for each supplied tag.
|
||||
Leave non string elements untouched.
|
||||
"""
|
||||
for tag in tags:
|
||||
start = "<" + tag + ">"
|
||||
end = "</" + tag.split()[0] + ">"
|
||||
L = [(start + (x or "") + end) if isinstance(x, str) else x for x in L]
|
||||
return L
|
||||
items = [(start + (x or "") + end) if isinstance(x, str) else x for x in items]
|
||||
return items
|
||||
|
||||
|
||||
def add_query_param(url: str, key: str, value: str) -> str:
|
||||
"add parameter key=value to the given URL"
|
||||
# Parse the URL
|
||||
parsed_url = urlparse(url)
|
||||
# Parse the query parameters
|
||||
query_params = parse_qs(parsed_url.query)
|
||||
# Add or update the query parameter
|
||||
query_params[key] = [value]
|
||||
# Encode the query parameters
|
||||
encoded_query_params = urlencode(query_params, doseq=True)
|
||||
# Construct the new URL
|
||||
new_url_parts = parsed_url._replace(query=encoded_query_params)
|
||||
new_url = urlunparse(new_url_parts)
|
||||
return new_url
|
||||
|
||||
|
||||
class DEFAULT_TABLE_PREFERENCES(object):
|
||||
|
@ -477,13 +495,15 @@ class GenTable:
|
|||
H.append('<span class="gt_export_icons">')
|
||||
if self.xls_link:
|
||||
H.append(
|
||||
' <a href="%s&fmt=xls">%s</a>' % (self.base_url, scu.ICON_XLS)
|
||||
f""" <a href="{add_query_param(self.base_url, "fmt", "xls")
|
||||
}">{scu.ICON_XLS}</a>"""
|
||||
)
|
||||
if self.xls_link and self.pdf_link:
|
||||
H.append(" ")
|
||||
if self.pdf_link:
|
||||
H.append(
|
||||
' <a href="%s&fmt=pdf">%s</a>' % (self.base_url, scu.ICON_PDF)
|
||||
f""" <a href="{add_query_param(self.base_url, "fmt", "pdf")
|
||||
}">{scu.ICON_PDF}</a>"""
|
||||
)
|
||||
H.append("</span>")
|
||||
H.append("</p>")
|
||||
|
@ -582,9 +602,11 @@ class GenTable:
|
|||
for line in data_list:
|
||||
Pt.append(
|
||||
[
|
||||
Paragraph(SU(str(x)), CellStyle)
|
||||
if (not isinstance(x, Paragraph))
|
||||
else x
|
||||
(
|
||||
Paragraph(SU(str(x)), CellStyle)
|
||||
if (not isinstance(x, Paragraph))
|
||||
else x
|
||||
)
|
||||
for x in line
|
||||
]
|
||||
)
|
||||
|
|
|
@ -109,7 +109,7 @@ def sidebar_common():
|
|||
{sidebar_dept()}
|
||||
<h2 class="insidebar">Scolarité</h2>
|
||||
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br>
|
||||
<a href="{scu.NotesURL()}" class="sidebar">Programmes</a> <br>
|
||||
<a href="{scu.NotesURL()}" class="sidebar">Formations</a> <br>
|
||||
"""
|
||||
]
|
||||
if current_user.has_permission(Permission.AbsChange):
|
||||
|
@ -175,10 +175,9 @@ def sidebar(etudid: int = None):
|
|||
inscription = etud.inscription_courante()
|
||||
if inscription:
|
||||
formsemestre = inscription.formsemestre
|
||||
nbabs, nbabsjust = sco_assiduites.formsemestre_get_assiduites_count(
|
||||
nbabsnj, nbabsjust, _ = sco_assiduites.formsemestre_get_assiduites_count(
|
||||
etudid, formsemestre
|
||||
)
|
||||
nbabsnj = nbabs - nbabsjust
|
||||
H.append(
|
||||
f"""<span title="absences du {
|
||||
formsemestre.date_debut.strftime("%d/%m/%Y")
|
||||
|
@ -186,7 +185,7 @@ def sidebar(etudid: int = None):
|
|||
formsemestre.date_fin.strftime("%d/%m/%Y")
|
||||
}">({
|
||||
sco_preferences.get_preference("assi_metrique", None)})
|
||||
<br>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
|
||||
<br>{nbabsjust:1g} J., {nbabsnj:1g} N.J.</span>"""
|
||||
)
|
||||
H.append("<ul>")
|
||||
if current_user.has_permission(Permission.AbsChange):
|
||||
|
|
|
@ -124,9 +124,9 @@ def table_billets(
|
|||
else:
|
||||
billet_dict["nomprenom"] = billet.etudiant.nomprenom
|
||||
billet_dict["_nomprenom_order"] = billet.etudiant.sort_key
|
||||
billet_dict[
|
||||
"_nomprenom_td_attrs"
|
||||
] = f'id="{billet.etudiant.id}" class="etudinfo"'
|
||||
billet_dict["_nomprenom_td_attrs"] = (
|
||||
f'id="{billet.etudiant.id}" class="etudinfo"'
|
||||
)
|
||||
if with_links:
|
||||
billet_dict["_nomprenom_target"] = url_for(
|
||||
"scolar.fiche_etud",
|
||||
|
|
|
@ -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 g, url_for
|
||||
from flask import flash, g, url_for
|
||||
from flask_mail import Message
|
||||
|
||||
from app import db
|
||||
|
@ -46,7 +46,6 @@ from app.models.etudiants import Identite
|
|||
from app.models.events import Scolog
|
||||
from app.models.formsemestre import FormSemestre
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
@ -68,7 +67,7 @@ def abs_notify(etudid: int, date: str | datetime.datetime):
|
|||
if not formsemestre:
|
||||
return # non inscrit a la date, pas de notification
|
||||
|
||||
nbabs, nbabsjust = sco_assiduites.get_assiduites_count_in_interval(
|
||||
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count_in_interval(
|
||||
etudid,
|
||||
metrique=scu.translate_assiduites_metric(
|
||||
sco_preferences.get_preference(
|
||||
|
@ -283,10 +282,17 @@ def abs_notification_message(
|
|||
)
|
||||
|
||||
template = prefs["abs_notification_mail_tmpl"]
|
||||
txt = ""
|
||||
if template:
|
||||
txt = prefs["abs_notification_mail_tmpl"] % values
|
||||
try:
|
||||
txt = prefs["abs_notification_mail_tmpl"] % values
|
||||
except KeyError:
|
||||
flash("Mail non envoyé: format invalide (voir paramétrage)")
|
||||
log("abs_notification_message: invalid key in abs_notification_mail_tmpl")
|
||||
txt = ""
|
||||
else:
|
||||
log("abs_notification_message: empty template, not sending message")
|
||||
if not txt:
|
||||
return None
|
||||
|
||||
subject = f"""[ScoDoc] Trop d'absences pour {etud.nomprenom}"""
|
||||
|
|
|
@ -20,8 +20,11 @@ class Trace:
|
|||
|
||||
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)
|
||||
- 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
|
||||
|
|
|
@ -17,7 +17,7 @@ from app.models import (
|
|||
ModuleImplInscription,
|
||||
ScoDocSiteConfig,
|
||||
)
|
||||
from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_justified
|
||||
from app.models.assiduites import Assiduite, Justificatif
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_cache
|
||||
|
@ -37,21 +37,34 @@ class CountCalculator:
|
|||
------------
|
||||
1. Initialisation : La classe peut être initialisée avec des horaires personnalisés
|
||||
pour le matin, le midi et le soir, ainsi qu'une durée de pause déjeuner.
|
||||
Si non spécifiés, les valeurs par défaut seront chargées depuis la configuration `ScoDocSiteConfig`.
|
||||
Si non spécifiés, les valeurs par défaut seront
|
||||
chargées depuis la configuration `ScoDocSiteConfig`.
|
||||
Exemple d'initialisation :
|
||||
calculator = CountCalculator(morning="08:00", noon="13:00", evening="18:00", nb_heures_par_jour=8)
|
||||
calculator = CountCalculator(
|
||||
morning="08:00",
|
||||
noon="13:00",
|
||||
evening="18:00",
|
||||
nb_heures_par_jour=8
|
||||
)
|
||||
|
||||
2. Ajout d'assiduités :
|
||||
Exemple d'ajout d'assiduité :
|
||||
- calculator.compute_assiduites(etudiant.assiduites)
|
||||
- calculator.compute_assiduites([<Assiduite>, <Assiduite>, <Assiduite>, <Assiduite>])
|
||||
- calculator.compute_assiduites([
|
||||
<Assiduite>,
|
||||
<Assiduite>,
|
||||
<Assiduite>,
|
||||
<Assiduite>
|
||||
])
|
||||
|
||||
3. Accès aux métriques : Après l'ajout des assiduités, on peut accéder aux métriques telles que :
|
||||
3. Accès aux métriques : Après l'ajout des assiduités,
|
||||
on peut accéder aux métriques telles que :
|
||||
le nombre total de jours, de demi-journées et d'heures calculées.
|
||||
Exemple d'accès aux métriques :
|
||||
metrics = calculator.to_dict()
|
||||
|
||||
4.Réinitialisation du comptage: Si besoin on peut réinitialisé le compteur sans perdre la configuration
|
||||
4.Réinitialisation du comptage: Si besoin on peut réinitialiser
|
||||
le compteur sans perdre la configuration
|
||||
(horaires personnalisés)
|
||||
Exemple de réinitialisation :
|
||||
calculator.reset()
|
||||
|
@ -61,8 +74,10 @@ class CountCalculator:
|
|||
- reset() : Réinitialise les compteurs de la classe.
|
||||
- add_half_day(day: date, is_morning: bool) : Ajoute une demi-journée au comptage.
|
||||
- add_day(day: date) : Ajoute un jour complet au comptage.
|
||||
- compute_long_assiduite(assi: Assiduite) : Traite les assiduités s'étendant sur plus d'un jour.
|
||||
- compute_assiduites(assiduites: Query | list) : Calcule les métriques pour une collection d'assiduités.
|
||||
- compute_long_assiduite(assi: Assiduite) : Traite les assiduités
|
||||
s'étendant sur plus d'un jour.
|
||||
- compute_assiduites(assiduites: Query | list) : Calcule les métriques pour
|
||||
une collection d'assiduités.
|
||||
- to_dict() : Retourne les métriques sous forme de dictionnaire.
|
||||
|
||||
Notes :
|
||||
|
@ -85,17 +100,14 @@ class CountCalculator:
|
|||
evening: str = None,
|
||||
nb_heures_par_jour: int = None,
|
||||
) -> None:
|
||||
# Transformation d'une heure "HH:MM" en time(h,m)
|
||||
STR_TIME = lambda x: time(*list(map(int, x.split(":"))))
|
||||
|
||||
self.morning: time = STR_TIME(
|
||||
self.morning: time = str_to_time(
|
||||
morning if morning else ScoDocSiteConfig.get("assi_morning_time", "08:00")
|
||||
)
|
||||
# Date pivot pour déterminer les demi-journées
|
||||
self.noon: time = STR_TIME(
|
||||
self.noon: time = str_to_time(
|
||||
noon if noon else ScoDocSiteConfig.get("assi_lunch_time", "13:00")
|
||||
)
|
||||
self.evening: time = STR_TIME(
|
||||
self.evening: time = str_to_time(
|
||||
evening if evening else ScoDocSiteConfig.get("assi_afternoon_time", "18:00")
|
||||
)
|
||||
|
||||
|
@ -103,10 +115,6 @@ class CountCalculator:
|
|||
scu.NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
|
||||
delta_total: timedelta = datetime.combine(
|
||||
date.min, self.evening
|
||||
) - datetime.combine(date.min, self.morning)
|
||||
|
||||
# Sera utilisé pour les assiduités longues (> 1 journée)
|
||||
self.nb_heures_par_jour = (
|
||||
nb_heures_par_jour
|
||||
|
@ -340,17 +348,27 @@ class CountCalculator:
|
|||
|
||||
def setup_data(self):
|
||||
"""Met en forme les données
|
||||
pour les journées et les demi-journées : au lieu d'avoir list[str] on a le nombre (len(list[str]))
|
||||
pour les journées et les demi-journées :
|
||||
au lieu d'avoir list[str] on a le nombre (len(list[str]))
|
||||
"""
|
||||
for key in self.data:
|
||||
self.data[key]["journee"] = len(self.data[key]["journee"])
|
||||
self.data[key]["demi"] = len(self.data[key]["demi"])
|
||||
for value in self.data.values():
|
||||
value["journee"] = len(value["journee"])
|
||||
value["demi"] = len(value["demi"])
|
||||
|
||||
def to_dict(self, only_total: bool = True) -> dict[str, int | float]:
|
||||
"""Retourne les métriques sous la forme d'un dictionnaire"""
|
||||
return self.data["total"] if only_total else self.data
|
||||
|
||||
|
||||
def str_to_time(time_str: str) -> time:
|
||||
"""Convertit une chaîne de caractères représentant une heure en objet time
|
||||
exemples :
|
||||
- "08:00" -> time(8, 0)
|
||||
- "18:00:00" -> time(18, 0, 0)
|
||||
"""
|
||||
return time(*list(map(int, time_str.split(":"))))
|
||||
|
||||
|
||||
def get_assiduites_stats(
|
||||
assiduites: Query, metric: str = "all", filtered: dict[str, object] = None
|
||||
) -> dict[str, int | float]:
|
||||
|
@ -643,7 +661,7 @@ def create_absence_billet(
|
|||
db.session.add(justi)
|
||||
db.session.commit()
|
||||
|
||||
compute_assiduites_justified(etud.id, [justi])
|
||||
justi.justifier_assiduites()
|
||||
|
||||
calculator: CountCalculator = CountCalculator()
|
||||
calculator.compute_assiduites([assiduite_unique])
|
||||
|
@ -651,9 +669,9 @@ def create_absence_billet(
|
|||
|
||||
|
||||
# Gestion du cache
|
||||
def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]:
|
||||
def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int, int]:
|
||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||
tuple (nb abs non justifiées, nb abs justifiées)
|
||||
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
|
||||
Utilise un cache.
|
||||
"""
|
||||
metrique = sco_preferences.get_preference("assi_metrique", sem["formsemestre_id"])
|
||||
|
@ -667,19 +685,19 @@ def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]:
|
|||
|
||||
def formsemestre_get_assiduites_count(
|
||||
etudid: int, formsemestre: FormSemestre, moduleimpl_id: int = None
|
||||
) -> tuple[int, int]:
|
||||
) -> tuple[int, int, int]:
|
||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||
tuple (nb abs non justifiées, nb abs justifiées)
|
||||
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
|
||||
Utilise un cache.
|
||||
"""
|
||||
metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id)
|
||||
return get_assiduites_count_in_interval(
|
||||
etudid,
|
||||
date_debut=scu.localize_datetime(
|
||||
datetime.combine(formsemestre.date_debut, time(8, 0))
|
||||
datetime.combine(formsemestre.date_debut, time(0, 0))
|
||||
),
|
||||
date_fin=scu.localize_datetime(
|
||||
datetime.combine(formsemestre.date_fin, time(18, 0))
|
||||
datetime.combine(formsemestre.date_fin, time(23, 0))
|
||||
),
|
||||
metrique=scu.translate_assiduites_metric(metrique),
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
|
@ -694,14 +712,14 @@ def get_assiduites_count_in_interval(
|
|||
date_debut: datetime = None,
|
||||
date_fin: datetime = None,
|
||||
moduleimpl_id: int = None,
|
||||
):
|
||||
) -> tuple[int, int, int]:
|
||||
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
||||
tuple (nb abs, nb abs justifiées)
|
||||
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
|
||||
On peut spécifier les dates comme datetime ou iso.
|
||||
Utilise un cache.
|
||||
"""
|
||||
date_debut_iso = date_debut_iso or date_debut.isoformat()
|
||||
date_fin_iso = date_fin_iso or date_fin.isoformat()
|
||||
date_debut_iso = date_debut_iso or date_debut.strftime("%Y-%m-%d")
|
||||
date_fin_iso = date_fin_iso or date_fin.strftime("%Y-%m-%d")
|
||||
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites"
|
||||
|
||||
r = sco_cache.AbsSemEtudCache.get(key)
|
||||
|
@ -726,9 +744,10 @@ def get_assiduites_count_in_interval(
|
|||
if not ans:
|
||||
log("warning: get_assiduites_count failed to cache")
|
||||
|
||||
nb_abs: dict = r["absent"][metrique]
|
||||
nb_abs_just: dict = r["absent_just"][metrique]
|
||||
return (nb_abs, nb_abs_just)
|
||||
nb_abs: int = r["absent"][metrique]
|
||||
nb_abs_nj: int = r["absent_non_just"][metrique]
|
||||
nb_abs_just: int = r["absent_just"][metrique]
|
||||
return (nb_abs_nj, nb_abs_just, nb_abs)
|
||||
|
||||
|
||||
def invalidate_assiduites_count(etudid: int, sem: dict):
|
||||
|
@ -756,7 +775,6 @@ def invalidate_assiduites_etud_date(etudid: int, the_date: datetime):
|
|||
pour cet étudiant et cette date.
|
||||
Invalide cache absence et caches semestre
|
||||
"""
|
||||
from app.scodoc import sco_compute_moy
|
||||
|
||||
# Semestres a cette date:
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
||||
|
@ -776,17 +794,9 @@ def invalidate_assiduites_etud_date(etudid: int, the_date: datetime):
|
|||
# Invalide les PDF et les absences:
|
||||
for sem in sems:
|
||||
# Inval cache bulletin et/ou note_table
|
||||
if sco_compute_moy.formsemestre_expressions_use_abscounts(
|
||||
sem["formsemestre_id"]
|
||||
):
|
||||
# certaines formules utilisent les absences
|
||||
pdfonly = False
|
||||
else:
|
||||
# efface toujours le PDF car il affiche en général les absences
|
||||
pdfonly = True
|
||||
|
||||
# efface toujours le PDF car il affiche en général les absences
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=sem["formsemestre_id"], pdfonly=pdfonly
|
||||
formsemestre_id=sem["formsemestre_id"], pdfonly=True
|
||||
)
|
||||
|
||||
# Inval cache compteurs absences:
|
||||
|
@ -818,4 +828,4 @@ def simple_invalidate_cache(obj: dict, etudid: str | int = None):
|
|||
pattern=f"tableau-etud-{etudid}*"
|
||||
)
|
||||
# Invalide les tableaux "bilan dept"
|
||||
sco_cache.RequeteTableauAssiduiteCache.delete_pattern(pattern=f"tableau-dept*")
|
||||
sco_cache.RequeteTableauAssiduiteCache.delete_pattern(pattern="tableau-dept*")
|
||||
|
|
|
@ -55,7 +55,7 @@ from app.models import (
|
|||
ScoDocSiteConfig,
|
||||
)
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError, ScoTemporaryError
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import sco_assiduites
|
||||
|
@ -196,7 +196,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||
pid = partition["partition_id"]
|
||||
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
|
||||
# --- Absences
|
||||
I["nbabs"], I["nbabsjust"] = sco_assiduites.get_assiduites_count(etudid, nt.sem)
|
||||
_, I["nbabsjust"], I["nbabs"] = sco_assiduites.get_assiduites_count(etudid, nt.sem)
|
||||
|
||||
# --- Decision Jury
|
||||
infos, dpv = etud_descr_situation_semestre(
|
||||
|
@ -318,7 +318,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||
if nt.bonus_ues is not None:
|
||||
u["cur_moy_ue_txt"] += " (+ues)"
|
||||
u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"])
|
||||
if ue_status["coef_ue"] != None:
|
||||
if ue_status["coef_ue"] is not None:
|
||||
u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"])
|
||||
else:
|
||||
u["coef_ue_txt"] = "-"
|
||||
|
@ -346,14 +346,14 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||
# auparavant on filtrait les modules sans notes
|
||||
# si ue_status['cur_moy_ue'] != 'NA' alors u['modules'] = [] (pas de moyenne => pas de modules)
|
||||
|
||||
u[
|
||||
"modules_capitalized"
|
||||
] = [] # modules de l'UE capitalisée (liste vide si pas capitalisée)
|
||||
u["modules_capitalized"] = (
|
||||
[]
|
||||
) # modules de l'UE capitalisée (liste vide si pas capitalisée)
|
||||
if ue_status["is_capitalized"] and ue_status["formsemestre_id"] is not None:
|
||||
sem_origin = db.session.get(FormSemestre, ue_status["formsemestre_id"])
|
||||
u[
|
||||
"ue_descr_txt"
|
||||
] = f'capitalisée le {ndb.DateISOtoDMY(ue_status["event_date"])}'
|
||||
u["ue_descr_txt"] = (
|
||||
f'capitalisée le {ndb.DateISOtoDMY(ue_status["event_date"])}'
|
||||
)
|
||||
u["ue_descr_html"] = (
|
||||
f"""<a href="{ url_for( 'notes.formsemestre_bulletinetud',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=sem_origin.id, etudid=etudid)}"
|
||||
|
@ -471,7 +471,7 @@ def _ue_mod_bulletin(
|
|||
) # peut etre 'NI'
|
||||
is_malus = mod["module"]["module_type"] == ModuleType.MALUS
|
||||
if bul_show_abs_modules:
|
||||
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||
mod_abs = [nbabs, nbabsjust]
|
||||
mod["mod_abs_txt"] = scu.fmt_abs(mod_abs)
|
||||
else:
|
||||
|
@ -558,6 +558,8 @@ def _ue_mod_bulletin(
|
|||
).order_by(Evaluation.numero, Evaluation.date_debut)
|
||||
# (plus ancienne d'abord)
|
||||
for e in all_evals:
|
||||
if e.is_blocked():
|
||||
continue # ignore évaluations bloquées
|
||||
if not e.visibulletin and version != "long":
|
||||
continue
|
||||
is_complete = e.id in complete_eval_ids
|
||||
|
@ -610,19 +612,22 @@ def _ue_mod_bulletin(
|
|||
e_dict["coef_txt"] = ""
|
||||
else:
|
||||
e_dict["coef_txt"] = scu.fmt_coef(e.coefficient)
|
||||
if e.evaluation_type == scu.EVALUATION_RATTRAPAGE:
|
||||
if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE:
|
||||
e_dict["coef_txt"] = "rat."
|
||||
elif e.evaluation_type == scu.EVALUATION_SESSION2:
|
||||
elif e.evaluation_type == Evaluation.EVALUATION_SESSION2:
|
||||
e_dict["coef_txt"] = "Ses. 2"
|
||||
|
||||
if modimpl_results.evaluations_etat[e.id].nb_attente:
|
||||
mod_attente = True # une eval en attente dans ce module
|
||||
|
||||
if ((not is_malus) or (val != "NP")) and (
|
||||
(e.evaluation_type == scu.EVALUATION_NORMALE or not np.isnan(val))
|
||||
(
|
||||
e.evaluation_type == Evaluation.EVALUATION_NORMALE
|
||||
or not np.isnan(val)
|
||||
)
|
||||
):
|
||||
# ne liste pas les eval malus sans notes
|
||||
# ni les rattrapages et sessions 2 si pas de note
|
||||
# ni les rattrapages, sessions 2 et bonus si pas de note
|
||||
if e.id in complete_eval_ids:
|
||||
mod["evaluations"].append(e_dict)
|
||||
else:
|
||||
|
@ -731,7 +736,11 @@ def etud_descr_situation_semestre(
|
|||
infos["refcomp_specialite_long"] = ""
|
||||
if formsemestre.formation.is_apc():
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||
parcour_id = res.etuds_parcour_id[etudid]
|
||||
try:
|
||||
parcour_id = res.etuds_parcour_id[etudid]
|
||||
except KeyError as exc:
|
||||
log("sco_bulletins: ScoTemporaryError 240222")
|
||||
raise ScoTemporaryError() from exc
|
||||
parcour: ApcParcours = (
|
||||
db.session.get(ApcParcours, parcour_id) if parcour_id is not None else None
|
||||
)
|
||||
|
|
|
@ -50,14 +50,11 @@ import traceback
|
|||
|
||||
import reportlab
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate,
|
||||
DocIf,
|
||||
Paragraph,
|
||||
Spacer,
|
||||
Frame,
|
||||
PageBreak,
|
||||
)
|
||||
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
|
||||
from reportlab.platypus import Table, KeepInFrame
|
||||
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
|
@ -213,26 +210,26 @@ class BulletinGenerator:
|
|||
story.append(PageBreak()) # insert page break at end
|
||||
|
||||
return story
|
||||
else:
|
||||
# Generation du document PDF
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
report = io.BytesIO() # in-memory document, no disk file
|
||||
document = sco_pdf.BaseDocTemplate(report)
|
||||
document.addPageTemplates(
|
||||
sco_pdf.ScoDocPageTemplate(
|
||||
document,
|
||||
author="%s %s (E. Viennet) [%s]"
|
||||
% (sco_version.SCONAME, sco_version.SCOVERSION, self.description),
|
||||
title=f"""Bulletin {sem["titremois"]} de {etat_civil}""",
|
||||
subject="Bulletin de note",
|
||||
margins=self.margins,
|
||||
server_name=self.server_name,
|
||||
filigranne=self.filigranne,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
)
|
||||
|
||||
# Generation du document PDF
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
report = io.BytesIO() # in-memory document, no disk file
|
||||
document = sco_pdf.BaseDocTemplate(report)
|
||||
document.addPageTemplates(
|
||||
sco_pdf.ScoDocPageTemplate(
|
||||
document,
|
||||
author=f"""{sco_version.SCONAME} {
|
||||
sco_version.SCOVERSION} (E. Viennet) [{self.description}]""",
|
||||
title=f"""Bulletin {sem["titremois"]} de {etat_civil}""",
|
||||
subject="Bulletin de note",
|
||||
margins=self.margins,
|
||||
server_name=self.server_name,
|
||||
filigranne=self.filigranne,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
)
|
||||
document.build(story)
|
||||
data = report.getvalue()
|
||||
)
|
||||
document.build(story)
|
||||
data = report.getvalue()
|
||||
return data
|
||||
|
||||
def buildTableObject(self, P, pdfTableStyle, colWidths):
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
"""Génération du bulletin en format JSON
|
||||
"""Génération du bulletin en format JSON (formations classiques)
|
||||
|
||||
"""
|
||||
import datetime
|
||||
|
@ -296,7 +296,7 @@ def formsemestre_bulletinetud_published_dict(
|
|||
|
||||
# --- Absences
|
||||
if prefs["bul_show_abs"]:
|
||||
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
|
||||
|
||||
# --- Décision Jury
|
||||
|
|
|
@ -62,10 +62,12 @@ 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
|
||||
from app.scodoc import sco_pdf
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import (
|
||||
codes_cursus,
|
||||
sco_cache,
|
||||
sco_pdf,
|
||||
sco_preferences,
|
||||
)
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
@ -111,7 +113,8 @@ def assemble_bulletins_pdf(
|
|||
return data
|
||||
|
||||
|
||||
def replacement_function(match):
|
||||
def replacement_function(match) -> str:
|
||||
"remplace logo par balise html img"
|
||||
balise = match.group(1)
|
||||
name = match.group(3)
|
||||
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
|
||||
|
@ -211,7 +214,11 @@ def process_field(
|
|||
)
|
||||
|
||||
|
||||
def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
|
||||
def get_formsemestre_bulletins_pdf(
|
||||
formsemestre_id,
|
||||
version="selectedevals",
|
||||
groups_infos=None, # si indiqué, ne prend que ces groupes
|
||||
):
|
||||
"Document pdf avec tous les bulletins du semestre, et filename"
|
||||
from app.but import bulletin_but_court
|
||||
from app.scodoc import sco_bulletins
|
||||
|
@ -226,13 +233,22 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
|
|||
raise ScoValueError(
|
||||
"get_formsemestre_bulletins_pdf: version de bulletin demandée invalide !"
|
||||
)
|
||||
cached = sco_cache.SemBulletinsPDFCache.get(str(formsemestre_id) + "_" + version)
|
||||
|
||||
etuds = formsemestre.get_inscrits(include_demdef=True, order=True)
|
||||
if groups_infos is None:
|
||||
gr_key = ""
|
||||
else:
|
||||
etudids = {m["etudid"] for m in groups_infos.members}
|
||||
etuds = [etud for etud in etuds if etud.id in etudids]
|
||||
gr_key = groups_infos.get_groups_key()
|
||||
|
||||
cache_key = str(formsemestre_id) + "_" + version + "_" + gr_key
|
||||
cached = sco_cache.SemBulletinsPDFCache.get(cache_key)
|
||||
if cached:
|
||||
return cached[1], cached[0]
|
||||
fragments = []
|
||||
# Make each bulletin
|
||||
|
||||
for etud in formsemestre.get_inscrits(include_demdef=True, order=True):
|
||||
for etud in etuds:
|
||||
if version == "butcourt":
|
||||
frag = bulletin_but_court.bulletin_but_court_pdf_frag(etud, formsemestre)
|
||||
else:
|
||||
|
@ -262,7 +278,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
|
|||
sco_pdf.PDFLOCK.release()
|
||||
#
|
||||
date_iso = time.strftime("%Y-%m-%d")
|
||||
filename = "bul-%s-%s.pdf" % (formsemestre.titre_num(), date_iso)
|
||||
filename = f"bul-{formsemestre.titre_num()}-{date_iso}.pdf"
|
||||
filename = scu.unescape_html(filename).replace(" ", "_").replace("&", "")
|
||||
# fill cache
|
||||
sco_cache.SemBulletinsPDFCache.set(
|
||||
|
|
|
@ -51,7 +51,7 @@ from reportlab.lib.colors import Color, blue
|
|||
from reportlab.lib.units import cm, mm
|
||||
from reportlab.platypus import KeepTogether, Paragraph, Spacer, Table
|
||||
|
||||
from app.models import BulAppreciations
|
||||
from app.models import BulAppreciations, Evaluation
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import (
|
||||
gen_tables,
|
||||
|
@ -715,9 +715,15 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||
eval_style = ""
|
||||
t = {
|
||||
"module": '<bullet indent="2mm">•</bullet> ' + e["name"],
|
||||
"coef": ("<i>" + e["coef_txt"] + "</i>")
|
||||
if prefs["bul_show_coef"]
|
||||
else "",
|
||||
"coef": (
|
||||
(
|
||||
f"<i>{e['coef_txt']}</i>"
|
||||
if e["evaluation_type"] != Evaluation.EVALUATION_BONUS
|
||||
else "bonus"
|
||||
)
|
||||
if prefs["bul_show_coef"]
|
||||
else ""
|
||||
),
|
||||
"_hidden": hidden,
|
||||
"_module_target": e["target_html"],
|
||||
# '_module_help' : ,
|
||||
|
|
|
@ -260,7 +260,7 @@ def make_xml_formsemestre_bulletinetud(
|
|||
numero=str(mod["numero"]),
|
||||
titre=quote_xml_attr(mod["titre"]),
|
||||
abbrev=quote_xml_attr(mod["abbrev"]),
|
||||
code_apogee=quote_xml_attr(mod["code_apogee"])
|
||||
code_apogee=quote_xml_attr(mod["code_apogee"]),
|
||||
# ects=ects ects des modules maintenant inutilisés
|
||||
)
|
||||
x_ue.append(x_mod)
|
||||
|
@ -347,7 +347,7 @@ def make_xml_formsemestre_bulletinetud(
|
|||
|
||||
# --- Absences
|
||||
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
|
||||
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
|
||||
# --- Decision Jury
|
||||
if (
|
||||
|
|
|
@ -67,7 +67,7 @@ class ScoDocCache:
|
|||
keys are prefixed by the current departement: g.scodoc_dept MUST be set.
|
||||
"""
|
||||
|
||||
timeout = None # ttl, infinite by default
|
||||
timeout = 3600 # ttl, one hour by default
|
||||
prefix = ""
|
||||
verbose = False # if true, verbose logging (debug)
|
||||
|
||||
|
@ -201,7 +201,7 @@ class AbsSemEtudCache(ScoDocCache):
|
|||
"""
|
||||
|
||||
prefix = "ABSE"
|
||||
timeout = 60 * 60 # ttl 60 minutes
|
||||
timeout = 600 # ttl 10 minutes
|
||||
|
||||
|
||||
class SemBulletinsPDFCache(ScoDocCache):
|
||||
|
@ -233,7 +233,6 @@ class SemInscriptionsCache(ScoDocCache):
|
|||
"""
|
||||
|
||||
prefix = "SI"
|
||||
duration = 12 * 60 * 60 # ttl 12h
|
||||
|
||||
|
||||
class TableRecapCache(ScoDocCache):
|
||||
|
@ -243,7 +242,6 @@ class TableRecapCache(ScoDocCache):
|
|||
"""
|
||||
|
||||
prefix = "RECAP"
|
||||
duration = 12 * 60 * 60 # ttl 12h
|
||||
|
||||
|
||||
class TableRecapWithEvalsCache(ScoDocCache):
|
||||
|
@ -253,7 +251,6 @@ class TableRecapWithEvalsCache(ScoDocCache):
|
|||
"""
|
||||
|
||||
prefix = "RECAPWITHEVALS"
|
||||
duration = 12 * 60 * 60 # ttl 12h
|
||||
|
||||
|
||||
class TableJuryCache(ScoDocCache):
|
||||
|
@ -263,7 +260,6 @@ class TableJuryCache(ScoDocCache):
|
|||
"""
|
||||
|
||||
prefix = "RECAPJURY"
|
||||
duration = 12 * 60 * 60 # ttl 12h
|
||||
|
||||
|
||||
class TableJuryWithEvalsCache(ScoDocCache):
|
||||
|
@ -273,7 +269,6 @@ class TableJuryWithEvalsCache(ScoDocCache):
|
|||
"""
|
||||
|
||||
prefix = "RECAPJURYWITHEVALS"
|
||||
duration = 12 * 60 * 60 # ttl 12h
|
||||
|
||||
|
||||
def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False)
|
||||
|
|
|
@ -114,7 +114,7 @@ def index_html(showcodes=0, showsemtable=0):
|
|||
# aucun semestre courant: affiche aide
|
||||
H.append(
|
||||
"""<h2 class="listesems">Aucune session en cours !</h2>
|
||||
<p>Pour ajouter une session, aller dans <a href="Notes" id="link-programmes">Programmes</a>,
|
||||
<p>Pour ajouter une session, aller dans <a href="Notes" id="link-programmes">Formations</a>,
|
||||
choisissez une formation, puis suivez le lien "<em>UE, modules, semestres</em>".
|
||||
</p><p>
|
||||
Là, en bas de page, suivez le lien
|
||||
|
@ -208,6 +208,8 @@ def index_html(showcodes=0, showsemtable=0):
|
|||
"""<hr>
|
||||
<h3>Assistance</h3>
|
||||
<ul>
|
||||
<li><a class="stdlink" href="https://scodoc.org/Contact" target="_blank"
|
||||
rel="noopener noreferrer">Contact (Discord)</a></li>
|
||||
<li><a class="stdlink" href="sco_dump_and_send_db">Envoyer données</a></li>
|
||||
</ul>
|
||||
"""
|
||||
|
@ -336,15 +338,15 @@ def _style_sems(sems):
|
|||
else:
|
||||
sem["semestre_id_n"] = sem["semestre_id"]
|
||||
# pour édition codes Apogée:
|
||||
sem[
|
||||
"_etapes_apo_str_td_attrs"
|
||||
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['etapes_apo_str']}" """
|
||||
sem[
|
||||
"_elt_annee_apo_td_attrs"
|
||||
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_annee_apo']}" """
|
||||
sem[
|
||||
"_elt_sem_apo_td_attrs"
|
||||
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
|
||||
sem["_etapes_apo_str_td_attrs"] = (
|
||||
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['etapes_apo_str']}" """
|
||||
)
|
||||
sem["_elt_annee_apo_td_attrs"] = (
|
||||
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_annee_apo']}" """
|
||||
)
|
||||
sem["_elt_sem_apo_td_attrs"] = (
|
||||
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
|
||||
)
|
||||
|
||||
|
||||
def delete_dept(dept_id: int) -> str:
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
from flask.templating import render_template
|
||||
|
||||
from app import db
|
||||
from app.but import apc_edit_ue
|
||||
from app.models import UniteEns, Matiere, Module, FormSemestre, ModuleImpl
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc import codes_cursus
|
||||
|
@ -48,6 +47,8 @@ def html_edit_formation_apc(
|
|||
- Les ressources
|
||||
- Les SAÉs
|
||||
"""
|
||||
from app.but import cursus_but
|
||||
|
||||
cursus = formation.get_cursus()
|
||||
assert cursus.APC_SAE
|
||||
|
||||
|
@ -101,18 +102,26 @@ def html_edit_formation_apc(
|
|||
),
|
||||
}
|
||||
|
||||
html_ue_warning = {
|
||||
semestre_idx: cursus_but.formation_semestre_niveaux_warning(
|
||||
formation, semestre_idx
|
||||
)
|
||||
for semestre_idx in semestre_ids
|
||||
}
|
||||
|
||||
H = [
|
||||
render_template(
|
||||
"pn/form_ues.j2",
|
||||
formation=formation,
|
||||
semestre_ids=semestre_ids,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
ues_by_sem=ues_by_sem,
|
||||
ects_by_sem=ects_by_sem,
|
||||
scu=scu,
|
||||
codes_cursus=codes_cursus,
|
||||
ects_by_sem=ects_by_sem,
|
||||
editable=editable,
|
||||
formation=formation,
|
||||
html_ue_warning=html_ue_warning,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
semestre_ids=semestre_ids,
|
||||
tag_editable=tag_editable,
|
||||
ues_by_sem=ues_by_sem,
|
||||
),
|
||||
]
|
||||
for semestre_idx in semestre_ids:
|
||||
|
|
|
@ -412,7 +412,7 @@ def module_move(module_id, after=0, redirect=True):
|
|||
db.session.add(neigh)
|
||||
db.session.commit()
|
||||
module.formation.invalidate_cached_sems()
|
||||
# redirect to ue_list page:
|
||||
# redirect to ue_table page:
|
||||
if redirect:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
|
@ -454,7 +454,7 @@ def ue_move(ue_id, after=0, redirect=1):
|
|||
db.session.commit()
|
||||
ue.formation.invalidate_cached_sems()
|
||||
|
||||
# redirect to ue_list page
|
||||
# redirect to ue_table page
|
||||
if redirect:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
|
|
|
@ -106,9 +106,9 @@ def do_module_create(args) -> int:
|
|||
if int(args.get("semestre_id", 0)) != ue.semestre_idx:
|
||||
raise ScoValueError("Formation incompatible: contacter le support ScoDoc")
|
||||
# create
|
||||
cnx = ndb.GetDBConnexion()
|
||||
module_id = _moduleEditor.create(cnx, args)
|
||||
log(f"do_module_create: created {module_id} with {args}")
|
||||
module = Module.create_from_dict(args)
|
||||
db.session.commit()
|
||||
log(f"do_module_create: created {module.id} with {args}")
|
||||
|
||||
# news
|
||||
ScolarNews.add(
|
||||
|
@ -117,7 +117,7 @@ def do_module_create(args) -> int:
|
|||
text=f"Modification de la formation {formation.acronyme}",
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
return module_id
|
||||
return module.id
|
||||
|
||||
|
||||
def module_create(
|
||||
|
@ -666,7 +666,7 @@ def module_edit(
|
|||
"explanation": "numéro (1, 2, 3, 4, ...) pour ordre d'affichage",
|
||||
"type": "int",
|
||||
"default": default_num,
|
||||
"allow_null": False,
|
||||
"allow_null": True,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
@ -682,8 +682,11 @@ def module_edit(
|
|||
"input_type": "checkbox",
|
||||
"vertical": True,
|
||||
"dom_id": "tf_module_parcours",
|
||||
"labels": [parcour.libelle for parcour in ref_comp.parcours]
|
||||
+ ["Tous (tronc commun)"],
|
||||
"labels": [
|
||||
f" {parcour.libelle} (<b>{parcour.code}</b>)"
|
||||
for parcour in ref_comp.parcours
|
||||
]
|
||||
+ [" Tous (tronc commun)"],
|
||||
"allowed_values": [
|
||||
str(parcour.id) for parcour in ref_comp.parcours
|
||||
]
|
||||
|
@ -811,6 +814,10 @@ def module_edit(
|
|||
)
|
||||
)
|
||||
else:
|
||||
if isinstance(tf[2]["numero"], str):
|
||||
tf[2]["numero"] = tf[2]["numero"].strip()
|
||||
if not isinstance(tf[2]["numero"], int) and not tf[2]["numero"]:
|
||||
tf[2]["numero"] = tf[2]["numero"] or default_num
|
||||
if create:
|
||||
if not matiere_id:
|
||||
# formulaire avec choix UE de rattachement
|
||||
|
|
|
@ -766,7 +766,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
|||
"libjs/jQuery-tagEditor/jquery.caret.min.js",
|
||||
"js/module_tag_editor.js",
|
||||
],
|
||||
page_title=f"Programme {formation.acronyme} v{formation.version}",
|
||||
page_title=f"Formation {formation.acronyme} v{formation.version}",
|
||||
),
|
||||
f"""<h2>{formation.html()} {lockicon}
|
||||
</h2>
|
||||
|
@ -888,11 +888,13 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||
H.append(
|
||||
f"""
|
||||
<div class="formation_ue_list">
|
||||
<div class="ue_list_tit">Programme pédagogique:</div>
|
||||
<div class="ue_list_tit">Formation (programme pédagogique):</div>
|
||||
<form>
|
||||
<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>
|
||||
> 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>
|
||||
"""
|
||||
)
|
||||
|
@ -1054,7 +1056,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||
)
|
||||
# <li>(debug) <a class="stdlink" href="check_form_integrity?formation_id=%(formation_id)s">Vérifier cohérence</a></li>
|
||||
|
||||
warn, _ = sco_formsemestre_validation.check_formation_ues(formation_id)
|
||||
warn, _ = sco_formsemestre_validation.check_formation_ues(formation)
|
||||
H.append(warn)
|
||||
|
||||
H.append(html_sco_header.sco_footer())
|
||||
|
|
|
@ -70,9 +70,9 @@ def evaluation_check_absences(evaluation: Evaluation):
|
|||
deb <= Assiduite.date_fin,
|
||||
)
|
||||
|
||||
abs_etudids = set(assi.etudid for assi in assiduites)
|
||||
abs_nj_etudids = set(assi.etudid for assi in assiduites if assi.est_just is False)
|
||||
just_etudids = set(assi.etudid for assi in assiduites if assi.est_just is True)
|
||||
abs_etudids = {assi.etudid for assi in assiduites}
|
||||
abs_nj_etudids = {assi.etudid for assi in assiduites if assi.est_just is False}
|
||||
just_etudids = {assi.etudid for assi in assiduites if assi.est_just is True}
|
||||
|
||||
# Les notes:
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
|
||||
|
|
|
@ -31,96 +31,15 @@
|
|||
import flask
|
||||
from flask import url_for, g
|
||||
from flask_login import current_user
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db, log
|
||||
|
||||
from app.models import Evaluation
|
||||
from app.models.evaluations import check_convert_evaluation_args
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import AccessDenied
|
||||
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_moduleimpl
|
||||
|
||||
|
||||
_evaluationEditor = ndb.EditableTable(
|
||||
"notes_evaluation",
|
||||
"evaluation_id",
|
||||
(
|
||||
"evaluation_id",
|
||||
"moduleimpl_id",
|
||||
"date_debut",
|
||||
"date_fin",
|
||||
"description",
|
||||
"note_max",
|
||||
"coefficient",
|
||||
"visibulletin",
|
||||
"publish_incomplete",
|
||||
"evaluation_type",
|
||||
"numero",
|
||||
),
|
||||
sortkey="numero, date_debut desc", # plus recente d'abord
|
||||
output_formators={
|
||||
"numero": ndb.int_null_is_zero,
|
||||
},
|
||||
input_formators={
|
||||
"visibulletin": bool,
|
||||
"publish_incomplete": bool,
|
||||
"evaluation_type": int,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_evaluations_dict(args: dict) -> list[dict]:
|
||||
"""Liste evaluations, triées numero (or most recent date first).
|
||||
Fonction de transition pour ancien code ScoDoc7.
|
||||
|
||||
Ajoute les champs:
|
||||
'duree' : '2h30'
|
||||
'matin' : 1 (commence avant 12:00) ou 0
|
||||
'apresmidi' : 1 (termine après 12:00) ou 0
|
||||
'descrheure' : ' de 15h00 à 16h30'
|
||||
"""
|
||||
# calcule duree (chaine de car.) de chaque evaluation et ajoute jour_iso, matin, apresmidi
|
||||
return [
|
||||
e.to_dict()
|
||||
for e in Evaluation.query.filter_by(**args).order_by(
|
||||
sa.desc(Evaluation.numero), sa.desc(Evaluation.date_debut)
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def do_evaluation_list_in_formsemestre(formsemestre_id):
|
||||
"list evaluations in this formsemestre"
|
||||
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
||||
evals = []
|
||||
for modimpl in mods:
|
||||
evals += get_evaluations_dict(args={"moduleimpl_id": modimpl["moduleimpl_id"]})
|
||||
return evals
|
||||
|
||||
|
||||
def do_evaluation_edit(args):
|
||||
"edit an evaluation"
|
||||
evaluation_id = args["evaluation_id"]
|
||||
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
if evaluation is None:
|
||||
raise ValueError("evaluation inexistante !")
|
||||
|
||||
if not evaluation.moduleimpl.can_edit_evaluation(current_user):
|
||||
raise AccessDenied(
|
||||
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
|
||||
)
|
||||
args["moduleimpl_id"] = evaluation.moduleimpl.id
|
||||
check_convert_evaluation_args(evaluation.moduleimpl, args)
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_evaluationEditor.edit(cnx, args)
|
||||
# inval cache pour ce semestre
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=evaluation.moduleimpl.formsemestre_id
|
||||
)
|
||||
|
||||
|
||||
# ancien _notes_getall
|
||||
|
|
|
@ -31,14 +31,12 @@ import datetime
|
|||
import time
|
||||
|
||||
import flask
|
||||
from flask import url_for, render_template
|
||||
from flask import g
|
||||
from flask import g, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
from flask import request
|
||||
|
||||
from app import db
|
||||
from app.models import Evaluation, Module, ModuleImpl
|
||||
from app.models.evaluations import heure_to_time
|
||||
from app.models.evaluations import heure_to_time, check_and_convert_evaluation_args
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
@ -108,7 +106,7 @@ def evaluation_create_form(
|
|||
raise ValueError("missing evaluation_id parameter")
|
||||
initvalues = evaluation.to_dict()
|
||||
moduleimpl_id = initvalues["moduleimpl_id"]
|
||||
submitlabel = "Modifier les données"
|
||||
submitlabel = "Modifier l'évaluation"
|
||||
action = "Modification d'une évaluation"
|
||||
link = ""
|
||||
# Note maximale actuelle dans cette éval ?
|
||||
|
@ -142,6 +140,15 @@ def evaluation_create_form(
|
|||
else:
|
||||
poids = 0.0
|
||||
initvalues[f"poids_{ue.id}"] = poids
|
||||
# Blocage
|
||||
if edit:
|
||||
initvalues["blocked"] = evaluation.is_blocked()
|
||||
initvalues["blocked_until"] = (
|
||||
evaluation.blocked_until.strftime("%d/%m/%Y")
|
||||
if evaluation.blocked_until
|
||||
and evaluation.blocked_until < Evaluation.BLOCKED_FOREVER
|
||||
else ""
|
||||
)
|
||||
#
|
||||
form = [
|
||||
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
|
||||
|
@ -183,7 +190,8 @@ def evaluation_create_form(
|
|||
{
|
||||
"size": 6,
|
||||
"type": "float", # peut être négatif (!)
|
||||
"explanation": "coef. dans le module (choisi librement par l'enseignant, non utilisé pour rattrapage et 2ème session)",
|
||||
"explanation": """coef. dans le module (choisi librement par
|
||||
l'enseignant, non utilisé pour rattrapage, 2ème session et bonus)""",
|
||||
"allow_null": False,
|
||||
},
|
||||
)
|
||||
|
@ -195,7 +203,7 @@ def evaluation_create_form(
|
|||
"size": 4,
|
||||
"type": "float",
|
||||
"title": "Notes de 0 à",
|
||||
"explanation": f"barème (note max actuelle: {min_note_max_str})",
|
||||
"explanation": f"""barème (note max actuelle: {min_note_max_str}).""",
|
||||
"allow_null": False,
|
||||
"max_value": scu.NOTES_MAX,
|
||||
"min_value": min_note_max,
|
||||
|
@ -206,7 +214,8 @@ def evaluation_create_form(
|
|||
{
|
||||
"size": 36,
|
||||
"type": "text",
|
||||
"explanation": """type d'évaluation, apparait sur le bulletins longs. Exemples: "contrôle court", "examen de TP", "examen final".""",
|
||||
"explanation": """type d'évaluation, apparait sur le bulletins longs.
|
||||
Exemples: "contrôle court", "examen de TP", "examen final".""",
|
||||
},
|
||||
),
|
||||
(
|
||||
|
@ -230,16 +239,20 @@ def evaluation_create_form(
|
|||
{
|
||||
"input_type": "menu",
|
||||
"title": "Modalité",
|
||||
"allowed_values": (
|
||||
scu.EVALUATION_NORMALE,
|
||||
scu.EVALUATION_RATTRAPAGE,
|
||||
scu.EVALUATION_SESSION2,
|
||||
),
|
||||
"allowed_values": Evaluation.VALID_EVALUATION_TYPES,
|
||||
"type": "int",
|
||||
"labels": (
|
||||
"Normale",
|
||||
"Rattrapage (remplace si meilleure note)",
|
||||
"Deuxième session (remplace toujours)",
|
||||
(
|
||||
"Bonus "
|
||||
+ (
|
||||
"(pondéré par poids et ajouté aux moyennes de ce module)"
|
||||
if is_apc
|
||||
else "(ajouté à la moyenne de ce module)"
|
||||
)
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
|
@ -251,8 +264,10 @@ def evaluation_create_form(
|
|||
{
|
||||
"size": 6,
|
||||
"type": "float",
|
||||
"explanation": "importance de l'évaluation (multiplie les poids ci-dessous)",
|
||||
"explanation": """importance de l'évaluation (multiplie les poids ci-dessous).
|
||||
Non utilisé pour les bonus.""",
|
||||
"allow_null": False,
|
||||
"dom_id": "evaluation-edit-coef",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
@ -294,6 +309,28 @@ def evaluation_create_form(
|
|||
},
|
||||
),
|
||||
)
|
||||
# Bloquage / date prise en compte
|
||||
form += [
|
||||
(
|
||||
"blocked",
|
||||
{
|
||||
"input_type": "boolcheckbox",
|
||||
"title": "Bloquer la prise en compte",
|
||||
"explanation": """empêche la prise en compte
|
||||
(ne sera pas visible sur les bulletins ni dans les tableaux)""",
|
||||
"dom_id": "evaluation-edit-blocked",
|
||||
},
|
||||
),
|
||||
(
|
||||
"blocked_until",
|
||||
{
|
||||
"input_type": "datedmy",
|
||||
"title": "Date déblocage",
|
||||
"size": 12,
|
||||
"explanation": "sera débloquée à partir de cette date",
|
||||
},
|
||||
),
|
||||
]
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
vals,
|
||||
|
@ -324,7 +361,9 @@ def evaluation_create_form(
|
|||
+ "\n".join(H)
|
||||
+ "\n"
|
||||
+ tf[1]
|
||||
+ render_template("scodoc/help/evaluations.j2", is_apc=is_apc)
|
||||
+ render_template(
|
||||
"scodoc/help/evaluations.j2", is_apc=is_apc, modimpl=modimpl
|
||||
)
|
||||
+ render_template("sco_timepicker.j2")
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
|
@ -342,6 +381,8 @@ def evaluation_create_form(
|
|||
raise ScoValueError("Date (j/m/a) invalide") from exc
|
||||
else:
|
||||
date_debut = None
|
||||
args["date_debut"] = date_debut
|
||||
args["date_fin"] = date_debut # même jour
|
||||
args.pop("jour", None)
|
||||
if date_debut and args.get("heure_debut"):
|
||||
try:
|
||||
|
@ -350,7 +391,8 @@ def evaluation_create_form(
|
|||
raise ScoValueError("Heure début invalide") from exc
|
||||
args["date_debut"] = datetime.datetime.combine(date_debut, heure_debut)
|
||||
args.pop("heure_debut", None)
|
||||
# note: ce formulaire ne permet de créer que des évaluation avec debut et fin sur le même jour.
|
||||
# note: ce formulaire ne permet de créer que des évaluations
|
||||
# avec debut et fin sur le même jour.
|
||||
if date_debut and args.get("heure_fin"):
|
||||
try:
|
||||
heure_fin = heure_to_time(args["heure_fin"])
|
||||
|
@ -358,8 +400,22 @@ def evaluation_create_form(
|
|||
raise ScoValueError("Heure fin invalide") from exc
|
||||
args["date_fin"] = datetime.datetime.combine(date_debut, heure_fin)
|
||||
args.pop("heure_fin", None)
|
||||
# Blocage:
|
||||
if args.get("blocked"):
|
||||
if args.get("blocked_until"):
|
||||
try:
|
||||
args["blocked_until"] = datetime.datetime.strptime(
|
||||
args["blocked_until"], "%d/%m/%Y"
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("Date déblocage (j/m/a) invalide") from exc
|
||||
else: # bloquage coché sans date
|
||||
args["blocked_until"] = Evaluation.BLOCKED_FOREVER
|
||||
else: # si pas coché, efface date déblocage
|
||||
args["blocked_until"] = None
|
||||
#
|
||||
if edit:
|
||||
check_and_convert_evaluation_args(args, modimpl)
|
||||
evaluation.from_dict(args)
|
||||
else:
|
||||
# création d'une evaluation
|
||||
|
|
|
@ -40,16 +40,14 @@ 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, ModuleImpl
|
||||
from app.models import Evaluation, FormSemestre, ModuleImpl, Module
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_cal
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_moduleimpl
|
||||
|
@ -114,9 +112,10 @@ def do_evaluation_etat(
|
|||
nb_neutre,
|
||||
nb_att,
|
||||
moy, median, mini, maxi : # notes, en chaine, sur 20
|
||||
last_modif: datetime,
|
||||
maxi_num : note max, numérique
|
||||
last_modif: datetime, *
|
||||
gr_complets, gr_incomplets,
|
||||
evalcomplete
|
||||
evalcomplete *
|
||||
}
|
||||
evalcomplete est vrai si l'eval est complete (tous les inscrits
|
||||
à ce module ont des notes)
|
||||
|
@ -130,11 +129,12 @@ def do_evaluation_etat(
|
|||
) # { etudid : note }
|
||||
|
||||
# ---- Liste des groupes complets et incomplets
|
||||
E = sco_evaluation_db.get_evaluations_dict(args={"evaluation_id": evaluation_id})[0]
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
is_malus = Mod["module_type"] == ModuleType.MALUS # True si module de malus
|
||||
formsemestre_id = M["formsemestre_id"]
|
||||
evaluation = Evaluation.get_evaluation(evaluation_id)
|
||||
modimpl: ModuleImpl = evaluation.moduleimpl
|
||||
module: Module = modimpl.module
|
||||
|
||||
is_malus = module.module_type == ModuleType.MALUS # True si module de malus
|
||||
formsemestre_id = modimpl.formsemestre_id
|
||||
# Si partition_id is None, prend 'all' ou bien la premiere:
|
||||
if partition_id is None:
|
||||
if select_first_partition:
|
||||
|
@ -150,9 +150,7 @@ def do_evaluation_etat(
|
|||
insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
|
||||
formsemestre_id
|
||||
)
|
||||
insmod = sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||
moduleimpl_id=E["moduleimpl_id"]
|
||||
)
|
||||
insmod = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=modimpl.id)
|
||||
insmodset = {x["etudid"] for x in insmod}
|
||||
# retire de insem ceux qui ne sont pas inscrits au module
|
||||
ins = [i for i in insem if i["etudid"] in insmodset]
|
||||
|
@ -175,9 +173,9 @@ def do_evaluation_etat(
|
|||
maxi_num = None
|
||||
else:
|
||||
median = scu.fmt_note(median_num)
|
||||
moy = scu.fmt_note(moy_num, E["note_max"])
|
||||
mini = scu.fmt_note(mini_num, E["note_max"])
|
||||
maxi = scu.fmt_note(maxi_num, E["note_max"])
|
||||
moy = scu.fmt_note(moy_num, evaluation.note_max)
|
||||
mini = scu.fmt_note(mini_num, evaluation.note_max)
|
||||
maxi = scu.fmt_note(maxi_num, evaluation.note_max)
|
||||
# cherche date derniere modif note
|
||||
if len(etuds_notes_dict):
|
||||
t = [x["date"] for x in etuds_notes_dict.values()]
|
||||
|
@ -218,25 +216,17 @@ def do_evaluation_etat(
|
|||
|
||||
gr_incomplets = list(group_nb_missing.keys())
|
||||
gr_incomplets.sort()
|
||||
if (
|
||||
(total_nb_missing > 0)
|
||||
and (E["evaluation_type"] != scu.EVALUATION_RATTRAPAGE)
|
||||
and (E["evaluation_type"] != scu.EVALUATION_SESSION2)
|
||||
):
|
||||
complete = False
|
||||
else:
|
||||
complete = True
|
||||
|
||||
complete = (
|
||||
(total_nb_missing == 0)
|
||||
or (E["evaluation_type"] == scu.EVALUATION_RATTRAPAGE)
|
||||
or (E["evaluation_type"] == scu.EVALUATION_SESSION2)
|
||||
or (evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE)
|
||||
and not evaluation.is_blocked()
|
||||
)
|
||||
evalattente = (total_nb_missing > 0) and (
|
||||
(total_nb_missing == total_nb_att) or E["publish_incomplete"]
|
||||
(total_nb_missing == total_nb_att) or evaluation.publish_incomplete
|
||||
)
|
||||
# mais ne met pas en attente les evals immediates sans aucune notes:
|
||||
if E["publish_incomplete"] and nb_notes == 0:
|
||||
if evaluation.publish_incomplete and nb_notes == 0:
|
||||
evalattente = False
|
||||
|
||||
# Calcul moyenne dans chaque groupe de TD
|
||||
|
@ -247,10 +237,10 @@ def do_evaluation_etat(
|
|||
{
|
||||
"group_id": group_id,
|
||||
"group_name": group_by_id[group_id]["group_name"],
|
||||
"gr_moy": scu.fmt_note(gr_moy, E["note_max"]),
|
||||
"gr_median": scu.fmt_note(gr_median, E["note_max"]),
|
||||
"gr_mini": scu.fmt_note(gr_mini, E["note_max"]),
|
||||
"gr_maxi": scu.fmt_note(gr_maxi, E["note_max"]),
|
||||
"gr_moy": scu.fmt_note(gr_moy, evaluation.note_max),
|
||||
"gr_median": scu.fmt_note(gr_median, evaluation.note_max),
|
||||
"gr_mini": scu.fmt_note(gr_mini, evaluation.note_max),
|
||||
"gr_maxi": scu.fmt_note(gr_maxi, evaluation.note_max),
|
||||
"gr_nb_notes": len(notes),
|
||||
"gr_nb_att": len([x for x in notes if x == scu.NOTES_ATTENTE]),
|
||||
}
|
||||
|
@ -283,8 +273,9 @@ def do_evaluation_etat(
|
|||
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"]
|
||||
utilise e["blocked"], e["etat"]["evalcomplete"], e["etat"]["nb_notes"], e["etat"]["last_modif"]
|
||||
->
|
||||
nb_evals : nb total qcq soit état
|
||||
nb_eval_completes (= prises en compte)
|
||||
nb_evals_en_cours (= avec des notes, mais pas complete)
|
||||
nb_evals_vides (= sans aucune note)
|
||||
|
@ -292,14 +283,16 @@ def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
|
|||
|
||||
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
|
||||
nb_evals_completes, nb_evals_en_cours, nb_evals_vides, nb_evals_blocked = 0, 0, 0, 0
|
||||
dates = []
|
||||
for e in etat_evals:
|
||||
if e["etat"]["blocked"]:
|
||||
nb_evals_blocked += 1
|
||||
if e["etat"]["evalcomplete"]:
|
||||
nb_evals_completes += 1
|
||||
elif e["etat"]["nb_notes"] == 0:
|
||||
nb_evals_vides += 1
|
||||
else:
|
||||
elif not e["etat"]["blocked"]:
|
||||
nb_evals_en_cours += 1
|
||||
last_modif = e["etat"]["last_modif"]
|
||||
if last_modif is not None:
|
||||
|
@ -309,6 +302,8 @@ def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
|
|||
last_modif = sorted(dates)[-1] if dates else ""
|
||||
|
||||
return {
|
||||
"nb_evals": len(etat_evals),
|
||||
"nb_evals_blocked": nb_evals_blocked,
|
||||
"nb_evals_completes": nb_evals_completes,
|
||||
"nb_evals_en_cours": nb_evals_en_cours,
|
||||
"nb_evals_vides": nb_evals_vides,
|
||||
|
@ -499,13 +494,14 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
|
|||
"""Experimental: un tableau indiquant pour chaque évaluation
|
||||
le nombre de jours avant la publication des notes.
|
||||
|
||||
N'indique pas les évaluations de rattrapage ni celles des modules de bonus/malus.
|
||||
N'indique que les évaluations "normales" (pas rattrapage, ni bonus, ni session2,
|
||||
ni celles des modules de bonus/malus).
|
||||
"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
evaluations = formsemestre.get_evaluations()
|
||||
rows = []
|
||||
for e in evaluations:
|
||||
if (e.evaluation_type != scu.EVALUATION_NORMALE) or (
|
||||
if (e.evaluation_type != Evaluation.EVALUATION_NORMALE) or (
|
||||
e.moduleimpl.module.module_type == ModuleType.MALUS
|
||||
):
|
||||
continue
|
||||
|
@ -519,9 +515,9 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
|
|||
{
|
||||
"date_first_complete": date_first_complete,
|
||||
"delai_correction": delai_correction,
|
||||
"jour": e.date_debut.strftime("%d/%m/%Y")
|
||||
if e.date_debut
|
||||
else "sans date",
|
||||
"jour": (
|
||||
e.date_debut.strftime("%d/%m/%Y") if e.date_debut else "sans date"
|
||||
),
|
||||
"_jour_target": url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
|
@ -611,13 +607,17 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True)
|
|||
# Indique l'UE
|
||||
ue = modimpl.module.ue
|
||||
H.append(f"<p><b>UE : {ue.acronyme}</b></p>")
|
||||
if (
|
||||
modimpl.module.module_type == ModuleType.MALUS
|
||||
or evaluation.evaluation_type == Evaluation.EVALUATION_BONUS
|
||||
):
|
||||
# store min/max values used by JS client-side checks:
|
||||
H.append(
|
||||
"""<span id="eval_note_min" class="sco-hidden">-20.</span>
|
||||
<span id="eval_note_max" class="sco-hidden">20.</span>"""
|
||||
)
|
||||
else:
|
||||
# date et absences (pas pour evals de malus)
|
||||
# date et absences (pas pour evals bonus ni des modules de malus)
|
||||
if evaluation.date_debut is not None:
|
||||
H.append(f"<p>Réalisée le <b>{evaluation.descr_date()}</b> ")
|
||||
group_id = sco_groups.get_default_group(modimpl.formsemestre_id)
|
||||
|
|
|
@ -534,7 +534,7 @@ def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, line
|
|||
# description evaluation
|
||||
ws.append_single_cell_row(scu.unescape_html(description), style_titres)
|
||||
ws.append_single_cell_row(
|
||||
f"Evaluation {evaluation.descr_date()} (coef. {(evaluation.coefficient or 0.0):g})",
|
||||
f"Evaluation {evaluation.descr_date()} (coef. {(evaluation.coefficient):g})",
|
||||
style,
|
||||
)
|
||||
# ligne blanche
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"""Exception handling
|
||||
"""
|
||||
from flask_login import current_user
|
||||
import app
|
||||
|
||||
|
||||
# --- Exceptions
|
||||
|
@ -237,8 +238,11 @@ class ScoTemporaryError(ScoValueError):
|
|||
|
||||
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>Erreur temporaire</p>
|
||||
<p>Veuillez ré-essayer. Si le problème persiste (ou s'il venait
|
||||
à se produire fréquemment), merci de contacter l'assistance ScoDoc
|
||||
(voir <a href="https://scodoc.org/Contact/">les informations de contact</a>).
|
||||
</p>
|
||||
"""
|
||||
app.clear_scodoc_cache()
|
||||
super().__init__(msg)
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
import xml.dom.minidom
|
||||
|
||||
import flask
|
||||
from flask import flash, g, url_for
|
||||
from flask import flash, g, request, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -495,7 +495,7 @@ def formation_list_table() -> GenTable:
|
|||
returns a table
|
||||
"""
|
||||
formations: list[Formation] = Formation.query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
title = "Programmes pédagogiques"
|
||||
title = "Formations (programmes pédagogiques)"
|
||||
lockicon = scu.icontag(
|
||||
"lock32_img", title="Comporte des semestres verrouillés", border="0"
|
||||
)
|
||||
|
@ -527,6 +527,12 @@ def formation_list_table() -> GenTable:
|
|||
"_titre_id": f"""titre-{acronyme_no_spaces}""",
|
||||
"version": formation.version or 0,
|
||||
"commentaire": formation.commentaire or "",
|
||||
"referentiel": (
|
||||
f"""{formation.referentiel_competence.specialite} {
|
||||
formation.referentiel_competence.get_version()}"""
|
||||
if formation.referentiel_competence
|
||||
else ""
|
||||
),
|
||||
}
|
||||
# Ajoute les semestres associés à chaque formation:
|
||||
row["formsemestres"] = formation.formsemestres.order_by(
|
||||
|
@ -603,6 +609,7 @@ def formation_list_table() -> GenTable:
|
|||
"formation_code",
|
||||
"version",
|
||||
"titre",
|
||||
"referentiel",
|
||||
"commentaire",
|
||||
"sems_list_txt",
|
||||
)
|
||||
|
@ -615,6 +622,7 @@ def formation_list_table() -> GenTable:
|
|||
"version": "Version",
|
||||
"formation_code": "Code",
|
||||
"sems_list_txt": "Semestres",
|
||||
"referentiel": "Réf.",
|
||||
}
|
||||
return GenTable(
|
||||
columns_ids=columns_ids,
|
||||
|
@ -627,7 +635,7 @@ def formation_list_table() -> GenTable:
|
|||
html_class="formation_list_table table_leftalign",
|
||||
html_with_td_classes=True,
|
||||
html_sortable=True,
|
||||
base_url="{request.base_url}?formation_id={formation_id}",
|
||||
base_url=f"{request.base_url}",
|
||||
page_title=title,
|
||||
pdf_title=title,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
|
|
|
@ -31,6 +31,7 @@ import flask
|
|||
from flask import url_for, flash, redirect
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
|
@ -63,8 +64,6 @@ from app.scodoc import html_sco_header
|
|||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_compute_moy
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups_copy
|
||||
from app.scodoc import sco_modalites
|
||||
|
@ -304,12 +303,16 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
|||
{
|
||||
"input_type": "text_suggest",
|
||||
"size": 50,
|
||||
"title": "(Co-)Directeur(s) des études"
|
||||
if index
|
||||
else "Directeur des études",
|
||||
"explanation": "(facultatif) taper le début du nom et choisir dans le menu"
|
||||
if index
|
||||
else "(obligatoire) taper le début du nom et choisir dans le menu",
|
||||
"title": (
|
||||
"(Co-)Directeur(s) des études"
|
||||
if index
|
||||
else "Directeur des études"
|
||||
),
|
||||
"explanation": (
|
||||
"(facultatif) taper le début du nom et choisir dans le menu"
|
||||
if index
|
||||
else "(obligatoire) taper le début du nom et choisir dans le menu"
|
||||
),
|
||||
"allowed_values": allowed_user_names,
|
||||
"allow_null": index, # > 0, # il faut au moins un responsable de semestre
|
||||
"text_suggest_options": {
|
||||
|
@ -356,9 +359,11 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
|||
"title": "Semestre dans la formation",
|
||||
"allowed_values": semestre_id_list,
|
||||
"labels": semestre_id_labels,
|
||||
"explanation": "en BUT, on ne peut pas modifier le semestre après création"
|
||||
if is_apc
|
||||
else "",
|
||||
"explanation": (
|
||||
"en BUT, on ne peut pas modifier le semestre après création"
|
||||
if is_apc
|
||||
else ""
|
||||
),
|
||||
"attributes": ['onchange="change_semestre_id();"'] if is_apc else "",
|
||||
},
|
||||
),
|
||||
|
@ -568,7 +573,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
|||
"input_type": "checkbox",
|
||||
"title": "Publication",
|
||||
"allowed_values": ["X"],
|
||||
"explanation": "publier le bulletin sur le portail étudiants",
|
||||
"explanation": "publier le bulletin sur la passerelle étudiants",
|
||||
"labels": [""],
|
||||
},
|
||||
),
|
||||
|
@ -1107,7 +1112,8 @@ def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
|
|||
f"""<b>impossible de supprimer {module.code} ({module.titre or ""})
|
||||
car il y a {nb_evals} évaluations définies
|
||||
(<a href="{
|
||||
url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
|
||||
url_for("notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
|
||||
}" class="stdlink">supprimez-les d\'abord</a>)</b>"""
|
||||
]
|
||||
ok = False
|
||||
|
@ -1227,7 +1233,11 @@ def formsemestre_clone(formsemestre_id):
|
|||
return "".join(H) + msg + tf[1] + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1: # cancel
|
||||
return flask.redirect(
|
||||
"formsemestre_status?formsemestre_id=%s" % formsemestre_id
|
||||
url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
resp = User.get_user_from_nomplogin(tf[2]["responsable_id"])
|
||||
|
@ -1327,21 +1337,12 @@ def do_formsemestre_clone(
|
|||
% (pname, pvalue, formsemestre_id)
|
||||
)
|
||||
|
||||
# 5- Copy formules utilisateur
|
||||
objs = sco_compute_moy.formsemestre_ue_computation_expr_list(
|
||||
cnx, args={"formsemestre_id": orig_formsemestre_id}
|
||||
)
|
||||
for obj in objs:
|
||||
args = obj.copy()
|
||||
args["formsemestre_id"] = formsemestre_id
|
||||
_ = sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, args)
|
||||
|
||||
# 6- Copie les parcours
|
||||
# 5- Copie les parcours
|
||||
formsemestre.parcours = formsemestre_orig.parcours
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
|
||||
# 7- Copy partitions and groups
|
||||
# 6- Copy partitions and groups
|
||||
if clone_partitions:
|
||||
sco_groups_copy.clone_partitions_and_groups(
|
||||
orig_formsemestre_id, formsemestre_id
|
||||
|
@ -1350,9 +1351,9 @@ def do_formsemestre_clone(
|
|||
return formsemestre_id
|
||||
|
||||
|
||||
def formsemestre_delete(formsemestre_id):
|
||||
def formsemestre_delete(formsemestre_id: int) -> str | flask.Response:
|
||||
"""Delete a formsemestre (affiche avertissements)"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
H = [
|
||||
html_sco_header.html_sem_header("Suppression du semestre"),
|
||||
"""<div class="ue_warning"><span>Attention !</span>
|
||||
|
@ -1370,17 +1371,18 @@ Ceci n'est possible que si :
|
|||
</ol>
|
||||
</div>""",
|
||||
]
|
||||
|
||||
evals = sco_evaluation_db.do_evaluation_list_in_formsemestre(formsemestre_id)
|
||||
if evals:
|
||||
evaluations = (
|
||||
Evaluation.query.join(ModuleImpl)
|
||||
.filter_by(formsemestre_id=formsemestre.id)
|
||||
.all()
|
||||
)
|
||||
if evaluations:
|
||||
H.append(
|
||||
f"""<p class="warning">Attention: il y a {len(evals)} évaluations
|
||||
f"""<p class="warning">Attention: il y a {len(evaluations)} évaluations
|
||||
dans ce semestre
|
||||
(sa suppression entrainera l'effacement définif des notes) !</p>"""
|
||||
)
|
||||
submit_label = (
|
||||
f"Confirmer la suppression (du semestre et des {len(evals)} évaluations !)"
|
||||
)
|
||||
submit_label = f"Confirmer la suppression (du semestre et des {len(evaluations)} évaluations !)"
|
||||
else:
|
||||
submit_label = "Confirmer la suppression du semestre"
|
||||
tf = TrivialFormulator(
|
||||
|
@ -1407,8 +1409,10 @@ Ceci n'est possible que si :
|
|||
)
|
||||
else:
|
||||
H.append(tf[1])
|
||||
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1: # cancel
|
||||
|
||||
if tf[0] == -1: # cancel
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_status",
|
||||
|
@ -1416,10 +1420,9 @@ Ceci n'est possible que si :
|
|||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
return flask.redirect(
|
||||
"formsemestre_delete2?formsemestre_id=" + str(formsemestre_id)
|
||||
)
|
||||
return flask.redirect(
|
||||
"formsemestre_delete2?formsemestre_id=" + str(formsemestre_id)
|
||||
)
|
||||
|
||||
|
||||
def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
|
||||
|
@ -1480,106 +1483,165 @@ def formsemestre_has_decisions_or_compensations(
|
|||
return False, ""
|
||||
|
||||
|
||||
def do_formsemestre_delete(formsemestre_id):
|
||||
def do_formsemestre_delete(formsemestre_id: int):
|
||||
"""delete formsemestre, and all its moduleimpls.
|
||||
No checks, no warnings: erase all !
|
||||
"""
|
||||
cnx = ndb.GetDBConnexion()
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
|
||||
sco_cache.EvaluationCache.invalidate_sem(formsemestre_id)
|
||||
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
sco_cache.EvaluationCache.invalidate_sem(formsemestre.id)
|
||||
titre_sem = formsemestre.titre_annee()
|
||||
# --- Destruction des modules de ce semestre
|
||||
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
||||
for mod in mods:
|
||||
for modimpl in formsemestre.modimpls:
|
||||
# evaluations
|
||||
evals = sco_evaluation_db.get_evaluations_dict(
|
||||
args={"moduleimpl_id": mod["moduleimpl_id"]}
|
||||
)
|
||||
for e in evals:
|
||||
ndb.SimpleQuery(
|
||||
"DELETE FROM notes_notes WHERE evaluation_id=%(evaluation_id)s",
|
||||
e,
|
||||
for e in modimpl.evaluations:
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"""DELETE FROM notes_notes WHERE evaluation_id=:evaluation_id"""
|
||||
),
|
||||
{"evaluation_id": e.id},
|
||||
)
|
||||
ndb.SimpleQuery(
|
||||
"DELETE FROM notes_notes_log WHERE evaluation_id=%(evaluation_id)s",
|
||||
e,
|
||||
)
|
||||
ndb.SimpleQuery(
|
||||
"DELETE FROM notes_evaluation WHERE id=%(evaluation_id)s",
|
||||
e,
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"""DELETE FROM notes_notes_log WHERE evaluation_id=:evaluation_id"""
|
||||
),
|
||||
{"evaluation_id": e.id},
|
||||
)
|
||||
|
||||
sco_moduleimpl.do_moduleimpl_delete(
|
||||
mod["moduleimpl_id"], formsemestre_id=formsemestre_id
|
||||
)
|
||||
db.session.delete(e)
|
||||
db.session.delete(modimpl)
|
||||
# --- Desinscription des etudiants
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
req = "DELETE FROM notes_formsemestre_inscription WHERE formsemestre_id=%(formsemestre_id)s"
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"DELETE FROM notes_formsemestre_inscription WHERE formsemestre_id=:formsemestre_id"
|
||||
),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
|
||||
# --- Suppression des evenements
|
||||
req = "DELETE FROM scolar_events WHERE formsemestre_id=%(formsemestre_id)s"
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
db.session.execute(
|
||||
sa.text("DELETE FROM scolar_events WHERE formsemestre_id=:formsemestre_id"),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
# --- Suppression des appreciations
|
||||
req = "DELETE FROM notes_appreciations WHERE formsemestre_id=%(formsemestre_id)s"
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"DELETE FROM notes_appreciations WHERE formsemestre_id=:formsemestre_id"
|
||||
),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
# --- Supression des validations (!!!)
|
||||
req = "DELETE FROM scolar_formsemestre_validation WHERE formsemestre_id=%(formsemestre_id)s"
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"DELETE FROM scolar_formsemestre_validation WHERE formsemestre_id=:formsemestre_id"
|
||||
),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
# --- Supression des references a ce semestre dans les compensations:
|
||||
req = "UPDATE scolar_formsemestre_validation SET compense_formsemestre_id=NULL WHERE compense_formsemestre_id=%(formsemestre_id)s"
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"""UPDATE scolar_formsemestre_validation
|
||||
SET compense_formsemestre_id=NULL
|
||||
WHERE compense_formsemestre_id=:formsemestre_id"""
|
||||
),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
# --- Suppression des autorisations
|
||||
req = "DELETE FROM scolar_autorisation_inscription WHERE origin_formsemestre_id=%(formsemestre_id)s"
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"DELETE FROM scolar_autorisation_inscription WHERE origin_formsemestre_id=:formsemestre_id"
|
||||
),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
# --- Suppression des coefs d'UE capitalisées
|
||||
req = "DELETE FROM notes_formsemestre_uecoef WHERE formsemestre_id=%(formsemestre_id)s"
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"DELETE FROM notes_formsemestre_uecoef WHERE formsemestre_id=:formsemestre_id"
|
||||
),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
# --- Suppression des item du menu custom
|
||||
req = "DELETE FROM notes_formsemestre_custommenu WHERE formsemestre_id=%(formsemestre_id)s"
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"DELETE FROM notes_formsemestre_custommenu WHERE formsemestre_id=:formsemestre_id"
|
||||
),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
# --- Suppression des formules
|
||||
req = "DELETE FROM notes_formsemestre_ue_computation_expr WHERE formsemestre_id=%(formsemestre_id)s"
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"DELETE FROM notes_formsemestre_ue_computation_expr WHERE formsemestre_id=:formsemestre_id"
|
||||
),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
# --- Suppression des preferences
|
||||
req = "DELETE FROM sco_prefs WHERE formsemestre_id=%(formsemestre_id)s"
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
db.session.execute(
|
||||
sa.text("DELETE FROM sco_prefs WHERE formsemestre_id=:formsemestre_id"),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
# --- Suppression des groupes et partitions
|
||||
req = """DELETE FROM group_membership
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"""
|
||||
DELETE FROM group_membership
|
||||
WHERE group_id IN
|
||||
(SELECT gm.group_id FROM group_membership gm, partition p, group_descr gd
|
||||
WHERE gm.group_id = gd.id AND gd.partition_id = p.id
|
||||
AND p.formsemestre_id=%(formsemestre_id)s)
|
||||
AND p.formsemestre_id=:formsemestre_id)
|
||||
"""
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
req = """DELETE FROM group_descr
|
||||
),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"""
|
||||
DELETE FROM group_descr
|
||||
WHERE id IN
|
||||
(SELECT gd.id FROM group_descr gd, partition p
|
||||
WHERE gd.partition_id = p.id
|
||||
AND p.formsemestre_id=%(formsemestre_id)s)
|
||||
AND p.formsemestre_id=:formsemestre_id)
|
||||
"""
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
req = "DELETE FROM partition WHERE formsemestre_id=%(formsemestre_id)s"
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
db.session.execute(
|
||||
sa.text("DELETE FROM partition WHERE formsemestre_id=:formsemestre_id"),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
# --- Responsables
|
||||
req = """DELETE FROM notes_formsemestre_responsables
|
||||
WHERE formsemestre_id=%(formsemestre_id)s"""
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"DELETE FROM notes_formsemestre_responsables WHERE formsemestre_id=:formsemestre_id"
|
||||
),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
# --- Etapes
|
||||
req = """DELETE FROM notes_formsemestre_etapes
|
||||
WHERE formsemestre_id=%(formsemestre_id)s"""
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"DELETE FROM notes_formsemestre_etapes WHERE formsemestre_id=:formsemestre_id"
|
||||
),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
# --- SemSets
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"DELETE FROM notes_semset_formsemestre WHERE formsemestre_id=:formsemestre_id"
|
||||
),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
# --- Dispenses d'UE
|
||||
req = """DELETE FROM "dispenseUE" WHERE formsemestre_id=%(formsemestre_id)s"""
|
||||
cursor.execute(req, {"formsemestre_id": formsemestre_id})
|
||||
db.session.execute(
|
||||
sa.text("""DELETE FROM "dispenseUE" WHERE formsemestre_id=:formsemestre_id"""),
|
||||
{"formsemestre_id": formsemestre_id},
|
||||
)
|
||||
# --- Destruction du semestre
|
||||
sco_formsemestre._formsemestreEditor.delete(cnx, formsemestre_id)
|
||||
db.session.delete(formsemestre)
|
||||
|
||||
# news
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_SEM,
|
||||
obj=formsemestre_id,
|
||||
text="Suppression du semestre %(titre)s" % sem,
|
||||
text=f"Suppression du semestre {titre_sem}",
|
||||
max_frequency=0,
|
||||
)
|
||||
|
||||
|
@ -1636,13 +1698,13 @@ def formsemestre_change_publication_bul(
|
|||
|
||||
def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
|
||||
"""Changement manuel des coefficients des UE capitalisées."""
|
||||
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
|
||||
if not ok:
|
||||
return err
|
||||
|
||||
footer = html_sco_header.sco_footer()
|
||||
help = """<p class="help">
|
||||
help_msg = """<p class="help">
|
||||
Seuls les modules ont un coefficient. Cependant, il est nécessaire d'affecter un coefficient aux UE capitalisée pour pouvoir les prendre en compte dans la moyenne générale.
|
||||
</p>
|
||||
<p class="help">ScoDoc calcule normalement le coefficient d'une UE comme la somme des
|
||||
|
@ -1665,17 +1727,16 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
|
|||
"""
|
||||
H = [
|
||||
html_sco_header.html_sem_header("Coefficients des UE du semestre"),
|
||||
help,
|
||||
help_msg,
|
||||
]
|
||||
#
|
||||
ues, modimpls = _get_sem_ues_modimpls(formsemestre_id)
|
||||
ues, modimpls = _get_sem_ues_modimpls(formsemestre)
|
||||
sum_coefs_by_ue_id = {}
|
||||
for ue in ues:
|
||||
ue["sum_coefs"] = sum(
|
||||
[
|
||||
mod["module"]["coefficient"]
|
||||
for mod in modimpls
|
||||
if mod["module"]["ue_id"] == ue["ue_id"]
|
||||
]
|
||||
sum_coefs_by_ue_id[ue.id] = sum(
|
||||
modimpl.module.coefficient or 0.0
|
||||
for modimpl in modimpls
|
||||
if modimpl.module.ue_id == ue.id
|
||||
)
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
|
@ -1684,20 +1745,20 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
|
|||
form = [("formsemestre_id", {"input_type": "hidden"})]
|
||||
for ue in ues:
|
||||
coefs = sco_formsemestre.formsemestre_uecoef_list(
|
||||
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue["ue_id"]}
|
||||
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue.id}
|
||||
)
|
||||
if coefs:
|
||||
initvalues["ue_" + str(ue["ue_id"])] = coefs[0]["coefficient"]
|
||||
initvalues["ue_" + str(ue.id)] = coefs[0]["coefficient"]
|
||||
else:
|
||||
initvalues["ue_" + str(ue["ue_id"])] = "auto"
|
||||
initvalues["ue_" + str(ue.id)] = "auto"
|
||||
descr = {
|
||||
"size": 10,
|
||||
"title": ue["acronyme"],
|
||||
"explanation": "somme coefs modules = %s" % ue["sum_coefs"],
|
||||
"title": ue.acronyme,
|
||||
"explanation": f"somme coefs modules = {sum_coefs_by_ue_id[ue.id]}",
|
||||
}
|
||||
if ue["ue_id"] == err_ue_id:
|
||||
if ue.id == err_ue_id:
|
||||
descr["dom_id"] = "erroneous_ue"
|
||||
form.append(("ue_" + str(ue["ue_id"]), descr))
|
||||
form.append(("ue_" + str(ue.id), descr))
|
||||
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
|
@ -1722,12 +1783,12 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
|
|||
# 1- supprime les coef qui ne sont plus forcés
|
||||
# 2- modifie ou cree les coefs
|
||||
ue_deleted = []
|
||||
ue_modified = []
|
||||
ue_modified: list[tuple[UniteEns, float]] = []
|
||||
msg = []
|
||||
for ue in ues:
|
||||
val = tf[2]["ue_" + str(ue["ue_id"])]
|
||||
val = tf[2]["ue_" + str(ue.id)]
|
||||
coefs = sco_formsemestre.formsemestre_uecoef_list(
|
||||
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue["ue_id"]}
|
||||
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue.id}
|
||||
)
|
||||
if val == "" or val == "auto":
|
||||
# supprime ce coef (il sera donc calculé automatiquement)
|
||||
|
@ -1737,13 +1798,11 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
|
|||
try:
|
||||
val = float(val)
|
||||
if (not coefs) or (coefs[0]["coefficient"] != val):
|
||||
ue["coef"] = val
|
||||
ue_modified.append(ue)
|
||||
except:
|
||||
ue_modified.append((ue, val))
|
||||
except ValueError:
|
||||
ok = False
|
||||
msg.append(
|
||||
"valeur invalide (%s) pour le coefficient de l'UE %s"
|
||||
% (val, ue["acronyme"])
|
||||
f"valeur invalide ({val}) pour le coefficient de l'UE {ue.acronyme}"
|
||||
)
|
||||
|
||||
if not ok:
|
||||
|
@ -1755,26 +1814,24 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
|
|||
)
|
||||
|
||||
# apply modifications
|
||||
for ue in ue_modified:
|
||||
for ue, val in ue_modified:
|
||||
sco_formsemestre.do_formsemestre_uecoef_edit_or_create(
|
||||
cnx, formsemestre_id, ue["ue_id"], ue["coef"]
|
||||
cnx, formsemestre_id, ue.id, val
|
||||
)
|
||||
for ue in ue_deleted:
|
||||
sco_formsemestre.do_formsemestre_uecoef_delete(
|
||||
cnx, formsemestre_id, ue["ue_id"]
|
||||
)
|
||||
sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre_id, ue.id)
|
||||
|
||||
if ue_modified or ue_deleted:
|
||||
message = ["""<h3>Modification effectuées</h3>"""]
|
||||
if ue_modified:
|
||||
message.append("""<h4>Coefs modifiés dans les UE:<h4><ul>""")
|
||||
for ue in ue_modified:
|
||||
message.append("<li>%(acronyme)s : %(coef)s</li>" % ue)
|
||||
for ue, val in ue_modified:
|
||||
message.append(f"<li>{ue.acronyme} : {val}</li>")
|
||||
message.append("</ul>")
|
||||
if ue_deleted:
|
||||
message.append("""<h4>Coefs supprimés dans les UE:<h4><ul>""")
|
||||
for ue in ue_deleted:
|
||||
message.append("<li>%(acronyme)s</li>" % ue)
|
||||
message.append(f"<li>{ue.acronyme}</li>")
|
||||
message.append("</ul>")
|
||||
else:
|
||||
message = ["""<h3>Aucune modification</h3>"""]
|
||||
|
@ -1792,21 +1849,19 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
|
|||
"""
|
||||
|
||||
|
||||
def _get_sem_ues_modimpls(formsemestre_id, modimpls=None):
|
||||
def _get_sem_ues_modimpls(
|
||||
formsemestre: FormSemestre,
|
||||
) -> tuple[list[UniteEns], list[ModuleImpl]]:
|
||||
"""Get liste des UE du semestre (à partir des moduleimpls)
|
||||
(utilisé quand on ne peut pas construire nt et faire nt.get_ues_stat_dict())
|
||||
"""
|
||||
if modimpls is None:
|
||||
modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
||||
uedict = {}
|
||||
modimpls = formsemestre.modimpls.all()
|
||||
for modimpl in modimpls:
|
||||
mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0]
|
||||
modimpl["module"] = mod
|
||||
if not mod["ue_id"] in uedict:
|
||||
ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0]
|
||||
uedict[ue["ue_id"]] = ue
|
||||
if not modimpl.module.ue_id in uedict:
|
||||
uedict[modimpl.module.ue.id] = modimpl.module.ue
|
||||
ues = list(uedict.values())
|
||||
ues.sort(key=lambda u: u["numero"])
|
||||
ues.sort(key=lambda u: u.numero)
|
||||
return ues, modimpls
|
||||
|
||||
|
||||
|
|
|
@ -191,12 +191,26 @@ def do_formsemestre_inscription_edit(args=None, formsemestre_id=None):
|
|||
) # > modif inscription semestre
|
||||
|
||||
|
||||
def do_formsemestre_desinscription(etudid, formsemestre_id):
|
||||
def check_if_has_decision_jury(
|
||||
formsemestre: FormSemestre, etudids: list[int] | set[int]
|
||||
):
|
||||
"raise exception if one of the etuds has a decision in formsemestre"
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
for etudid in etudids:
|
||||
if nt.etud_has_decision(etudid):
|
||||
etud = Identite.query.get(etudid)
|
||||
raise ScoValueError(
|
||||
f"""désinscription impossible: l'étudiant {etud.nomprenom} a
|
||||
une décision de jury (la supprimer avant si nécessaire)"""
|
||||
)
|
||||
|
||||
|
||||
def do_formsemestre_desinscription(
|
||||
etudid, formsemestre_id: int, check_has_dec_jury=True
|
||||
):
|
||||
"""Désinscription d'un étudiant.
|
||||
Si semestre extérieur et dernier inscrit, suppression de ce semestre.
|
||||
"""
|
||||
from app.scodoc import sco_formsemestre_edit
|
||||
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
etud = Identite.get_etud(etudid)
|
||||
# -- check lock
|
||||
|
@ -204,13 +218,8 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
|
|||
raise ScoValueError("désinscription impossible: semestre verrouille")
|
||||
|
||||
# -- Si decisions de jury, désinscription interdite
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
||||
if nt.etud_has_decision(etudid):
|
||||
raise ScoValueError(
|
||||
f"""désinscription impossible: l'étudiant {etud.nomprenom} a
|
||||
une décision de jury (la supprimer avant si nécessaire)"""
|
||||
)
|
||||
if check_has_dec_jury:
|
||||
check_if_has_decision_jury(formsemestre, [etudid])
|
||||
|
||||
insem = do_formsemestre_inscription_list(
|
||||
args={"formsemestre_id": formsemestre_id, "etudid": etudid}
|
||||
|
@ -247,17 +256,14 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
|
|||
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
|
||||
# --- Semestre extérieur
|
||||
if formsemestre.modalite == "EXT":
|
||||
inscrits = do_formsemestre_inscription_list(
|
||||
args={"formsemestre_id": formsemestre_id}
|
||||
)
|
||||
nbinscrits = len(inscrits)
|
||||
if nbinscrits == 0:
|
||||
if 0 == len(formsemestre.inscriptions):
|
||||
log(
|
||||
f"""do_formsemestre_desinscription:
|
||||
suppression du semestre extérieur {formsemestre}"""
|
||||
)
|
||||
flash("Semestre exterieur supprimé")
|
||||
sco_formsemestre_edit.do_formsemestre_delete(formsemestre_id)
|
||||
db.session.delete(formsemestre)
|
||||
db.session.commit()
|
||||
flash(f"Semestre extérieur supprimé: {formsemestre.titre_annee()}")
|
||||
|
||||
logdb(
|
||||
cnx,
|
||||
|
@ -305,18 +311,15 @@ def do_formsemestre_inscription_with_modules(
|
|||
# 2- inscrit aux groupes
|
||||
for group_id in group_ids:
|
||||
if group_id and group_id not in gdone:
|
||||
group = GroupDescr.query.get_or_404(group_id)
|
||||
_ = GroupDescr.query.get_or_404(group_id)
|
||||
sco_groups.set_group(etudid, group_id)
|
||||
gdone[group_id] = 1
|
||||
|
||||
# Inscription à tous les modules de ce semestre
|
||||
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
|
||||
formsemestre_id=formsemestre_id
|
||||
)
|
||||
for mod in modimpls:
|
||||
if mod["ue"]["type"] != UE_SPORT:
|
||||
for modimpl in formsemestre.modimpls:
|
||||
if modimpl.module.ue.type != UE_SPORT:
|
||||
sco_moduleimpl.do_moduleimpl_inscription_create(
|
||||
{"moduleimpl_id": mod["moduleimpl_id"], "etudid": etudid},
|
||||
{"moduleimpl_id": modimpl.id, "etudid": etudid},
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
# Mise à jour des inscriptions aux parcours:
|
||||
|
@ -531,19 +534,17 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
|
|||
if not sem["etat"]:
|
||||
raise ScoValueError("Modification impossible: semestre verrouille")
|
||||
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
etud = Identite.get_etud(etudid)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
||||
footer = html_sco_header.sco_footer()
|
||||
H = [
|
||||
html_sco_header.sco_header()
|
||||
+ "<h2>Inscription de %s aux modules de %s (%s - %s)</h2>"
|
||||
% (etud["nomprenom"], sem["titre_num"], sem["date_debut"], sem["date_fin"])
|
||||
html_sco_header.sco_header(),
|
||||
f"""<h2>Inscription de {etud.nomprenom} aux modules de {formsemestre.titre_mois()}</h2>""",
|
||||
]
|
||||
|
||||
# Cherche les moduleimpls et les inscriptions
|
||||
mods = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
|
||||
inscr = sco_moduleimpl.do_moduleimpl_inscription_list(etudid=etudid)
|
||||
# Formulaire
|
||||
modimpls_by_ue_ids = collections.defaultdict(list) # ue_id : [ moduleimpl_id ]
|
||||
|
@ -551,26 +552,26 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
|
|||
ues = []
|
||||
ue_ids = set()
|
||||
initvalues = {}
|
||||
for mod in mods:
|
||||
ue_id = mod["ue"]["ue_id"]
|
||||
for modimpl in formsemestre.modimpls:
|
||||
ue_id = modimpl.module.ue.id
|
||||
if not ue_id in ue_ids:
|
||||
ues.append(mod["ue"])
|
||||
ues.append(modimpl.module.ue)
|
||||
ue_ids.add(ue_id)
|
||||
modimpls_by_ue_ids[ue_id].append(mod["moduleimpl_id"])
|
||||
modimpls_by_ue_ids[ue_id].append(modimpl.id)
|
||||
|
||||
modimpls_by_ue_names[ue_id].append(
|
||||
"%s %s" % (mod["module"]["code"] or "", mod["module"]["titre"] or "")
|
||||
f"{modimpl.module.code or ''} {modimpl.module.titre or ''}"
|
||||
)
|
||||
vals = scu.get_request_args()
|
||||
if not vals.get("tf_submitted", False):
|
||||
# inscrit ?
|
||||
for ins in inscr:
|
||||
if ins["moduleimpl_id"] == mod["moduleimpl_id"]:
|
||||
key = "moduleimpls_%s" % ue_id
|
||||
if ins["moduleimpl_id"] == modimpl.id:
|
||||
key = f"moduleimpls_{ue_id}"
|
||||
if key in initvalues:
|
||||
initvalues[key].append(str(mod["moduleimpl_id"]))
|
||||
initvalues[key].append(str(modimpl.id))
|
||||
else:
|
||||
initvalues[key] = [str(mod["moduleimpl_id"])]
|
||||
initvalues[key] = [str(modimpl.id)]
|
||||
break
|
||||
|
||||
descr = [
|
||||
|
@ -578,35 +579,38 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
|
|||
("etudid", {"input_type": "hidden"}),
|
||||
]
|
||||
for ue in ues:
|
||||
ue_id = ue["ue_id"]
|
||||
ue_descr = ue["acronyme"]
|
||||
if ue["type"] != UE_STANDARD:
|
||||
ue_descr += " <em>%s</em>" % UE_TYPE_NAME[ue["type"]]
|
||||
ue_id = ue.id
|
||||
ue_descr = ue.acronyme
|
||||
if ue.type != UE_STANDARD:
|
||||
ue_descr += f" <em>{UE_TYPE_NAME[ue.type]}</em>"
|
||||
ue_status = nt.get_etud_ue_status(etudid, ue_id)
|
||||
if ue_status and ue_status["is_capitalized"]:
|
||||
sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"])
|
||||
ue_descr += (
|
||||
' <a class="discretelink" href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" title="%s">(capitalisée le %s)'
|
||||
% (
|
||||
sem_origin["formsemestre_id"],
|
||||
etudid,
|
||||
sem_origin["titreannee"],
|
||||
ndb.DateISOtoDMY(ue_status["event_date"]),
|
||||
)
|
||||
)
|
||||
ue_descr += f"""
|
||||
<a class="discretelink" href="{ url_for(
|
||||
'notes.formsemestre_bulletinetud', scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=sem_origin["formsemestre_id"],
|
||||
etudid = etudid
|
||||
)}" title="{sem_origin['titreannee']}">(capitalisée le {
|
||||
ndb.DateISOtoDMY(ue_status["event_date"])
|
||||
})
|
||||
"""
|
||||
descr.append(
|
||||
(
|
||||
"sec_%s" % ue_id,
|
||||
f"sec_{ue_id}",
|
||||
{
|
||||
"input_type": "separator",
|
||||
"title": """<b>%s :</b> <a href="#" onclick="chkbx_select('%s', true);">inscrire</a> | <a href="#" onclick="chkbx_select('%s', false);">désinscrire</a> à tous les modules"""
|
||||
% (ue_descr, ue_id, ue_id),
|
||||
"title": f"""<b>{ue_descr} :</b>
|
||||
<a href="#" onclick="chkbx_select('{ue_id}', true);">inscrire</a> | <a
|
||||
href="#" onclick="chkbx_select('{ue_id}', false);">désinscrire</a>
|
||||
à tous les modules
|
||||
""",
|
||||
},
|
||||
)
|
||||
)
|
||||
descr.append(
|
||||
(
|
||||
"moduleimpls_%s" % ue_id,
|
||||
f"moduleimpls_{ue_id}",
|
||||
{
|
||||
"input_type": "checkbox",
|
||||
"title": "",
|
||||
|
@ -654,112 +658,98 @@ function chkbx_select(field_id, state) {
|
|||
"""
|
||||
)
|
||||
return "\n".join(H) + "\n" + tf[1] + footer
|
||||
elif tf[0] == -1:
|
||||
if tf[0] == -1:
|
||||
return flask.redirect(
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
else:
|
||||
# Inscriptions aux modules choisis
|
||||
# il faut desinscrire des modules qui ne figurent pas
|
||||
# et inscrire aux autres, sauf si deja inscrit
|
||||
a_desinscrire = {}.fromkeys([x["moduleimpl_id"] for x in mods])
|
||||
insdict = {}
|
||||
for ins in inscr:
|
||||
insdict[ins["moduleimpl_id"]] = ins
|
||||
for ue in ues:
|
||||
ue_id = ue["ue_id"]
|
||||
for moduleimpl_id in [int(x) for x in tf[2]["moduleimpls_%s" % ue_id]]:
|
||||
if moduleimpl_id in a_desinscrire:
|
||||
del a_desinscrire[moduleimpl_id]
|
||||
# supprime ceux auxquel pas inscrit
|
||||
moduleimpls_a_desinscrire = list(a_desinscrire.keys())
|
||||
for moduleimpl_id in moduleimpls_a_desinscrire:
|
||||
if moduleimpl_id not in insdict:
|
||||
|
||||
# Inscriptions aux modules choisis
|
||||
# il faut desinscrire des modules qui ne figurent pas
|
||||
# et inscrire aux autres, sauf si deja inscrit
|
||||
a_desinscrire = {}.fromkeys([x.id for x in formsemestre.modimpls])
|
||||
insdict = {}
|
||||
for ins in inscr:
|
||||
insdict[ins["moduleimpl_id"]] = ins
|
||||
for ue in ues:
|
||||
for moduleimpl_id in [int(x) for x in tf[2][f"moduleimpls_{ue.id}"]]:
|
||||
if moduleimpl_id in a_desinscrire:
|
||||
del a_desinscrire[moduleimpl_id]
|
||||
# supprime ceux auxquel pas inscrit
|
||||
moduleimpls_a_desinscrire = list(a_desinscrire.keys())
|
||||
for moduleimpl_id in moduleimpls_a_desinscrire:
|
||||
if moduleimpl_id not in insdict:
|
||||
del a_desinscrire[moduleimpl_id]
|
||||
|
||||
a_inscrire = set()
|
||||
for ue in ues:
|
||||
ue_id = ue["ue_id"]
|
||||
a_inscrire.update(
|
||||
int(x) for x in tf[2]["moduleimpls_%s" % ue_id]
|
||||
) # conversion en int !
|
||||
# supprime ceux auquel deja inscrit:
|
||||
for ins in inscr:
|
||||
if ins["moduleimpl_id"] in a_inscrire:
|
||||
a_inscrire.remove(ins["moduleimpl_id"])
|
||||
# dict des modules:
|
||||
modsdict = {}
|
||||
for mod in mods:
|
||||
modsdict[mod["moduleimpl_id"]] = mod
|
||||
#
|
||||
if (not a_inscrire) and (not a_desinscrire):
|
||||
H.append(
|
||||
"""<h3>Aucune modification à effectuer</h3>
|
||||
<p><a class="stdlink" href="%s">retour à la fiche étudiant</a></p>
|
||||
"""
|
||||
% url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
return "\n".join(H) + footer
|
||||
|
||||
H.append("<h3>Confirmer les modifications:</h3>")
|
||||
if a_desinscrire:
|
||||
H.append(
|
||||
"<p>%s va être <b>désinscrit%s</b> des modules:<ul><li>"
|
||||
% (etud["nomprenom"], etud["ne"])
|
||||
)
|
||||
H.append(
|
||||
"</li><li>".join(
|
||||
[
|
||||
"%s (%s)"
|
||||
% (
|
||||
modsdict[x]["module"]["titre"],
|
||||
modsdict[x]["module"]["code"] or "(module sans code)",
|
||||
)
|
||||
for x in a_desinscrire
|
||||
]
|
||||
)
|
||||
+ "</p>"
|
||||
)
|
||||
H.append("</li></ul>")
|
||||
if a_inscrire:
|
||||
H.append(
|
||||
"<p>%s va être <b>inscrit%s</b> aux modules:<ul><li>"
|
||||
% (etud["nomprenom"], etud["ne"])
|
||||
)
|
||||
H.append(
|
||||
"</li><li>".join(
|
||||
[
|
||||
"%s (%s)"
|
||||
% (
|
||||
modsdict[x]["module"]["titre"],
|
||||
modsdict[x]["module"]["code"] or "(module sans code)",
|
||||
)
|
||||
for x in a_inscrire
|
||||
]
|
||||
)
|
||||
+ "</p>"
|
||||
)
|
||||
H.append("</li></ul>")
|
||||
modulesimpls_ainscrire = ",".join(str(x) for x in a_inscrire)
|
||||
modulesimpls_adesinscrire = ",".join(str(x) for x in a_desinscrire)
|
||||
a_inscrire = set()
|
||||
for ue in ues:
|
||||
a_inscrire.update(
|
||||
int(x) for x in tf[2][f"moduleimpls_{ue.id}"]
|
||||
) # conversion en int !
|
||||
# supprime ceux auquel deja inscrit:
|
||||
for ins in inscr:
|
||||
if ins["moduleimpl_id"] in a_inscrire:
|
||||
a_inscrire.remove(ins["moduleimpl_id"])
|
||||
# dict des modules:
|
||||
modimpls_by_id = {modimpl.id: modimpl for modimpl in formsemestre.modimpls}
|
||||
#
|
||||
if (not a_inscrire) and (not a_desinscrire):
|
||||
H.append(
|
||||
"""<form action="do_moduleimpl_incription_options">
|
||||
<input type="hidden" name="etudid" value="%s"/>
|
||||
<input type="hidden" name="modulesimpls_ainscrire" value="%s"/>
|
||||
<input type="hidden" name="modulesimpls_adesinscrire" value="%s"/>
|
||||
<input type ="submit" value="Confirmer"/>
|
||||
<input type ="button" value="Annuler" onclick="document.location='%s';"/>
|
||||
</form>
|
||||
f"""<h3>Aucune modification à effectuer</h3>
|
||||
<p><a class="stdlink" href="{
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">retour à la fiche étudiant</a></p>
|
||||
"""
|
||||
% (
|
||||
etudid,
|
||||
modulesimpls_ainscrire,
|
||||
modulesimpls_adesinscrire,
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
)
|
||||
)
|
||||
return "\n".join(H) + footer
|
||||
|
||||
H.append("<h3>Confirmer les modifications:</h3>")
|
||||
if a_desinscrire:
|
||||
H.append(
|
||||
f"""<p>{etud.nomprenom} va être <b>désinscrit{etud.e}</b> des modules:<ul><li>"""
|
||||
)
|
||||
H.append(
|
||||
"</li><li>".join(
|
||||
[
|
||||
f"""{modimpls_by_id[x].module.titre or ''} ({
|
||||
modimpls_by_id[x].module.code or '(module sans code)'})"""
|
||||
for x in a_desinscrire
|
||||
]
|
||||
)
|
||||
+ "</p>"
|
||||
)
|
||||
H.append("</li></ul>")
|
||||
if a_inscrire:
|
||||
H.append(
|
||||
f"""<p>{etud.nomprenom} va être <b>inscrit{etud.e}</b> aux modules:<ul><li>"""
|
||||
)
|
||||
H.append(
|
||||
"</li><li>".join(
|
||||
[
|
||||
f"""{modimpls_by_id[x].module.titre or ''} ({
|
||||
modimpls_by_id[x].module.code or '(module sans code)'})"""
|
||||
for x in a_inscrire
|
||||
]
|
||||
)
|
||||
+ "</p>"
|
||||
)
|
||||
H.append("</li></ul>")
|
||||
modulesimpls_ainscrire = ",".join(str(x) for x in a_inscrire)
|
||||
modulesimpls_adesinscrire = ",".join(str(x) for x in a_desinscrire)
|
||||
H.append(
|
||||
f"""
|
||||
<form action="do_moduleimpl_incription_options">
|
||||
<input type="hidden" name="etudid" value="{etudid}"/>
|
||||
<input type="hidden" name="modulesimpls_ainscrire" value="{modulesimpls_ainscrire}"/>
|
||||
<input type="hidden" name="modulesimpls_adesinscrire" value="{modulesimpls_adesinscrire}"/>
|
||||
<input type ="submit" value="Confirmer"/>
|
||||
<input type ="button" value="Annuler" onclick="document.location='{
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}';"/>
|
||||
</form>
|
||||
"""
|
||||
)
|
||||
return "\n".join(H) + footer
|
||||
|
||||
|
||||
def do_moduleimpl_incription_options(
|
||||
etudid, modulesimpls_ainscrire, modulesimpls_adesinscrire
|
||||
|
@ -784,9 +774,7 @@ def do_moduleimpl_incription_options(
|
|||
# verifie que ce module existe bien
|
||||
mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
|
||||
if len(mods) != 1:
|
||||
raise ScoValueError(
|
||||
"inscription: invalid moduleimpl_id: %s" % moduleimpl_id
|
||||
)
|
||||
raise ScoValueError(f"inscription: invalid moduleimpl_id: {moduleimpl_id}")
|
||||
mod = mods[0]
|
||||
sco_moduleimpl.do_moduleimpl_inscription_create(
|
||||
{"moduleimpl_id": moduleimpl_id, "etudid": etudid},
|
||||
|
@ -798,7 +786,7 @@ def do_moduleimpl_incription_options(
|
|||
mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
|
||||
if len(mods) != 1:
|
||||
raise ScoValueError(
|
||||
"desinscription: invalid moduleimpl_id: %s" % moduleimpl_id
|
||||
f"desinscription: invalid moduleimpl_id: {moduleimpl_id}"
|
||||
)
|
||||
mod = mods[0]
|
||||
inscr = sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||
|
@ -806,8 +794,7 @@ def do_moduleimpl_incription_options(
|
|||
)
|
||||
if not inscr:
|
||||
raise ScoValueError(
|
||||
"pas inscrit a ce module ! (etudid=%s, moduleimpl_id=%s)"
|
||||
% (etudid, moduleimpl_id)
|
||||
f"pas inscrit a ce module ! (etudid={etudid}, moduleimpl_id={moduleimpl_id})"
|
||||
)
|
||||
oid = inscr[0]["moduleimpl_inscription_id"]
|
||||
sco_moduleimpl.do_moduleimpl_inscription_delete(
|
||||
|
@ -816,11 +803,13 @@ def do_moduleimpl_incription_options(
|
|||
|
||||
H = [
|
||||
html_sco_header.sco_header(),
|
||||
"""<h3>Modifications effectuées</h3>
|
||||
<p><a class="stdlink" href="%s">
|
||||
Retour à la fiche étudiant</a></p>
|
||||
"""
|
||||
% url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
f"""<h3>Modifications effectuées</h3>
|
||||
<p><a class="stdlink" href="{
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">
|
||||
Retour à la fiche étudiant</a>
|
||||
</p>
|
||||
""",
|
||||
html_sco_header.sco_footer(),
|
||||
]
|
||||
return "\n".join(H)
|
||||
|
@ -864,49 +853,59 @@ def formsemestre_inscrits_ailleurs(formsemestre_id):
|
|||
"""Page listant les étudiants inscrits dans un autre semestre
|
||||
dont les dates recouvrent le semestre indiqué.
|
||||
"""
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
H = [
|
||||
html_sco_header.html_sem_header(
|
||||
"Inscriptions multiples parmi les étudiants du semestre ",
|
||||
init_qtip=True,
|
||||
javascripts=["js/etud_info.js"],
|
||||
)
|
||||
]
|
||||
insd = list_inscrits_ailleurs(formsemestre_id)
|
||||
# liste ordonnée par nom
|
||||
etudlist = [
|
||||
sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
for etudid in insd.keys()
|
||||
if insd[etudid]
|
||||
]
|
||||
etudlist.sort(key=lambda x: x["nom"])
|
||||
etudlist = [Identite.get_etud(etudid) for etudid, sems in insd.items() if sems]
|
||||
etudlist.sort(key=lambda x: x.sort_key)
|
||||
if etudlist:
|
||||
H.append("<ul>")
|
||||
for etud in etudlist:
|
||||
H.append(
|
||||
'<li><a href="%s" class="discretelink">%s</a> : '
|
||||
% (
|
||||
f"""<li><a id="{etud.id}" class="discretelink etudinfo"
|
||||
href={
|
||||
url_for(
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud["etudid"],
|
||||
),
|
||||
etud["nomprenom"],
|
||||
)
|
||||
etudid=etud.id,
|
||||
)
|
||||
}
|
||||
>{etud.nomprenom}</a> :
|
||||
"""
|
||||
)
|
||||
l = []
|
||||
for s in insd[etud["etudid"]]:
|
||||
for s in insd[etud.id]:
|
||||
l.append(
|
||||
'<a class="discretelink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a>'
|
||||
% s
|
||||
f"""<a class="discretelink" href="{
|
||||
url_for('notes.formsemestre_status',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
|
||||
)}">{s['titremois']}</a>"""
|
||||
)
|
||||
H.append(", ".join(l))
|
||||
H.append("</li>")
|
||||
H.append("</ul>")
|
||||
H.append("<p>Total: %d étudiants concernés.</p>" % len(etudlist))
|
||||
H.append(
|
||||
"""<p class="help">Ces étudiants sont inscrits dans le semestre sélectionné et aussi dans d'autres semestres qui se déroulent en même temps ! <br>Sauf exception, cette situation est anormale:</p>
|
||||
f"""
|
||||
</ul>
|
||||
<p><b>Total: {len(etudlist)} étudiants concernés.</b></p>
|
||||
|
||||
<p class="help">Ces étudiants sont inscrits dans le semestre sélectionné et aussi
|
||||
dans d'autres semestres qui se déroulent en même temps !
|
||||
</p>
|
||||
<p>
|
||||
<b>Sauf exception, cette situation est anormale:</b>
|
||||
</p>
|
||||
<ul>
|
||||
<li>vérifier que les dates des semestres se suivent sans se chevaucher</li>
|
||||
<li>ou si besoin désinscrire le(s) étudiant(s) de l'un des semestres (via leurs fiches individuelles).</li>
|
||||
<li>vérifier que les dates des semestres se suivent <em>sans se chevaucher</em>
|
||||
</li>
|
||||
<li>ou bien si besoin désinscrire le(s) étudiant(s) de l'un des semestres
|
||||
(via leurs fiches individuelles).
|
||||
</li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -909,37 +909,6 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||
return "\n".join(H)
|
||||
|
||||
|
||||
def html_expr_diagnostic(diagnostics):
|
||||
"""Affiche messages d'erreur des formules utilisateurs"""
|
||||
H = []
|
||||
H.append('<div class="ue_warning">Erreur dans des formules utilisateurs:<ul>')
|
||||
last_id, last_msg = None, None
|
||||
for diag in diagnostics:
|
||||
if "moduleimpl_id" in diag:
|
||||
mod = sco_moduleimpl.moduleimpl_withmodule_list(
|
||||
moduleimpl_id=diag["moduleimpl_id"]
|
||||
)[0]
|
||||
H.append(
|
||||
'<li>module <a href="moduleimpl_status?moduleimpl_id=%s">%s</a>: %s</li>'
|
||||
% (
|
||||
diag["moduleimpl_id"],
|
||||
mod["module"]["abbrev"] or mod["module"]["code"] or "?",
|
||||
diag["msg"],
|
||||
)
|
||||
)
|
||||
else:
|
||||
if diag["ue_id"] != last_id or diag["msg"] != last_msg:
|
||||
ue = sco_edit_ue.ue_list({"ue_id": diag["ue_id"]})[0]
|
||||
H.append(
|
||||
'<li>UE "%s": %s</li>'
|
||||
% (ue["acronyme"] or ue["titre"] or "?", diag["msg"])
|
||||
)
|
||||
last_id, last_msg = diag["ue_id"], diag["msg"]
|
||||
|
||||
H.append("</ul></div>")
|
||||
return "".join(H)
|
||||
|
||||
|
||||
def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None):
|
||||
"""En-tête HTML des pages "semestre" """
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
|
@ -1081,9 +1050,6 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
|
|||
>Toutes évaluations (même incomplètes) visibles</div>"""
|
||||
)
|
||||
|
||||
if nt.expr_diagnostics:
|
||||
H.append(html_expr_diagnostic(nt.expr_diagnostics))
|
||||
|
||||
if nt.parcours.APC_SAE:
|
||||
# BUT: tableau ressources puis SAE
|
||||
ressources = [
|
||||
|
@ -1207,7 +1173,8 @@ def formsemestre_tableau_modules(
|
|||
moduleimpl_id=modimpl.id,
|
||||
)
|
||||
mod_descr = "Module " + (mod.titre or "")
|
||||
if mod.is_apc():
|
||||
is_apc = mod.is_apc() # SAE ou ressource
|
||||
if is_apc:
|
||||
coef_descr = ", ".join(
|
||||
[
|
||||
f"{ue.acronyme}: {co}"
|
||||
|
@ -1227,6 +1194,7 @@ def formsemestre_tableau_modules(
|
|||
[u.get_nomcomplet() for u in modimpl.enseignants]
|
||||
)
|
||||
mod_nb_inscrits = nt.modimpls_results[modimpl.id].nb_inscrits_module
|
||||
mod_is_conforme = modimpl.check_apc_conformity(nt)
|
||||
ue = modimpl.module.ue
|
||||
if show_ues and (prev_ue_id != ue.id):
|
||||
prev_ue_id = ue.id
|
||||
|
@ -1234,10 +1202,12 @@ def formsemestre_tableau_modules(
|
|||
if use_ue_coefs:
|
||||
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_title">{titre}</span>
|
||||
</td><td colspan="2">"""
|
||||
f"""<tr class="formsemestre_status_ue">
|
||||
<td colspan="4">
|
||||
<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(
|
||||
|
@ -1246,7 +1216,7 @@ def formsemestre_tableau_modules(
|
|||
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="{
|
||||
<span class="warning">formule inutilisée en ScoDoc 9: <a href="{
|
||||
url_for("notes.delete_ue_expr", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ue_id=ue.id )
|
||||
}
|
||||
">supprimer</a></span>"""
|
||||
|
@ -1260,21 +1230,26 @@ def formsemestre_tableau_modules(
|
|||
fontorange = ""
|
||||
|
||||
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"]
|
||||
and not etat["nb_evals_blocked"] > 0
|
||||
):
|
||||
H.append(f'<tr class="formsemestre_status_green{fontorange}">')
|
||||
tr_classes = f"formsemestre_status_green{fontorange}"
|
||||
else:
|
||||
H.append(f'<tr class="formsemestre_status{fontorange}">')
|
||||
|
||||
tr_classes = f"formsemestre_status{fontorange}"
|
||||
if etat["attente"]:
|
||||
tr_classes += " modimpl_attente"
|
||||
if not mod_is_conforme:
|
||||
tr_classes += " modimpl_non_conforme"
|
||||
if etat["nb_evals_blocked"] > 0:
|
||||
tr_classes += " modimpl_has_blocked"
|
||||
H.append(
|
||||
f"""
|
||||
<td class="formsemestre_status_code""><a
|
||||
<tr class="{tr_classes}">
|
||||
<td class="formsemestre_status_code"><a
|
||||
href="{moduleimpl_status_url}"
|
||||
title="{mod_descr}" class="stdlink">{mod.code}</a></td>
|
||||
<td class="scotext"><a href="{moduleimpl_status_url}" title="{mod_descr}"
|
||||
|
@ -1312,17 +1287,20 @@ def formsemestre_tableau_modules(
|
|||
ModuleType.SAE,
|
||||
):
|
||||
H.append('<td class="evals">')
|
||||
nb_evals = (
|
||||
etat["nb_evals_completes"]
|
||||
+ etat["nb_evals_en_cours"]
|
||||
+ etat["nb_evals_vides"]
|
||||
)
|
||||
nb_evals = etat["nb_evals"]
|
||||
if nb_evals != 0:
|
||||
if etat["nb_evals_blocked"] > 0:
|
||||
blocked_txt = f"""<span class="nb_evals_blocked">{
|
||||
etat["nb_evals_blocked"]} bloquée{'s'
|
||||
if etat["nb_evals_blocked"] > 1 else ''}</span>"""
|
||||
else:
|
||||
blocked_txt = ""
|
||||
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>"""
|
||||
{etat["nb_evals_completes"]} ok {blocked_txt}
|
||||
</a>"""
|
||||
)
|
||||
if etat["nb_evals_en_cours"] > 0:
|
||||
H.append(
|
||||
|
@ -1334,7 +1312,12 @@ def formsemestre_tableau_modules(
|
|||
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>"""
|
||||
title="Il y a des notes en attente"><span class="evals_attente">en attente</span></a></span>"""
|
||||
)
|
||||
if not mod_is_conforme:
|
||||
H.append(
|
||||
f""" <span><a class="redlink" href="{moduleimpl_status_url}"
|
||||
title="évaluations non conformes">[non conforme]</a></span>"""
|
||||
)
|
||||
elif mod.module_type == ModuleType.MALUS:
|
||||
nb_malus_notes = sum(
|
||||
|
|
|
@ -34,7 +34,7 @@ from flask import url_for, flash, g, request
|
|||
from flask_login import current_user
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app.models.etudiants import Identite
|
||||
from app.models import Identite, Evaluation
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import db, log
|
||||
|
@ -232,7 +232,9 @@ def formsemestre_validation_etud_form(
|
|||
H.append(
|
||||
tf_error_message(
|
||||
f"""Impossible de statuer sur cet étudiant: il a des notes en
|
||||
attente dans des évaluations de ce semestre (voir <a href="{
|
||||
attente dans des évaluations de ce semestre (voir
|
||||
<a class="stdlink"
|
||||
href="{
|
||||
url_for( "notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
||||
}">tableau de bord</a>)
|
||||
|
@ -241,6 +243,26 @@ def formsemestre_validation_etud_form(
|
|||
)
|
||||
return "\n".join(H + footer)
|
||||
|
||||
evaluations_a_debloquer = Evaluation.get_evaluations_blocked_for_etud(
|
||||
formsemestre, etud
|
||||
)
|
||||
if evaluations_a_debloquer:
|
||||
links_evals = [
|
||||
f"""<a class="stdlink" href="{url_for(
|
||||
'notes.evaluation_listenotes', scodoc_dept=g.scodoc_dept, evaluation_id=e.id
|
||||
)}">{e.description} en {e.moduleimpl.module.code}</a>"""
|
||||
for e in evaluations_a_debloquer
|
||||
]
|
||||
H.append(
|
||||
tf_error_message(
|
||||
f"""Impossible de statuer sur cet étudiant:
|
||||
il a des notes dans des évaluations qui seront débloquées plus tard:
|
||||
voir {", ".join(links_evals)}
|
||||
"""
|
||||
)
|
||||
)
|
||||
return "\n".join(H + footer)
|
||||
|
||||
# Infos si pas de semestre précédent
|
||||
if not Se.prev:
|
||||
if Se.sem["semestre_id"] == 1:
|
||||
|
@ -700,8 +722,8 @@ def formsemestre_recap_parcours_table(
|
|||
f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
|
||||
)
|
||||
# Absences (nb d'abs non just. dans ce semestre)
|
||||
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||
H.append(f"""<td class="rcp_abs">{nbabs - nbabsjust}</td>""")
|
||||
nbabsnj = sco_assiduites.get_assiduites_count(etudid, sem)[0]
|
||||
H.append(f"""<td class="rcp_abs">{nbabsnj}</td>""")
|
||||
|
||||
# UEs
|
||||
for ue in ues:
|
||||
|
@ -1217,7 +1239,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
|
|||
<div id="ue_list_code" class="sco_box sco_green_bg">
|
||||
<!-- filled by ue_sharing_code -->
|
||||
</div>
|
||||
{check_formation_ues(formation.id)[0]}
|
||||
{check_formation_ues(formation)[0]}
|
||||
{html_sco_header.sco_footer()}
|
||||
"""
|
||||
|
||||
|
@ -1376,15 +1398,14 @@ def _invalidate_etud_formation_caches(etudid, formation_id):
|
|||
) # > modif decision UE (inval tous semestres avec cet etudiant, ok mais conservatif)
|
||||
|
||||
|
||||
def check_formation_ues(formation_id):
|
||||
def check_formation_ues(formation: Formation) -> tuple[str, dict[int, list[UniteEns]]]:
|
||||
"""Verifie que les UE d'une formation sont chacune utilisée dans un seul semestre_id
|
||||
Si ce n'est pas le cas, c'est probablement (mais pas forcément) une erreur de
|
||||
définition du programme: cette fonction retourne un bout de HTML
|
||||
à afficher pour prévenir l'utilisateur, ou '' si tout est ok.
|
||||
"""
|
||||
ues = sco_edit_ue.ue_list({"formation_id": formation_id})
|
||||
ue_multiples = {} # { ue_id : [ liste des formsemestre ] }
|
||||
for ue in ues:
|
||||
for ue in formation.ues:
|
||||
# formsemestres utilisant cette ue ?
|
||||
sems = ndb.SimpleDictFetch(
|
||||
"""SELECT DISTINCT sem.id AS formsemestre_id, sem.*
|
||||
|
@ -1394,13 +1415,13 @@ def check_formation_ues(formation_id):
|
|||
AND mi.formsemestre_id = sem.id
|
||||
AND mod.ue_id = %(ue_id)s
|
||||
""",
|
||||
{"ue_id": ue["ue_id"], "formation_id": formation_id},
|
||||
{"ue_id": ue.id, "formation_id": formation.id},
|
||||
)
|
||||
semestre_ids = set([x["semestre_id"] for x in sems])
|
||||
semestre_ids = {x["semestre_id"] for x in sems}
|
||||
if (
|
||||
len(semestre_ids) > 1
|
||||
): # plusieurs semestres d'indices differents dans le cursus
|
||||
ue_multiples[ue["ue_id"]] = sems
|
||||
ue_multiples[ue.id] = sems
|
||||
|
||||
if not ue_multiples:
|
||||
return "", {}
|
||||
|
@ -1416,20 +1437,20 @@ def check_formation_ues(formation_id):
|
|||
<ul>
|
||||
"""
|
||||
]
|
||||
for ue in ues:
|
||||
if ue["ue_id"] in ue_multiples:
|
||||
for ue in formation.ues:
|
||||
if ue.id in ue_multiples:
|
||||
sems = [
|
||||
sco_formsemestre.get_formsemestre(x["formsemestre_id"])
|
||||
for x in ue_multiples[ue["ue_id"]]
|
||||
for x in ue_multiples[ue.id]
|
||||
]
|
||||
slist = ", ".join(
|
||||
[
|
||||
"""%(titreannee)s (<em>semestre <b class="fontred">%(semestre_id)s</b></em>)"""
|
||||
% s
|
||||
f"""{s['titreannee']
|
||||
} (<em>semestre <b class="fontred">{s['semestre_id']}</b></em>)"""
|
||||
for s in sems
|
||||
]
|
||||
)
|
||||
H.append("<li><b>%s</b> : %s</li>" % (ue["acronyme"], slist))
|
||||
H.append(f"<li><b>{ue.acronyme}</b> : {slist}</li>")
|
||||
H.append("</ul></div>")
|
||||
|
||||
return "\n".join(H), ue_multiples
|
||||
|
|
|
@ -331,6 +331,7 @@ class DisplayedGroupsInfos:
|
|||
empty_list_select_all=True,
|
||||
moduleimpl_id=None, # used to find formsemestre when unspecified
|
||||
):
|
||||
group_ids = [] if group_ids is None else group_ids
|
||||
if isinstance(group_ids, int):
|
||||
if group_ids:
|
||||
group_ids = [group_ids] # cas ou un seul parametre, pas de liste
|
||||
|
@ -466,6 +467,10 @@ class DisplayedGroupsInfos:
|
|||
else None
|
||||
)
|
||||
|
||||
def get_groups_key(self) -> str:
|
||||
"clé identifiant les groupes sélectionnés, utile pour cache"
|
||||
return "-".join(str(x) for x in sorted(self.group_ids))
|
||||
|
||||
|
||||
# Ancien ZScolar.group_list renommé ici en group_table
|
||||
def groups_table(
|
||||
|
@ -514,10 +519,11 @@ def groups_table(
|
|||
"paiementinscription_str": "Paiement",
|
||||
"etudarchive": "Fichiers",
|
||||
"annotations_str": "Annotations",
|
||||
"bourse_str": "Boursier",
|
||||
"bourse_str": "Boursier", # requière ViewEtudData
|
||||
"etape": "Etape",
|
||||
"semestre_groupe": "Semestre-Groupe", # pour Moodle
|
||||
"annee": "annee_admission",
|
||||
"nationalite": "nationalite", # requière ViewEtudData
|
||||
}
|
||||
|
||||
# ajoute colonnes pour groupes
|
||||
|
@ -559,53 +565,61 @@ def groups_table(
|
|||
moodle_sem_name = groups_infos.formsemestre["session_id"]
|
||||
moodle_groupenames = set()
|
||||
# ajoute liens
|
||||
for etud in groups_infos.members:
|
||||
if etud["email"]:
|
||||
etud["_email_target"] = "mailto:" + etud["email"]
|
||||
for etud_info in groups_infos.members:
|
||||
if etud_info["email"]:
|
||||
etud_info["_email_target"] = "mailto:" + etud_info["email"]
|
||||
else:
|
||||
etud["_email_target"] = ""
|
||||
if etud["emailperso"]:
|
||||
etud["_emailperso_target"] = "mailto:" + etud["emailperso"]
|
||||
etud_info["_email_target"] = ""
|
||||
if etud_info["emailperso"]:
|
||||
etud_info["_emailperso_target"] = "mailto:" + etud_info["emailperso"]
|
||||
else:
|
||||
etud["_emailperso_target"] = ""
|
||||
etud_info["_emailperso_target"] = ""
|
||||
fiche_url = url_for(
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud_info["etudid"]
|
||||
)
|
||||
etud["_nom_disp_target"] = fiche_url
|
||||
etud["_nom_disp_order"] = etud_sort_key(etud)
|
||||
etud["_prenom_target"] = fiche_url
|
||||
etud_info["_nom_disp_target"] = fiche_url
|
||||
etud_info["_nom_disp_order"] = etud_sort_key(etud_info)
|
||||
etud_info["_prenom_target"] = fiche_url
|
||||
|
||||
etud["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"])
|
||||
etud["bourse_str"] = "oui" if etud["boursier"] else "non"
|
||||
if etud["etat"] == "D":
|
||||
etud["_css_row_class"] = "etuddem"
|
||||
etud_info["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (
|
||||
etud_info["etudid"]
|
||||
)
|
||||
etud_info["bourse_str"] = "oui" if etud_info["boursier"] else "non"
|
||||
if etud_info["etat"] == "D":
|
||||
etud_info["_css_row_class"] = "etuddem"
|
||||
# et groupes:
|
||||
for partition_id in etud["partitions"]:
|
||||
etud[partition_id] = etud["partitions"][partition_id]["group_name"]
|
||||
for partition_id in etud_info["partitions"]:
|
||||
etud_info[partition_id] = etud_info["partitions"][partition_id][
|
||||
"group_name"
|
||||
]
|
||||
# Ajoute colonne pour moodle: semestre_groupe, de la forme
|
||||
# RT-DUT-FI-S3-2021-PARTITION-GROUPE
|
||||
moodle_groupename = []
|
||||
if groups_infos.selected_partitions:
|
||||
# il y a des groupes selectionnes, utilise leurs partitions
|
||||
for partition_id in groups_infos.selected_partitions:
|
||||
if partition_id in etud["partitions"]:
|
||||
if partition_id in etud_info["partitions"]:
|
||||
moodle_groupename.append(
|
||||
partitions_name[partition_id]
|
||||
+ "-"
|
||||
+ etud["partitions"][partition_id]["group_name"]
|
||||
+ etud_info["partitions"][partition_id]["group_name"]
|
||||
)
|
||||
else:
|
||||
# pas de groupes sélectionnés: prend le premier s'il y en a un
|
||||
moodle_groupename = ["tous"]
|
||||
if etud["partitions"]:
|
||||
for p in etud["partitions"].items(): # partitions is an OrderedDict
|
||||
if etud_info["partitions"]:
|
||||
for p in etud_info[
|
||||
"partitions"
|
||||
].items(): # partitions is an OrderedDict
|
||||
moodle_groupename = [
|
||||
partitions_name[p[0]] + "-" + p[1]["group_name"]
|
||||
]
|
||||
break
|
||||
|
||||
moodle_groupenames |= set(moodle_groupename)
|
||||
etud["semestre_groupe"] = moodle_sem_name + "-" + "+".join(moodle_groupename)
|
||||
etud_info["semestre_groupe"] = (
|
||||
moodle_sem_name + "-" + "+".join(moodle_groupename)
|
||||
)
|
||||
|
||||
if groups_infos.nbdem > 1:
|
||||
s = "s"
|
||||
|
@ -714,9 +728,11 @@ def groups_table(
|
|||
});
|
||||
</script>
|
||||
""",
|
||||
"""<span class="warning_unauthorized">accès aux données personnelles interdit</span>"""
|
||||
if not can_view_etud_data
|
||||
else "",
|
||||
(
|
||||
"""<span class="warning_unauthorized">accès aux données personnelles interdit</span>"""
|
||||
if not can_view_etud_data
|
||||
else ""
|
||||
),
|
||||
]
|
||||
)
|
||||
H.append("</div></form>")
|
||||
|
@ -768,13 +784,7 @@ def groups_table(
|
|||
|
||||
return "".join(H)
|
||||
|
||||
elif (
|
||||
fmt == "pdf"
|
||||
or fmt == "xml"
|
||||
or fmt == "json"
|
||||
or fmt == "xls"
|
||||
or fmt == "moodlecsv"
|
||||
):
|
||||
elif fmt in {"pdf", "xml", "json", "xls", "moodlecsv"}:
|
||||
if fmt == "moodlecsv":
|
||||
fmt = "csv"
|
||||
return tab.make_page(fmt=fmt)
|
||||
|
@ -789,7 +799,7 @@ def groups_table(
|
|||
with_paiement=with_paiement,
|
||||
server_name=request.url_root,
|
||||
)
|
||||
filename = "liste_%s" % groups_infos.groups_filename
|
||||
filename = f"liste_{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:
|
||||
|
@ -823,6 +833,7 @@ def groups_table(
|
|||
"fax",
|
||||
"date_naissance",
|
||||
"lieu_naissance",
|
||||
"nationalite",
|
||||
"bac",
|
||||
"specialite",
|
||||
"annee_bac",
|
||||
|
@ -845,16 +856,16 @@ def groups_table(
|
|||
# remplis infos lycee si on a que le code lycée
|
||||
# et ajoute infos inscription
|
||||
for m in groups_infos.members:
|
||||
etud = sco_etud.get_etud_info(m["etudid"], filled=True)[0]
|
||||
m.update(etud)
|
||||
sco_etud.etud_add_lycee_infos(etud)
|
||||
etud_info = sco_etud.get_etud_info(m["etudid"], filled=True)[0]
|
||||
m.update(etud_info)
|
||||
sco_etud.etud_add_lycee_infos(etud_info)
|
||||
# et ajoute le parcours
|
||||
Se = sco_cursus.get_situation_etud_cursus(
|
||||
etud, groups_infos.formsemestre_id
|
||||
etud_info, groups_infos.formsemestre_id
|
||||
)
|
||||
m["parcours"] = Se.get_cursus_descr()
|
||||
m["code_cursus"], _ = sco_report.get_code_cursus_etud(
|
||||
etud["etudid"], sems=etud["sems"]
|
||||
etud_info["etudid"], sems=etud_info["sems"]
|
||||
)
|
||||
rows = [[m.get(k, "") for k in keys] for m in groups_infos.members]
|
||||
title = "etudiants_%s" % groups_infos.groups_filename
|
||||
|
@ -905,9 +916,11 @@ 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,
|
||||
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>""",
|
||||
(
|
||||
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>",
|
||||
]
|
||||
)
|
||||
|
|
|
@ -36,13 +36,14 @@ from flask import url_for, g, request
|
|||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import db, log
|
||||
from app.models import Formation, FormSemestre, GroupDescr
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Formation, FormSemestre, GroupDescr, Identite
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_preferences
|
||||
|
@ -50,62 +51,69 @@ from app.scodoc import sco_pv_dict
|
|||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False):
|
||||
def _list_authorized_etuds_by_sem(
|
||||
formsemestre: FormSemestre, ignore_jury=False
|
||||
) -> tuple[dict[int, dict], list[dict], dict[int, Identite]]:
|
||||
"""Liste des etudiants autorisés à s'inscrire dans sem.
|
||||
delai = nb de jours max entre la date de l'autorisation et celle de debut du semestre cible.
|
||||
ignore_jury: si vrai, considère tous les étudiants comme autorisés, même
|
||||
s'ils n'ont pas de décision de jury.
|
||||
"""
|
||||
src_sems = list_source_sems(sem, delai=delai)
|
||||
inscrits = list_inscrits(sem["formsemestre_id"])
|
||||
src_sems = _list_source_sems(formsemestre)
|
||||
inscrits = list_inscrits(formsemestre.id)
|
||||
r = {}
|
||||
candidats = {} # etudid : etud (tous les etudiants candidats)
|
||||
nb = 0 # debug
|
||||
for src in src_sems:
|
||||
src_formsemestre: FormSemestre
|
||||
for src_formsemestre in src_sems:
|
||||
if ignore_jury:
|
||||
# liste de tous les inscrits au semestre (sans dems)
|
||||
liste = list_inscrits(src["formsemestre_id"]).values()
|
||||
etud_list = list_inscrits(src_formsemestre.id).values()
|
||||
else:
|
||||
# liste des étudiants autorisés par le jury à s'inscrire ici
|
||||
liste = list_etuds_from_sem(src, sem)
|
||||
etud_list = _list_etuds_from_sem(src_formsemestre, formsemestre)
|
||||
liste_filtree = []
|
||||
for e in liste:
|
||||
for e in etud_list:
|
||||
# Filtre ceux qui se sont déjà inscrit dans un semestre APRES le semestre src
|
||||
auth_used = False # autorisation deja utilisée ?
|
||||
etud = sco_etud.get_etud_info(etudid=e["etudid"], filled=True)[0]
|
||||
for isem in etud["sems"]:
|
||||
if ndb.DateDMYtoISO(isem["date_debut"]) >= ndb.DateDMYtoISO(
|
||||
src["date_fin"]
|
||||
):
|
||||
etud = Identite.get_etud(e["etudid"])
|
||||
for inscription in etud.inscriptions():
|
||||
if inscription.formsemestre.date_debut >= src_formsemestre.date_fin:
|
||||
auth_used = True
|
||||
if not auth_used:
|
||||
candidats[e["etudid"]] = etud
|
||||
liste_filtree.append(e)
|
||||
nb += 1
|
||||
r[src["formsemestre_id"]] = {
|
||||
r[src_formsemestre.id] = {
|
||||
"etuds": liste_filtree,
|
||||
"infos": {
|
||||
"id": src["formsemestre_id"],
|
||||
"title": src["titreannee"],
|
||||
"title_target": "formsemestre_status?formsemestre_id=%s"
|
||||
% src["formsemestre_id"],
|
||||
"id": src_formsemestre.id,
|
||||
"title": src_formsemestre.titre_annee(),
|
||||
"title_target": url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=src_formsemestre.id,
|
||||
),
|
||||
"filename": "etud_autorises",
|
||||
},
|
||||
}
|
||||
# ajoute attribut inscrit qui indique si l'étudiant est déjà inscrit dans le semestre dest.
|
||||
for e in r[src["formsemestre_id"]]["etuds"]:
|
||||
for e in r[src_formsemestre.id]["etuds"]:
|
||||
e["inscrit"] = e["etudid"] in inscrits
|
||||
|
||||
# Ajoute liste des etudiants actuellement inscrits
|
||||
for e in inscrits.values():
|
||||
e["inscrit"] = True
|
||||
r[sem["formsemestre_id"]] = {
|
||||
r[formsemestre.id] = {
|
||||
"etuds": list(inscrits.values()),
|
||||
"infos": {
|
||||
"id": sem["formsemestre_id"],
|
||||
"title": "Semestre cible: " + sem["titreannee"],
|
||||
"title_target": "formsemestre_status?formsemestre_id=%s"
|
||||
% sem["formsemestre_id"],
|
||||
"id": formsemestre.id,
|
||||
"title": "Semestre cible: " + formsemestre.titre_annee(),
|
||||
"title_target": url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
),
|
||||
"comment": " actuellement inscrits dans ce semestre",
|
||||
"help": "Ces étudiants sont actuellement inscrits dans ce semestre. Si vous les décochez, il seront désinscrits.",
|
||||
"filename": "etud_inscrits",
|
||||
|
@ -115,7 +123,7 @@ def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False):
|
|||
return r, inscrits, candidats
|
||||
|
||||
|
||||
def list_inscrits(formsemestre_id, with_dems=False):
|
||||
def list_inscrits(formsemestre_id: int, with_dems=False) -> list[dict]:
|
||||
"""Étudiants déjà inscrits à ce semestre
|
||||
{ etudid : etud }
|
||||
"""
|
||||
|
@ -133,28 +141,27 @@ def list_inscrits(formsemestre_id, with_dems=False):
|
|||
return inscr
|
||||
|
||||
|
||||
def list_etuds_from_sem(src, dst) -> list[dict]:
|
||||
"""Liste des etudiants du semestre src qui sont autorisés à passer dans le semestre dst."""
|
||||
target = dst["semestre_id"]
|
||||
dpv = sco_pv_dict.dict_pvjury(src["formsemestre_id"])
|
||||
def _list_etuds_from_sem(src: FormSemestre, dst: FormSemestre) -> list[dict]:
|
||||
"""Liste des étudiants du semestre src qui sont autorisés à passer dans le semestre dst."""
|
||||
target_semestre_id = dst.semestre_id
|
||||
dpv = sco_pv_dict.dict_pvjury(src.id)
|
||||
if not dpv:
|
||||
return []
|
||||
etuds = [
|
||||
x["identite"]
|
||||
for x in dpv["decisions"]
|
||||
if target in [a["semestre_id"] for a in x["autorisations"]]
|
||||
if target_semestre_id in [a["semestre_id"] for a in x["autorisations"]]
|
||||
]
|
||||
return etuds
|
||||
|
||||
|
||||
def list_inscrits_date(sem):
|
||||
"""Liste les etudiants inscrits dans n'importe quel semestre
|
||||
du même département
|
||||
SAUF sem à la date de début de sem.
|
||||
def list_inscrits_date(formsemestre: FormSemestre):
|
||||
"""Liste les etudiants inscrits à la date de début de formsemestre
|
||||
dans n'importe quel semestre du même département
|
||||
SAUF formsemestre
|
||||
"""
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
sem["date_debut_iso"] = ndb.DateDMYtoISO(sem["date_debut"])
|
||||
cursor.execute(
|
||||
"""SELECT ins.etudid
|
||||
FROM
|
||||
|
@ -166,12 +173,18 @@ def list_inscrits_date(sem):
|
|||
AND S.date_fin >= %(date_debut_iso)s
|
||||
AND S.dept_id = %(dept_id)s
|
||||
""",
|
||||
sem,
|
||||
{
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"date_debut_iso": formsemestre.date_debut.isoformat(),
|
||||
"dept_id": formsemestre.dept_id,
|
||||
},
|
||||
)
|
||||
return [x[0] for x in cursor.fetchall()]
|
||||
|
||||
|
||||
def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False):
|
||||
def do_inscrit(
|
||||
formsemestre: FormSemestre, etudids, inscrit_groupes=False, inscrit_parcours=False
|
||||
):
|
||||
"""Inscrit ces etudiants dans ce semestre
|
||||
(la liste doit avoir été vérifiée au préalable)
|
||||
En option:
|
||||
|
@ -181,12 +194,11 @@ def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False):
|
|||
(si les deux sont vrais, inscrit_parcours n'a pas d'effet)
|
||||
"""
|
||||
# TODO à ré-écrire pour utiliser les modèles, notamment GroupDescr
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
|
||||
formsemestre.setup_parcours_groups()
|
||||
log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}")
|
||||
for etudid in etudids:
|
||||
sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
|
||||
sem["formsemestre_id"],
|
||||
formsemestre.id,
|
||||
etudid,
|
||||
etat=scu.INSCRIT,
|
||||
method="formsemestre_inscr_passage",
|
||||
|
@ -210,7 +222,7 @@ def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False):
|
|||
|
||||
cursem_groups_by_name = {
|
||||
g["group_name"]: g
|
||||
for g in sco_groups.get_sem_groups(sem["formsemestre_id"])
|
||||
for g in sco_groups.get_sem_groups(formsemestre.id)
|
||||
if g["group_name"]
|
||||
}
|
||||
|
||||
|
@ -234,53 +246,46 @@ def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False):
|
|||
sco_groups.change_etud_group_in_partition(etudid, group)
|
||||
|
||||
|
||||
def do_desinscrit(sem: dict, etudids: list[int]):
|
||||
def do_desinscrit(
|
||||
formsemestre: FormSemestre, etudids: list[int], check_has_dec_jury=True
|
||||
):
|
||||
"désinscrit les étudiants indiqués du formsemestre"
|
||||
log(f"do_desinscrit: {etudids}")
|
||||
for etudid in etudids:
|
||||
sco_formsemestre_inscriptions.do_formsemestre_desinscription(
|
||||
etudid, sem["formsemestre_id"]
|
||||
etudid, formsemestre.id, check_has_dec_jury=check_has_dec_jury
|
||||
)
|
||||
|
||||
|
||||
def list_source_sems(sem, delai=None) -> list[dict]:
|
||||
def _list_source_sems(formsemestre: FormSemestre) -> list[FormSemestre]:
|
||||
"""Liste des semestres sources
|
||||
sem est le semestre destination
|
||||
formsemestre est le semestre destination
|
||||
"""
|
||||
# liste des semestres débutant a moins
|
||||
# de delai (en jours) de la date de fin du semestre d'origine.
|
||||
sems = sco_formsemestre.do_formsemestre_list()
|
||||
othersems = []
|
||||
d, m, y = [int(x) for x in sem["date_debut"].split("/")]
|
||||
date_debut_dst = datetime.date(y, m, d)
|
||||
|
||||
delais = datetime.timedelta(delai)
|
||||
for s in sems:
|
||||
if s["formsemestre_id"] == sem["formsemestre_id"]:
|
||||
continue # saute le semestre destination
|
||||
if s["date_fin"]:
|
||||
d, m, y = [int(x) for x in s["date_fin"].split("/")]
|
||||
date_fin = datetime.date(y, m, d)
|
||||
if date_debut_dst - date_fin > delais:
|
||||
continue # semestre trop ancien
|
||||
if date_fin > date_debut_dst:
|
||||
continue # semestre trop récent
|
||||
# Elimine les semestres de formations speciales (sans parcours)
|
||||
if s["semestre_id"] == codes_cursus.NO_SEMESTRE_ID:
|
||||
continue
|
||||
#
|
||||
formation: Formation = Formation.query.get_or_404(s["formation_id"])
|
||||
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
|
||||
if not parcours.ALLOW_SEM_SKIP:
|
||||
if s["semestre_id"] < (sem["semestre_id"] - 1):
|
||||
continue
|
||||
othersems.append(s)
|
||||
return othersems
|
||||
# liste des semestres du même type de cursus terminant
|
||||
# pas trop loin de la date de début du semestre destination
|
||||
date_fin_min = formsemestre.date_debut - datetime.timedelta(days=275)
|
||||
date_fin_max = formsemestre.date_debut + datetime.timedelta(days=45)
|
||||
return (
|
||||
FormSemestre.query.filter(
|
||||
FormSemestre.dept_id == formsemestre.dept_id,
|
||||
# saute le semestre destination:
|
||||
FormSemestre.id != formsemestre.id,
|
||||
# et les semestres de formations speciales (monosemestres):
|
||||
FormSemestre.semestre_id != codes_cursus.NO_SEMESTRE_ID,
|
||||
# semestre pas trop dans le futur
|
||||
FormSemestre.date_fin <= date_fin_max,
|
||||
# ni trop loin dans le passé
|
||||
FormSemestre.date_fin >= date_fin_min,
|
||||
)
|
||||
.join(Formation)
|
||||
.filter_by(type_parcours=formsemestre.formation.type_parcours)
|
||||
).all()
|
||||
|
||||
|
||||
# view, GET, POST
|
||||
def formsemestre_inscr_passage(
|
||||
formsemestre_id,
|
||||
etuds=[],
|
||||
etuds: str | list[int] | list[str] | int | None = None,
|
||||
inscrit_groupes=False,
|
||||
inscrit_parcours=False,
|
||||
submitted=False,
|
||||
|
@ -300,36 +305,42 @@ def formsemestre_inscr_passage(
|
|||
- Confirmation: indiquer les étudiants inscrits et ceux désinscrits, le total courant.
|
||||
|
||||
"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
inscrit_groupes = int(inscrit_groupes)
|
||||
inscrit_parcours = int(inscrit_parcours)
|
||||
ignore_jury = int(ignore_jury)
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
# -- check lock
|
||||
if not sem["etat"]:
|
||||
if not formsemestre.etat:
|
||||
raise ScoValueError("opération impossible: semestre verrouille")
|
||||
header = html_sco_header.sco_header(page_title="Passage des étudiants")
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Passage des étudiants",
|
||||
init_qtip=True,
|
||||
javascripts=["js/etud_info.js"],
|
||||
)
|
||||
]
|
||||
footer = html_sco_header.sco_footer()
|
||||
H = [header]
|
||||
etuds = [] if etuds is None else etuds
|
||||
if isinstance(etuds, str):
|
||||
# list de strings, vient du form de confirmation
|
||||
# string, vient du form de confirmation
|
||||
etuds = [int(x) for x in etuds.split(",") if x]
|
||||
elif isinstance(etuds, int):
|
||||
etuds = [etuds]
|
||||
elif etuds and isinstance(etuds[0], str):
|
||||
etuds = [int(x) for x in etuds]
|
||||
|
||||
auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(
|
||||
sem, ignore_jury=ignore_jury
|
||||
auth_etuds_by_sem, inscrits, candidats = _list_authorized_etuds_by_sem(
|
||||
formsemestre, ignore_jury=ignore_jury
|
||||
)
|
||||
etuds_set = set(etuds)
|
||||
candidats_set = set(candidats)
|
||||
inscrits_set = set(inscrits)
|
||||
candidats_non_inscrits = candidats_set - inscrits_set
|
||||
inscrits_ailleurs = set(list_inscrits_date(sem))
|
||||
inscrits_ailleurs = set(list_inscrits_date(formsemestre))
|
||||
|
||||
def set_to_sorted_etud_list(etudset):
|
||||
def set_to_sorted_etud_list(etudset) -> list[Identite]:
|
||||
etuds = [candidats[etudid] for etudid in etudset]
|
||||
etuds.sort(key=itemgetter("nom"))
|
||||
etuds.sort(key=lambda e: e.sort_key)
|
||||
return etuds
|
||||
|
||||
if submitted:
|
||||
|
@ -340,7 +351,7 @@ def formsemestre_inscr_passage(
|
|||
|
||||
if not submitted:
|
||||
H += _build_page(
|
||||
sem,
|
||||
formsemestre,
|
||||
auth_etuds_by_sem,
|
||||
inscrits,
|
||||
candidats_non_inscrits,
|
||||
|
@ -355,30 +366,31 @@ def formsemestre_inscr_passage(
|
|||
if a_inscrire:
|
||||
H.append("<h3>Étudiants à inscrire</h3><ol>")
|
||||
for etud in set_to_sorted_etud_list(a_inscrire):
|
||||
H.append("<li>%(nomprenom)s</li>" % etud)
|
||||
H.append(f"<li>{etud.nomprenom}</li>")
|
||||
H.append("</ol>")
|
||||
a_inscrire_en_double = inscrits_ailleurs.intersection(a_inscrire)
|
||||
if a_inscrire_en_double:
|
||||
H.append("<h3>dont étudiants déjà inscrits:</h3><ul>")
|
||||
for etud in set_to_sorted_etud_list(a_inscrire_en_double):
|
||||
H.append('<li class="inscrailleurs">%(nomprenom)s</li>' % etud)
|
||||
H.append(f'<li class="inscrit-ailleurs">{etud.nomprenom}</li>')
|
||||
H.append("</ul>")
|
||||
if a_desinscrire:
|
||||
H.append("<h3>Étudiants à désinscrire</h3><ol>")
|
||||
for etudid in a_desinscrire:
|
||||
H.append(
|
||||
'<li class="desinscription">%(nomprenom)s</li>'
|
||||
% inscrits[etudid]
|
||||
)
|
||||
a_desinscrire_ident = sorted(
|
||||
(Identite.query.get(eid) for eid in a_desinscrire),
|
||||
key=lambda x: x.sort_key,
|
||||
)
|
||||
for etud in a_desinscrire_ident:
|
||||
H.append(f'<li class="desinscription">{etud.nomprenom}</li>')
|
||||
H.append("</ol>")
|
||||
todo = a_inscrire or a_desinscrire
|
||||
if not todo:
|
||||
H.append("""<h3>Il n'y a rien à modifier !</h3>""")
|
||||
H.append(
|
||||
scu.confirm_dialog(
|
||||
dest_url="formsemestre_inscr_passage"
|
||||
if todo
|
||||
else "formsemestre_status",
|
||||
dest_url=(
|
||||
"formsemestre_inscr_passage" if todo else "formsemestre_status"
|
||||
),
|
||||
message="<p>Confirmer ?</p>" if todo else "",
|
||||
add_headers=False,
|
||||
cancel_url="formsemestre_inscr_passage?formsemestre_id="
|
||||
|
@ -395,16 +407,26 @@ def formsemestre_inscr_passage(
|
|||
)
|
||||
)
|
||||
else:
|
||||
# check decisions jury ici pour éviter de recontruire le cache
|
||||
# après chaque desinscription
|
||||
sco_formsemestre_inscriptions.check_if_has_decision_jury(
|
||||
formsemestre, a_desinscrire
|
||||
)
|
||||
# check decisions jury ici pour éviter de recontruire le cache
|
||||
# après chaque desinscription
|
||||
sco_formsemestre_inscriptions.check_if_has_decision_jury(
|
||||
formsemestre, a_desinscrire
|
||||
)
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
# Inscription des étudiants au nouveau semestre:
|
||||
do_inscrit(
|
||||
sem,
|
||||
formsemestre,
|
||||
a_inscrire,
|
||||
inscrit_groupes=inscrit_groupes,
|
||||
inscrit_parcours=inscrit_parcours,
|
||||
)
|
||||
# Désinscriptions:
|
||||
do_desinscrit(sem, a_desinscrire)
|
||||
do_desinscrit(formsemestre, a_desinscrire, check_has_dec_jury=False)
|
||||
|
||||
H.append(
|
||||
f"""<h3>Opération effectuée</h3>
|
||||
|
@ -441,7 +463,7 @@ def formsemestre_inscr_passage(
|
|||
|
||||
|
||||
def _build_page(
|
||||
sem,
|
||||
formsemestre: FormSemestre,
|
||||
auth_etuds_by_sem,
|
||||
inscrits,
|
||||
candidats_non_inscrits,
|
||||
|
@ -450,7 +472,6 @@ def _build_page(
|
|||
inscrit_parcours=False,
|
||||
ignore_jury=False,
|
||||
):
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
|
||||
inscrit_groupes = int(inscrit_groupes)
|
||||
inscrit_parcours = int(inscrit_parcours)
|
||||
ignore_jury = int(ignore_jury)
|
||||
|
@ -472,7 +493,7 @@ def _build_page(
|
|||
),
|
||||
f"""<form name="f" method="post" action="{request.base_url}">
|
||||
|
||||
<input type="hidden" name="formsemestre_id" value="{sem['formsemestre_id']}"/>
|
||||
<input type="hidden" name="formsemestre_id" value="{formsemestre.id}"/>
|
||||
|
||||
<input type="submit" name="submitted" value="Appliquer les modifications"/>
|
||||
<a href="#help">aide</a>
|
||||
|
@ -491,7 +512,7 @@ def _build_page(
|
|||
</div>
|
||||
|
||||
<div>{scu.EMO_WARNING}
|
||||
<em>Seuls les semestres dont la date de fin est antérieure à la date de début
|
||||
<em>Seuls les semestres dont la date de fin est proche de la date de début
|
||||
de ce semestre ({formsemestre.date_debut.strftime("%d/%m/%Y")}) sont pris en
|
||||
compte.</em>
|
||||
</div>
|
||||
|
@ -499,7 +520,7 @@ def _build_page(
|
|||
|
||||
<input type="submit" name="submitted" value="Appliquer les modifications"/>
|
||||
|
||||
{formsemestre_inscr_passage_help(sem)}
|
||||
{formsemestre_inscr_passage_help(formsemestre)}
|
||||
|
||||
</form>
|
||||
""",
|
||||
|
@ -524,19 +545,20 @@ def _build_page(
|
|||
return H
|
||||
|
||||
|
||||
def formsemestre_inscr_passage_help(sem: dict):
|
||||
def formsemestre_inscr_passage_help(formsemestre: FormSemestre):
|
||||
"texte d'aide en bas de la page passage des étudiants"
|
||||
return f"""<div class="pas_help"><h3><a name="help">Explications</a></h3>
|
||||
<p>Cette page permet d'inscrire des étudiants dans le semestre destination
|
||||
<a class="stdlink"
|
||||
href="{
|
||||
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=sem["formsemestre_id"] )
|
||||
}">{sem['titreannee']}</a>,
|
||||
url_for("notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id )
|
||||
}">{formsemestre.titre_annee()}</a>,
|
||||
et d'en désincrire si besoin.
|
||||
</p>
|
||||
<p>Les étudiants sont groupés par semestre d'origine. Ceux qui sont en caractères
|
||||
<span class="inscrit">gras</span> sont déjà inscrits dans le semestre destination.
|
||||
Ceux qui sont en <span class"inscrailleurs">gras et en rouge</span> sont inscrits
|
||||
<span class="deja-inscrit">gras</span> sont déjà inscrits dans le semestre destination.
|
||||
Ceux qui sont en <span class="inscrit-ailleurs">gras et en rouge</span> sont inscrits
|
||||
dans un <em>autre</em> semestre.
|
||||
</p>
|
||||
<p>Au départ, les étudiants déjà inscrits sont sélectionnés; vous pouvez ajouter
|
||||
|
@ -555,7 +577,7 @@ def formsemestre_inscr_passage_help(sem: dict):
|
|||
conserve les groupes, on conserve les parcours (là aussi, pensez à les cocher dans
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_editwithmodules", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=sem["formsemestre_id"] )
|
||||
formsemestre_id=formsemestre.id )
|
||||
}">modifier le semestre</a> avant de faire passer les étudiants).
|
||||
</a>
|
||||
|
||||
|
@ -656,25 +678,24 @@ def etuds_select_boxes(
|
|||
H.append("</div>")
|
||||
for etud in etuds:
|
||||
if etud.get("inscrit", False):
|
||||
c = " inscrit"
|
||||
c = " deja-inscrit"
|
||||
checked = 'checked="checked"'
|
||||
else:
|
||||
checked = ""
|
||||
if etud["etudid"] in inscrits_ailleurs:
|
||||
c = " inscrailleurs"
|
||||
c = " inscrit-ailleurs"
|
||||
else:
|
||||
c = ""
|
||||
sco_etud.format_etud_ident(etud)
|
||||
if etud["etudid"]:
|
||||
elink = """<a class="discretelink %s" href="%s">%s</a>""" % (
|
||||
c,
|
||||
url_for(
|
||||
"scolar.fiche_etud",
|
||||
elink = f"""<a id="{etud['etudid']}" class="discretelink etudinfo {c}"
|
||||
href="{ url_for(
|
||||
'scolar.fiche_etud',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud["etudid"],
|
||||
),
|
||||
etud["nomprenom"],
|
||||
)
|
||||
etudid=etud['etudid'],
|
||||
)
|
||||
}">{etud['nomprenom']}</a>
|
||||
"""
|
||||
else:
|
||||
# ce n'est pas un etudiant ScoDoc
|
||||
elink = etud["nomprenom"]
|
||||
|
|
|
@ -490,9 +490,9 @@ def _make_table_notes(
|
|||
rlinks = {"_table_part": "head"}
|
||||
for e in evaluations:
|
||||
rlinks[e.id] = "afficher"
|
||||
rlinks[
|
||||
"_" + str(e.id) + "_help"
|
||||
] = "afficher seulement les notes de cette évaluation"
|
||||
rlinks["_" + str(e.id) + "_help"] = (
|
||||
"afficher seulement les notes de cette évaluation"
|
||||
)
|
||||
rlinks["_" + str(e.id) + "_target"] = url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
|
@ -709,9 +709,9 @@ def _add_eval_columns(
|
|||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
|
||||
|
||||
if evaluation.date_debut:
|
||||
titles[
|
||||
evaluation.id
|
||||
] = f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})"
|
||||
titles[evaluation.id] = (
|
||||
f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})"
|
||||
)
|
||||
else:
|
||||
titles[evaluation.id] = f"{evaluation.description} "
|
||||
|
||||
|
@ -820,14 +820,17 @@ def _add_eval_columns(
|
|||
row_moys[evaluation.id] = scu.fmt_note(
|
||||
sum_notes / nb_notes, keep_numeric=keep_numeric
|
||||
)
|
||||
row_moys[
|
||||
"_" + str(evaluation.id) + "_help"
|
||||
] = "moyenne sur %d notes (%s le %s)" % (
|
||||
nb_notes,
|
||||
evaluation.description,
|
||||
evaluation.date_debut.strftime("%d/%m/%Y")
|
||||
if evaluation.date_debut
|
||||
else "",
|
||||
row_moys["_" + str(evaluation.id) + "_help"] = (
|
||||
"moyenne sur %d notes (%s le %s)"
|
||||
% (
|
||||
nb_notes,
|
||||
evaluation.description,
|
||||
(
|
||||
evaluation.date_debut.strftime("%d/%m/%Y")
|
||||
if evaluation.date_debut
|
||||
else ""
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
row_moys[evaluation.id] = ""
|
||||
|
@ -884,8 +887,9 @@ def _add_moymod_column(
|
|||
row["_" + col_id + "_td_attrs"] = ' class="moyenne" '
|
||||
if etudid in inscrits and not isinstance(val, str):
|
||||
notes.append(val)
|
||||
nb_notes = nb_notes + 1
|
||||
sum_notes += val
|
||||
if not np.isnan(val):
|
||||
nb_notes = nb_notes + 1
|
||||
sum_notes += val
|
||||
row_coefs[col_id] = "(avec abs)"
|
||||
if is_apc:
|
||||
row_poids[col_id] = "à titre indicatif"
|
||||
|
|
|
@ -56,6 +56,7 @@ _moduleimplEditor = ndb.EditableTable(
|
|||
|
||||
def do_moduleimpl_create(args):
|
||||
"create a moduleimpl"
|
||||
# TODO remplacer par une methode de ModuleImpl qui appelle super().create_from_dict() puis invalide le formsemestre
|
||||
cnx = ndb.GetDBConnexion()
|
||||
r = _moduleimplEditor.create(cnx, args)
|
||||
sco_cache.invalidate_formsemestre(
|
||||
|
@ -90,7 +91,9 @@ def do_moduleimpl_delete(oid, formsemestre_id=None):
|
|||
) # > moduleimpl_delete
|
||||
|
||||
|
||||
def moduleimpl_list(moduleimpl_id=None, formsemestre_id=None, module_id=None):
|
||||
def moduleimpl_list(
|
||||
moduleimpl_id=None, formsemestre_id=None, module_id=None
|
||||
) -> list[dict]:
|
||||
"list moduleimpls"
|
||||
args = locals()
|
||||
cnx = ndb.GetDBConnexion()
|
||||
|
@ -109,91 +112,6 @@ def do_moduleimpl_edit(args, formsemestre_id=None, cnx=None):
|
|||
) # > modif moduleimpl
|
||||
|
||||
|
||||
def moduleimpl_withmodule_list(
|
||||
moduleimpl_id=None, formsemestre_id=None, module_id=None, sort_by_ue=False
|
||||
) -> list:
|
||||
"""Liste les moduleimpls et ajoute dans chacun
|
||||
l'UE, la matière et le module auxquels ils appartiennent.
|
||||
Tri la liste par:
|
||||
- pour les formations classiques: semestre/UE/numero_matiere/numero_module;
|
||||
- pour le BUT: ignore UEs sauf si sort_by_ue et matières dans le tri.
|
||||
|
||||
NB: Cette fonction faisait partie de l'API ScoDoc 7.
|
||||
"""
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_edit_matiere
|
||||
from app.scodoc import sco_edit_module
|
||||
|
||||
modimpls = moduleimpl_list(
|
||||
**{
|
||||
"moduleimpl_id": moduleimpl_id,
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"module_id": module_id,
|
||||
}
|
||||
)
|
||||
if not modimpls:
|
||||
return []
|
||||
ues = {}
|
||||
matieres = {}
|
||||
modules = {}
|
||||
for mi in modimpls:
|
||||
module_id = mi["module_id"]
|
||||
if not mi["module_id"] in modules:
|
||||
modules[module_id] = sco_edit_module.module_list(
|
||||
args={"module_id": module_id}
|
||||
)[0]
|
||||
mi["module"] = modules[module_id]
|
||||
ue_id = mi["module"]["ue_id"]
|
||||
if not ue_id in ues:
|
||||
ues[ue_id] = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
|
||||
mi["ue"] = ues[ue_id]
|
||||
matiere_id = mi["module"]["matiere_id"]
|
||||
if not matiere_id in matieres:
|
||||
matieres[matiere_id] = sco_edit_matiere.matiere_list(
|
||||
args={"matiere_id": matiere_id}
|
||||
)[0]
|
||||
mi["matiere"] = matieres[matiere_id]
|
||||
|
||||
mod = modimpls[0]["module"]
|
||||
formation = db.session.get(Formation, mod["formation_id"])
|
||||
|
||||
if formation.is_apc():
|
||||
# tri par numero_module
|
||||
if sort_by_ue:
|
||||
modimpls.sort(
|
||||
key=lambda x: (
|
||||
x["ue"]["numero"],
|
||||
x["ue"]["ue_id"],
|
||||
x["module"]["module_type"],
|
||||
x["module"]["numero"],
|
||||
x["module"]["code"],
|
||||
)
|
||||
)
|
||||
else:
|
||||
modimpls.sort(
|
||||
key=lambda x: (
|
||||
x["module"]["module_type"],
|
||||
x["module"]["numero"],
|
||||
x["module"]["code"],
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Formations classiques, avec matières:
|
||||
# tri par semestre/UE/numero_matiere/numero_module
|
||||
modimpls.sort(
|
||||
key=lambda x: (
|
||||
x["ue"]["numero"],
|
||||
x["ue"]["ue_id"],
|
||||
x["matiere"]["numero"],
|
||||
x["matiere"]["matiere_id"],
|
||||
x["module"]["numero"],
|
||||
x["module"]["code"],
|
||||
)
|
||||
)
|
||||
|
||||
return modimpls
|
||||
|
||||
|
||||
def moduleimpls_in_external_ue(ue_id):
|
||||
"""List of modimpls in this ue"""
|
||||
cursor = ndb.SimpleQuery(
|
||||
|
@ -254,9 +172,9 @@ _moduleimpl_inscriptionEditor = ndb.EditableTable(
|
|||
)
|
||||
|
||||
|
||||
def do_moduleimpl_inscription_create(args, formsemestre_id=None):
|
||||
def do_moduleimpl_inscription_create(args, formsemestre_id=None, cnx=None):
|
||||
"create a moduleimpl_inscription"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cnx = cnx or ndb.GetDBConnexion()
|
||||
try:
|
||||
r = _moduleimpl_inscriptionEditor.create(cnx, args)
|
||||
except psycopg2.errors.UniqueViolation as exc:
|
||||
|
@ -270,7 +188,7 @@ def do_moduleimpl_inscription_create(args, formsemestre_id=None):
|
|||
cnx,
|
||||
method="moduleimpl_inscription",
|
||||
etudid=args["etudid"],
|
||||
msg="inscription module %s" % args["moduleimpl_id"],
|
||||
msg=f"inscription module {args['moduleimpl_id']}",
|
||||
commit=False,
|
||||
)
|
||||
return r
|
||||
|
@ -297,32 +215,29 @@ def do_moduleimpl_inscrit_etuds(moduleimpl_id, formsemestre_id, etudids, reset=F
|
|||
args={"formsemestre_id": formsemestre_id, "etudid": etudid}
|
||||
)
|
||||
if not insem:
|
||||
raise ScoValueError("%s n'est pas inscrit au semestre !" % etudid)
|
||||
raise ScoValueError(f"{etudid} n'est pas inscrit au semestre !")
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# Desinscriptions
|
||||
if reset:
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(
|
||||
"delete from notes_moduleimpl_inscription where moduleimpl_id = %(moduleimpl_id)s",
|
||||
{"moduleimpl_id": moduleimpl_id},
|
||||
)
|
||||
# Inscriptions au module:
|
||||
inmod_set = set(
|
||||
[
|
||||
# hum ?
|
||||
x["etudid"]
|
||||
for x in do_moduleimpl_inscription_list(moduleimpl_id=moduleimpl_id)
|
||||
]
|
||||
)
|
||||
inmod_set = {
|
||||
x["etudid"] for x in do_moduleimpl_inscription_list(moduleimpl_id=moduleimpl_id)
|
||||
}
|
||||
for etudid in etudids:
|
||||
# deja inscrit ?
|
||||
# déja inscrit ?
|
||||
if not etudid in inmod_set:
|
||||
do_moduleimpl_inscription_create(
|
||||
{"moduleimpl_id": moduleimpl_id, "etudid": etudid},
|
||||
formsemestre_id=formsemestre_id,
|
||||
cnx=cnx,
|
||||
)
|
||||
|
||||
cnx.commit()
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=formsemestre_id
|
||||
) # > moduleimpl_inscrit_etuds
|
||||
|
|
|
@ -409,34 +409,32 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
|
|||
H.append(
|
||||
'<h3>Étudiants avec UEs capitalisées (ADM):</h3><ul class="ue_inscr_list">'
|
||||
)
|
||||
ues = [
|
||||
sco_edit_ue.ue_list({"ue_id": ue_id})[0] for ue_id in ues_cap_info.keys()
|
||||
]
|
||||
ues.sort(key=lambda u: u["numero"])
|
||||
ues = [UniteEns.query.get_or_404(ue_id) for ue_id in ues_cap_info.keys()]
|
||||
ues.sort(key=lambda u: u.numero)
|
||||
for ue in ues:
|
||||
H.append(
|
||||
f"""<li class="tit"><span class="tit">{ue['acronyme']}: {ue['titre']}</span>"""
|
||||
f"""<li class="tit"><span class="tit">{ue.acronyme}: {ue.titre or ''}</span>"""
|
||||
)
|
||||
H.append("<ul>")
|
||||
for info in ues_cap_info[ue["ue_id"]]:
|
||||
etud = sco_etud.get_etud_info(etudid=info["etudid"], filled=True)[0]
|
||||
for info in ues_cap_info[ue.id]:
|
||||
etud = Identite.get_etud(info["etudid"])
|
||||
H.append(
|
||||
f"""<li class="etud"><a class="discretelink etudinfo"
|
||||
id="{info['etudid']}"
|
||||
id="{etud.id}"
|
||||
href="{
|
||||
url_for(
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud["etudid"],
|
||||
etudid=etud.id,
|
||||
)
|
||||
}">{etud["nomprenom"]}</a>"""
|
||||
}">{etud.nomprenom}</a>"""
|
||||
)
|
||||
if info["ue_status"]["event_date"]:
|
||||
H.append(
|
||||
f"""(cap. le {info["ue_status"]["event_date"].strftime("%d/%m/%Y")})"""
|
||||
)
|
||||
if is_apc:
|
||||
is_inscrit_ue = (etud["etudid"], ue["id"]) not in res.dispense_ues
|
||||
is_inscrit_ue = (etud.id, ue.id) not in res.dispense_ues
|
||||
else:
|
||||
# CLASSIQUE
|
||||
is_inscrit_ue = info["is_ins"]
|
||||
|
@ -468,8 +466,8 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
|
|||
H.append(
|
||||
f"""<div><a class="stdlink" href="{
|
||||
url_for("notes.etud_desinscrit_ue",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etud["etudid"],
|
||||
formsemestre_id=formsemestre_id, ue_id=ue["ue_id"])
|
||||
scodoc_dept=g.scodoc_dept, etudid=etud.id,
|
||||
formsemestre_id=formsemestre_id, ue_id=ue.id)
|
||||
}">désinscrire {"des modules" if not is_apc else ""} de cette UE</a></div>
|
||||
"""
|
||||
)
|
||||
|
@ -479,8 +477,8 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
|
|||
H.append(
|
||||
f"""<div><a class="stdlink" href="{
|
||||
url_for("notes.etud_inscrit_ue",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etud["etudid"],
|
||||
formsemestre_id=formsemestre_id, ue_id=ue["ue_id"])
|
||||
scodoc_dept=g.scodoc_dept, etudid=etud.id,
|
||||
formsemestre_id=formsemestre_id, ue_id=ue.id)
|
||||
}">inscrire à {"" if is_apc else "tous les modules de"} cette UE</a></div>
|
||||
"""
|
||||
)
|
||||
|
@ -619,7 +617,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
|||
<p>L'inscription ou désinscription aux UEs du BUT n'affecte pas les inscriptions aux modules
|
||||
mais permet de "dispenser" un étudiant de suivre certaines UEs de son parcours.
|
||||
</p>
|
||||
<p>Il peut s'agit d'étudiants redoublants ayant déjà acquis l'UE, ou d'une UE
|
||||
<p>Il peut s'agir d'étudiants redoublants ayant déjà acquis l'UE, ou d'une UE
|
||||
présente dans le semestre mais pas dans le parcours de l'étudiant, ou bien d'autres
|
||||
cas particuliers.
|
||||
</p>
|
||||
|
|
|
@ -127,12 +127,12 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
|
|||
"args": {
|
||||
"group_ids": group_id,
|
||||
"evaluation_id": evaluation.id,
|
||||
"date_debut": evaluation.date_debut.isoformat()
|
||||
if evaluation.date_debut
|
||||
else "",
|
||||
"date_fin": evaluation.date_fin.isoformat()
|
||||
if evaluation.date_fin
|
||||
else "",
|
||||
"date_debut": (
|
||||
evaluation.date_debut.isoformat() if evaluation.date_debut else ""
|
||||
),
|
||||
"date_fin": (
|
||||
evaluation.date_fin.isoformat() if evaluation.date_fin else ""
|
||||
),
|
||||
},
|
||||
"enabled": evaluation.date_debut is not None
|
||||
and evaluation.date_fin is not None,
|
||||
|
@ -355,17 +355,14 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
Ses notes ne peuvent pas être prises en compte dans les moyennes d'UE.
|
||||
</div>"""
|
||||
)
|
||||
#
|
||||
if has_expression and nt.expr_diagnostics:
|
||||
H.append(sco_formsemestre_status.html_expr_diagnostic(nt.expr_diagnostics))
|
||||
#
|
||||
|
||||
if formsemestre_has_decisions(formsemestre_id):
|
||||
H.append(
|
||||
"""<ul class="tf-msg">
|
||||
<li class="tf-msg warning">Décisions de jury saisies: seul le ou la responsable du
|
||||
"""<div class="formsemestre-warning-box">
|
||||
<div class="warning">Décisions de jury saisies: seul le ou la responsable du
|
||||
semestre peut saisir des notes (elle devra modifier les décisions de jury).
|
||||
</li>
|
||||
</ul>"""
|
||||
</div>
|
||||
</div>"""
|
||||
)
|
||||
#
|
||||
H.append(
|
||||
|
@ -522,16 +519,22 @@ def _ligne_evaluation(
|
|||
partition_id=partition_id,
|
||||
select_first_partition=True,
|
||||
)
|
||||
if evaluation.evaluation_type in (
|
||||
scu.EVALUATION_RATTRAPAGE,
|
||||
scu.EVALUATION_SESSION2,
|
||||
):
|
||||
if evaluation.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE:
|
||||
tr_class = "mievr mievr_rattr"
|
||||
elif evaluation.evaluation_type == Evaluation.EVALUATION_SESSION2:
|
||||
tr_class = "mievr mievr_session2"
|
||||
elif evaluation.evaluation_type == Evaluation.EVALUATION_BONUS:
|
||||
tr_class = "mievr mievr_bonus"
|
||||
else:
|
||||
tr_class = "mievr"
|
||||
|
||||
if not evaluation.visibulletin:
|
||||
tr_class += " non_visible_inter"
|
||||
tr_class_1 = "mievr"
|
||||
if evaluation.is_blocked():
|
||||
tr_class += " evaluation_blocked"
|
||||
tr_class_1 += " evaluation_blocked"
|
||||
|
||||
if not first_eval:
|
||||
H.append("""<tr><td colspan="8"> </td></tr>""")
|
||||
tr_class_1 += " mievr_spaced"
|
||||
|
@ -565,14 +568,18 @@ def _ligne_evaluation(
|
|||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||
}" class="mievr_evalnodate">Évaluation sans date</a>"""
|
||||
)
|
||||
H.append(f" <em>{evaluation.description or ''}</em>")
|
||||
if evaluation.evaluation_type == scu.EVALUATION_RATTRAPAGE:
|
||||
H.append(f" <em>{evaluation.description}</em>")
|
||||
if evaluation.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE:
|
||||
H.append(
|
||||
"""<span class="mievr_rattr" title="remplace si meilleure note">rattrapage</span>"""
|
||||
)
|
||||
elif evaluation.evaluation_type == scu.EVALUATION_SESSION2:
|
||||
elif evaluation.evaluation_type == Evaluation.EVALUATION_SESSION2:
|
||||
H.append(
|
||||
"""<span class="mievr_rattr" title="remplace autres notes">session 2</span>"""
|
||||
"""<span class="mievr_session2" title="remplace autres notes">session 2</span>"""
|
||||
)
|
||||
elif evaluation.evaluation_type == Evaluation.EVALUATION_BONUS:
|
||||
H.append(
|
||||
"""<span class="mievr_bonus" title="s'ajoute aux moyennes de ce module">bonus</span>"""
|
||||
)
|
||||
#
|
||||
if etat["last_modif"]:
|
||||
|
@ -608,8 +615,15 @@ def _ligne_evaluation(
|
|||
else:
|
||||
H.append(arrow_none)
|
||||
|
||||
if etat["evalcomplete"]:
|
||||
etat_txt = f"""(prise en compte{
|
||||
if evaluation.is_blocked():
|
||||
etat_txt = f"""évaluation bloquée {
|
||||
"jusqu'au " + evaluation.blocked_until.strftime("%d/%m/%Y")
|
||||
if evaluation.blocked_until < Evaluation.BLOCKED_FOREVER
|
||||
else "" }
|
||||
"""
|
||||
etat_descr = """prise en compte bloquée"""
|
||||
elif etat["evalcomplete"]:
|
||||
etat_txt = f"""Moyenne (prise en compte{
|
||||
""
|
||||
if evaluation.visibulletin
|
||||
else ", cachée en intermédiaire"})
|
||||
|
@ -618,7 +632,7 @@ def _ligne_evaluation(
|
|||
", évaluation cachée sur les bulletins en version intermédiaire et sur la passerelle"
|
||||
}"""
|
||||
elif etat["evalattente"] and not evaluation.publish_incomplete:
|
||||
etat_txt = "(prise en compte, mais <b>notes en attente</b>)"
|
||||
etat_txt = "Moyenne (prise en compte, mais <b>notes en attente</b>)"
|
||||
etat_descr = "il y a des notes en attente"
|
||||
elif evaluation.publish_incomplete:
|
||||
etat_txt = """(prise en compte <b>immédiate</b>)"""
|
||||
|
@ -626,28 +640,29 @@ def _ligne_evaluation(
|
|||
"il manque des notes, mais la prise en compte immédiate a été demandée"
|
||||
)
|
||||
elif etat["nb_notes"] != 0:
|
||||
etat_txt = "(<b>non</b> prise en compte)"
|
||||
etat_txt = "Moyenne (<b>non</b> prise en compte)"
|
||||
etat_descr = "il manque des notes"
|
||||
else:
|
||||
etat_txt = ""
|
||||
if can_edit_evals and etat_txt:
|
||||
etat_txt = f"""<a href="{ url_for("notes.evaluation_edit",
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||
}" title="{etat_descr}">{etat_txt}</a>"""
|
||||
if etat_txt:
|
||||
if can_edit_evals:
|
||||
etat_txt = f"""<a href="{ url_for("notes.evaluation_edit",
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||
}" title="{etat_descr}">{etat_txt}</a>"""
|
||||
|
||||
H.append(
|
||||
f"""</span></span></td>
|
||||
</tr>
|
||||
<tr class="{tr_class}">
|
||||
<tr class="{tr_class} mievr_in">
|
||||
<th class="moduleimpl_evaluations" colspan="2"> </th>
|
||||
<th class="moduleimpl_evaluations">Durée</th>
|
||||
<th class="moduleimpl_evaluations">Coef.</th>
|
||||
<th class="moduleimpl_evaluations">Notes</th>
|
||||
<th class="moduleimpl_evaluations">Abs</th>
|
||||
<th class="moduleimpl_evaluations">N</th>
|
||||
<th class="moduleimpl_evaluations" colspan="2">Moyenne {etat_txt}</th>
|
||||
<th class="moduleimpl_evaluations moduleimpl_evaluation_moy" colspan="2"><span>{etat_txt}</span></th>
|
||||
</tr>
|
||||
<tr class="{tr_class}">
|
||||
<tr class="{tr_class} mievr_in">
|
||||
<td class="mievr">"""
|
||||
)
|
||||
if can_edit_evals:
|
||||
|
@ -829,7 +844,7 @@ def _evaluation_poids_html(evaluation: Evaluation, max_poids: float = 0.0) -> st
|
|||
+ "\n".join(
|
||||
[
|
||||
f"""<div title="poids vers {ue.acronyme}: {poids:g}">
|
||||
<div style="--size:{math.sqrt(poids*(evaluation.coefficient or 0.)/max_poids*144)}px;
|
||||
<div style="--size:{math.sqrt(poids*(evaluation.coefficient)/max_poids*144)}px;
|
||||
{'background-color: ' + ue.color + ';' if ue.color else ''}
|
||||
"></div>
|
||||
</div>"""
|
||||
|
|
|
@ -36,7 +36,7 @@ import sqlalchemy as sa
|
|||
|
||||
from app import log
|
||||
from app.auth.models import User
|
||||
from app.but import cursus_but
|
||||
from app.but import cursus_but, validations_view
|
||||
from app.models import Adresse, EtudAnnotation, FormSemestre, Identite, ScoDocSiteConfig
|
||||
from app.scodoc import (
|
||||
codes_cursus,
|
||||
|
@ -445,6 +445,14 @@ def fiche_etud(etudid=None):
|
|||
# Liens vers compétences BUT
|
||||
if last_formsemestre and last_formsemestre.formation.is_apc():
|
||||
but_cursus = cursus_but.EtudCursusBUT(etud, last_formsemestre.formation)
|
||||
refcomp = last_formsemestre.formation.referentiel_competence
|
||||
if refcomp:
|
||||
ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau(
|
||||
refcomp, etud
|
||||
)
|
||||
ects_total = sum((v.ects() for v in ue_validation_by_niveau.values()))
|
||||
else:
|
||||
ects_total = ""
|
||||
info[
|
||||
"but_cursus_mkup"
|
||||
] = f"""
|
||||
|
@ -454,15 +462,20 @@ def fiche_etud(etudid=None):
|
|||
cursus=but_cursus,
|
||||
scu=scu,
|
||||
)}
|
||||
<div class="link_validation_rcues">
|
||||
<a class="stdlink" href="{url_for("notes.validation_rcues",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid,
|
||||
formsemestre_id=last_formsemestre.id)}"
|
||||
title="Visualiser les compétences BUT"
|
||||
>
|
||||
<img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="100px"/>
|
||||
<div>Compétences BUT</div>
|
||||
</a>
|
||||
<div class="fiche_but_col2">
|
||||
<div class="link_validation_rcues">
|
||||
<a class="stdlink" href="{url_for("notes.validation_rcues",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid,
|
||||
formsemestre_id=last_formsemestre.id)}"
|
||||
title="Visualiser les compétences BUT"
|
||||
>
|
||||
<img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="100px"/>
|
||||
<div style="text-align: center;">Compétences BUT</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="fiche_total_etcs">
|
||||
Total ECTS BUT: {ects_total:g}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
|
|
@ -48,20 +48,17 @@ from wtforms import (
|
|||
HiddenField,
|
||||
SelectMultipleField,
|
||||
)
|
||||
from app.models import ModuleImpl
|
||||
from app.models import Evaluation, ModuleImpl
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import ScoValueError
|
||||
from app.scodoc import html_sco_header, sco_preferences
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc.sco_excel import ScoExcelBook, COLORS
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_permissions_check
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc import sco_etud
|
||||
import sco_version
|
||||
|
@ -138,11 +135,7 @@ class PlacementForm(FlaskForm):
|
|||
|
||||
def set_evaluation_infos(self, evaluation_id):
|
||||
"""Initialise les données du formulaire avec les données de l'évaluation."""
|
||||
eval_data = sco_evaluation_db.get_evaluations_dict(
|
||||
{"evaluation_id": evaluation_id}
|
||||
)
|
||||
if not eval_data:
|
||||
raise ScoValueError("invalid evaluation_id")
|
||||
_ = Evaluation.get_evaluation(evaluation_id) # check exist ?
|
||||
self.groups_tree, self.has_groups, self.nb_groups = _get_group_info(
|
||||
evaluation_id
|
||||
)
|
||||
|
@ -239,14 +232,12 @@ class PlacementRunner:
|
|||
self.groups_ids = [
|
||||
gid if gid != TOUS else form.tous_id for gid in form["groups"].data
|
||||
]
|
||||
self.eval_data = sco_evaluation_db.get_evaluations_dict(
|
||||
{"evaluation_id": self.evaluation_id}
|
||||
)[0]
|
||||
self.evaluation = Evaluation.get_evaluation(self.evaluation_id)
|
||||
self.groups = sco_groups.listgroups(self.groups_ids)
|
||||
self.gr_title_filename = sco_groups.listgroups_filename(self.groups)
|
||||
# gr_title = sco_groups.listgroups_abbrev(d['groups'])
|
||||
self.current_user = current_user
|
||||
self.moduleimpl_id = self.eval_data["moduleimpl_id"]
|
||||
self.moduleimpl_id = self.evaluation.moduleimpl_id
|
||||
self.moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(self.moduleimpl_id)
|
||||
# TODO: à revoir pour utiliser modèle ModuleImpl
|
||||
self.moduleimpl_data = sco_moduleimpl.moduleimpl_list(
|
||||
|
@ -260,20 +251,25 @@ class PlacementRunner:
|
|||
)
|
||||
self.evalname = "%s-%s" % (
|
||||
self.module_data["code"] or "?",
|
||||
ndb.DateDMYtoISO(self.eval_data["jour"]),
|
||||
(
|
||||
self.evaluation.date_debut.strftime("%Y-%m-%d_%Hh%M")
|
||||
if self.evaluation.date_debut
|
||||
else ""
|
||||
),
|
||||
)
|
||||
if self.eval_data["description"]:
|
||||
self.evaltitre = self.eval_data["description"]
|
||||
if self.evaluation.description:
|
||||
self.evaltitre = self.evaluation.description
|
||||
else:
|
||||
self.evaltitre = "évaluation du %s" % self.eval_data["jour"]
|
||||
self.evaltitre = f"""évaluation{
|
||||
self.evaluation.date_debut.strftime(' du %d/%m/%Y à %Hh%M')
|
||||
if self.evaluation.date_debut else ''}"""
|
||||
self.desceval = [ # une liste de chaines: description de l'evaluation
|
||||
"%s" % self.sem["titreannee"],
|
||||
self.sem["titreannee"],
|
||||
"Module : %s - %s"
|
||||
% (self.module_data["code"] or "?", self.module_data["abbrev"] or ""),
|
||||
"Surveillants : %s" % self.surveillants,
|
||||
"Batiment : %(batiment)s - Salle : %(salle)s" % self.__dict__,
|
||||
"Controle : %s (coef. %g)"
|
||||
% (self.evaltitre, self.eval_data["coefficient"]),
|
||||
"Controle : %s (coef. %g)" % (self.evaltitre, self.evaluation.coefficient),
|
||||
]
|
||||
self.styles = None
|
||||
self.plan = None
|
||||
|
@ -339,10 +335,10 @@ class PlacementRunner:
|
|||
|
||||
def _production_pdf(self):
|
||||
pdf_title = "<br>".join(self.desceval)
|
||||
pdf_title += (
|
||||
"\nDate : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s"
|
||||
% self.eval_data
|
||||
)
|
||||
pdf_title += f"""\nDate : {self.evaluation.date_debut.strftime("%d/%m/%Y")
|
||||
if self.evaluation.date_debut else '-'
|
||||
} - Horaire : {self.evaluation.heure_debut()} à {self.evaluation.heure_fin()
|
||||
}"""
|
||||
filename = "placement_%(evalname)s_%(gr_title_filename)s" % self.__dict__
|
||||
titles = {
|
||||
"nom": "Nom",
|
||||
|
@ -489,8 +485,10 @@ class PlacementRunner:
|
|||
worksheet.append_blank_row()
|
||||
worksheet.append_single_cell_row(desceval, self.styles["titres"])
|
||||
worksheet.append_single_cell_row(
|
||||
"Date : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s"
|
||||
% self.eval_data,
|
||||
f"""Date : {self.evaluation.date_debut.strftime("%d/%m/%Y")
|
||||
if self.evaluation.date_debut else '-'
|
||||
} - Horaire : {self.evaluation.heure_debut()} à {self.evaluation.heure_fin()
|
||||
}""",
|
||||
self.styles["titres"],
|
||||
)
|
||||
|
||||
|
|
|
@ -72,9 +72,7 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict:
|
|||
moy_ues.append(
|
||||
(
|
||||
ue["acronyme"],
|
||||
scu.fmt_note(
|
||||
nt.get_etud_ue_status(etudid, ue["ue_id"])["moy"]
|
||||
),
|
||||
scu.fmt_note(ue_status["moy"]),
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
@ -107,7 +105,9 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict:
|
|||
rangs.append(["rang_" + code_module, rang_module])
|
||||
|
||||
# Absences
|
||||
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, nt.sem)
|
||||
nbabsnj, nbabsjust, _ = sco_assiduites.get_assiduites_count(
|
||||
etudid, nt.sem
|
||||
)
|
||||
# En BUT, prend tout, sinon ne prend que les semestre validés par le jury
|
||||
if nt.is_apc or (
|
||||
dec
|
||||
|
@ -127,7 +127,7 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict:
|
|||
("date_debut", s["date_debut"]),
|
||||
("date_fin", s["date_fin"]),
|
||||
("periode", "%s - %s" % (s["mois_debut"], s["mois_fin"])),
|
||||
("AbsNonJust", nbabs - nbabsjust),
|
||||
("AbsNonJust", nbabsnj),
|
||||
("AbsJust", nbabsjust),
|
||||
]
|
||||
# ajout des 2 champs notes des modules et classement dans chaque module
|
||||
|
|
|
@ -1606,7 +1606,7 @@ class BasePreferences:
|
|||
{
|
||||
"initvalue": 1,
|
||||
"title": "Afficher icône indiquant si les bulletins sont publiés",
|
||||
"explanation": "décocher si vous n'avez pas de portail étudiant publiant les bulletins",
|
||||
"explanation": "décocher si vous n'avez pas de passerelle ou portail étudiant publiant les bulletins",
|
||||
"input_type": "boolcheckbox",
|
||||
"labels": ["non", "oui"],
|
||||
"category": "bul",
|
||||
|
|
|
@ -139,9 +139,9 @@ def feuille_preparation_jury(formsemestre_id):
|
|||
main_partition_id, ""
|
||||
)
|
||||
# absences:
|
||||
e_nbabs, e_nbabsjust = sco_assiduites.get_assiduites_count(etud.id, sem)
|
||||
nbabs[etud.id] = e_nbabs
|
||||
nbabsjust[etud.id] = e_nbabs - e_nbabsjust
|
||||
_, nbabsjust[etud.id], nbabs[etud.id] = sco_assiduites.get_assiduites_count(
|
||||
etud.id, sem
|
||||
)
|
||||
|
||||
# Codes des UE "semestre précédent":
|
||||
ue_prev_codes = list(prev_moy_ue.keys())
|
||||
|
|
|
@ -139,9 +139,8 @@ def dict_pvjury(
|
|||
dec_ue_list = _descr_decisions_ues(
|
||||
nt, etudid, d["decisions_ue"], d["decision_sem"]
|
||||
)
|
||||
d["decisions_ue_nb"] = len(
|
||||
dec_ue_list
|
||||
) # avec les UE capitalisées, donc des éventuels doublons
|
||||
# avec les UE capitalisées, donc des éventuels doublons:
|
||||
d["decisions_ue_nb"] = len(dec_ue_list)
|
||||
# Mais sur la description (eg sur les bulletins), on ne veut pas
|
||||
# afficher ces doublons: on uniquifie sur ue_code
|
||||
_codes = set()
|
||||
|
@ -291,8 +290,10 @@ def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem) -> list[dict]:
|
|||
)
|
||||
)
|
||||
):
|
||||
ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
|
||||
uelist.append(ue)
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
assert ue
|
||||
# note modernisation code: on utilise des dict tant que get_etud_ue_status renvoie des dicts
|
||||
uelist.append(ue.to_dict())
|
||||
# Les UE capitalisées dans d'autres semestres:
|
||||
if etudid in nt.validations.ue_capitalisees.index:
|
||||
for ue_id in nt.validations.ue_capitalisees.loc[[etudid]]["ue_id"]:
|
||||
|
|
|
@ -81,7 +81,7 @@ def formsemestre_recapcomplet(
|
|||
|
||||
mode_jury: cache modules, affiche lien saisie decision jury
|
||||
xml_with_decisions: publie décisions de jury dans xml et json
|
||||
force_publishing: publie les xml et json même si bulletins non publiés
|
||||
force_publishing: publie les xml et json même si bulletins non publiés (sur la passerelle)
|
||||
selected_etudid: etudid sélectionné (pour scroller au bon endroit)
|
||||
"""
|
||||
if not isinstance(formsemestre_id, int):
|
||||
|
@ -398,7 +398,7 @@ def gen_formsemestre_recapcomplet_json(
|
|||
) -> dict:
|
||||
"""JSON export: liste tous les bulletins JSON
|
||||
:param xml_nodate(bool): indique la date courante (attribut docdate)
|
||||
:param force_publishing: donne les bulletins même si non "publiés sur portail"
|
||||
:param force_publishing: donne les bulletins même si non "publiés sur la passerelle"
|
||||
:returns: dict
|
||||
"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
|
@ -442,13 +442,13 @@ def gen_formsemestre_recapcomplet_json(
|
|||
|
||||
|
||||
def formsemestres_bulletins(annee_scolaire):
|
||||
"""Tous les bulletins des semestres publiés des semestres de l'année indiquée.
|
||||
"""Tous les bulletins des semestres de l'année indiquée.
|
||||
:param annee_scolaire(int): année de début de l'année scolaire
|
||||
:returns: JSON
|
||||
"""
|
||||
js_list = []
|
||||
sems = sco_formsemestre.list_formsemestre_by_etape(annee_scolaire=annee_scolaire)
|
||||
log("formsemestres_bulletins(%s): %d sems" % (annee_scolaire, len(sems)))
|
||||
log(f"formsemestres_bulletins({annee_scolaire}): {len(sems)} sems")
|
||||
for sem in sems:
|
||||
js_data = gen_formsemestre_recapcomplet_json(
|
||||
sem["formsemestre_id"], force_publishing=False
|
||||
|
|
|
@ -134,12 +134,12 @@ def _displayNote(val):
|
|||
return val
|
||||
|
||||
|
||||
def _check_notes(notes: list[(int, float)], evaluation: Evaluation):
|
||||
# XXX typehint : float or str
|
||||
def _check_notes(notes: list[(int, float | str)], evaluation: Evaluation):
|
||||
"""notes is a list of tuples (etudid, value)
|
||||
mod is the module (used to ckeck type, for malus)
|
||||
returns list of valid notes (etudid, float value)
|
||||
and 4 lists of etudid: etudids_invalids, etudids_without_notes, etudids_absents, etudid_to_suppress
|
||||
and 4 lists of etudid:
|
||||
etudids_invalids, etudids_without_notes, etudids_absents, etudid_to_suppress
|
||||
"""
|
||||
note_max = evaluation.note_max or 0.0
|
||||
module: Module = evaluation.moduleimpl.module
|
||||
|
@ -148,7 +148,10 @@ def _check_notes(notes: list[(int, float)], evaluation: Evaluation):
|
|||
scu.ModuleType.RESSOURCE,
|
||||
scu.ModuleType.SAE,
|
||||
):
|
||||
note_min = scu.NOTES_MIN
|
||||
if evaluation.evaluation_type == Evaluation.EVALUATION_BONUS:
|
||||
note_min, note_max = -20, 20
|
||||
else:
|
||||
note_min = scu.NOTES_MIN
|
||||
elif module.module_type == ModuleType.MALUS:
|
||||
note_min = -20.0
|
||||
else:
|
||||
|
@ -528,6 +531,7 @@ def notes_add(
|
|||
|
||||
Return: tuple (etudids_changed, nb_suppress, etudids_with_decision)
|
||||
"""
|
||||
assert evaluation_id is not None
|
||||
now = psycopg2.Timestamp(*time.localtime()[:6])
|
||||
|
||||
# Vérifie inscription et valeur note
|
||||
|
@ -539,7 +543,7 @@ def notes_add(
|
|||
}
|
||||
for etudid, value in notes:
|
||||
if check_inscription and (etudid not in inscrits):
|
||||
raise NoteProcessError(f"etudiant {etudid} non inscrit dans ce module")
|
||||
raise NoteProcessError(f"étudiant {etudid} non inscrit dans ce module")
|
||||
if (value is not None) and not isinstance(value, float):
|
||||
raise NoteProcessError(
|
||||
f"etudiant {etudid}: valeur de note invalide ({value})"
|
||||
|
@ -880,7 +884,7 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
|
|||
if evaluation.date_debut:
|
||||
indication_date = evaluation.date_debut.date().isoformat()
|
||||
else:
|
||||
indication_date = scu.sanitize_filename(evaluation.description or "")[:12]
|
||||
indication_date = scu.sanitize_filename(evaluation.description)[:12]
|
||||
eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
|
||||
|
||||
date_str = (
|
||||
|
|
|
@ -35,7 +35,7 @@ from flask import g, url_for
|
|||
from flask_login import current_user
|
||||
|
||||
from app import db, log
|
||||
from app.models import Admission, Adresse, Identite, ScolarNews
|
||||
from app.models import Admission, Adresse, FormSemestre, Identite, ScolarNews
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
@ -94,6 +94,7 @@ def formsemestre_synchro_etuds(
|
|||
que l'on va importer/inscrire
|
||||
"""
|
||||
etuds = etuds or []
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
inscrits_without_key = inscrits_without_key or []
|
||||
log(f"formsemestre_synchro_etuds: formsemestre_id={formsemestre_id}")
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
|
@ -109,12 +110,13 @@ def formsemestre_synchro_etuds(
|
|||
raise ScoValueError("opération impossible: semestre verrouille")
|
||||
if not sem["etapes"]:
|
||||
raise ScoValueError(
|
||||
"""opération impossible: ce semestre n'a pas de code étape
|
||||
(voir "<a href="formsemestre_editwithmodules?formsemestre_id=%(formsemestre_id)s">Modifier ce semestre</a>")
|
||||
f"""opération impossible: ce semestre n'a pas de code étape
|
||||
(voir <a class="stdlink" href="{
|
||||
url_for('notes.formsemestre_editwithmodules',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||
}">Modifier ce semestre</a>)
|
||||
"""
|
||||
% sem
|
||||
)
|
||||
header = html_sco_header.sco_header(page_title="Synchronisation étudiants")
|
||||
footer = html_sco_header.sco_footer()
|
||||
base_url = url_for(
|
||||
"notes.formsemestre_synchro_etuds",
|
||||
|
@ -165,7 +167,13 @@ def formsemestre_synchro_etuds(
|
|||
suffix=scu.XLSX_SUFFIX,
|
||||
)
|
||||
|
||||
H = [header]
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Synchronisation étudiants",
|
||||
init_qtip=True,
|
||||
javascripts=["js/etud_info.js"],
|
||||
)
|
||||
]
|
||||
if not submitted:
|
||||
H += _build_page(
|
||||
sem,
|
||||
|
@ -184,7 +192,7 @@ def formsemestre_synchro_etuds(
|
|||
inscrits_without_key
|
||||
)
|
||||
log("a_desinscrire_without_key=%s" % a_desinscrire_without_key)
|
||||
inscrits_ailleurs = set(sco_inscr_passage.list_inscrits_date(sem))
|
||||
inscrits_ailleurs = set(sco_inscr_passage.list_inscrits_date(formsemestre))
|
||||
a_inscrire = a_inscrire.intersection(etuds_set)
|
||||
|
||||
if not dialog_confirmed:
|
||||
|
@ -205,10 +213,12 @@ def formsemestre_synchro_etuds(
|
|||
|
||||
a_inscrire_en_double = inscrits_ailleurs.intersection(a_inscrire)
|
||||
if a_inscrire_en_double:
|
||||
H.append("<h3>dont étudiants déjà inscrits:</h3><ol>")
|
||||
H.append(
|
||||
"<h3>dont étudiants déjà inscrits dans un autre semestre:</h3><ol>"
|
||||
)
|
||||
for key in a_inscrire_en_double:
|
||||
nom = f"""{etudsapo_ident[key]['nom']} {etudsapo_ident[key].get("prenom", "")}"""
|
||||
H.append(f'<li class="inscrailleurs">{nom}</li>')
|
||||
H.append(f'<li class="inscrit-ailleurs">{nom}</li>')
|
||||
H.append("</ol>")
|
||||
|
||||
if a_desinscrire:
|
||||
|
@ -260,16 +270,26 @@ def formsemestre_synchro_etuds(
|
|||
etudids_a_desinscrire = [nip2etudid(x) for x in a_desinscrire]
|
||||
etudids_a_desinscrire += a_desinscrire_without_key
|
||||
#
|
||||
# check decisions jury ici pour éviter de recontruire le cache
|
||||
# après chaque desinscription
|
||||
sco_formsemestre_inscriptions.check_if_has_decision_jury(
|
||||
formsemestre, a_desinscrire
|
||||
)
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
do_import_etuds_from_portal(sem, a_importer, etudsapo_ident)
|
||||
sco_inscr_passage.do_inscrit(sem, etudids_a_inscrire)
|
||||
sco_inscr_passage.do_desinscrit(sem, etudids_a_desinscrire)
|
||||
do_import_etuds_from_portal(formsemestre, a_importer, etudsapo_ident)
|
||||
sco_inscr_passage.do_inscrit(formsemestre, etudids_a_inscrire)
|
||||
sco_inscr_passage.do_desinscrit(
|
||||
formsemestre, etudids_a_desinscrire, check_has_dec_jury=False
|
||||
)
|
||||
|
||||
H.append(
|
||||
"""<h3>Opération effectuée</h3>
|
||||
f"""<h3>Opération effectuée</h3>
|
||||
<ul>
|
||||
<li><a class="stdlink" href="formsemestre_synchro_etuds?formsemestre_id=%s">Continuer la synchronisation</a></li>"""
|
||||
% formsemestre_id
|
||||
<li><a class="stdlink" href="{
|
||||
url_for('notes.formsemestre_synchro_etuds',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
|
||||
)}">Continuer la synchronisation</a>
|
||||
</li>"""
|
||||
)
|
||||
#
|
||||
partitions = sco_groups.get_partitions_list(
|
||||
|
@ -279,8 +299,9 @@ def formsemestre_synchro_etuds(
|
|||
H.append(
|
||||
f"""<li><a class="stdlink" href="{
|
||||
url_for("scolar.partition_editor",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
||||
}">Répartir les groupes de {partitions[0]["partition_name"]}</a></li>
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
|
||||
)}">Répartir les groupes de {partitions[0]["partition_name"]}</a>
|
||||
</li>
|
||||
"""
|
||||
)
|
||||
|
||||
|
@ -618,7 +639,7 @@ def get_annee_naissance(ddmmyyyyy: str) -> int:
|
|||
return None
|
||||
|
||||
|
||||
def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
|
||||
def do_import_etuds_from_portal(formsemestre: FormSemestre, a_importer, etudsapo_ident):
|
||||
"""Inscrit les etudiants Apogee dans ce semestre."""
|
||||
log(f"do_import_etuds_from_portal: a_importer={a_importer}")
|
||||
if not a_importer:
|
||||
|
@ -672,7 +693,7 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
|
|||
|
||||
# Inscription au semestre
|
||||
sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
|
||||
sem["formsemestre_id"],
|
||||
formsemestre.id,
|
||||
etud.id,
|
||||
etat=scu.INSCRIT,
|
||||
etape=args["etape"],
|
||||
|
@ -716,7 +737,7 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
|
|||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_INSCR,
|
||||
text=f"Import Apogée de {len(created_etudids)} étudiants en ",
|
||||
obj=sem["formsemestre_id"],
|
||||
obj=formsemestre.id,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -60,16 +60,14 @@ from app.models.formsemestre import FormSemestre
|
|||
|
||||
|
||||
from app import db, log
|
||||
from app.models import Evaluation, ModuleImpl, UniteEns
|
||||
from app.models import Evaluation, Identite, ModuleImpl, UniteEns
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_edit_matiere
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_saisie_notes
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||
|
@ -83,10 +81,10 @@ def external_ue_create(
|
|||
acronyme="",
|
||||
ue_type=codes_cursus.UE_STANDARD,
|
||||
ects=0.0,
|
||||
) -> int:
|
||||
) -> ModuleImpl:
|
||||
"""Crée UE/matiere/module dans la formation du formsemestre
|
||||
puis un moduleimpl.
|
||||
Return: moduleimpl_id
|
||||
Return: moduleimpl
|
||||
"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
log(f"creating external UE in {formsemestre}: {acronyme}")
|
||||
|
@ -139,28 +137,30 @@ def external_ue_create(
|
|||
"module_id": module_id,
|
||||
"formsemestre_id": formsemestre_id,
|
||||
# affecte le 1er responsable du semestre comme resp. du module
|
||||
"responsable_id": formsemestre.responsables[0].id
|
||||
if len(formsemestre.responsables)
|
||||
else None,
|
||||
"responsable_id": (
|
||||
formsemestre.responsables[0].id
|
||||
if len(formsemestre.responsables)
|
||||
else None
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
return moduleimpl_id
|
||||
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
assert modimpl
|
||||
return modimpl
|
||||
|
||||
|
||||
def external_ue_inscrit_et_note(
|
||||
moduleimpl_id: int, formsemestre_id: int, notes_etuds: dict
|
||||
moduleimpl: ModuleImpl, formsemestre_id: int, notes_etuds: dict
|
||||
):
|
||||
"""Inscrit les étudiants au moduleimpl, crée au besoin une évaluation
|
||||
et enregistre les notes.
|
||||
"""
|
||||
moduleimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||
log(
|
||||
f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl_id}, notes_etuds={notes_etuds})"
|
||||
f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl.id}, notes_etuds={notes_etuds})"
|
||||
)
|
||||
# Inscription des étudiants
|
||||
sco_moduleimpl.do_moduleimpl_inscrit_etuds(
|
||||
moduleimpl_id,
|
||||
moduleimpl.id,
|
||||
formsemestre_id,
|
||||
list(notes_etuds.keys()),
|
||||
)
|
||||
|
@ -175,7 +175,7 @@ def external_ue_inscrit_et_note(
|
|||
note_max=20.0,
|
||||
coefficient=1.0,
|
||||
publish_incomplete=True,
|
||||
evaluation_type=scu.EVALUATION_NORMALE,
|
||||
evaluation_type=Evaluation.EVALUATION_NORMALE,
|
||||
visibulletin=False,
|
||||
description="note externe",
|
||||
)
|
||||
|
@ -188,12 +188,12 @@ def external_ue_inscrit_et_note(
|
|||
)
|
||||
|
||||
|
||||
def get_existing_external_ue(formation_id: int) -> list[dict]:
|
||||
"Liste de toutes les UE externes définies dans cette formation"
|
||||
return sco_edit_ue.ue_list(args={"formation_id": formation_id, "is_external": True})
|
||||
def get_existing_external_ue(formation_id: int) -> list[UniteEns]:
|
||||
"Liste de toutes les UEs externes définies dans cette formation"
|
||||
return UniteEns.query.filter_by(formation_id=formation_id, is_external=True).all()
|
||||
|
||||
|
||||
def get_external_moduleimpl_id(formsemestre_id: int, ue_id: int) -> int:
|
||||
def get_external_moduleimpl(formsemestre_id: int, ue_id: int) -> ModuleImpl:
|
||||
"moduleimpl correspondant à l'UE externe indiquée de ce formsemestre"
|
||||
r = ndb.SimpleDictFetch(
|
||||
"""
|
||||
|
@ -205,7 +205,10 @@ def get_external_moduleimpl_id(formsemestre_id: int, ue_id: int) -> int:
|
|||
{"ue_id": ue_id, "formsemestre_id": formsemestre_id},
|
||||
)
|
||||
if r:
|
||||
return r[0]["moduleimpl_id"]
|
||||
modimpl_id = r[0]["moduleimpl_id"]
|
||||
modimpl = ModuleImpl.query.get(modimpl_id)
|
||||
assert modimpl
|
||||
return modimpl
|
||||
else:
|
||||
raise ScoValueError(
|
||||
f"""Aucun module externe ne correspond
|
||||
|
@ -225,20 +228,20 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
|
|||
|
||||
En BUT, pas d'UEs externes. Voir https://scodoc.org/git/ScoDoc/ScoDoc/issues/542
|
||||
"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
# Contrôle d'accès:
|
||||
if not formsemestre.can_be_edited_by(current_user):
|
||||
raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
|
||||
if formsemestre.formation.is_apc():
|
||||
raise ScoValueError("Impossible d'ajouter une UE externe en BUT")
|
||||
|
||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||
etud = Identite.get_etud(etudid)
|
||||
formation_id = formsemestre.formation.id
|
||||
existing_external_ue = get_existing_external_ue(formation_id)
|
||||
|
||||
H = [
|
||||
html_sco_header.html_sem_header(
|
||||
"Ajout d'une UE externe pour %(nomprenom)s" % etud,
|
||||
f"Ajout d'une UE externe pour {etud.nomprenom}",
|
||||
javascripts=["js/sco_ue_external.js"],
|
||||
),
|
||||
"""<p class="help">Cette page permet d'indiquer que l'étudiant a suivi une UE
|
||||
|
@ -275,10 +278,10 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
|
|||
"input_type": "menu",
|
||||
"title": "UE externe existante:",
|
||||
"allowed_values": [""]
|
||||
+ [str(ue["ue_id"]) for ue in existing_external_ue],
|
||||
+ [str(ue.id) for ue in existing_external_ue],
|
||||
"labels": [default_label]
|
||||
+ [
|
||||
"%s (%s)" % (ue["titre"], ue["acronyme"])
|
||||
f"{ue.titre or ''} ({ue.acronyme})"
|
||||
for ue in existing_external_ue
|
||||
],
|
||||
"attributes": ['onchange="update_external_ue_form();"'],
|
||||
|
@ -364,7 +367,7 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
|
|||
)
|
||||
if tf[2]["existing_ue"]:
|
||||
ue_id = int(tf[2]["existing_ue"])
|
||||
moduleimpl_id = get_external_moduleimpl_id(formsemestre_id, ue_id)
|
||||
modimpl = get_external_moduleimpl(formsemestre_id, ue_id)
|
||||
else:
|
||||
acronyme = tf[2]["acronyme"].strip()
|
||||
if not acronyme:
|
||||
|
@ -375,7 +378,7 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
|
|||
+ tf[1]
|
||||
+ html_footer
|
||||
)
|
||||
moduleimpl_id = external_ue_create(
|
||||
modimpl = external_ue_create(
|
||||
formsemestre_id,
|
||||
titre=tf[2]["titre"],
|
||||
acronyme=acronyme,
|
||||
|
@ -384,7 +387,7 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
|
|||
)
|
||||
|
||||
external_ue_inscrit_et_note(
|
||||
moduleimpl_id,
|
||||
modimpl,
|
||||
formsemestre_id,
|
||||
{etudid: note_value},
|
||||
)
|
||||
|
|
|
@ -48,16 +48,15 @@ Opérations:
|
|||
import datetime
|
||||
from flask import request
|
||||
|
||||
from app.models import FormSemestre
|
||||
from app.models import Evaluation, FormSemestre
|
||||
from app.scodoc.intervals import intervalmap
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_users
|
||||
import sco_version
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
import sco_version
|
||||
|
||||
# deux notes (de même uid) sont considérées comme de la même opération si
|
||||
# elles sont séparées de moins de 2*tolerance:
|
||||
|
@ -149,10 +148,8 @@ def list_operations(evaluation_id):
|
|||
|
||||
def evaluation_list_operations(evaluation_id):
|
||||
"""Page listing operations on evaluation"""
|
||||
E = sco_evaluation_db.get_evaluations_dict({"evaluation_id": evaluation_id})[0]
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
|
||||
Ops = list_operations(evaluation_id)
|
||||
evaluation = Evaluation.get_evaluation(evaluation_id)
|
||||
operations = list_operations(evaluation_id)
|
||||
|
||||
columns_ids = ("datestr", "user_name", "nb_notes", "comment")
|
||||
titles = {
|
||||
|
@ -164,11 +161,14 @@ def evaluation_list_operations(evaluation_id):
|
|||
tab = GenTable(
|
||||
titles=titles,
|
||||
columns_ids=columns_ids,
|
||||
rows=Ops,
|
||||
rows=operations,
|
||||
html_sortable=False,
|
||||
html_title="<h2>Opérations sur l'évaluation %s du %s</h2>"
|
||||
% (E["description"], E["jour"]),
|
||||
preferences=sco_preferences.SemPreferences(M["formsemestre_id"]),
|
||||
html_title=f"""<h2>Opérations sur l'évaluation {evaluation.description} {
|
||||
evaluation.date_debut.strftime("du %d/%m/%Y") if evaluation.date_debut else "(sans date)"
|
||||
}</h2>""",
|
||||
preferences=sco_preferences.SemPreferences(
|
||||
evaluation.moduleimpl.formsemestre_id
|
||||
),
|
||||
)
|
||||
return tab.make_page()
|
||||
|
||||
|
|
|
@ -130,7 +130,8 @@ def print_progress_bar(
|
|||
decimals - Optional : nombres de chiffres après la virgule (Int)
|
||||
length - Optional : taille de la barre en nombre de caractères (Int)
|
||||
fill - Optional : charactère de remplissange de la barre (Str)
|
||||
autosize - Optional : Choisir automatiquement la taille de la barre en fonction du terminal (Bool)
|
||||
autosize - Optional : Choisir automatiquement la taille de la barre
|
||||
en fonction du terminal (Bool)
|
||||
"""
|
||||
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
|
||||
color = TerminalColor.RED
|
||||
|
@ -174,11 +175,15 @@ class BiDirectionalEnum(Enum):
|
|||
@classmethod
|
||||
def contains(cls, attr: str):
|
||||
"""Vérifie sur un attribut existe dans l'enum"""
|
||||
|
||||
# Existe dans la classe parent de Enum (EnumType)
|
||||
# pylint: disable-next=no-member
|
||||
return attr.upper() in cls._member_names_
|
||||
|
||||
@classmethod
|
||||
def all(cls, keys=True):
|
||||
"""Retourne toutes les clés de l'enum"""
|
||||
# pylint: disable-next=no-member
|
||||
return cls._member_names_ if keys else list(cls._value2member_map_.keys())
|
||||
|
||||
@classmethod
|
||||
|
@ -207,6 +212,9 @@ class EtatAssiduite(int, BiDirectionalEnum):
|
|||
ABSENT = 2
|
||||
|
||||
def version_lisible(self) -> str:
|
||||
"""Retourne une version lisible des états d'assiduités
|
||||
Est utilisé pour les vues.
|
||||
"""
|
||||
return {
|
||||
EtatAssiduite.PRESENT: "Présence",
|
||||
EtatAssiduite.ABSENT: "Absence",
|
||||
|
@ -225,6 +233,9 @@ class EtatJustificatif(int, BiDirectionalEnum):
|
|||
MODIFIE = 3
|
||||
|
||||
def version_lisible(self) -> str:
|
||||
"""Retourne une version lisible des états de justificatifs
|
||||
Est utilisé pour les vues.
|
||||
"""
|
||||
return {
|
||||
EtatJustificatif.VALIDE: "valide",
|
||||
EtatJustificatif.ATTENTE: "soumis",
|
||||
|
@ -254,11 +265,13 @@ class NonWorkDays(int, BiDirectionalEnum):
|
|||
cls, formsemestre_id: int = None, dept_id: int = None
|
||||
) -> list["NonWorkDays"]:
|
||||
"""
|
||||
get_all_non_work_days Récupère la liste des non workdays (str) depuis les préférences
|
||||
get_all_non_work_days Récupère la liste des non workdays
|
||||
(str) depuis les préférences
|
||||
puis renvoie une liste BiDirectionnalEnum<int> NonWorkDays
|
||||
|
||||
Example:
|
||||
non_work_days : list[NonWorkDays] = NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
||||
non_work_days : list[NonWorkDays] =
|
||||
NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
||||
if datetime.datetime.now().weekday() in non_work_days:
|
||||
print("Aujourd'hui est un jour non travaillé")
|
||||
|
||||
|
@ -269,6 +282,8 @@ class NonWorkDays(int, BiDirectionalEnum):
|
|||
Returns:
|
||||
list[NonWorkDays]: La liste des NonWorkDays en version BiDirectionnalEnum<int>
|
||||
"""
|
||||
# Import circulaire
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
return [
|
||||
|
@ -454,10 +469,6 @@ NOTES_MENTIONS_LABS = (
|
|||
"Excellent",
|
||||
)
|
||||
|
||||
EVALUATION_NORMALE = 0
|
||||
EVALUATION_RATTRAPAGE = 1
|
||||
EVALUATION_SESSION2 = 2
|
||||
|
||||
# Dates et années scolaires
|
||||
# Ces dates "pivot" sont paramétrables dans les préférences générales
|
||||
# on donne ici les valeurs par défaut.
|
||||
|
|
|
@ -25,12 +25,14 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
"""Classe stockant le VDI avec le code étape (noms de fichiers maquettes et code semestres)
|
||||
"""Apogée: gestion du VDI avec le code étape (noms de fichiers maquettes et code semestres)
|
||||
"""
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
class ApoEtapeVDI(object):
|
||||
"""Classe stockant le VDI avec le code étape (noms de fichiers maquettes et code semestres)"""
|
||||
|
||||
_ETAPE_VDI_SEP = "!"
|
||||
|
||||
def __init__(self, etape_vdi: str = None, etape: str = "", vdi: str = ""):
|
||||
|
@ -110,7 +112,8 @@ class ApoEtapeVDI(object):
|
|||
elif len(t) == 2:
|
||||
etape, vdi = t
|
||||
else:
|
||||
raise ValueError("invalid code etape")
|
||||
# code étape invalide
|
||||
etape, vdi = "", ""
|
||||
return etape, vdi
|
||||
else:
|
||||
return etape_vdi, ""
|
||||
|
|
|
@ -35,6 +35,11 @@
|
|||
min-width: var(--sco-content-min-width);
|
||||
max-width: var(--sco-content-max-width);
|
||||
}
|
||||
div.jury_but_warning {
|
||||
background-color: yellow;
|
||||
border-color: red;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
div.jury_but_box_title {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
|
|
@ -273,6 +273,10 @@ section>div:nth-child(1) {
|
|||
min-width: 80px;
|
||||
display: inline-block;
|
||||
}
|
||||
div.eval-bonus {
|
||||
color: #197614;
|
||||
background-color: pink;
|
||||
}
|
||||
|
||||
.ueBonus,
|
||||
.ueBonus h3 {
|
||||
|
@ -280,7 +284,7 @@ section>div:nth-child(1) {
|
|||
color: #000 !important;
|
||||
}
|
||||
/* UE Capitalisée */
|
||||
.synthese .ue.capitalisee,
|
||||
.synthese .ue.capitalisee,
|
||||
.ue.capitalisee>h3{
|
||||
background: var(--couleurFondTitresUECapitalisee);;
|
||||
}
|
||||
|
|
|
@ -962,10 +962,18 @@ td.fichetitre2 .fl {
|
|||
div.section_but {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-items: flex-end;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
div.fiche_but_col2 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
div.fiche_total_etcs {
|
||||
font-weight: bold;
|
||||
margin-top: 16px;
|
||||
}
|
||||
div.section_but > div.link_validation_rcues {
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
|
@ -1461,6 +1469,9 @@ span.eval_title {
|
|||
font-size: 14pt;
|
||||
}
|
||||
|
||||
#evaluation-edit-blocked td, #evaluation-edit-coef td {
|
||||
padding-top: 24px;
|
||||
}
|
||||
/* #saisie_notes span.eval_title {
|
||||
border-bottom: 1px solid rgb(100,100,100);
|
||||
}
|
||||
|
@ -1793,11 +1804,42 @@ table.formsemestre_status {
|
|||
tr.formsemestre_status {
|
||||
background-color: rgb(90%, 90%, 90%);
|
||||
}
|
||||
|
||||
table.formsemestre_status tr td:first-child {
|
||||
padding-left: 4px;
|
||||
}
|
||||
table.formsemestre_status tr td:last-child {
|
||||
padding-right: 8px;
|
||||
}
|
||||
tr.formsemestre_status_green {
|
||||
background-color: #eff7f2;
|
||||
}
|
||||
|
||||
tr.modimpl_non_conforme td {
|
||||
background-color: #ffc458;
|
||||
}
|
||||
tr.modimpl_non_conforme td, tr.modimpl_attente td {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
tr.modimpl_has_blocked span.nb_evals_blocked, tr span.evals_attente {
|
||||
background-color: yellow;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
tr.modimpl_has_blocked span.nb_evals_blocked {
|
||||
color: red;
|
||||
}
|
||||
tr span.evals_attente {
|
||||
background-color: orange;
|
||||
color: green;
|
||||
}
|
||||
table.formsemestre_status a.redlink {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
tr.formsemestre_status_ue {
|
||||
background-color: rgb(90%, 90%, 90%);
|
||||
}
|
||||
|
@ -2075,15 +2117,23 @@ th.moduleimpl_evaluations a:hover {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
tr.mievr_in.evaluation_blocked th.moduleimpl_evaluation_moy span, tr.evaluation_blocked th.moduleimpl_evaluation_moy a {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
background-color: yellow;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
tr.mievr {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
tr.mievr_rattr {
|
||||
tr.mievr_rattr, tr.mievr_session2, tr.mievr_bonus {
|
||||
background-color: #dddddd;
|
||||
}
|
||||
|
||||
span.mievr_rattr {
|
||||
span.mievr_rattr, span.mievr_session2, span.mievr_bonus {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
font-size: 80%;
|
||||
|
@ -2129,6 +2179,16 @@ tr.mievr.non_visible_inter th {
|
|||
);
|
||||
}
|
||||
|
||||
tr.mievr_tit.evaluation_blocked td,tr.mievr_tit.evaluation_blocked th {
|
||||
background-image: radial-gradient(#bd7777 1px, transparent 1px);
|
||||
background-size: 10px 10px;
|
||||
}
|
||||
tr.mievr_in.evaluation_blocked td, tr.mievr_in.evaluation_blocked th {
|
||||
background-color: rgb(195, 235, 255);
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
|
||||
tr.mievr th {
|
||||
background-color: white;
|
||||
}
|
||||
|
@ -2139,6 +2199,7 @@ tr.mievr td.mievr {
|
|||
|
||||
tr.mievr td.mievr_menu {
|
||||
width: 110px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
tr.mievr td.mievr_dur {
|
||||
|
@ -2411,6 +2472,29 @@ div.formation_list_ues_titre {
|
|||
color: #eee;
|
||||
}
|
||||
|
||||
div.formation_semestre_niveaux_warning {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
padding: 4px;
|
||||
margin-top: 8px;
|
||||
margin-left: 24px;
|
||||
margin-right: 24px;
|
||||
background-color: yellow;
|
||||
border-radius: 8px;
|
||||
}
|
||||
div.formation_semestre_niveaux_warning div {
|
||||
color: black;
|
||||
font-size: 110%;
|
||||
}
|
||||
div.formation_semestre_niveaux_warning ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
div.formation_semestre_niveaux_warning ul li:before {
|
||||
content: '⚠️';
|
||||
margin-right: 10px; /* Adjust space between emoji and text */
|
||||
}
|
||||
|
||||
div.formation_list_modules,
|
||||
div.formation_list_ues {
|
||||
border-radius: 18px;
|
||||
|
@ -2426,6 +2510,7 @@ div.formation_list_ues {
|
|||
}
|
||||
|
||||
div.formation_list_ues_content {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
div.formation_list_modules {
|
||||
|
@ -2508,7 +2593,13 @@ div.formation_parcs > div {
|
|||
opacity: 0.7;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
padding: 4px 8px;
|
||||
padding: 2px 6px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
div.formation_parcs > div.ue_tc {
|
||||
color: black;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
div.formation_parcs > div.focus {
|
||||
|
@ -3316,14 +3407,35 @@ li.tf-msg {
|
|||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
font-weight: bold;
|
||||
div.formsemestre-warning-box {
|
||||
background-color: yellow;
|
||||
border-radius: 4px;
|
||||
margin-top: 12px;
|
||||
margin-left: 0px;
|
||||
padding-left: 0px;
|
||||
padding-right: 4px;
|
||||
padding-top: 2px;
|
||||
/* padding-bottom: 1px; */
|
||||
}
|
||||
|
||||
.warning, .warning-bloquant {
|
||||
color: red;
|
||||
margin-left: 16px;
|
||||
margin-bottom: 8px;
|
||||
min-width: var(--sco-content-min-width);
|
||||
max-width: var(--sco-content-max-width);
|
||||
}
|
||||
|
||||
.warning::before {
|
||||
content: url(/ScoDoc/static/icons/warning_img.png);
|
||||
vertical-align: -80%;
|
||||
content:"";
|
||||
margin-right: 8px;
|
||||
height:32px;
|
||||
width: 32px;
|
||||
background-size: 32px 32px;
|
||||
background-image: url(/ScoDoc/static/icons/warning_std.svg);
|
||||
background-repeat: no-repeat;
|
||||
display: inline-block;
|
||||
vertical-align: -40%;
|
||||
}
|
||||
|
||||
.warning-light {
|
||||
|
@ -3336,6 +3448,19 @@ li.tf-msg {
|
|||
/* EMO_WARNING, "⚠️" */
|
||||
}
|
||||
|
||||
.warning-bloquant::before {
|
||||
content:"";
|
||||
margin-right: 8px;
|
||||
height:32px;
|
||||
width: 32px;
|
||||
background-size: 32px 32px;
|
||||
background-image: url(/ScoDoc/static/icons/warning_bloquant.svg);
|
||||
background-repeat: no-repeat;
|
||||
display: inline-block;
|
||||
vertical-align: -40%;
|
||||
}
|
||||
|
||||
|
||||
p.error {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
|
@ -3714,10 +3839,17 @@ span.sp_etape {
|
|||
color: black;
|
||||
}
|
||||
|
||||
.inscrailleurs {
|
||||
.deja-inscrit {
|
||||
font-weight: bold;
|
||||
color: rgb(1, 76, 1) !important;
|
||||
}
|
||||
.inscrit-ailleurs {
|
||||
font-weight: bold;
|
||||
color: red !important;
|
||||
}
|
||||
div.etuds_select_boxes {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
span.paspaye,
|
||||
span.paspaye a {
|
||||
|
@ -4682,6 +4814,10 @@ table.table_recap th.col_malus {
|
|||
font-weight: bold;
|
||||
color: rgb(165, 0, 0);
|
||||
}
|
||||
table.table_recap td.col_eval_bonus,
|
||||
table.table_recap th.col_eval_bonus {
|
||||
color: #90c;
|
||||
}
|
||||
|
||||
table.table_recap tr.ects td {
|
||||
color: rgb(160, 86, 3);
|
||||
|
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.5 KiB |
|
@ -0,0 +1 @@
|
|||
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m208.587 54.407-201.17 348.437c-21.073 36.499 5.268 82.122 47.413 82.122h402.34c42.145 0 68.486-45.624 47.413-82.122l-201.17-348.437c-21.072-36.498-73.754-36.498-94.826 0z" fill="#da4a54"/><path d="m54.83 443.76c-6.802 0-10.267-4.242-11.727-6.771s-3.401-7.65 0-13.542l201.17-348.436c3.401-5.891 8.807-6.771 11.727-6.771s8.326.88 11.727 6.771l201.17 348.436c3.401 5.891 1.46 11.013 0 13.541-1.46 2.529-4.925 6.771-11.727 6.771h-402.34z" fill="#f6e266"/><g fill="#544f57"><path d="m256 327.138c-14.379 0-26.036-11.657-26.036-26.036v-119.216c0-14.379 11.657-26.036 26.036-26.036 14.379 0 26.036 11.657 26.036 26.036v119.217c0 14.379-11.657 26.035-26.036 26.035z"/><circle cx="256" cy="381.152" r="26.036"/></g></svg>
|
After Width: | Height: | Size: 857 B |
|
@ -491,14 +491,15 @@ class releveBUT extends HTMLElement {
|
|||
let output = "";
|
||||
evaluations.forEach((evaluation) => {
|
||||
output += `
|
||||
<div class=eval>
|
||||
<div class="eval ${evaluation.evaluation_type == 3 ? "eval-bonus" : ""}">
|
||||
<div>${this.URL(evaluation.url, evaluation.description || "Évaluation")}</div>
|
||||
<div>
|
||||
${evaluation.note.value}
|
||||
<em>Coef. ${evaluation.coef ?? "*"}</em>
|
||||
<em>${evaluation.evaluation_type == 0 ? "Coef." : evaluation.evaluation_type == 3 ? "Bonus" : ""
|
||||
} ${evaluation.coef ?? ""}</em>
|
||||
</div>
|
||||
<div class=complement>
|
||||
<div>Coef</div><div>${evaluation.coef}</div>
|
||||
<div>${evaluation.evaluation_type == 0 ? "Coef." : ""}</div><div>${evaluation.coef ?? ""}</div>
|
||||
<div>Max. promo.</div><div>${evaluation.note.max}</div>
|
||||
<div>Moy. promo.</div><div>${evaluation.note.moy}</div>
|
||||
<div>Min. promo.</div><div>${evaluation.note.min}</div>
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
"""
|
||||
Gestion des listes d'assiduités et justificatifs
|
||||
(affichage, pagination, filtrage, options d'affichage, tableaux)
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from flask import url_for
|
||||
|
@ -8,10 +13,18 @@ from sqlalchemy import desc, literal, union, asc
|
|||
from app import db, g
|
||||
from app.auth.models import User
|
||||
from app.models import Assiduite, Identite, Justificatif
|
||||
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, to_bool
|
||||
from app.scodoc.sco_utils import (
|
||||
EtatAssiduite,
|
||||
EtatJustificatif,
|
||||
to_bool,
|
||||
date_debut_annee_scolaire,
|
||||
date_fin_annee_scolaire,
|
||||
localize_datetime,
|
||||
)
|
||||
from app.tables import table_builder as tb
|
||||
from app.scodoc.sco_cache import RequeteTableauAssiduiteCache
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_preferences import get_preference
|
||||
|
||||
|
||||
class Pagination:
|
||||
|
@ -26,9 +39,11 @@ class Pagination:
|
|||
On peut ensuite récupérer les éléments de la page courante avec la méthode `items()`
|
||||
|
||||
Cette classe ne permet pas de changer de page.
|
||||
(Pour cela, il faut créer une nouvelle instance, avec la collection originelle et la nouvelle page)
|
||||
(Pour cela, il faut créer une nouvelle instance,
|
||||
avec la collection originelle et la nouvelle page)
|
||||
|
||||
l'intéret est de ne pas garder en mémoire toute la collection, mais seulement la page courante
|
||||
l'intéret est de ne pas garder en mémoire toute la collection,
|
||||
mais seulement la page courante
|
||||
|
||||
"""
|
||||
|
||||
|
@ -37,9 +52,11 @@ class Pagination:
|
|||
__init__ Instancie un nouvel objet Pagination
|
||||
|
||||
Args:
|
||||
collection (list): La collection à paginer. Il s'agit par exemple d'une requête
|
||||
collection (list): La collection à paginer.
|
||||
Il s'agit par exemple d'une requête
|
||||
page (int, optional): le numéro de la page à voir. Defaults to 1.
|
||||
per_page (int, optional): le nombre d'éléments par page. Defaults to -1. (-1 = pas de pagination/tout afficher)
|
||||
per_page (int, optional): le nombre d'éléments par page.
|
||||
Defaults to -1. (-1 = pas de pagination/tout afficher)
|
||||
"""
|
||||
# par défaut le total des pages est 1 (même si la collection est vide)
|
||||
self.total_pages = 1
|
||||
|
@ -195,6 +212,17 @@ class ListeAssiJusti(tb.Table):
|
|||
r = query_finale.all()
|
||||
RequeteTableauAssiduiteCache.set(cle_cache, r)
|
||||
|
||||
# Filtrer Si préférence "Limiter les assiduités à l'année courante"
|
||||
if get_preference("assi_limit_annee"):
|
||||
annee_debut = localize_datetime(date_debut_annee_scolaire())
|
||||
annee_fin = localize_datetime(date_fin_annee_scolaire())
|
||||
r = [
|
||||
obj
|
||||
for obj in r
|
||||
if obj._asdict()["date_debut"] >= annee_debut
|
||||
and obj._asdict()["date_fin"] <= annee_fin
|
||||
]
|
||||
|
||||
# Paginer la requête pour ne pas envoyer trop d'informations au client
|
||||
pagination: Pagination = self.paginer(r, no_pagination=self.no_pagination)
|
||||
self.total_pages = pagination.total_pages
|
||||
|
@ -212,15 +240,17 @@ class ListeAssiJusti(tb.Table):
|
|||
attributs `page` et `NB_PAR_PAGE` de la classe `ListeAssiJusti`.
|
||||
|
||||
Args:
|
||||
collection (list): La collection à paginer. Il s'agit par exemple d'une requête qui a déjà
|
||||
collection (list): La collection à paginer.
|
||||
Il s'agit par exemple d'une requête qui a déjà
|
||||
été construite et qui est prête à être exécutée.
|
||||
|
||||
Returns:
|
||||
Pagination: Un objet Pagination qui encapsule les résultats de la requête paginée.
|
||||
Pagination: Un objet Pagination qui encapsule les résultats de
|
||||
la requête paginée.
|
||||
|
||||
Note:
|
||||
Cette méthode ne modifie pas la collection originelle; elle renvoie plutôt un nouvel
|
||||
objet qui contient les résultats paginés.
|
||||
Cette méthode ne modifie pas la collection originelle;
|
||||
elle renvoie plutôt un nouvel objet qui contient les résultats paginés.
|
||||
"""
|
||||
return Pagination(
|
||||
collection,
|
||||
|
@ -232,29 +262,35 @@ class ListeAssiJusti(tb.Table):
|
|||
"""
|
||||
Combine les requêtes d'assiduités et de justificatifs en une seule requête.
|
||||
|
||||
Cette fonction prend en entrée deux requêtes optionnelles, une pour les assiduités
|
||||
et une pour les justificatifs, et renvoie une requête combinée qui sélectionne
|
||||
un ensemble spécifique de colonnes pour chaque type d'objet.
|
||||
Cette fonction prend en entrée deux requêtes optionnelles,
|
||||
une pour les assiduités et une pour les justificatifs,
|
||||
et renvoie une requête combinée qui sélectionne un ensemble
|
||||
spécifique de colonnes pour chaque type d'objet.
|
||||
|
||||
Les colonnes sélectionnées sont:
|
||||
- obj_id: l'identifiant de l'objet (assiduite_id pour les assiduités, justif_id pour les justificatifs)
|
||||
- obj_id: l'identifiant de l'objet
|
||||
(assiduite_id pour les assiduités, justif_id pour les justificatifs)
|
||||
- etudid: l'identifiant de l'étudiant
|
||||
- entry_date: la date de saisie de l'objet
|
||||
- date_debut: la date de début de l'objet
|
||||
- date_fin: la date de fin de l'objet
|
||||
- etat: l'état de l'objet
|
||||
- type: le type de l'objet ("assiduite" pour les assiduités, "justificatif" pour les justificatifs)
|
||||
- type: le type de l'objet
|
||||
("assiduite" pour les assiduités, "justificatif" pour les justificatifs)
|
||||
- est_just : si l'assiduité est justifié (booléen) None pour les justificatifs
|
||||
- user_id : l'identifiant de l'utilisateur qui a signalé l'assiduité ou le justificatif
|
||||
- user_id : l'identifiant de l'utilisateur qui a
|
||||
signalé l'assiduité ou le justificatif
|
||||
|
||||
Args:
|
||||
query_assiduite (sqlalchemy.orm.Query, optional): Une requête SQLAlchemy
|
||||
pour les assiduités.
|
||||
Si None (default), aucune assiduité ne sera incluse dans la requête combinée.
|
||||
Si None (default), aucune assiduité ne sera incluse
|
||||
dans la requête combinée.
|
||||
|
||||
query_justificatif (sqlalchemy.orm.Query, optional): Une requête SQLAlchemy
|
||||
pour les justificatifs.
|
||||
Si None (default), aucun justificatif ne sera inclus dans la requête combinée.
|
||||
Si None (default), aucun justificatif ne sera
|
||||
inclus dans la requête combinée.
|
||||
|
||||
Returns:
|
||||
sqlalchemy.orm.Query: Une requête combinée qui peut être exécutée pour
|
||||
|
@ -599,10 +635,15 @@ class AssiFiltre:
|
|||
|
||||
Args:
|
||||
type_obj (int, optional): type d'objet (0:Tout, 1: Assi, 2:Justi). Defaults to 0.
|
||||
entry_date (tuple[int, datetime], optional): (0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||
date_debut (tuple[int, datetime], optional): (0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||
date_fin (tuple[int, datetime], optional): (0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||
etats (list[int | EtatJustificatif | EtatAssiduite], optional): liste d'états valides (int | EtatJustificatif | EtatAssiduite). Defaults to None.
|
||||
entry_date (tuple[int, datetime], optional):
|
||||
(0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||
date_debut (tuple[int, datetime], optional):
|
||||
(0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||
date_fin (tuple[int, datetime], optional):
|
||||
(0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||
etats (list[int | EtatJustificatif | EtatAssiduite], optional):
|
||||
liste d'états valides (int | EtatJustificatif | EtatAssiduite).
|
||||
Defaults to None.
|
||||
"""
|
||||
|
||||
self.filtres = {"type_obj": type_obj}
|
||||
|
@ -637,7 +678,7 @@ class AssiFiltre:
|
|||
|
||||
type_filtrage, date = val_filtre
|
||||
|
||||
match (type_filtrage):
|
||||
match type_filtrage:
|
||||
# On garde uniquement les dates supérieures au filtre
|
||||
case 2:
|
||||
query_filtree = query_filtree.filter(
|
||||
|
@ -734,6 +775,10 @@ class AssiJustifData:
|
|||
|
||||
@staticmethod
|
||||
def from_etudiants(*etudiants: Identite) -> "AssiJustifData":
|
||||
"""
|
||||
Génère un object AssiJustifData à partir d'une liste d'étudiants
|
||||
(Récupère les assiduités et justificatifs des étudiants)
|
||||
"""
|
||||
data = AssiJustifData()
|
||||
data.assiduites_query = Assiduite.query.filter(
|
||||
Assiduite.etudid.in_([e.etudid for e in etudiants])
|
||||
|
@ -745,4 +790,5 @@ class AssiJustifData:
|
|||
return data
|
||||
|
||||
def get(self) -> tuple[Query, Query]:
|
||||
"Renvoi les requêtes d'assiduités et justificatifs"
|
||||
return self.assiduites_query, self.justificatifs_query
|
||||
|
|
|
@ -13,7 +13,7 @@ import numpy as np
|
|||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.comp.res_common import ResultatsSemestre
|
||||
from app.models import Identite, FormSemestre, UniteEns
|
||||
from app.models import Identite, Evaluation, FormSemestre, UniteEns
|
||||
from app.scodoc.codes_cursus import UE_SPORT, DEF
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_groups
|
||||
|
@ -405,15 +405,22 @@ class TableRecap(tb.Table):
|
|||
val = notes_db[etudid]["value"]
|
||||
else:
|
||||
# Note manquante mais prise en compte immédiate: affiche ATT
|
||||
val = scu.NOTES_ATTENTE
|
||||
val = (
|
||||
scu.NOTES_ATTENTE
|
||||
if e.evaluation_type != Evaluation.EVALUATION_BONUS
|
||||
else ""
|
||||
)
|
||||
content = self.fmt_note(val)
|
||||
classes = col_classes + [
|
||||
{
|
||||
"ABS": "abs",
|
||||
"ATT": "att",
|
||||
"EXC": "exc",
|
||||
}.get(content, "")
|
||||
]
|
||||
if e.evaluation_type != Evaluation.EVALUATION_BONUS:
|
||||
classes = col_classes + [
|
||||
{
|
||||
"ABS": "abs",
|
||||
"ATT": "att",
|
||||
"EXC": "exc",
|
||||
}.get(content, "")
|
||||
]
|
||||
else:
|
||||
classes = col_classes + ["col_eval_bonus"]
|
||||
row.add_cell(
|
||||
col_id, title, content, group="eval", classes=classes
|
||||
)
|
||||
|
@ -450,7 +457,7 @@ class TableRecap(tb.Table):
|
|||
row_descr_eval.add_cell(
|
||||
col_id,
|
||||
None,
|
||||
e.description or "",
|
||||
e.description,
|
||||
target=url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
|
@ -613,9 +620,11 @@ class RowRecap(tb.Row):
|
|||
def add_abs(self):
|
||||
"Ajoute les colonnes absences"
|
||||
# Absences (nb d'abs non just. dans ce semestre)
|
||||
nbabs, nbabsjust = self.table.res.formsemestre.get_abs_count(self.etud.id)
|
||||
self.add_cell("nbabs", "Abs", nbabs, "abs")
|
||||
self.add_cell("nbabsjust", "Just.", nbabsjust, "abs")
|
||||
_, nbabsjust, nbabs = self.table.res.formsemestre.get_abs_count(self.etud.id)
|
||||
self.add_cell("nbabs", "Abs", f"{nbabs:1.0f}", "abs", raw_content=nbabs)
|
||||
self.add_cell(
|
||||
"nbabsjust", "Just.", f"{nbabsjust:1.0f}", "abs", raw_content=nbabsjust
|
||||
)
|
||||
|
||||
def add_moyennes_cols(
|
||||
self,
|
||||
|
|
|
@ -37,7 +37,7 @@ class TableAssi(tb.Table):
|
|||
convert_values=False,
|
||||
**kwargs,
|
||||
):
|
||||
self.rows: list["RowEtud"] = [] # juste pour que VSCode nous aide sur .rows
|
||||
self.rows: list["RowAssi"] = [] # juste pour que VSCode nous aide sur .rows
|
||||
classes = ["gt_table"]
|
||||
self.dates = [str(dates[0]) + "T00:00", str(dates[1]) + "T23:59"]
|
||||
self.formsemestre = formsemestre
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue