Merge branch 'scodoc-master' into pe-but-v4

This commit is contained in:
Cléo Baras 2024-02-27 16:20:32 +01:00
commit 8acd9a12d4
70 changed files with 1900 additions and 1278 deletions

View File

@ -104,9 +104,11 @@ class BulletinBUT:
"competence": None, # XXX TODO lien avec référentiel "competence": None, # XXX TODO lien avec référentiel
"moyenne": None, "moyenne": None,
# Le bonus sport appliqué sur cette UE # Le bonus sport appliqué sur cette UE
"bonus": fmt_note(res.bonus_ues[ue.id][etud.id]) "bonus": (
if res.bonus_ues is not None and ue.id in res.bonus_ues fmt_note(res.bonus_ues[ue.id][etud.id])
else fmt_note(0.0), 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]), "malus": fmt_note(res.malus[ue.id][etud.id]),
"capitalise": None, # "AAAA-MM-JJ" TODO #sco93 "capitalise": None, # "AAAA-MM-JJ" TODO #sco93
"ressources": self.etud_ue_mod_results(etud, ue, res.ressources), "ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
@ -181,14 +183,16 @@ class BulletinBUT:
"is_external": ue_capitalisee.is_external, "is_external": ue_capitalisee.is_external,
"date_capitalisation": ue_capitalisee.event_date, "date_capitalisation": ue_capitalisee.event_date,
"formsemestre_id": ue_capitalisee.formsemestre_id, "formsemestre_id": ue_capitalisee.formsemestre_id,
"bul_orig_url": url_for( "bul_orig_url": (
"notes.formsemestre_bulletinetud", url_for(
scodoc_dept=g.scodoc_dept, "notes.formsemestre_bulletinetud",
etudid=etud.id, scodoc_dept=g.scodoc_dept,
formsemestre_id=ue_capitalisee.formsemestre_id, etudid=etud.id,
) formsemestre_id=ue_capitalisee.formsemestre_id,
if ue_capitalisee.formsemestre_id )
else None, if ue_capitalisee.formsemestre_id
else None
),
"ressources": {}, # sans détail en BUT "ressources": {}, # sans détail en BUT
"saes": {}, "saes": {},
} }
@ -227,13 +231,15 @@ class BulletinBUT:
"id": modimpl.id, "id": modimpl.id,
"titre": modimpl.module.titre, "titre": modimpl.module.titre,
"code_apogee": modimpl.module.code_apogee, "code_apogee": modimpl.module.code_apogee,
"url": url_for( "url": (
"notes.moduleimpl_status", url_for(
scodoc_dept=g.scodoc_dept, "notes.moduleimpl_status",
moduleimpl_id=modimpl.id, scodoc_dept=g.scodoc_dept,
) moduleimpl_id=modimpl.id,
if has_request_context() )
else "na", if has_request_context()
else "na"
),
"moyenne": { "moyenne": {
# # moyenne indicative de module: moyenne des UE, # # moyenne indicative de module: moyenne des UE,
# # ignorant celles sans notes (nan) # # ignorant celles sans notes (nan)
@ -242,18 +248,20 @@ class BulletinBUT:
# "max": fmt_note(moyennes_etuds.max()), # "max": fmt_note(moyennes_etuds.max()),
# "moy": fmt_note(moyennes_etuds.mean()), # "moy": fmt_note(moyennes_etuds.mean()),
}, },
"evaluations": [ "evaluations": (
self.etud_eval_results(etud, e) [
for e in modimpl.evaluations self.etud_eval_results(etud, e)
if (e.visibulletin or version == "long") for e in modimpl.evaluations
and (e.id in modimpl_results.evaluations_etat) if (e.visibulletin or version == "long")
and ( and (e.id in modimpl_results.evaluations_etat)
modimpl_results.evaluations_etat[e.id].is_complete and (
or self.prefs["bul_show_all_evals"] modimpl_results.evaluations_etat[e.id].is_complete
) or self.prefs["bul_show_all_evals"]
] )
if version != "short" ]
else [], if version != "short"
else []
),
} }
return d return d
@ -274,35 +282,43 @@ class BulletinBUT:
poids = collections.defaultdict(lambda: 0.0) poids = collections.defaultdict(lambda: 0.0)
d = { d = {
"id": e.id, "id": e.id,
"coef": fmt_note(e.coefficient) "coef": (
if e.evaluation_type == scu.EVALUATION_NORMALE fmt_note(e.coefficient)
else None, if e.evaluation_type == Evaluation.EVALUATION_NORMALE
else None
),
"date_debut": e.date_debut.isoformat() if e.date_debut 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, "date_fin": e.date_fin.isoformat() if e.date_fin else None,
"description": e.description, "description": e.description,
"evaluation_type": e.evaluation_type, "evaluation_type": e.evaluation_type,
"note": { "note": (
"value": fmt_note( {
eval_notes[etud.id], "value": fmt_note(
note_max=e.note_max, 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), "min": fmt_note(notes_ok.min(), note_max=e.note_max),
"moy": fmt_note(notes_ok.mean(), 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, "poids": poids,
"url": url_for( "url": (
"notes.evaluation_listenotes", url_for(
scodoc_dept=g.scodoc_dept, "notes.evaluation_listenotes",
evaluation_id=e.id, scodoc_dept=g.scodoc_dept,
) evaluation_id=e.id,
if has_request_context() )
else "na", if has_request_context()
else "na"
),
# deprecated (supprimer avant #sco9.7) # deprecated (supprimer avant #sco9.7)
"date": e.date_debut.isoformat() if e.date_debut else None, "date": e.date_debut.isoformat() if e.date_debut else None,
"heure_debut": e.date_debut.time().isoformat("minutes") "heure_debut": (
if e.date_debut e.date_debut.time().isoformat("minutes") if e.date_debut else None
else None, ),
"heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None, "heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None,
} }
return d return d
@ -524,9 +540,9 @@ class BulletinBUT:
d.update(infos) d.update(infos)
# --- Rangs # --- Rangs
d[ d["rang_nt"] = (
"rang_nt" f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}" )
d["rang_txt"] = "Rang " + d["rang_nt"] d["rang_txt"] = "Rang " + d["rang_nt"]
d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"])) d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))

View File

@ -119,6 +119,12 @@ def _build_bulletin_but_infos(
refcomp = formsemestre.formation.referentiel_competence refcomp = formsemestre.formation.referentiel_competence
if refcomp is None: if refcomp is None:
raise ScoNoReferentielCompetences(formation=formsemestre.formation) 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( ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau(
refcomp, etud refcomp, etud
) )

View File

@ -24,7 +24,7 @@ from reportlab.lib.colors import blue
from reportlab.lib.units import cm, mm from reportlab.lib.units import cm, mm
from reportlab.platypus import Paragraph, Spacer 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.sco_bulletins_standard import BulletinGeneratorStandard
from app.scodoc import gen_tables from app.scodoc import gen_tables
from app.scodoc.codes_cursus import UE_SPORT 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=()): def evaluations_rows(self, rows, evaluations: list[dict], ue_acros=()):
"lignes des évaluations" "lignes des évaluations"
for e in evaluations: 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 = { t = {
"titre": f"{e['description'] or ''}", "titre": f"{e['description'] or ''}",
"moyenne": e["note"]["value"], "moyenne": e["note"]["value"],
@ -431,7 +435,10 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
), ),
"coef": coef, "coef": coef,
"_coef_pdf": Paragraph( "_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": [ "_pdf_style": [
( (

View File

@ -23,29 +23,21 @@ from app.comp.res_but import ResultatsSemestreBUT
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models.but_refcomp import ( from app.models.but_refcomp import (
ApcAnneeParcours,
ApcCompetence, ApcCompetence,
ApcNiveau, ApcNiveau,
ApcParcours, ApcParcours,
ApcParcoursNiveauCompetence,
ApcReferentielCompetences, ApcReferentielCompetences,
) )
from app.models import Scolog, ScolarAutorisationInscription from app.models.ues import UEParcours
from app.models.but_validations import ( from app.models.but_validations import ApcValidationRCUE
ApcValidationAnnee,
ApcValidationRCUE,
)
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.formations import Formation 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.ues import UniteEns
from app.models.validations import ScolarFormSemestreValidation from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import codes_cursus as sco_codes 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.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.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
from app.scodoc import sco_cursus_dut from app.scodoc import sco_cursus_dut
@ -440,11 +432,16 @@ def formsemestre_warning_apc_setup(
""" """
if not formsemestre.formation.is_apc(): if not formsemestre.formation.is_apc():
return "" 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: if formsemestre.formation.referentiel_competence is None:
return f"""<div class="formsemestre_status_warning"> return f"""<div class="formsemestre_status_warning">
La <a class="stdlink" href="{ La <a class="stdlink" href="{url_formation}">formation
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id) n'est pas associée à un référentiel de compétence.</a>
}">formation n'est pas associée à un référentiel de compétence.</a>
</div> </div>
""" """
H = [] H = []
@ -462,7 +459,9 @@ def formsemestre_warning_apc_setup(
) )
if nb_ues_sans_parcours != nb_ues_tot: if nb_ues_sans_parcours != nb_ues_tot:
H.append( 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 # Vérifie les niveaux de chaque parcours
for parcour in formsemestre.parcours or [None]: for parcour in formsemestre.parcours or [None]:
@ -489,7 +488,8 @@ def formsemestre_warning_apc_setup(
if not H: if not H:
return "" return ""
return f"""<div class="formsemestre_status_warning"> 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> <ul>
<li>{ '</li><li>'.join(H) }</li> <li>{ '</li><li>'.join(H) }</li>
</ul> </ul>
@ -502,6 +502,78 @@ 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 !</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( def ue_associee_au_niveau_du_parcours(
ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S" ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S"
) -> UniteEns: ) -> UniteEns:

View File

@ -77,7 +77,7 @@ from app.models.but_refcomp import (
ApcNiveau, ApcNiveau,
ApcParcours, ApcParcours,
) )
from app.models import Scolog, ScolarAutorisationInscription from app.models import Evaluation, Scolog, ScolarAutorisationInscription
from app.models.but_validations import ( from app.models.but_validations import (
ApcValidationAnnee, ApcValidationAnnee,
ApcValidationRCUE, ApcValidationRCUE,
@ -260,11 +260,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
else [] else []
) )
# ---- Niveaux et RCUEs # ---- Niveaux et RCUEs
niveaux_by_parcours = ( niveaux_by_parcours = formsemestre.formation.referentiel_competence.get_niveaux_by_parcours(
formsemestre.formation.referentiel_competence.get_niveaux_by_parcours( self.annee_but, [self.parcour] if self.parcour else None
self.annee_but, [self.parcour] if self.parcour else None )[
)[1] 1
) ]
self.niveaux_competences = niveaux_by_parcours["TC"] + ( self.niveaux_competences = niveaux_by_parcours["TC"] + (
niveaux_by_parcours[self.parcour.id] if self.parcour else [] 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 # self.codes = [] # pas de décision annuelle sur semestres impairs
elif self.inscription_etat != scu.INSCRIT: elif self.inscription_etat != scu.INSCRIT:
self.codes = [ self.codes = [
sco_codes.DEM (
if self.inscription_etat == scu.DEMISSION sco_codes.DEM
else sco_codes.DEF, if self.inscription_etat == scu.DEMISSION
else sco_codes.DEF
),
# propose aussi d'autres codes, au cas où... # propose aussi d'autres codes, au cas où...
sco_codes.DEM (
if self.inscription_etat != scu.DEMISSION sco_codes.DEM
else sco_codes.DEF, if self.inscription_etat != scu.DEMISSION
else sco_codes.DEF
),
sco_codes.ABAN, sco_codes.ABAN,
sco_codes.ABL, sco_codes.ABL,
sco_codes.EXCLU, sco_codes.EXCLU,
@ -595,11 +599,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# Ordonne par numéro d'UE # Ordonne par numéro d'UE
niv_rcue = sorted( niv_rcue = sorted(
self.rcue_by_niveau.items(), self.rcue_by_niveau.items(),
key=lambda x: x[1].ue_1.numero key=lambda x: (
if x[1].ue_1 x[1].ue_1.numero if x[1].ue_1 else x[1].ue_2.numero if x[1].ue_2 else 0
else x[1].ue_2.numero ),
if x[1].ue_2
else 0,
) )
return { return {
niveau_id: DecisionsProposeesRCUE(self, rcue, self.inscription_etat) 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é. Return: True si au moins un code modifié et enregistré.
""" """
modif = False modif = False
# Vérification notes en attente dans formsemestre origine if only_validantes:
if only_validantes and self.has_notes_en_attente(): if self.has_notes_en_attente():
return False # 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 # Toujours valider dans l'ordre UE, RCUE, Année
annee_scolaire = self.formsemestre.annee_scolaire() annee_scolaire = self.formsemestre.annee_scolaire()
@ -1488,9 +1496,11 @@ class DecisionsProposeesUE(DecisionsProposees):
self.validation = None # cache toute validation self.validation = None # cache toute validation
self.explanation = "non inscrit (dem. ou déf.)" self.explanation = "non inscrit (dem. ou déf.)"
self.codes = [ self.codes = [
sco_codes.DEM (
if res.get_etud_etat(etud.id) == scu.DEMISSION sco_codes.DEM
else sco_codes.DEF if res.get_etud_etat(etud.id) == scu.DEMISSION
else sco_codes.DEF
)
] ]
return return

View File

@ -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&nbsp;:</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: def infos_fiche_etud_html(etudid: int) -> str:
"""Section html pour fiche etudiant """Section html pour fiche etudiant

View File

@ -35,7 +35,6 @@ moyenne générale d'une UE.
""" """
import dataclasses import dataclasses
from dataclasses import dataclass from dataclasses import dataclass
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import sqlalchemy as sa import sqlalchemy as sa
@ -151,17 +150,18 @@ class ModuleImplResults:
self.evaluations_completes_dict = {} self.evaluations_completes_dict = {}
for evaluation in moduleimpl.evaluations: for evaluation in moduleimpl.evaluations:
eval_df = self._load_evaluation_notes(evaluation) eval_df = self._load_evaluation_notes(evaluation)
# is_complete ssi tous les inscrits (non dem) au semestre ont une note # is_complete ssi
# ou évaluation déclarée "à prise en compte immédiate" # tous les inscrits (non dem) au module ont une note
# Les évaluations de rattrapage et 2eme session sont toujours complètes # 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. etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
is_complete = ( is_complete = (
(evaluation.evaluation_type == scu.EVALUATION_RATTRAPAGE) (evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE)
or (evaluation.evaluation_type == scu.EVALUATION_SESSION2)
or (evaluation.publish_incomplete) or (evaluation.publish_incomplete)
or (not etudids_sans_note) or (not etudids_sans_note)
) ) and not evaluation.is_blocked()
self.evaluations_completes.append(is_complete) self.evaluations_completes.append(is_complete)
self.evaluations_completes_dict[evaluation.id] = is_complete self.evaluations_completes_dict[evaluation.id] = is_complete
self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note
@ -186,7 +186,7 @@ class ModuleImplResults:
].index ].index
) )
if evaluation.publish_incomplete: 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 eval_etudids_attente |= etudids_sans_note
# Synthèse pour état du module: # Synthèse pour état du module:
self.etudids_attente |= eval_etudids_attente self.etudids_attente |= eval_etudids_attente
@ -240,19 +240,20 @@ class ModuleImplResults:
).formsemestre.inscriptions ).formsemestre.inscriptions
] ]
def get_evaluations_coefs(self, moduleimpl: ModuleImpl) -> np.array: def get_evaluations_coefs(self, modimpl: ModuleImpl) -> np.array:
"""Coefficients des évaluations. """Coefficients des évaluations.
Les coefs des évals incomplètes et non "normales" (session 2, rattrapage) Les coefs des évals incomplètes, rattrapage, session 2, bonus sont forcés à zéro.
sont zéro.
Résultat: 2d-array of floats, shape (nb_evals, 1) Résultat: 2d-array of floats, shape (nb_evals, 1)
""" """
return ( return (
np.array( np.array(
[ [
e.coefficient (
if e.evaluation_type == scu.EVALUATION_NORMALE e.coefficient
else 0.0 if e.evaluation_type == Evaluation.EVALUATION_NORMALE
for e in moduleimpl.evaluations else 0.0
)
for e in modimpl.evaluations
], ],
dtype=float, dtype=float,
) )
@ -276,7 +277,7 @@ class ModuleImplResults:
) / [e.note_max / 20.0 for e in moduleimpl.evaluations] ) / [e.note_max / 20.0 for e in moduleimpl.evaluations]
def get_eval_notes_dict(self, evaluation_id: int) -> dict: 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 } { etudid : valeur }
avec les valeurs float, ou "ABS" ou EXC avec les valeurs float, ou "ABS" ou EXC
""" """
@ -285,7 +286,7 @@ class ModuleImplResults:
for (etudid, x) in self.evals_notes[evaluation_id].items() 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. """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 Rattrapage: la moyenne du module est la meilleure note entre moyenne
des autres évals et la note eval rattrapage. des autres évals et la note eval rattrapage.
@ -293,25 +294,41 @@ class ModuleImplResults:
eval_list = [ eval_list = [
e e
for e in moduleimpl.evaluations for e in moduleimpl.evaluations
if e.evaluation_type == scu.EVALUATION_RATTRAPAGE if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE
] ]
if eval_list: if eval_list:
return eval_list[0] return eval_list[0]
return None 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. """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. Session 2: remplace la note de moyenne des autres évals.
""" """
eval_list = [ eval_list = [
e e
for e in moduleimpl.evaluations for e in moduleimpl.evaluations
if e.evaluation_type == scu.EVALUATION_SESSION2 if e.evaluation_type == Evaluation.EVALUATION_SESSION2
] ]
if eval_list: if eval_list:
return eval_list[0] return eval_list[0]
return None 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): class ModuleImplResultsAPC(ModuleImplResults):
"Calcul des moyennes de modules à la mode BUT" "Calcul des moyennes de modules à la mode BUT"
@ -356,7 +373,7 @@ class ModuleImplResultsAPC(ModuleImplResults):
# et dans dans evals_poids_etuds # et dans dans evals_poids_etuds
# (rappel: la comparaison est toujours false face à un NaN) # (rappel: la comparaison est toujours false face à un NaN)
# shape: (nb_etuds, nb_evals, nb_ues) # 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( evals_poids_etuds = np.where(
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE, np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
poids_stacked, poids_stacked,
@ -364,10 +381,20 @@ class ModuleImplResultsAPC(ModuleImplResults):
) )
# Calcule la moyenne pondérée sur les notes disponibles: # 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 = 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) with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etuds_moy_module = np.sum( etuds_moy_module = np.sum(
evals_poids_etuds * evals_notes_stacked, axis=1 evals_poids_etuds * evals_notes_stacked, axis=1
) / np.sum(evals_poids_etuds, 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 # Session2 : quand elle existe, remplace la note de module
eval_session2 = self.get_evaluation_session2(modimpl) eval_session2 = self.get_evaluation_session2(modimpl)
@ -416,6 +443,30 @@ class ModuleImplResultsAPC(ModuleImplResults):
) )
return self.etuds_moy_module 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]: def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
"""Charge poids des évaluations d'un module et retourne un dataframe """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 evals_coefs_etuds * evals_notes_20, axis=1
) / np.sum(evals_coefs_etuds, 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 # Session2 : quand elle existe, remplace la note de module
eval_session2 = self.get_evaluation_session2(modimpl) eval_session2 = self.get_evaluation_session2(modimpl)
if eval_session2: if eval_session2:
@ -571,3 +629,22 @@ class ModuleImplResultsClassic(ModuleImplResults):
) )
return self.etuds_moy_module 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

View File

@ -205,11 +205,12 @@ class ResultatsSemestre(ResultatsCache):
"coefficient" : float, # 0 si None "coefficient" : float, # 0 si None
"description" : str, # de l'évaluation, "" si None "description" : str, # de l'évaluation, "" si None
"etat" { "etat" {
"blocked" : bool, # vrai si prise en compte bloquée
"evalcomplete" : bool, "evalcomplete" : bool,
"last_modif" : datetime.datetime | None, # saisie de note la plus récente "last_modif" : datetime.datetime | None, # saisie de note la plus récente
"nb_notes" : int, # nb notes d'étudiants inscrits "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) "jour" : datetime.datetime, # e.date_debut or datetime.datetime(1900, 1, 1)
"publish_incomplete" : bool, "publish_incomplete" : bool,
} }
@ -230,15 +231,16 @@ class ResultatsSemestre(ResultatsCache):
date_modif = cursor.one_or_none() date_modif = cursor.one_or_none()
last_modif = date_modif[0] if date_modif else None last_modif = date_modif[0] if date_modif else None
return { return {
"coefficient": evaluation.coefficient or 0.0, "coefficient": evaluation.coefficient,
"description": evaluation.description or "", "description": evaluation.description,
"evaluation_id": evaluation.id,
"jour": evaluation.date_debut or datetime.datetime(1900, 1, 1),
"etat": { "etat": {
"blocked": evaluation.is_blocked(),
"evalcomplete": etat.is_complete, "evalcomplete": etat.is_complete,
"nb_notes": etat.nb_notes, "nb_notes": etat.nb_notes,
"last_modif": last_modif, "last_modif": last_modif,
}, },
"evaluation_id": evaluation.id,
"jour": evaluation.date_debut or datetime.datetime(1900, 1, 1),
"publish_incomplete": evaluation.publish_incomplete, "publish_incomplete": evaluation.publish_incomplete,
} }
@ -432,9 +434,24 @@ class ResultatsSemestre(ResultatsCache):
ue_cap_dict["compense_formsemestre_id"] = None ue_cap_dict["compense_formsemestre_id"] = None
return ue_cap_dict 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. """L'état de l'UE pour cet étudiant.
Result: dict, ou None si l'UE n'est pas dans ce semestre. 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: UniteEns = db.session.get(UniteEns, ue_id)
ue_dict = ue.to_dict() ue_dict = ue.to_dict()
@ -455,7 +472,7 @@ class ResultatsSemestre(ResultatsCache):
"ects": 0.0, "ects": 0.0,
"ects_ue": ue.ects, "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 return None
if not self.validations: if not self.validations:
self.validations = res_sem.load_formsemestre_validations(self.formsemestre) 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, "is_external": ue_cap["is_external"] if is_capitalized else ue.is_external,
"coef_ue": coef_ue, "coef_ue": coef_ue,
"ects_pot": ue.ects or 0.0, "ects_pot": ue.ects or 0.0,
"ects": self.validations.decisions_jury_ues.get(etudid, {}) "ects": (
.get(ue.id, {}) self.validations.decisions_jury_ues.get(etudid, {})
.get("ects", 0.0) .get(ue.id, {})
if self.validations.decisions_jury_ues .get("ects", 0.0)
else 0.0, if self.validations.decisions_jury_ues
else 0.0
),
"ects_ue": ue.ects, "ects_ue": ue.ects,
"cur_moy_ue": cur_moy_ue, "cur_moy_ue": cur_moy_ue,
"moy": moy_ue, "moy": moy_ue,

View File

@ -125,7 +125,7 @@ class Identite(models.ScoDocModel):
) )
# Champs "protégés" par ViewEtudData (RGPD) # Champs "protégés" par ViewEtudData (RGPD)
protected_attrs = {"boursier"} protected_attrs = {"boursier", "nationalite"}
def __repr__(self): def __repr__(self):
return ( return (

View File

@ -10,6 +10,7 @@ from flask_login import current_user
import sqlalchemy as sa import sqlalchemy as sa
from app import db, log from app import db, log
from app import models
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.events import ScolarNews from app.models.events import ScolarNews
from app.models.notes import NotesNotes from app.models.notes import NotesNotes
@ -23,10 +24,8 @@ MAX_EVALUATION_DURATION = datetime.timedelta(days=365)
NOON = datetime.time(12, 00) NOON = datetime.time(12, 00)
DEFAULT_EVALUATION_TIME = datetime.time(8, 0) DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
VALID_EVALUATION_TYPES = {0, 1, 2}
class Evaluation(models.ScoDocModel):
class Evaluation(db.Model):
"""Evaluation (contrôle, examen, ...)""" """Evaluation (contrôle, examen, ...)"""
__tablename__ = "notes_evaluation" __tablename__ = "notes_evaluation"
@ -38,9 +37,9 @@ class Evaluation(db.Model):
) )
date_debut = db.Column(db.DateTime(timezone=True), nullable=True) date_debut = db.Column(db.DateTime(timezone=True), nullable=True)
date_fin = db.Column(db.DateTime(timezone=True), nullable=True) date_fin = db.Column(db.DateTime(timezone=True), nullable=True)
description = db.Column(db.Text) description = db.Column(db.Text, nullable=False)
note_max = db.Column(db.Float) note_max = db.Column(db.Float, nullable=False)
coefficient = db.Column(db.Float) coefficient = db.Column(db.Float, nullable=False)
visibulletin = db.Column( visibulletin = db.Column(
db.Boolean, nullable=False, default=True, server_default="true" db.Boolean, nullable=False, default=True, server_default="true"
) )
@ -48,15 +47,30 @@ class Evaluation(db.Model):
publish_incomplete = db.Column( publish_incomplete = db.Column(
db.Boolean, nullable=False, default=False, server_default="false" 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( evaluation_type = db.Column(
db.Integer, nullable=False, default=0, server_default="0" 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 # ordre de presentation (par défaut, le plus petit numero
# est la plus ancienne eval): # est la plus ancienne eval):
numero = db.Column(db.Integer, nullable=False, default=0) numero = db.Column(db.Integer, nullable=False, default=0)
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True) 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): def __repr__(self):
return f"""<Evaluation {self.id} { return f"""<Evaluation {self.id} {
self.date_debut.isoformat() if self.date_debut else ''} "{ self.date_debut.isoformat() if self.date_debut else ''} "{
@ -70,13 +84,14 @@ class Evaluation(db.Model):
date_fin: datetime.datetime = None, date_fin: datetime.datetime = None,
description=None, description=None,
note_max=None, note_max=None,
blocked_until=None,
coefficient=None, coefficient=None,
visibulletin=None, visibulletin=None,
publish_incomplete=None, publish_incomplete=None,
evaluation_type=None, evaluation_type=None,
numero=None, numero=None,
**kw, # ceci pour absorber les éventuel arguments excedentaires **kw, # ceci pour absorber les éventuel arguments excedentaires
): ) -> "Evaluation":
"""Create an evaluation. Check permission and all arguments. """Create an evaluation. Check permission and all arguments.
Ne crée pas les poids vers les UEs. Ne crée pas les poids vers les UEs.
Add to session, do not commit. Add to session, do not commit.
@ -88,7 +103,7 @@ class Evaluation(db.Model):
args = locals() args = locals()
del args["cls"] del args["cls"]
del args["kw"] del args["kw"]
check_convert_evaluation_args(moduleimpl, args) check_and_convert_evaluation_args(args, moduleimpl)
# Check numeros # Check numeros
Evaluation.moduleimpl_evaluation_renumber(moduleimpl, only_if_unumbered=True) Evaluation.moduleimpl_evaluation_renumber(moduleimpl, only_if_unumbered=True)
if not "numero" in args or args["numero"] is None: if not "numero" in args or args["numero"] is None:
@ -199,6 +214,10 @@ class Evaluation(db.Model):
def to_dict_api(self) -> dict: def to_dict_api(self) -> dict:
"Représentation dict pour API JSON" "Représentation dict pour API JSON"
return { return {
"blocked": self.is_blocked(),
"blocked_until": (
self.blocked_until.isoformat() if self.blocked_until else ""
),
"coefficient": self.coefficient, "coefficient": self.coefficient,
"date_debut": self.date_debut.isoformat() if self.date_debut else "", "date_debut": self.date_debut.isoformat() if self.date_debut else "",
"date_fin": self.date_fin.isoformat() if self.date_fin else "", "date_fin": self.date_fin.isoformat() if self.date_fin else "",
@ -235,15 +254,6 @@ class Evaluation(db.Model):
return e_dict 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 @classmethod
def get_evaluation( def get_evaluation(
cls, evaluation_id: int | str, dept_id: int = None cls, evaluation_id: int | str, dept_id: int = None
@ -361,19 +371,6 @@ class Evaluation(db.Model):
Chaine vide si non renseignée.""" Chaine vide si non renseignée."""
return self.date_fin.time().isoformat("minutes") if self.date_fin else "" 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: def is_matin(self) -> bool:
"Evaluation commençant le matin (faux si pas de date)" "Evaluation commençant le matin (faux si pas de date)"
if not self.date_debut: if not self.date_debut:
@ -386,6 +383,14 @@ class Evaluation(db.Model):
return False return False
return self.date_debut.time() >= NOON 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: def set_default_poids(self) -> bool:
"""Initialize les poids vers les UE à leurs valeurs par défaut """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. C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.
@ -474,6 +479,29 @@ class Evaluation(db.Model):
""" """
return NotesNotes.query.filter_by(etudid=etud.id, evaluation_id=self.id).first() 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): class EvaluationUEPoids(db.Model):
"""Poids des évaluations (BUT) """Poids des évaluations (BUT)
@ -531,7 +559,7 @@ def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
return e_dict return e_dict
def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict): def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
"""Check coefficient, dates and duration, raises exception if invalid. """Check coefficient, dates and duration, raises exception if invalid.
Convert date and time strings to date and time objects. Convert date and time strings to date and time objects.
@ -546,7 +574,7 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
# --- evaluation_type # --- evaluation_type
try: try:
data["evaluation_type"] = int(data.get("evaluation_type", 0) or 0) 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") raise ScoValueError("invalid evaluation_type value")
except ValueError as exc: except ValueError as exc:
raise ScoValueError("invalid evaluation_type value") from exc raise ScoValueError("invalid evaluation_type value") from exc
@ -571,7 +599,7 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
if coef < 0: if coef < 0:
raise ScoValueError("invalid coefficient value (must be positive or null)") raise ScoValueError("invalid coefficient value (must be positive or null)")
data["coefficient"] = coef data["coefficient"] = coef
# --- date de l'évaluation # --- date de l'évaluation dans le semestre ?
formsemestre = moduleimpl.formsemestre formsemestre = moduleimpl.formsemestre
date_debut = data.get("date_debut", None) date_debut = data.get("date_debut", None)
if date_debut: if date_debut:
@ -612,6 +640,8 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
"Heures de l'évaluation incohérentes !", "Heures de l'évaluation incohérentes !",
dest_url="javascript:history.back();", 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: def heure_to_time(heure: str) -> datetime.time:
@ -641,3 +671,6 @@ def _moduleimpl_evaluation_insert_before(
db.session.add(e) db.session.add(e)
db.session.commit() db.session.commit()
return n return n
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription

View File

@ -93,6 +93,10 @@ class FormSemestre(db.Model):
db.Boolean(), nullable=False, default=False, server_default="false" db.Boolean(), nullable=False, default=False, server_default="false"
) )
"Si vrai, la moyenne générale indicative BUT n'est pas calculée" "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( gestion_semestrielle = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" db.Boolean(), nullable=False, default=False, server_default="false"
) )

View File

@ -6,7 +6,12 @@ from flask import current_app, g
from app import db from app import db
from app import models from app import models
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models.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 import sco_utils as scu
from app.scodoc.codes_cursus import UE_SPORT from app.scodoc.codes_cursus import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
@ -100,6 +105,33 @@ class Module(models.ScoDocModel):
return args_dict 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): def clone(self):
"""Create a new copy of this module.""" """Create a new copy of this module."""
mod = Module( mod = Module(

View File

@ -126,7 +126,7 @@ class ScolarFormSemestreValidation(db.Model):
def ects(self) -> float: def ects(self) -> float:
"Les ECTS acquis par cette validation. (0 si ce n'est pas une validation d'UE)" "Les ECTS acquis par cette validation. (0 si ce n'est pas une validation d'UE)"
return ( return (
self.ue.ects self.ue.ects or 0.0
if (self.ue is not None) and (self.code in CODES_UE_VALIDES) if (self.ue is not None) and (self.code in CODES_UE_VALIDES)
else 0.0 else 0.0
) )

View File

@ -85,17 +85,6 @@ UE_ELECTIVE = 4 # UE "élective" dans certains cursus (UCAC?, ISCID)
UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...) UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...)
UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...) 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_TYPE_NAME = {
UE_STANDARD: "Standard", UE_STANDARD: "Standard",
UE_SPORT: "Sport/Culture (points bonus)", UE_SPORT: "Sport/Culture (points bonus)",
@ -104,8 +93,6 @@ UE_TYPE_NAME = {
UE_ELECTIVE: "Elective (ISCID)", UE_ELECTIVE: "Elective (ISCID)",
UE_PROFESSIONNELLE: "Professionnelle (ISCID)", UE_PROFESSIONNELLE: "Professionnelle (ISCID)",
UE_OPTIONNELLE: "Optionnelle", UE_OPTIONNELLE: "Optionnelle",
# UE_FONDAMENTALE : '"Fondamentale" (eg UCAC)',
# UE_OPTIONNELLE : '"Optionnelle" (UCAC)'
} }
# Couleurs RGB (dans [0.,1.]) des UE pour les bulletins: # Couleurs RGB (dans [0.,1.]) des UE pour les bulletins:

View File

@ -186,7 +186,7 @@ def sidebar(etudid: int = None):
formsemestre.date_fin.strftime("%d/%m/%Y") formsemestre.date_fin.strftime("%d/%m/%Y")
}">({ }">({
sco_preferences.get_preference("assi_metrique", None)}) sco_preferences.get_preference("assi_metrique", None)})
<br>{ nbabsjust } J., { nbabsnj } N.J.</span>""" <br>{nbabsjust:1.0f} J., {nbabsnj:1.0f} N.J.</span>"""
) )
H.append("<ul>") H.append("<ul>")
if current_user.has_permission(Permission.AbsChange): if current_user.has_permission(Permission.AbsChange):

View File

@ -124,9 +124,9 @@ def table_billets(
else: else:
billet_dict["nomprenom"] = billet.etudiant.nomprenom billet_dict["nomprenom"] = billet.etudiant.nomprenom
billet_dict["_nomprenom_order"] = billet.etudiant.sort_key billet_dict["_nomprenom_order"] = billet.etudiant.sort_key
billet_dict[ billet_dict["_nomprenom_td_attrs"] = (
"_nomprenom_td_attrs" f'id="{billet.etudiant.id}" class="etudinfo"'
] = f'id="{billet.etudiant.id}" class="etudinfo"' )
if with_links: if with_links:
billet_dict["_nomprenom_target"] = url_for( billet_dict["_nomprenom_target"] = url_for(
"scolar.fiche_etud", "scolar.fiche_etud",

View File

@ -34,7 +34,7 @@ Il suffit d'appeler abs_notify() après chaque ajout d'absence.
import datetime import datetime
from typing import Optional from typing import Optional
from flask import g, url_for from flask import flash, g, url_for
from flask_mail import Message from flask_mail import Message
from app import db from app import db
@ -46,7 +46,6 @@ from app.models.etudiants import Identite
from app.models.events import Scolog from app.models.events import Scolog
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc import sco_etud
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -283,10 +282,17 @@ def abs_notification_message(
) )
template = prefs["abs_notification_mail_tmpl"] template = prefs["abs_notification_mail_tmpl"]
txt = ""
if template: 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: else:
log("abs_notification_message: empty template, not sending message") log("abs_notification_message: empty template, not sending message")
if not txt:
return None return None
subject = f"""[ScoDoc] Trop d'absences pour {etud.nomprenom}""" subject = f"""[ScoDoc] Trop d'absences pour {etud.nomprenom}"""

View File

@ -55,7 +55,7 @@ from app.models import (
ScoDocSiteConfig, ScoDocSiteConfig,
) )
from app.scodoc.sco_permissions import Permission 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 html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_assiduites from app.scodoc import sco_assiduites
@ -318,7 +318,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
if nt.bonus_ues is not None: if nt.bonus_ues is not None:
u["cur_moy_ue_txt"] += " (+ues)" u["cur_moy_ue_txt"] += " (+ues)"
u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"]) 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"]) u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"])
else: else:
u["coef_ue_txt"] = "-" u["coef_ue_txt"] = "-"
@ -346,14 +346,14 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
# auparavant on filtrait les modules sans notes # auparavant on filtrait les modules sans notes
# si ue_status['cur_moy_ue'] != 'NA' alors u['modules'] = [] (pas de moyenne => pas de modules) # si ue_status['cur_moy_ue'] != 'NA' alors u['modules'] = [] (pas de moyenne => pas de modules)
u[ u["modules_capitalized"] = (
"modules_capitalized" []
] = [] # modules de l'UE capitalisée (liste vide si pas capitalisée) ) # 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: if ue_status["is_capitalized"] and ue_status["formsemestre_id"] is not None:
sem_origin = db.session.get(FormSemestre, ue_status["formsemestre_id"]) sem_origin = db.session.get(FormSemestre, ue_status["formsemestre_id"])
u[ u["ue_descr_txt"] = (
"ue_descr_txt" f'capitalisée le {ndb.DateISOtoDMY(ue_status["event_date"])}'
] = f'capitalisée le {ndb.DateISOtoDMY(ue_status["event_date"])}' )
u["ue_descr_html"] = ( u["ue_descr_html"] = (
f"""<a href="{ url_for( 'notes.formsemestre_bulletinetud', f"""<a href="{ url_for( 'notes.formsemestre_bulletinetud',
scodoc_dept=g.scodoc_dept, formsemestre_id=sem_origin.id, etudid=etudid)}" scodoc_dept=g.scodoc_dept, formsemestre_id=sem_origin.id, etudid=etudid)}"
@ -558,6 +558,8 @@ def _ue_mod_bulletin(
).order_by(Evaluation.numero, Evaluation.date_debut) ).order_by(Evaluation.numero, Evaluation.date_debut)
# (plus ancienne d'abord) # (plus ancienne d'abord)
for e in all_evals: for e in all_evals:
if e.is_blocked():
continue # ignore évaluations bloquées
if not e.visibulletin and version != "long": if not e.visibulletin and version != "long":
continue continue
is_complete = e.id in complete_eval_ids is_complete = e.id in complete_eval_ids
@ -610,19 +612,22 @@ def _ue_mod_bulletin(
e_dict["coef_txt"] = "" e_dict["coef_txt"] = ""
else: else:
e_dict["coef_txt"] = scu.fmt_coef(e.coefficient) 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." 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" e_dict["coef_txt"] = "Ses. 2"
if modimpl_results.evaluations_etat[e.id].nb_attente: if modimpl_results.evaluations_etat[e.id].nb_attente:
mod_attente = True # une eval en attente dans ce module mod_attente = True # une eval en attente dans ce module
if ((not is_malus) or (val != "NP")) and ( 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 # 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: if e.id in complete_eval_ids:
mod["evaluations"].append(e_dict) mod["evaluations"].append(e_dict)
else: else:
@ -731,7 +736,11 @@ def etud_descr_situation_semestre(
infos["refcomp_specialite_long"] = "" infos["refcomp_specialite_long"] = ""
if formsemestre.formation.is_apc(): if formsemestre.formation.is_apc():
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) 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 = ( parcour: ApcParcours = (
db.session.get(ApcParcours, parcour_id) if parcour_id is not None else None db.session.get(ApcParcours, parcour_id) if parcour_id is not None else None
) )

View File

@ -50,14 +50,11 @@ import traceback
import reportlab import reportlab
from reportlab.platypus import ( from reportlab.platypus import (
SimpleDocTemplate,
DocIf, DocIf,
Paragraph, Paragraph,
Spacer,
Frame,
PageBreak, PageBreak,
) )
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame from reportlab.platypus import Table, KeepInFrame
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
@ -213,26 +210,26 @@ class BulletinGenerator:
story.append(PageBreak()) # insert page break at end story.append(PageBreak()) # insert page break at end
return story return story
else:
# Generation du document PDF # Generation du document PDF
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
report = io.BytesIO() # in-memory document, no disk file report = io.BytesIO() # in-memory document, no disk file
document = sco_pdf.BaseDocTemplate(report) document = sco_pdf.BaseDocTemplate(report)
document.addPageTemplates( document.addPageTemplates(
sco_pdf.ScoDocPageTemplate( sco_pdf.ScoDocPageTemplate(
document, document,
author="%s %s (E. Viennet) [%s]" author=f"""{sco_version.SCONAME} {
% (sco_version.SCONAME, sco_version.SCOVERSION, self.description), sco_version.SCOVERSION} (E. Viennet) [{self.description}]""",
title=f"""Bulletin {sem["titremois"]} de {etat_civil}""", title=f"""Bulletin {sem["titremois"]} de {etat_civil}""",
subject="Bulletin de note", subject="Bulletin de note",
margins=self.margins, margins=self.margins,
server_name=self.server_name, server_name=self.server_name,
filigranne=self.filigranne, filigranne=self.filigranne,
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
)
) )
document.build(story) )
data = report.getvalue() document.build(story)
data = report.getvalue()
return data return data
def buildTableObject(self, P, pdfTableStyle, colWidths): def buildTableObject(self, P, pdfTableStyle, colWidths):

View File

@ -25,7 +25,7 @@
# #
############################################################################## ##############################################################################
"""Génération du bulletin en format JSON """Génération du bulletin en format JSON (formations classiques)
""" """
import datetime import datetime

View File

@ -62,10 +62,12 @@ from flask import g, request
from app import log, ScoValueError from app import log, ScoValueError
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, Identite from app.models import FormSemestre, Identite
from app.scodoc import sco_cache from app.scodoc import (
from app.scodoc import codes_cursus codes_cursus,
from app.scodoc import sco_pdf sco_cache,
from app.scodoc import sco_preferences sco_pdf,
sco_preferences,
)
from app.scodoc.sco_logos import find_logo from app.scodoc.sco_logos import find_logo
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -111,7 +113,8 @@ def assemble_bulletins_pdf(
return data return data
def replacement_function(match): def replacement_function(match) -> str:
"remplace logo par balise html img"
balise = match.group(1) balise = match.group(1)
name = match.group(3) name = match.group(3)
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id) 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" "Document pdf avec tous les bulletins du semestre, et filename"
from app.but import bulletin_but_court from app.but import bulletin_but_court
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
@ -226,13 +233,22 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
raise ScoValueError( raise ScoValueError(
"get_formsemestre_bulletins_pdf: version de bulletin demandée invalide !" "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: if cached:
return cached[1], cached[0] return cached[1], cached[0]
fragments = [] fragments = []
# Make each bulletin # Make each bulletin
for etud in etuds:
for etud in formsemestre.get_inscrits(include_demdef=True, order=True):
if version == "butcourt": if version == "butcourt":
frag = bulletin_but_court.bulletin_but_court_pdf_frag(etud, formsemestre) frag = bulletin_but_court.bulletin_but_court_pdf_frag(etud, formsemestre)
else: else:
@ -262,7 +278,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
sco_pdf.PDFLOCK.release() sco_pdf.PDFLOCK.release()
# #
date_iso = time.strftime("%Y-%m-%d") 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("&", "") filename = scu.unescape_html(filename).replace(" ", "_").replace("&", "")
# fill cache # fill cache
sco_cache.SemBulletinsPDFCache.set( sco_cache.SemBulletinsPDFCache.set(

View File

@ -51,7 +51,7 @@ from reportlab.lib.colors import Color, blue
from reportlab.lib.units import cm, mm from reportlab.lib.units import cm, mm
from reportlab.platypus import KeepTogether, Paragraph, Spacer, Table 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 import app.scodoc.sco_utils as scu
from app.scodoc import ( from app.scodoc import (
gen_tables, gen_tables,
@ -715,9 +715,15 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
eval_style = "" eval_style = ""
t = { t = {
"module": '<bullet indent="2mm">&bull;</bullet>&nbsp;' + e["name"], "module": '<bullet indent="2mm">&bull;</bullet>&nbsp;' + e["name"],
"coef": ("<i>" + e["coef_txt"] + "</i>") "coef": (
if prefs["bul_show_coef"] (
else "", f"<i>{e['coef_txt']}</i>"
if e["evaluation_type"] != Evaluation.EVALUATION_BONUS
else "bonus"
)
if prefs["bul_show_coef"]
else ""
),
"_hidden": hidden, "_hidden": hidden,
"_module_target": e["target_html"], "_module_target": e["target_html"],
# '_module_help' : , # '_module_help' : ,

View File

@ -67,7 +67,7 @@ class ScoDocCache:
keys are prefixed by the current departement: g.scodoc_dept MUST be set. 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 = "" prefix = ""
verbose = False # if true, verbose logging (debug) verbose = False # if true, verbose logging (debug)
@ -201,7 +201,7 @@ class AbsSemEtudCache(ScoDocCache):
""" """
prefix = "ABSE" prefix = "ABSE"
timeout = 60 * 60 # ttl 60 minutes timeout = 600 # ttl 10 minutes
class SemBulletinsPDFCache(ScoDocCache): class SemBulletinsPDFCache(ScoDocCache):
@ -233,7 +233,6 @@ class SemInscriptionsCache(ScoDocCache):
""" """
prefix = "SI" prefix = "SI"
duration = 12 * 60 * 60 # ttl 12h
class TableRecapCache(ScoDocCache): class TableRecapCache(ScoDocCache):
@ -243,7 +242,6 @@ class TableRecapCache(ScoDocCache):
""" """
prefix = "RECAP" prefix = "RECAP"
duration = 12 * 60 * 60 # ttl 12h
class TableRecapWithEvalsCache(ScoDocCache): class TableRecapWithEvalsCache(ScoDocCache):
@ -253,7 +251,6 @@ class TableRecapWithEvalsCache(ScoDocCache):
""" """
prefix = "RECAPWITHEVALS" prefix = "RECAPWITHEVALS"
duration = 12 * 60 * 60 # ttl 12h
class TableJuryCache(ScoDocCache): class TableJuryCache(ScoDocCache):
@ -263,7 +260,6 @@ class TableJuryCache(ScoDocCache):
""" """
prefix = "RECAPJURY" prefix = "RECAPJURY"
duration = 12 * 60 * 60 # ttl 12h
class TableJuryWithEvalsCache(ScoDocCache): class TableJuryWithEvalsCache(ScoDocCache):
@ -273,7 +269,6 @@ class TableJuryWithEvalsCache(ScoDocCache):
""" """
prefix = "RECAPJURYWITHEVALS" prefix = "RECAPJURYWITHEVALS"
duration = 12 * 60 * 60 # ttl 12h
def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False) def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False)

View File

@ -28,7 +28,6 @@
from flask.templating import render_template from flask.templating import render_template
from app import db from app import db
from app.but import apc_edit_ue
from app.models import UniteEns, Matiere, Module, FormSemestre, ModuleImpl from app.models import UniteEns, Matiere, Module, FormSemestre, ModuleImpl
from app.models.validations import ScolarFormSemestreValidation from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
@ -48,6 +47,8 @@ def html_edit_formation_apc(
- Les ressources - Les ressources
- Les SAÉs - Les SAÉs
""" """
from app.but import cursus_but
cursus = formation.get_cursus() cursus = formation.get_cursus()
assert cursus.APC_SAE 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 = [ H = [
render_template( render_template(
"pn/form_ues.j2", "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, 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: for semestre_idx in semestre_ids:

View File

@ -682,8 +682,11 @@ def module_edit(
"input_type": "checkbox", "input_type": "checkbox",
"vertical": True, "vertical": True,
"dom_id": "tf_module_parcours", "dom_id": "tf_module_parcours",
"labels": [parcour.libelle for parcour in ref_comp.parcours] "labels": [
+ ["Tous (tronc commun)"], f"&nbsp; {parcour.libelle} (<b>{parcour.code}</b>)"
for parcour in ref_comp.parcours
]
+ ["&nbsp; Tous (tronc commun)"],
"allowed_values": [ "allowed_values": [
str(parcour.id) for parcour in ref_comp.parcours str(parcour.id) for parcour in ref_comp.parcours
] ]

View File

@ -892,7 +892,9 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<form> <form>
<input type="checkbox" class="sco_tag_checkbox" <input type="checkbox" class="sco_tag_checkbox"
{'checked' if show_tags else ''} {'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> </form>
""" """
) )

View File

@ -31,96 +31,15 @@
import flask import flask
from flask import url_for, g from flask import url_for, g
from flask_login import current_user from flask_login import current_user
import sqlalchemy as sa
from app import db, log from app import db, log
from app.models import Evaluation from app.models import Evaluation
from app.models.evaluations import check_convert_evaluation_args
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import AccessDenied from app.scodoc.sco_exceptions import AccessDenied
from app.scodoc import sco_cache 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 # ancien _notes_getall

View File

@ -31,14 +31,12 @@ import datetime
import time import time
import flask import flask
from flask import url_for, render_template from flask import g, render_template, request, url_for
from flask import g
from flask_login import current_user from flask_login import current_user
from flask import request
from app import db from app import db
from app.models import Evaluation, Module, ModuleImpl 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 import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
@ -108,7 +106,7 @@ def evaluation_create_form(
raise ValueError("missing evaluation_id parameter") raise ValueError("missing evaluation_id parameter")
initvalues = evaluation.to_dict() initvalues = evaluation.to_dict()
moduleimpl_id = initvalues["moduleimpl_id"] moduleimpl_id = initvalues["moduleimpl_id"]
submitlabel = "Modifier les données" submitlabel = "Modifier l'évaluation"
action = "Modification d'une évaluation" action = "Modification d'une évaluation"
link = "" link = ""
# Note maximale actuelle dans cette éval ? # Note maximale actuelle dans cette éval ?
@ -142,6 +140,15 @@ def evaluation_create_form(
else: else:
poids = 0.0 poids = 0.0
initvalues[f"poids_{ue.id}"] = poids 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 = [ form = [
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}), ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
@ -183,7 +190,8 @@ def evaluation_create_form(
{ {
"size": 6, "size": 6,
"type": "float", # peut être négatif (!) "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, "allow_null": False,
}, },
) )
@ -195,7 +203,7 @@ def evaluation_create_form(
"size": 4, "size": 4,
"type": "float", "type": "float",
"title": "Notes de 0 à", "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, "allow_null": False,
"max_value": scu.NOTES_MAX, "max_value": scu.NOTES_MAX,
"min_value": min_note_max, "min_value": min_note_max,
@ -206,7 +214,8 @@ def evaluation_create_form(
{ {
"size": 36, "size": 36,
"type": "text", "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", "input_type": "menu",
"title": "Modalité", "title": "Modalité",
"allowed_values": ( "allowed_values": Evaluation.VALID_EVALUATION_TYPES,
scu.EVALUATION_NORMALE,
scu.EVALUATION_RATTRAPAGE,
scu.EVALUATION_SESSION2,
),
"type": "int", "type": "int",
"labels": ( "labels": (
"Normale", "Normale",
"Rattrapage (remplace si meilleure note)", "Rattrapage (remplace si meilleure note)",
"Deuxième session (remplace toujours)", "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, "size": 6,
"type": "float", "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, "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( tf = TrivialFormulator(
request.base_url, request.base_url,
vals, vals,
@ -324,7 +361,9 @@ def evaluation_create_form(
+ "\n".join(H) + "\n".join(H)
+ "\n" + "\n"
+ tf[1] + 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") + render_template("sco_timepicker.j2")
+ html_sco_header.sco_footer() + html_sco_header.sco_footer()
) )
@ -342,6 +381,8 @@ def evaluation_create_form(
raise ScoValueError("Date (j/m/a) invalide") from exc raise ScoValueError("Date (j/m/a) invalide") from exc
else: else:
date_debut = None date_debut = None
args["date_debut"] = date_debut
args["date_fin"] = date_debut # même jour
args.pop("jour", None) args.pop("jour", None)
if date_debut and args.get("heure_debut"): if date_debut and args.get("heure_debut"):
try: try:
@ -350,7 +391,8 @@ def evaluation_create_form(
raise ScoValueError("Heure début invalide") from exc raise ScoValueError("Heure début invalide") from exc
args["date_debut"] = datetime.datetime.combine(date_debut, heure_debut) args["date_debut"] = datetime.datetime.combine(date_debut, heure_debut)
args.pop("heure_debut", None) 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"): if date_debut and args.get("heure_fin"):
try: try:
heure_fin = heure_to_time(args["heure_fin"]) heure_fin = heure_to_time(args["heure_fin"])
@ -358,8 +400,22 @@ def evaluation_create_form(
raise ScoValueError("Heure fin invalide") from exc raise ScoValueError("Heure fin invalide") from exc
args["date_fin"] = datetime.datetime.combine(date_debut, heure_fin) args["date_fin"] = datetime.datetime.combine(date_debut, heure_fin)
args.pop("heure_fin", None) 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: if edit:
check_and_convert_evaluation_args(args, modimpl)
evaluation.from_dict(args) evaluation.from_dict(args)
else: else:
# création d'une evaluation # création d'une evaluation

View File

@ -40,16 +40,14 @@ from app import db
from app.auth.models import User from app.auth.models import User
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import Evaluation, FormSemestre, ModuleImpl from app.models import Evaluation, FormSemestre, ModuleImpl, Module
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_cal from app.scodoc import sco_cal
from app.scodoc import sco_evaluation_db 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_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
@ -114,9 +112,10 @@ def do_evaluation_etat(
nb_neutre, nb_neutre,
nb_att, nb_att,
moy, median, mini, maxi : # notes, en chaine, sur 20 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, gr_complets, gr_incomplets,
evalcomplete evalcomplete *
} }
evalcomplete est vrai si l'eval est complete (tous les inscrits evalcomplete est vrai si l'eval est complete (tous les inscrits
à ce module ont des notes) à ce module ont des notes)
@ -130,11 +129,12 @@ def do_evaluation_etat(
) # { etudid : note } ) # { etudid : note }
# ---- Liste des groupes complets et incomplets # ---- Liste des groupes complets et incomplets
E = sco_evaluation_db.get_evaluations_dict(args={"evaluation_id": evaluation_id})[0] evaluation = Evaluation.get_evaluation(evaluation_id)
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] modimpl: ModuleImpl = evaluation.moduleimpl
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] module: Module = modimpl.module
is_malus = Mod["module_type"] == ModuleType.MALUS # True si module de malus
formsemestre_id = M["formsemestre_id"] 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: # Si partition_id is None, prend 'all' ou bien la premiere:
if partition_id is None: if partition_id is None:
if select_first_partition: if select_first_partition:
@ -150,9 +150,7 @@ def do_evaluation_etat(
insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
formsemestre_id formsemestre_id
) )
insmod = sco_moduleimpl.do_moduleimpl_inscription_list( insmod = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=modimpl.id)
moduleimpl_id=E["moduleimpl_id"]
)
insmodset = {x["etudid"] for x in insmod} insmodset = {x["etudid"] for x in insmod}
# retire de insem ceux qui ne sont pas inscrits au module # retire de insem ceux qui ne sont pas inscrits au module
ins = [i for i in insem if i["etudid"] in insmodset] ins = [i for i in insem if i["etudid"] in insmodset]
@ -175,9 +173,9 @@ def do_evaluation_etat(
maxi_num = None maxi_num = None
else: else:
median = scu.fmt_note(median_num) median = scu.fmt_note(median_num)
moy = scu.fmt_note(moy_num, E["note_max"]) moy = scu.fmt_note(moy_num, evaluation.note_max)
mini = scu.fmt_note(mini_num, E["note_max"]) mini = scu.fmt_note(mini_num, evaluation.note_max)
maxi = scu.fmt_note(maxi_num, E["note_max"]) maxi = scu.fmt_note(maxi_num, evaluation.note_max)
# cherche date derniere modif note # cherche date derniere modif note
if len(etuds_notes_dict): if len(etuds_notes_dict):
t = [x["date"] for x in etuds_notes_dict.values()] 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 = list(group_nb_missing.keys())
gr_incomplets.sort() 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 = ( complete = (
(total_nb_missing == 0) (total_nb_missing == 0)
or (E["evaluation_type"] == scu.EVALUATION_RATTRAPAGE) or (evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE)
or (E["evaluation_type"] == scu.EVALUATION_SESSION2) and not evaluation.is_blocked()
) )
evalattente = (total_nb_missing > 0) and ( 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: # 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 evalattente = False
# Calcul moyenne dans chaque groupe de TD # Calcul moyenne dans chaque groupe de TD
@ -247,10 +237,10 @@ def do_evaluation_etat(
{ {
"group_id": group_id, "group_id": group_id,
"group_name": group_by_id[group_id]["group_name"], "group_name": group_by_id[group_id]["group_name"],
"gr_moy": scu.fmt_note(gr_moy, E["note_max"]), "gr_moy": scu.fmt_note(gr_moy, evaluation.note_max),
"gr_median": scu.fmt_note(gr_median, E["note_max"]), "gr_median": scu.fmt_note(gr_median, evaluation.note_max),
"gr_mini": scu.fmt_note(gr_mini, E["note_max"]), "gr_mini": scu.fmt_note(gr_mini, evaluation.note_max),
"gr_maxi": scu.fmt_note(gr_maxi, E["note_max"]), "gr_maxi": scu.fmt_note(gr_maxi, evaluation.note_max),
"gr_nb_notes": len(notes), "gr_nb_notes": len(notes),
"gr_nb_att": len([x for x in notes if x == scu.NOTES_ATTENTE]), "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: def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
"""Synthétise les états d'une liste d'évaluations """Synthétise les états d'une liste d'évaluations
evals: list of mappings (etats), 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_eval_completes (= prises en compte)
nb_evals_en_cours (= avec des notes, mais pas complete) nb_evals_en_cours (= avec des notes, mais pas complete)
nb_evals_vides (= sans aucune note) 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. 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 = [] dates = []
for e in etat_evals: for e in etat_evals:
if e["etat"]["blocked"]:
nb_evals_blocked += 1
if e["etat"]["evalcomplete"]: if e["etat"]["evalcomplete"]:
nb_evals_completes += 1 nb_evals_completes += 1
elif e["etat"]["nb_notes"] == 0: elif e["etat"]["nb_notes"] == 0:
nb_evals_vides += 1 nb_evals_vides += 1
else: elif not e["etat"]["blocked"]:
nb_evals_en_cours += 1 nb_evals_en_cours += 1
last_modif = e["etat"]["last_modif"] last_modif = e["etat"]["last_modif"]
if last_modif is not None: 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 "" last_modif = sorted(dates)[-1] if dates else ""
return { return {
"nb_evals": len(etat_evals),
"nb_evals_blocked": nb_evals_blocked,
"nb_evals_completes": nb_evals_completes, "nb_evals_completes": nb_evals_completes,
"nb_evals_en_cours": nb_evals_en_cours, "nb_evals_en_cours": nb_evals_en_cours,
"nb_evals_vides": nb_evals_vides, "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 """Experimental: un tableau indiquant pour chaque évaluation
le nombre de jours avant la publication des notes. 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) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
evaluations = formsemestre.get_evaluations() evaluations = formsemestre.get_evaluations()
rows = [] rows = []
for e in evaluations: 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 e.moduleimpl.module.module_type == ModuleType.MALUS
): ):
continue continue
@ -519,9 +515,9 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
{ {
"date_first_complete": date_first_complete, "date_first_complete": date_first_complete,
"delai_correction": delai_correction, "delai_correction": delai_correction,
"jour": e.date_debut.strftime("%d/%m/%Y") "jour": (
if e.date_debut e.date_debut.strftime("%d/%m/%Y") if e.date_debut else "sans date"
else "sans date", ),
"_jour_target": url_for( "_jour_target": url_for(
"notes.evaluation_listenotes", "notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
@ -611,13 +607,17 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True)
# Indique l'UE # Indique l'UE
ue = modimpl.module.ue ue = modimpl.module.ue
H.append(f"<p><b>UE : {ue.acronyme}</b></p>") 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: # store min/max values used by JS client-side checks:
H.append( H.append(
"""<span id="eval_note_min" class="sco-hidden">-20.</span> """<span id="eval_note_min" class="sco-hidden">-20.</span>
<span id="eval_note_max" class="sco-hidden">20.</span>""" <span id="eval_note_max" class="sco-hidden">20.</span>"""
) )
else: 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: if evaluation.date_debut is not None:
H.append(f"<p>Réalisée le <b>{evaluation.descr_date()}</b> ") H.append(f"<p>Réalisée le <b>{evaluation.descr_date()}</b> ")
group_id = sco_groups.get_default_group(modimpl.formsemestre_id) group_id = sco_groups.get_default_group(modimpl.formsemestre_id)

View File

@ -534,7 +534,7 @@ def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, line
# description evaluation # description evaluation
ws.append_single_cell_row(scu.unescape_html(description), style_titres) ws.append_single_cell_row(scu.unescape_html(description), style_titres)
ws.append_single_cell_row( 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, style,
) )
# ligne blanche # ligne blanche

View File

@ -28,6 +28,7 @@
"""Exception handling """Exception handling
""" """
from flask_login import current_user from flask_login import current_user
import app
# --- Exceptions # --- Exceptions
@ -237,8 +238,11 @@ class ScoTemporaryError(ScoValueError):
def __init__(self, msg: str = ""): def __init__(self, msg: str = ""):
msg = """ msg = """
<p>"Erreur temporaire</p> <p>Erreur temporaire</p>
<p>Veuillez -essayer. Si le problème persiste, merci de contacter l'assistance ScoDoc <p>Veuillez -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> </p>
""" """
app.clear_scodoc_cache()
super().__init__(msg) super().__init__(msg)

View File

@ -31,6 +31,7 @@ import flask
from flask import url_for, flash, redirect from flask import url_for, flash, redirect
from flask import g, request from flask import g, request
from flask_login import current_user from flask_login import current_user
import sqlalchemy as sa
from app import db from app import db
from app.auth.models import User 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 codes_cursus
from app.scodoc import sco_compute_moy from app.scodoc import sco_compute_moy
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups_copy from app.scodoc import sco_groups_copy
from app.scodoc import sco_modalites from app.scodoc import sco_modalites
@ -1113,7 +1112,8 @@ def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
f"""<b>impossible de supprimer {module.code} ({module.titre or ""}) f"""<b>impossible de supprimer {module.code} ({module.titre or ""})
car il y a {nb_evals} évaluations définies car il y a {nb_evals} évaluations définies
(<a href="{ (<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>""" }" class="stdlink">supprimez-les d\'abord</a>)</b>"""
] ]
ok = False ok = False
@ -1233,7 +1233,11 @@ def formsemestre_clone(formsemestre_id):
return "".join(H) + msg + tf[1] + html_sco_header.sco_footer() return "".join(H) + msg + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1: # cancel elif tf[0] == -1: # cancel
return flask.redirect( 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: else:
resp = User.get_user_from_nomplogin(tf[2]["responsable_id"]) resp = User.get_user_from_nomplogin(tf[2]["responsable_id"])
@ -1356,9 +1360,9 @@ def do_formsemestre_clone(
return formsemestre_id return formsemestre_id
def formsemestre_delete(formsemestre_id): def formsemestre_delete(formsemestre_id: int) -> str | flask.Response:
"""Delete a formsemestre (affiche avertissements)""" """Delete a formsemestre (affiche avertissements)"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
H = [ H = [
html_sco_header.html_sem_header("Suppression du semestre"), html_sco_header.html_sem_header("Suppression du semestre"),
"""<div class="ue_warning"><span>Attention !</span> """<div class="ue_warning"><span>Attention !</span>
@ -1376,17 +1380,18 @@ Ceci n'est possible que si :
</ol> </ol>
</div>""", </div>""",
] ]
evaluations = (
evals = sco_evaluation_db.do_evaluation_list_in_formsemestre(formsemestre_id) Evaluation.query.join(ModuleImpl)
if evals: .filter_by(formsemestre_id=formsemestre.id)
.all()
)
if evaluations:
H.append( 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 dans ce semestre
(sa suppression entrainera l'effacement définif des notes) !</p>""" (sa suppression entrainera l'effacement définif des notes) !</p>"""
) )
submit_label = ( submit_label = f"Confirmer la suppression (du semestre et des {len(evaluations)} évaluations !)"
f"Confirmer la suppression (du semestre et des {len(evals)} évaluations !)"
)
else: else:
submit_label = "Confirmer la suppression du semestre" submit_label = "Confirmer la suppression du semestre"
tf = TrivialFormulator( tf = TrivialFormulator(
@ -1413,8 +1418,10 @@ Ceci n'est possible que si :
) )
else: else:
H.append(tf[1]) H.append(tf[1])
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()
elif tf[0] == -1: # cancel
if tf[0] == -1: # cancel
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.formsemestre_status", "notes.formsemestre_status",
@ -1422,10 +1429,9 @@ Ceci n'est possible que si :
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
) )
) )
else: return flask.redirect(
return flask.redirect( "formsemestre_delete2?formsemestre_id=" + str(formsemestre_id)
"formsemestre_delete2?formsemestre_id=" + str(formsemestre_id) )
)
def formsemestre_delete2(formsemestre_id, dialog_confirmed=False): def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
@ -1486,106 +1492,165 @@ def formsemestre_has_decisions_or_compensations(
return False, "" return False, ""
def do_formsemestre_delete(formsemestre_id): def do_formsemestre_delete(formsemestre_id: int):
"""delete formsemestre, and all its moduleimpls. """delete formsemestre, and all its moduleimpls.
No checks, no warnings: erase all ! No checks, no warnings: erase all !
""" """
cnx = ndb.GetDBConnexion() formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sco_cache.EvaluationCache.invalidate_sem(formsemestre.id)
titre_sem = formsemestre.titre_annee()
sco_cache.EvaluationCache.invalidate_sem(formsemestre_id)
# --- Destruction des modules de ce semestre # --- Destruction des modules de ce semestre
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) for modimpl in formsemestre.modimpls:
for mod in mods:
# evaluations # evaluations
evals = sco_evaluation_db.get_evaluations_dict( for e in modimpl.evaluations:
args={"moduleimpl_id": mod["moduleimpl_id"]} db.session.execute(
) sa.text(
for e in evals: """DELETE FROM notes_notes WHERE evaluation_id=:evaluation_id"""
ndb.SimpleQuery( ),
"DELETE FROM notes_notes WHERE evaluation_id=%(evaluation_id)s", {"evaluation_id": e.id},
e,
) )
ndb.SimpleQuery( db.session.execute(
"DELETE FROM notes_notes_log WHERE evaluation_id=%(evaluation_id)s", sa.text(
e, """DELETE FROM notes_notes_log WHERE evaluation_id=:evaluation_id"""
) ),
ndb.SimpleQuery( {"evaluation_id": e.id},
"DELETE FROM notes_evaluation WHERE id=%(evaluation_id)s",
e,
) )
sco_moduleimpl.do_moduleimpl_delete( db.session.delete(e)
mod["moduleimpl_id"], formsemestre_id=formsemestre_id db.session.delete(modimpl)
)
# --- Desinscription des etudiants # --- Desinscription des etudiants
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) db.session.execute(
req = "DELETE FROM notes_formsemestre_inscription WHERE formsemestre_id=%(formsemestre_id)s" sa.text(
cursor.execute(req, {"formsemestre_id": formsemestre_id}) "DELETE FROM notes_formsemestre_inscription WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des evenements # --- Suppression des evenements
req = "DELETE FROM scolar_events WHERE formsemestre_id=%(formsemestre_id)s" db.session.execute(
cursor.execute(req, {"formsemestre_id": formsemestre_id}) sa.text("DELETE FROM scolar_events WHERE formsemestre_id=:formsemestre_id"),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des appreciations # --- Suppression des appreciations
req = "DELETE FROM notes_appreciations WHERE formsemestre_id=%(formsemestre_id)s" db.session.execute(
cursor.execute(req, {"formsemestre_id": formsemestre_id}) sa.text(
"DELETE FROM notes_appreciations WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Supression des validations (!!!) # --- Supression des validations (!!!)
req = "DELETE FROM scolar_formsemestre_validation WHERE formsemestre_id=%(formsemestre_id)s" db.session.execute(
cursor.execute(req, {"formsemestre_id": formsemestre_id}) 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: # --- 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" db.session.execute(
cursor.execute(req, {"formsemestre_id": formsemestre_id}) sa.text(
"""UPDATE scolar_formsemestre_validation
SET compense_formsemestre_id=NULL
WHERE compense_formsemestre_id=:formsemestre_id"""
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des autorisations # --- Suppression des autorisations
req = "DELETE FROM scolar_autorisation_inscription WHERE origin_formsemestre_id=%(formsemestre_id)s" db.session.execute(
cursor.execute(req, {"formsemestre_id": formsemestre_id}) sa.text(
"DELETE FROM scolar_autorisation_inscription WHERE origin_formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des coefs d'UE capitalisées # --- Suppression des coefs d'UE capitalisées
req = "DELETE FROM notes_formsemestre_uecoef WHERE formsemestre_id=%(formsemestre_id)s" db.session.execute(
cursor.execute(req, {"formsemestre_id": formsemestre_id}) sa.text(
"DELETE FROM notes_formsemestre_uecoef WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des item du menu custom # --- Suppression des item du menu custom
req = "DELETE FROM notes_formsemestre_custommenu WHERE formsemestre_id=%(formsemestre_id)s" db.session.execute(
cursor.execute(req, {"formsemestre_id": formsemestre_id}) sa.text(
"DELETE FROM notes_formsemestre_custommenu WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des formules # --- Suppression des formules
req = "DELETE FROM notes_formsemestre_ue_computation_expr WHERE formsemestre_id=%(formsemestre_id)s" db.session.execute(
cursor.execute(req, {"formsemestre_id": formsemestre_id}) sa.text(
"DELETE FROM notes_formsemestre_ue_computation_expr WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des preferences # --- Suppression des preferences
req = "DELETE FROM sco_prefs WHERE formsemestre_id=%(formsemestre_id)s" db.session.execute(
cursor.execute(req, {"formsemestre_id": formsemestre_id}) sa.text("DELETE FROM sco_prefs WHERE formsemestre_id=:formsemestre_id"),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des groupes et partitions # --- Suppression des groupes et partitions
req = """DELETE FROM group_membership db.session.execute(
sa.text(
"""
DELETE FROM group_membership
WHERE group_id IN WHERE group_id IN
(SELECT gm.group_id FROM group_membership gm, partition p, group_descr gd (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 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 WHERE id IN
(SELECT gd.id FROM group_descr gd, partition p (SELECT gd.id FROM group_descr gd, partition p
WHERE gd.partition_id = p.id 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" {"formsemestre_id": formsemestre_id},
cursor.execute(req, {"formsemestre_id": formsemestre_id}) )
db.session.execute(
sa.text("DELETE FROM partition WHERE formsemestre_id=:formsemestre_id"),
{"formsemestre_id": formsemestre_id},
)
# --- Responsables # --- Responsables
req = """DELETE FROM notes_formsemestre_responsables db.session.execute(
WHERE formsemestre_id=%(formsemestre_id)s""" sa.text(
cursor.execute(req, {"formsemestre_id": formsemestre_id}) "DELETE FROM notes_formsemestre_responsables WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Etapes # --- Etapes
req = """DELETE FROM notes_formsemestre_etapes db.session.execute(
WHERE formsemestre_id=%(formsemestre_id)s""" sa.text(
cursor.execute(req, {"formsemestre_id": formsemestre_id}) "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 # --- Dispenses d'UE
req = """DELETE FROM "dispenseUE" WHERE formsemestre_id=%(formsemestre_id)s""" db.session.execute(
cursor.execute(req, {"formsemestre_id": formsemestre_id}) sa.text("""DELETE FROM "dispenseUE" WHERE formsemestre_id=:formsemestre_id"""),
{"formsemestre_id": formsemestre_id},
)
# --- Destruction du semestre # --- Destruction du semestre
sco_formsemestre._formsemestreEditor.delete(cnx, formsemestre_id) db.session.delete(formsemestre)
# news # news
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_SEM, typ=ScolarNews.NEWS_SEM,
obj=formsemestre_id, obj=formsemestre_id,
text="Suppression du semestre %(titre)s" % sem, text=f"Suppression du semestre {titre_sem}",
max_frequency=0, max_frequency=0,
) )
@ -1678,7 +1743,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
sum_coefs_by_ue_id = {} sum_coefs_by_ue_id = {}
for ue in ues: for ue in ues:
sum_coefs_by_ue_id[ue.id] = sum( sum_coefs_by_ue_id[ue.id] = sum(
modimpl.module.coefficient modimpl.module.coefficient or 0.0
for modimpl in modimpls for modimpl in modimpls
if modimpl.module.ue_id == ue.id if modimpl.module.ue_id == ue.id
) )

View File

@ -191,12 +191,26 @@ def do_formsemestre_inscription_edit(args=None, formsemestre_id=None):
) # > modif inscription semestre ) # > 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. """Désinscription d'un étudiant.
Si semestre extérieur et dernier inscrit, suppression de ce semestre. Si semestre extérieur et dernier inscrit, suppression de ce semestre.
""" """
from app.scodoc import sco_formsemestre_edit
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
etud = Identite.get_etud(etudid) etud = Identite.get_etud(etudid)
# -- check lock # -- check lock
@ -204,13 +218,8 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
raise ScoValueError("désinscription impossible: semestre verrouille") raise ScoValueError("désinscription impossible: semestre verrouille")
# -- Si decisions de jury, désinscription interdite # -- Si decisions de jury, désinscription interdite
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) if check_has_dec_jury:
check_if_has_decision_jury(formsemestre, [etudid])
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)"""
)
insem = do_formsemestre_inscription_list( insem = do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id, "etudid": etudid} 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) sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
# --- Semestre extérieur # --- Semestre extérieur
if formsemestre.modalite == "EXT": if formsemestre.modalite == "EXT":
inscrits = do_formsemestre_inscription_list( if 0 == len(formsemestre.inscriptions):
args={"formsemestre_id": formsemestre_id}
)
nbinscrits = len(inscrits)
if nbinscrits == 0:
log( log(
f"""do_formsemestre_desinscription: f"""do_formsemestre_desinscription:
suppression du semestre extérieur {formsemestre}""" suppression du semestre extérieur {formsemestre}"""
) )
flash("Semestre exterieur supprimé") db.session.delete(formsemestre)
sco_formsemestre_edit.do_formsemestre_delete(formsemestre_id) db.session.commit()
flash(f"Semestre extérieur supprimé: {formsemestre.titre_annee()}")
logdb( logdb(
cnx, cnx,
@ -576,26 +582,29 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
ue_id = ue.id ue_id = ue.id
ue_descr = ue.acronyme ue_descr = ue.acronyme
if ue.type != UE_STANDARD: if ue.type != UE_STANDARD:
ue_descr += " <em>%s</em>" % UE_TYPE_NAME[ue.type] ue_descr += f" <em>{UE_TYPE_NAME[ue.type]}</em>"
ue_status = nt.get_etud_ue_status(etudid, ue_id) ue_status = nt.get_etud_ue_status(etudid, ue_id)
if ue_status and ue_status["is_capitalized"]: if ue_status and ue_status["is_capitalized"]:
sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"]) sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"])
ue_descr += ( ue_descr += f"""
' <a class="discretelink" href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" title="%s">(capitalisée le %s)' <a class="discretelink" href="{ url_for(
% ( 'notes.formsemestre_bulletinetud', scodoc_dept=g.scodoc_dept,
sem_origin["formsemestre_id"], formsemestre_id=sem_origin["formsemestre_id"],
etudid, etudid = etudid
sem_origin["titreannee"], )}" title="{sem_origin['titreannee']}">(capitalisée le {
ndb.DateISOtoDMY(ue_status["event_date"]), ndb.DateISOtoDMY(ue_status["event_date"])
) })
) """
descr.append( descr.append(
( (
"sec_%s" % ue_id, f"sec_{ue_id}",
{ {
"input_type": "separator", "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""" "title": f"""<b>{ue_descr} :</b>
% (ue_descr, ue_id, ue_id), <a href="#" onclick="chkbx_select('{ue_id}', true);">inscrire</a> | <a
href="#" onclick="chkbx_select('{ue_id}', false);">désinscrire</a>
à tous les modules
""",
}, },
) )
) )
@ -765,9 +774,7 @@ def do_moduleimpl_incription_options(
# verifie que ce module existe bien # verifie que ce module existe bien
mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id) mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
if len(mods) != 1: if len(mods) != 1:
raise ScoValueError( raise ScoValueError(f"inscription: invalid moduleimpl_id: {moduleimpl_id}")
"inscription: invalid moduleimpl_id: %s" % moduleimpl_id
)
mod = mods[0] mod = mods[0]
sco_moduleimpl.do_moduleimpl_inscription_create( sco_moduleimpl.do_moduleimpl_inscription_create(
{"moduleimpl_id": moduleimpl_id, "etudid": etudid}, {"moduleimpl_id": moduleimpl_id, "etudid": etudid},
@ -779,7 +786,7 @@ def do_moduleimpl_incription_options(
mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id) mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
if len(mods) != 1: if len(mods) != 1:
raise ScoValueError( raise ScoValueError(
"desinscription: invalid moduleimpl_id: %s" % moduleimpl_id f"desinscription: invalid moduleimpl_id: {moduleimpl_id}"
) )
mod = mods[0] mod = mods[0]
inscr = sco_moduleimpl.do_moduleimpl_inscription_list( inscr = sco_moduleimpl.do_moduleimpl_inscription_list(
@ -787,8 +794,7 @@ def do_moduleimpl_incription_options(
) )
if not inscr: if not inscr:
raise ScoValueError( raise ScoValueError(
"pas inscrit a ce module ! (etudid=%s, moduleimpl_id=%s)" f"pas inscrit a ce module ! (etudid={etudid}, moduleimpl_id={moduleimpl_id})"
% (etudid, moduleimpl_id)
) )
oid = inscr[0]["moduleimpl_inscription_id"] oid = inscr[0]["moduleimpl_inscription_id"]
sco_moduleimpl.do_moduleimpl_inscription_delete( sco_moduleimpl.do_moduleimpl_inscription_delete(
@ -797,11 +803,13 @@ def do_moduleimpl_incription_options(
H = [ H = [
html_sco_header.sco_header(), html_sco_header.sco_header(),
"""<h3>Modifications effectuées</h3> f"""<h3>Modifications effectuées</h3>
<p><a class="stdlink" href="%s"> <p><a class="stdlink" href="{
Retour à la fiche étudiant</a></p> url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
""" }">
% url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid), Retour à la fiche étudiant</a>
</p>
""",
html_sco_header.sco_footer(), html_sco_header.sco_footer(),
] ]
return "\n".join(H) return "\n".join(H)
@ -845,49 +853,59 @@ def formsemestre_inscrits_ailleurs(formsemestre_id):
"""Page listant les étudiants inscrits dans un autre semestre """Page listant les étudiants inscrits dans un autre semestre
dont les dates recouvrent le semestre indiqué. dont les dates recouvrent le semestre indiqué.
""" """
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
"Inscriptions multiples parmi les étudiants du semestre ", "Inscriptions multiples parmi les étudiants du semestre ",
init_qtip=True,
javascripts=["js/etud_info.js"],
) )
] ]
insd = list_inscrits_ailleurs(formsemestre_id) insd = list_inscrits_ailleurs(formsemestre_id)
# liste ordonnée par nom # liste ordonnée par nom
etudlist = [ etudlist = [Identite.get_etud(etudid) for etudid, sems in insd.items() if sems]
sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etudlist.sort(key=lambda x: x.sort_key)
for etudid in insd.keys()
if insd[etudid]
]
etudlist.sort(key=lambda x: x["nom"])
if etudlist: if etudlist:
H.append("<ul>") H.append("<ul>")
for etud in etudlist: for etud in etudlist:
H.append( H.append(
'<li><a href="%s" class="discretelink">%s</a> : ' f"""<li><a id="{etud.id}" class="discretelink etudinfo"
% ( href={
url_for( url_for(
"scolar.fiche_etud", "scolar.fiche_etud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"], etudid=etud.id,
), )
etud["nomprenom"], }
) >{etud.nomprenom}</a> :
"""
) )
l = [] l = []
for s in insd[etud["etudid"]]: for s in insd[etud.id]:
l.append( l.append(
'<a class="discretelink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a>' f"""<a class="discretelink" href="{
% s url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
)}">{s['titremois']}</a>"""
) )
H.append(", ".join(l)) H.append(", ".join(l))
H.append("</li>") H.append("</li>")
H.append("</ul>")
H.append("<p>Total: %d étudiants concernés.</p>" % len(etudlist))
H.append( 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> <ul>
<li>vérifier que les dates des semestres se suivent sans se chevaucher</li> <li>vérifier que les dates des semestres se suivent <em>sans se chevaucher</em>
<li>ou si besoin désinscrire le(s) étudiant(s) de l'un des semestres (via leurs fiches individuelles).</li> </li>
<li>ou bien si besoin désinscrire le(s) étudiant(s) de l'un des semestres
(via leurs fiches individuelles).
</li>
</ul> </ul>
""" """
) )

View File

@ -1173,7 +1173,8 @@ def formsemestre_tableau_modules(
moduleimpl_id=modimpl.id, moduleimpl_id=modimpl.id,
) )
mod_descr = "Module " + (mod.titre or "") mod_descr = "Module " + (mod.titre or "")
if mod.is_apc(): is_apc = mod.is_apc() # SAE ou ressource
if is_apc:
coef_descr = ", ".join( coef_descr = ", ".join(
[ [
f"{ue.acronyme}: {co}" f"{ue.acronyme}: {co}"
@ -1193,6 +1194,7 @@ def formsemestre_tableau_modules(
[u.get_nomcomplet() for u in modimpl.enseignants] [u.get_nomcomplet() for u in modimpl.enseignants]
) )
mod_nb_inscrits = nt.modimpls_results[modimpl.id].nb_inscrits_module mod_nb_inscrits = nt.modimpls_results[modimpl.id].nb_inscrits_module
mod_is_conforme = modimpl.check_apc_conformity(nt)
ue = modimpl.module.ue ue = modimpl.module.ue
if show_ues and (prev_ue_id != ue.id): if show_ues and (prev_ue_id != ue.id):
prev_ue_id = ue.id prev_ue_id = ue.id
@ -1200,10 +1202,12 @@ def formsemestre_tableau_modules(
if use_ue_coefs: if use_ue_coefs:
titre += f""" <b>(coef. {ue.coefficient or 0.0})</b>""" titre += f""" <b>(coef. {ue.coefficient or 0.0})</b>"""
H.append( H.append(
f"""<tr class="formsemestre_status_ue"><td colspan="4"> f"""<tr class="formsemestre_status_ue">
<span class="status_ue_acro">{ue.acronyme}</span> <td colspan="4">
<span class="status_ue_title">{titre}</span> <span class="status_ue_acro">{ue.acronyme}</span>
</td><td colspan="2">""" <span class="status_ue_title">{titre}</span>
</td>
<td colspan="2">"""
) )
expr = sco_compute_moy.get_ue_expression( expr = sco_compute_moy.get_ue_expression(
@ -1226,21 +1230,26 @@ def formsemestre_tableau_modules(
fontorange = "" fontorange = ""
etat = sco_evaluations.do_evaluation_etat_in_mod(nt, modimpl) etat = sco_evaluations.do_evaluation_etat_in_mod(nt, modimpl)
# if nt.parcours.APC_SAE:
# tbd style si module non conforme
if ( if (
etat["nb_evals_completes"] > 0 etat["nb_evals_completes"] > 0
and etat["nb_evals_en_cours"] == 0 and etat["nb_evals_en_cours"] == 0
and etat["nb_evals_vides"] == 0 and etat["nb_evals_vides"] == 0
and not etat["attente"] 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: 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( H.append(
f""" f"""
<td class="formsemestre_status_code""><a <tr class="{tr_classes}">
<td class="formsemestre_status_code"><a
href="{moduleimpl_status_url}" href="{moduleimpl_status_url}"
title="{mod_descr}" class="stdlink">{mod.code}</a></td> title="{mod_descr}" class="stdlink">{mod.code}</a></td>
<td class="scotext"><a href="{moduleimpl_status_url}" title="{mod_descr}" <td class="scotext"><a href="{moduleimpl_status_url}" title="{mod_descr}"
@ -1278,17 +1287,20 @@ def formsemestre_tableau_modules(
ModuleType.SAE, ModuleType.SAE,
): ):
H.append('<td class="evals">') H.append('<td class="evals">')
nb_evals = ( nb_evals = etat["nb_evals"]
etat["nb_evals_completes"]
+ etat["nb_evals_en_cours"]
+ etat["nb_evals_vides"]
)
if nb_evals != 0: 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( H.append(
f"""<a href="{moduleimpl_status_url}" f"""<a href="{moduleimpl_status_url}"
title="les évaluations 'ok' sont celles prises en compte dans les calculs" title="les évaluations 'ok' sont celles prises en compte dans les calculs"
class="formsemestre_status_link">{nb_evals} prévues, 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: if etat["nb_evals_en_cours"] > 0:
H.append( H.append(
@ -1300,7 +1312,12 @@ def formsemestre_tableau_modules(
if etat["attente"]: if etat["attente"]:
H.append( H.append(
f""" <span><a class="redlink" href="{moduleimpl_status_url}" 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: elif mod.module_type == ModuleType.MALUS:
nb_malus_notes = sum( nb_malus_notes = sum(

View File

@ -34,7 +34,7 @@ from flask import url_for, flash, g, request
from flask_login import current_user from flask_login import current_user
import sqlalchemy as sa 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.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import db, log from app import db, log
@ -232,7 +232,9 @@ def formsemestre_validation_etud_form(
H.append( H.append(
tf_error_message( tf_error_message(
f"""Impossible de statuer sur cet étudiant: il a des notes en 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", url_for( "notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">tableau de bord</a>) }">tableau de bord</a>)
@ -241,6 +243,26 @@ def formsemestre_validation_etud_form(
) )
return "\n".join(H + footer) 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 # Infos si pas de semestre précédent
if not Se.prev: if not Se.prev:
if Se.sem["semestre_id"] == 1: if Se.sem["semestre_id"] == 1:
@ -1399,7 +1421,7 @@ def check_formation_ues(formation: Formation) -> tuple[str, dict[int, list[Unite
if ( if (
len(semestre_ids) > 1 len(semestre_ids) > 1
): # plusieurs semestres d'indices differents dans le cursus ): # plusieurs semestres d'indices differents dans le cursus
ue_multiples[ue["ue_id"]] = sems ue_multiples[ue.id] = sems
if not ue_multiples: if not ue_multiples:
return "", {} return "", {}
@ -1423,12 +1445,12 @@ def check_formation_ues(formation: Formation) -> tuple[str, dict[int, list[Unite
] ]
slist = ", ".join( slist = ", ".join(
[ [
"""%(titreannee)s (<em>semestre <b class="fontred">%(semestre_id)s</b></em>)""" f"""{s['titreannee']
% s } (<em>semestre <b class="fontred">{s['semestre_id']}</b></em>)"""
for s in sems 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>") H.append("</ul></div>")
return "\n".join(H), ue_multiples return "\n".join(H), ue_multiples

View File

@ -331,6 +331,7 @@ class DisplayedGroupsInfos:
empty_list_select_all=True, empty_list_select_all=True,
moduleimpl_id=None, # used to find formsemestre when unspecified 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 isinstance(group_ids, int):
if group_ids: if group_ids:
group_ids = [group_ids] # cas ou un seul parametre, pas de liste group_ids = [group_ids] # cas ou un seul parametre, pas de liste
@ -466,6 +467,10 @@ class DisplayedGroupsInfos:
else None 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 # Ancien ZScolar.group_list renommé ici en group_table
def groups_table( def groups_table(
@ -514,10 +519,11 @@ def groups_table(
"paiementinscription_str": "Paiement", "paiementinscription_str": "Paiement",
"etudarchive": "Fichiers", "etudarchive": "Fichiers",
"annotations_str": "Annotations", "annotations_str": "Annotations",
"bourse_str": "Boursier", "bourse_str": "Boursier", # requière ViewEtudData
"etape": "Etape", "etape": "Etape",
"semestre_groupe": "Semestre-Groupe", # pour Moodle "semestre_groupe": "Semestre-Groupe", # pour Moodle
"annee": "annee_admission", "annee": "annee_admission",
"nationalite": "nationalite", # requière ViewEtudData
} }
# ajoute colonnes pour groupes # ajoute colonnes pour groupes
@ -559,53 +565,61 @@ def groups_table(
moodle_sem_name = groups_infos.formsemestre["session_id"] moodle_sem_name = groups_infos.formsemestre["session_id"]
moodle_groupenames = set() moodle_groupenames = set()
# ajoute liens # ajoute liens
for etud in groups_infos.members: for etud_info in groups_infos.members:
if etud["email"]: if etud_info["email"]:
etud["_email_target"] = "mailto:" + etud["email"] etud_info["_email_target"] = "mailto:" + etud_info["email"]
else: else:
etud["_email_target"] = "" etud_info["_email_target"] = ""
if etud["emailperso"]: if etud_info["emailperso"]:
etud["_emailperso_target"] = "mailto:" + etud["emailperso"] etud_info["_emailperso_target"] = "mailto:" + etud_info["emailperso"]
else: else:
etud["_emailperso_target"] = "" etud_info["_emailperso_target"] = ""
fiche_url = url_for( 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_info["_nom_disp_target"] = fiche_url
etud["_nom_disp_order"] = etud_sort_key(etud) etud_info["_nom_disp_order"] = etud_sort_key(etud_info)
etud["_prenom_target"] = fiche_url etud_info["_prenom_target"] = fiche_url
etud["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"]) etud_info["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (
etud["bourse_str"] = "oui" if etud["boursier"] else "non" etud_info["etudid"]
if etud["etat"] == "D": )
etud["_css_row_class"] = "etuddem" etud_info["bourse_str"] = "oui" if etud_info["boursier"] else "non"
if etud_info["etat"] == "D":
etud_info["_css_row_class"] = "etuddem"
# et groupes: # et groupes:
for partition_id in etud["partitions"]: for partition_id in etud_info["partitions"]:
etud[partition_id] = etud["partitions"][partition_id]["group_name"] etud_info[partition_id] = etud_info["partitions"][partition_id][
"group_name"
]
# Ajoute colonne pour moodle: semestre_groupe, de la forme # Ajoute colonne pour moodle: semestre_groupe, de la forme
# RT-DUT-FI-S3-2021-PARTITION-GROUPE # RT-DUT-FI-S3-2021-PARTITION-GROUPE
moodle_groupename = [] moodle_groupename = []
if groups_infos.selected_partitions: if groups_infos.selected_partitions:
# il y a des groupes selectionnes, utilise leurs partitions # il y a des groupes selectionnes, utilise leurs partitions
for partition_id in groups_infos.selected_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( moodle_groupename.append(
partitions_name[partition_id] partitions_name[partition_id]
+ "-" + "-"
+ etud["partitions"][partition_id]["group_name"] + etud_info["partitions"][partition_id]["group_name"]
) )
else: else:
# pas de groupes sélectionnés: prend le premier s'il y en a un # pas de groupes sélectionnés: prend le premier s'il y en a un
moodle_groupename = ["tous"] moodle_groupename = ["tous"]
if etud["partitions"]: if etud_info["partitions"]:
for p in etud["partitions"].items(): # partitions is an OrderedDict for p in etud_info[
"partitions"
].items(): # partitions is an OrderedDict
moodle_groupename = [ moodle_groupename = [
partitions_name[p[0]] + "-" + p[1]["group_name"] partitions_name[p[0]] + "-" + p[1]["group_name"]
] ]
break break
moodle_groupenames |= set(moodle_groupename) 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: if groups_infos.nbdem > 1:
s = "s" s = "s"
@ -714,9 +728,11 @@ def groups_table(
}); });
</script> </script>
""", """,
"""<span class="warning_unauthorized">accès aux données personnelles interdit</span>""" (
if not can_view_etud_data """<span class="warning_unauthorized">accès aux données personnelles interdit</span>"""
else "", if not can_view_etud_data
else ""
),
] ]
) )
H.append("</div></form>") H.append("</div></form>")
@ -768,13 +784,7 @@ def groups_table(
return "".join(H) return "".join(H)
elif ( elif fmt in {"pdf", "xml", "json", "xls", "moodlecsv"}:
fmt == "pdf"
or fmt == "xml"
or fmt == "json"
or fmt == "xls"
or fmt == "moodlecsv"
):
if fmt == "moodlecsv": if fmt == "moodlecsv":
fmt = "csv" fmt = "csv"
return tab.make_page(fmt=fmt) return tab.make_page(fmt=fmt)
@ -789,7 +799,7 @@ def groups_table(
with_paiement=with_paiement, with_paiement=with_paiement,
server_name=request.url_root, 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) return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
elif fmt == "allxls": elif fmt == "allxls":
if not can_view_etud_data: if not can_view_etud_data:
@ -823,6 +833,7 @@ def groups_table(
"fax", "fax",
"date_naissance", "date_naissance",
"lieu_naissance", "lieu_naissance",
"nationalite",
"bac", "bac",
"specialite", "specialite",
"annee_bac", "annee_bac",
@ -845,16 +856,16 @@ def groups_table(
# remplis infos lycee si on a que le code lycée # remplis infos lycee si on a que le code lycée
# et ajoute infos inscription # et ajoute infos inscription
for m in groups_infos.members: for m in groups_infos.members:
etud = sco_etud.get_etud_info(m["etudid"], filled=True)[0] etud_info = sco_etud.get_etud_info(m["etudid"], filled=True)[0]
m.update(etud) m.update(etud_info)
sco_etud.etud_add_lycee_infos(etud) sco_etud.etud_add_lycee_infos(etud_info)
# et ajoute le parcours # et ajoute le parcours
Se = sco_cursus.get_situation_etud_cursus( 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["parcours"] = Se.get_cursus_descr()
m["code_cursus"], _ = sco_report.get_code_cursus_etud( 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] rows = [[m.get(k, "") for k in keys] for m in groups_infos.members]
title = "etudiants_%s" % groups_infos.groups_filename title = "etudiants_%s" % groups_infos.groups_filename
@ -905,9 +916,11 @@ def tab_absences_html(groups_infos, etat=None):
% groups_infos.groups_query_args, % groups_infos.groups_query_args,
"""<li><a class="stdlink" href="trombino?%s&fmt=pdflist">Liste d'appel avec photos</a></li>""" """<li><a class="stdlink" href="trombino?%s&fmt=pdflist">Liste d'appel avec photos</a></li>"""
% groups_infos.groups_query_args, % 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) f"""<li><a class="stdlink" href="groups_export_annotations?{groups_infos.groups_query_args}">Liste des annotations</a></li>"""
else """<li class="unauthorized" title="non autorisé">Liste des annotations</li>""", if authuser.has_permission(Permission.ViewEtudData)
else """<li class="unauthorized" title="non autorisé">Liste des annotations</li>"""
),
"</ul>", "</ul>",
] ]
) )

View File

@ -36,13 +36,14 @@ from flask import url_for, g, request
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import db, log 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.gen_tables import GenTable
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
@ -50,62 +51,69 @@ from app.scodoc import sco_pv_dict
from app.scodoc.sco_exceptions import ScoValueError 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. """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. 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 ignore_jury: si vrai, considère tous les étudiants comme autorisés, même
s'ils n'ont pas de décision de jury. s'ils n'ont pas de décision de jury.
""" """
src_sems = list_source_sems(sem, delai=delai) src_sems = _list_source_sems(formsemestre)
inscrits = list_inscrits(sem["formsemestre_id"]) inscrits = list_inscrits(formsemestre.id)
r = {} r = {}
candidats = {} # etudid : etud (tous les etudiants candidats) candidats = {} # etudid : etud (tous les etudiants candidats)
nb = 0 # debug nb = 0 # debug
for src in src_sems: src_formsemestre: FormSemestre
for src_formsemestre in src_sems:
if ignore_jury: if ignore_jury:
# liste de tous les inscrits au semestre (sans dems) # 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: else:
# liste des étudiants autorisés par le jury à s'inscrire ici # 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 = [] 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 # Filtre ceux qui se sont déjà inscrit dans un semestre APRES le semestre src
auth_used = False # autorisation deja utilisée ? auth_used = False # autorisation deja utilisée ?
etud = sco_etud.get_etud_info(etudid=e["etudid"], filled=True)[0] etud = Identite.get_etud(e["etudid"])
for isem in etud["sems"]: for inscription in etud.inscriptions():
if ndb.DateDMYtoISO(isem["date_debut"]) >= ndb.DateDMYtoISO( if inscription.formsemestre.date_debut >= src_formsemestre.date_fin:
src["date_fin"]
):
auth_used = True auth_used = True
if not auth_used: if not auth_used:
candidats[e["etudid"]] = etud candidats[e["etudid"]] = etud
liste_filtree.append(e) liste_filtree.append(e)
nb += 1 nb += 1
r[src["formsemestre_id"]] = { r[src_formsemestre.id] = {
"etuds": liste_filtree, "etuds": liste_filtree,
"infos": { "infos": {
"id": src["formsemestre_id"], "id": src_formsemestre.id,
"title": src["titreannee"], "title": src_formsemestre.titre_annee(),
"title_target": "formsemestre_status?formsemestre_id=%s" "title_target": url_for(
% src["formsemestre_id"], "notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=src_formsemestre.id,
),
"filename": "etud_autorises", "filename": "etud_autorises",
}, },
} }
# ajoute attribut inscrit qui indique si l'étudiant est déjà inscrit dans le semestre dest. # 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 e["inscrit"] = e["etudid"] in inscrits
# Ajoute liste des etudiants actuellement inscrits # Ajoute liste des etudiants actuellement inscrits
for e in inscrits.values(): for e in inscrits.values():
e["inscrit"] = True e["inscrit"] = True
r[sem["formsemestre_id"]] = { r[formsemestre.id] = {
"etuds": list(inscrits.values()), "etuds": list(inscrits.values()),
"infos": { "infos": {
"id": sem["formsemestre_id"], "id": formsemestre.id,
"title": "Semestre cible: " + sem["titreannee"], "title": "Semestre cible: " + formsemestre.titre_annee(),
"title_target": "formsemestre_status?formsemestre_id=%s" "title_target": url_for(
% sem["formsemestre_id"], "notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
),
"comment": " actuellement inscrits dans ce semestre", "comment": " actuellement inscrits dans ce semestre",
"help": "Ces étudiants sont actuellement inscrits dans ce semestre. Si vous les décochez, il seront désinscrits.", "help": "Ces étudiants sont actuellement inscrits dans ce semestre. Si vous les décochez, il seront désinscrits.",
"filename": "etud_inscrits", "filename": "etud_inscrits",
@ -115,7 +123,7 @@ def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False):
return r, inscrits, candidats 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 """Étudiants déjà inscrits à ce semestre
{ etudid : etud } { etudid : etud }
""" """
@ -133,28 +141,27 @@ def list_inscrits(formsemestre_id, with_dems=False):
return inscr return inscr
def list_etuds_from_sem(src, dst) -> list[dict]: def _list_etuds_from_sem(src: FormSemestre, dst: FormSemestre) -> list[dict]:
"""Liste des etudiants du semestre src qui sont autorisés à passer dans le semestre dst.""" """Liste des étudiants du semestre src qui sont autorisés à passer dans le semestre dst."""
target = dst["semestre_id"] target_semestre_id = dst.semestre_id
dpv = sco_pv_dict.dict_pvjury(src["formsemestre_id"]) dpv = sco_pv_dict.dict_pvjury(src.id)
if not dpv: if not dpv:
return [] return []
etuds = [ etuds = [
x["identite"] x["identite"]
for x in dpv["decisions"] 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 return etuds
def list_inscrits_date(sem): def list_inscrits_date(formsemestre: FormSemestre):
"""Liste les etudiants inscrits dans n'importe quel semestre """Liste les etudiants inscrits à la date de début de formsemestre
du même département dans n'importe quel semestre du même département
SAUF sem à la date de début de sem. SAUF formsemestre
""" """
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
sem["date_debut_iso"] = ndb.DateDMYtoISO(sem["date_debut"])
cursor.execute( cursor.execute(
"""SELECT ins.etudid """SELECT ins.etudid
FROM FROM
@ -166,12 +173,18 @@ def list_inscrits_date(sem):
AND S.date_fin >= %(date_debut_iso)s AND S.date_fin >= %(date_debut_iso)s
AND S.dept_id = %(dept_id)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()] 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 """Inscrit ces etudiants dans ce semestre
(la liste doit avoir été vérifiée au préalable) (la liste doit avoir été vérifiée au préalable)
En option: 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) (si les deux sont vrais, inscrit_parcours n'a pas d'effet)
""" """
# TODO à ré-écrire pour utiliser les modèles, notamment GroupDescr # TODO à ré-écrire pour utiliser les modèles, notamment GroupDescr
formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
formsemestre.setup_parcours_groups() formsemestre.setup_parcours_groups()
log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}") log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}")
for etudid in etudids: for etudid in etudids:
sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
sem["formsemestre_id"], formsemestre.id,
etudid, etudid,
etat=scu.INSCRIT, etat=scu.INSCRIT,
method="formsemestre_inscr_passage", method="formsemestre_inscr_passage",
@ -210,7 +222,7 @@ def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False):
cursem_groups_by_name = { cursem_groups_by_name = {
g["group_name"]: g 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"] 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) 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" "désinscrit les étudiants indiqués du formsemestre"
log(f"do_desinscrit: {etudids}") log(f"do_desinscrit: {etudids}")
for etudid in etudids: for etudid in etudids:
sco_formsemestre_inscriptions.do_formsemestre_desinscription( 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 """Liste des semestres sources
sem est le semestre destination formsemestre est le semestre destination
""" """
# liste des semestres débutant a moins # liste des semestres du même type de cursus terminant
# de delai (en jours) de la date de fin du semestre d'origine. # pas trop loin de la date de début du semestre destination
sems = sco_formsemestre.do_formsemestre_list() date_fin_min = formsemestre.date_debut - datetime.timedelta(days=275)
othersems = [] date_fin_max = formsemestre.date_debut + datetime.timedelta(days=45)
d, m, y = [int(x) for x in sem["date_debut"].split("/")] return (
date_debut_dst = datetime.date(y, m, d) FormSemestre.query.filter(
FormSemestre.dept_id == formsemestre.dept_id,
delais = datetime.timedelta(delai) # saute le semestre destination:
for s in sems: FormSemestre.id != formsemestre.id,
if s["formsemestre_id"] == sem["formsemestre_id"]: # et les semestres de formations speciales (monosemestres):
continue # saute le semestre destination FormSemestre.semestre_id != codes_cursus.NO_SEMESTRE_ID,
if s["date_fin"]: # semestre pas trop dans le futur
d, m, y = [int(x) for x in s["date_fin"].split("/")] FormSemestre.date_fin <= date_fin_max,
date_fin = datetime.date(y, m, d) # ni trop loin dans le passé
if date_debut_dst - date_fin > delais: FormSemestre.date_fin >= date_fin_min,
continue # semestre trop ancien )
if date_fin > date_debut_dst: .join(Formation)
continue # semestre trop récent .filter_by(type_parcours=formsemestre.formation.type_parcours)
# Elimine les semestres de formations speciales (sans parcours) ).all()
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
# view, GET, POST
def formsemestre_inscr_passage( def formsemestre_inscr_passage(
formsemestre_id, formsemestre_id,
etuds=[], etuds: str | list[int] | list[str] | int | None = None,
inscrit_groupes=False, inscrit_groupes=False,
inscrit_parcours=False, inscrit_parcours=False,
submitted=False, submitted=False,
@ -300,36 +305,42 @@ def formsemestre_inscr_passage(
- Confirmation: indiquer les étudiants inscrits et ceux désinscrits, le total courant. - Confirmation: indiquer les étudiants inscrits et ceux désinscrits, le total courant.
""" """
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
inscrit_groupes = int(inscrit_groupes) inscrit_groupes = int(inscrit_groupes)
inscrit_parcours = int(inscrit_parcours) inscrit_parcours = int(inscrit_parcours)
ignore_jury = int(ignore_jury) ignore_jury = int(ignore_jury)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
# -- check lock # -- check lock
if not sem["etat"]: if not formsemestre.etat:
raise ScoValueError("opération impossible: semestre verrouille") 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() footer = html_sco_header.sco_footer()
H = [header] etuds = [] if etuds is None else etuds
if isinstance(etuds, str): 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] etuds = [int(x) for x in etuds.split(",") if x]
elif isinstance(etuds, int): elif isinstance(etuds, int):
etuds = [etuds] etuds = [etuds]
elif etuds and isinstance(etuds[0], str): elif etuds and isinstance(etuds[0], str):
etuds = [int(x) for x in etuds] etuds = [int(x) for x in etuds]
auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem( auth_etuds_by_sem, inscrits, candidats = _list_authorized_etuds_by_sem(
sem, ignore_jury=ignore_jury formsemestre, ignore_jury=ignore_jury
) )
etuds_set = set(etuds) etuds_set = set(etuds)
candidats_set = set(candidats) candidats_set = set(candidats)
inscrits_set = set(inscrits) inscrits_set = set(inscrits)
candidats_non_inscrits = candidats_set - inscrits_set 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 = [candidats[etudid] for etudid in etudset]
etuds.sort(key=itemgetter("nom")) etuds.sort(key=lambda e: e.sort_key)
return etuds return etuds
if submitted: if submitted:
@ -340,7 +351,7 @@ def formsemestre_inscr_passage(
if not submitted: if not submitted:
H += _build_page( H += _build_page(
sem, formsemestre,
auth_etuds_by_sem, auth_etuds_by_sem,
inscrits, inscrits,
candidats_non_inscrits, candidats_non_inscrits,
@ -355,30 +366,31 @@ def formsemestre_inscr_passage(
if a_inscrire: if a_inscrire:
H.append("<h3>Étudiants à inscrire</h3><ol>") H.append("<h3>Étudiants à inscrire</h3><ol>")
for etud in set_to_sorted_etud_list(a_inscrire): 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>") H.append("</ol>")
a_inscrire_en_double = inscrits_ailleurs.intersection(a_inscrire) a_inscrire_en_double = inscrits_ailleurs.intersection(a_inscrire)
if a_inscrire_en_double: if a_inscrire_en_double:
H.append("<h3>dont étudiants déjà inscrits:</h3><ul>") H.append("<h3>dont étudiants déjà inscrits:</h3><ul>")
for etud in set_to_sorted_etud_list(a_inscrire_en_double): 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>") H.append("</ul>")
if a_desinscrire: if a_desinscrire:
H.append("<h3>Étudiants à désinscrire</h3><ol>") H.append("<h3>Étudiants à désinscrire</h3><ol>")
for etudid in a_desinscrire: a_desinscrire_ident = sorted(
H.append( (Identite.query.get(eid) for eid in a_desinscrire),
'<li class="desinscription">%(nomprenom)s</li>' key=lambda x: x.sort_key,
% inscrits[etudid] )
) for etud in a_desinscrire_ident:
H.append(f'<li class="desinscription">{etud.nomprenom}</li>')
H.append("</ol>") H.append("</ol>")
todo = a_inscrire or a_desinscrire todo = a_inscrire or a_desinscrire
if not todo: if not todo:
H.append("""<h3>Il n'y a rien à modifier !</h3>""") H.append("""<h3>Il n'y a rien à modifier !</h3>""")
H.append( H.append(
scu.confirm_dialog( scu.confirm_dialog(
dest_url="formsemestre_inscr_passage" dest_url=(
if todo "formsemestre_inscr_passage" if todo else "formsemestre_status"
else "formsemestre_status", ),
message="<p>Confirmer ?</p>" if todo else "", message="<p>Confirmer ?</p>" if todo else "",
add_headers=False, add_headers=False,
cancel_url="formsemestre_inscr_passage?formsemestre_id=" cancel_url="formsemestre_inscr_passage?formsemestre_id="
@ -395,16 +407,26 @@ def formsemestre_inscr_passage(
) )
) )
else: 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(): with sco_cache.DeferredSemCacheManager():
# Inscription des étudiants au nouveau semestre: # Inscription des étudiants au nouveau semestre:
do_inscrit( do_inscrit(
sem, formsemestre,
a_inscrire, a_inscrire,
inscrit_groupes=inscrit_groupes, inscrit_groupes=inscrit_groupes,
inscrit_parcours=inscrit_parcours, inscrit_parcours=inscrit_parcours,
) )
# Désinscriptions: # Désinscriptions:
do_desinscrit(sem, a_desinscrire) do_desinscrit(formsemestre, a_desinscrire, check_has_dec_jury=False)
H.append( H.append(
f"""<h3>Opération effectuée</h3> f"""<h3>Opération effectuée</h3>
@ -441,7 +463,7 @@ def formsemestre_inscr_passage(
def _build_page( def _build_page(
sem, formsemestre: FormSemestre,
auth_etuds_by_sem, auth_etuds_by_sem,
inscrits, inscrits,
candidats_non_inscrits, candidats_non_inscrits,
@ -450,7 +472,6 @@ def _build_page(
inscrit_parcours=False, inscrit_parcours=False,
ignore_jury=False, ignore_jury=False,
): ):
formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
inscrit_groupes = int(inscrit_groupes) inscrit_groupes = int(inscrit_groupes)
inscrit_parcours = int(inscrit_parcours) inscrit_parcours = int(inscrit_parcours)
ignore_jury = int(ignore_jury) ignore_jury = int(ignore_jury)
@ -472,7 +493,7 @@ def _build_page(
), ),
f"""<form name="f" method="post" action="{request.base_url}"> 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"/> <input type="submit" name="submitted" value="Appliquer les modifications"/>
&nbsp;<a href="#help">aide</a> &nbsp;<a href="#help">aide</a>
@ -491,7 +512,7 @@ def _build_page(
</div> </div>
<div>{scu.EMO_WARNING} <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 de ce semestre ({formsemestre.date_debut.strftime("%d/%m/%Y")}) sont pris en
compte.</em> compte.</em>
</div> </div>
@ -499,7 +520,7 @@ def _build_page(
<input type="submit" name="submitted" value="Appliquer les modifications"/> <input type="submit" name="submitted" value="Appliquer les modifications"/>
{formsemestre_inscr_passage_help(sem)} {formsemestre_inscr_passage_help(formsemestre)}
</form> </form>
""", """,
@ -524,19 +545,20 @@ def _build_page(
return H 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" "texte d'aide en bas de la page passage des étudiants"
return f"""<div class="pas_help"><h3><a name="help">Explications</a></h3> 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 <p>Cette page permet d'inscrire des étudiants dans le semestre destination
<a class="stdlink" <a class="stdlink"
href="{ href="{
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=sem["formsemestre_id"] ) url_for("notes.formsemestre_status",
}">{sem['titreannee']}</a>, scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id )
}">{formsemestre.titre_annee()}</a>,
et d'en désincrire si besoin. et d'en désincrire si besoin.
</p> </p>
<p>Les étudiants sont groupés par semestre d'origine. Ceux qui sont en caractères <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. <span class="deja-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 Ceux qui sont en <span class="inscrit-ailleurs">gras et en rouge</span> sont inscrits
dans un <em>autre</em> semestre. dans un <em>autre</em> semestre.
</p> </p>
<p>Au départ, les étudiants déjà inscrits sont sélectionnés; vous pouvez ajouter <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 ( aussi, pensez à les cocher dans conserve les groupes, on conserve les parcours ( aussi, pensez à les cocher dans
<a class="stdlink" href="{ <a class="stdlink" href="{
url_for("notes.formsemestre_editwithmodules", scodoc_dept=g.scodoc_dept, 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). }">modifier le semestre</a> avant de faire passer les étudiants).
</a> </a>
@ -656,25 +678,24 @@ def etuds_select_boxes(
H.append("</div>") H.append("</div>")
for etud in etuds: for etud in etuds:
if etud.get("inscrit", False): if etud.get("inscrit", False):
c = " inscrit" c = " deja-inscrit"
checked = 'checked="checked"' checked = 'checked="checked"'
else: else:
checked = "" checked = ""
if etud["etudid"] in inscrits_ailleurs: if etud["etudid"] in inscrits_ailleurs:
c = " inscrailleurs" c = " inscrit-ailleurs"
else: else:
c = "" c = ""
sco_etud.format_etud_ident(etud) sco_etud.format_etud_ident(etud)
if etud["etudid"]: if etud["etudid"]:
elink = """<a class="discretelink %s" href="%s">%s</a>""" % ( elink = f"""<a id="{etud['etudid']}" class="discretelink etudinfo {c}"
c, href="{ url_for(
url_for( 'scolar.fiche_etud',
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"], etudid=etud['etudid'],
), )
etud["nomprenom"], }">{etud['nomprenom']}</a>
) """
else: else:
# ce n'est pas un etudiant ScoDoc # ce n'est pas un etudiant ScoDoc
elink = etud["nomprenom"] elink = etud["nomprenom"]

View File

@ -490,9 +490,9 @@ def _make_table_notes(
rlinks = {"_table_part": "head"} rlinks = {"_table_part": "head"}
for e in evaluations: for e in evaluations:
rlinks[e.id] = "afficher" rlinks[e.id] = "afficher"
rlinks[ rlinks["_" + str(e.id) + "_help"] = (
"_" + str(e.id) + "_help" "afficher seulement les notes de cette évaluation"
] = "afficher seulement les notes de cette évaluation" )
rlinks["_" + str(e.id) + "_target"] = url_for( rlinks["_" + str(e.id) + "_target"] = url_for(
"notes.evaluation_listenotes", "notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept, 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) notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
if evaluation.date_debut: if evaluation.date_debut:
titles[ titles[evaluation.id] = (
evaluation.id f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})"
] = f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})" )
else: else:
titles[evaluation.id] = f"{evaluation.description} " titles[evaluation.id] = f"{evaluation.description} "
@ -820,14 +820,17 @@ def _add_eval_columns(
row_moys[evaluation.id] = scu.fmt_note( row_moys[evaluation.id] = scu.fmt_note(
sum_notes / nb_notes, keep_numeric=keep_numeric sum_notes / nb_notes, keep_numeric=keep_numeric
) )
row_moys[ row_moys["_" + str(evaluation.id) + "_help"] = (
"_" + str(evaluation.id) + "_help" "moyenne sur %d notes (%s le %s)"
] = "moyenne sur %d notes (%s le %s)" % ( % (
nb_notes, nb_notes,
evaluation.description, evaluation.description,
evaluation.date_debut.strftime("%d/%m/%Y") (
if evaluation.date_debut evaluation.date_debut.strftime("%d/%m/%Y")
else "", if evaluation.date_debut
else ""
),
)
) )
else: else:
row_moys[evaluation.id] = "" row_moys[evaluation.id] = ""
@ -884,8 +887,9 @@ def _add_moymod_column(
row["_" + col_id + "_td_attrs"] = ' class="moyenne" ' row["_" + col_id + "_td_attrs"] = ' class="moyenne" '
if etudid in inscrits and not isinstance(val, str): if etudid in inscrits and not isinstance(val, str):
notes.append(val) notes.append(val)
nb_notes = nb_notes + 1 if not np.isnan(val):
sum_notes += val nb_notes = nb_notes + 1
sum_notes += val
row_coefs[col_id] = "(avec abs)" row_coefs[col_id] = "(avec abs)"
if is_apc: if is_apc:
row_poids[col_id] = "à titre indicatif" row_poids[col_id] = "à titre indicatif"

View File

@ -91,7 +91,9 @@ def do_moduleimpl_delete(oid, formsemestre_id=None):
) # > moduleimpl_delete ) # > 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" "list moduleimpls"
args = locals() args = locals()
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()

View File

@ -617,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 <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. mais permet de "dispenser" un étudiant de suivre certaines UEs de son parcours.
</p> </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 présente dans le semestre mais pas dans le parcours de l'étudiant, ou bien d'autres
cas particuliers. cas particuliers.
</p> </p>

View File

@ -519,16 +519,22 @@ def _ligne_evaluation(
partition_id=partition_id, partition_id=partition_id,
select_first_partition=True, select_first_partition=True,
) )
if evaluation.evaluation_type in ( if evaluation.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE:
scu.EVALUATION_RATTRAPAGE,
scu.EVALUATION_SESSION2,
):
tr_class = "mievr mievr_rattr" 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: else:
tr_class = "mievr" tr_class = "mievr"
if not evaluation.visibulletin: if not evaluation.visibulletin:
tr_class += " non_visible_inter" tr_class += " non_visible_inter"
tr_class_1 = "mievr" tr_class_1 = "mievr"
if evaluation.is_blocked():
tr_class += " evaluation_blocked"
tr_class_1 += " evaluation_blocked"
if not first_eval: if not first_eval:
H.append("""<tr><td colspan="8">&nbsp;</td></tr>""") H.append("""<tr><td colspan="8">&nbsp;</td></tr>""")
tr_class_1 += " mievr_spaced" tr_class_1 += " mievr_spaced"
@ -562,14 +568,18 @@ def _ligne_evaluation(
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id) scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
}" class="mievr_evalnodate">Évaluation sans date</a>""" }" class="mievr_evalnodate">Évaluation sans date</a>"""
) )
H.append(f"&nbsp;&nbsp;&nbsp; <em>{evaluation.description or ''}</em>") H.append(f"&nbsp;&nbsp;&nbsp; <em>{evaluation.description}</em>")
if evaluation.evaluation_type == scu.EVALUATION_RATTRAPAGE: if evaluation.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE:
H.append( H.append(
"""<span class="mievr_rattr" title="remplace si meilleure note">rattrapage</span>""" """<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( 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"]: if etat["last_modif"]:
@ -605,8 +615,15 @@ def _ligne_evaluation(
else: else:
H.append(arrow_none) H.append(arrow_none)
if etat["evalcomplete"]: if evaluation.is_blocked():
etat_txt = f"""(prise en compte{ 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 if evaluation.visibulletin
else ", cachée en intermédiaire"}) else ", cachée en intermédiaire"})
@ -615,7 +632,7 @@ def _ligne_evaluation(
", évaluation cachée sur les bulletins en version intermédiaire et sur la passerelle" ", évaluation cachée sur les bulletins en version intermédiaire et sur la passerelle"
}""" }"""
elif etat["evalattente"] and not evaluation.publish_incomplete: 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" etat_descr = "il y a des notes en attente"
elif evaluation.publish_incomplete: elif evaluation.publish_incomplete:
etat_txt = """(prise en compte <b>immédiate</b>)""" etat_txt = """(prise en compte <b>immédiate</b>)"""
@ -623,28 +640,29 @@ def _ligne_evaluation(
"il manque des notes, mais la prise en compte immédiate a été demandée" "il manque des notes, mais la prise en compte immédiate a été demandée"
) )
elif etat["nb_notes"] != 0: 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" etat_descr = "il manque des notes"
else: else:
etat_txt = "" etat_txt = ""
if can_edit_evals and etat_txt: if etat_txt:
etat_txt = f"""<a href="{ url_for("notes.evaluation_edit", if can_edit_evals:
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id) etat_txt = f"""<a href="{ url_for("notes.evaluation_edit",
}" title="{etat_descr}">{etat_txt}</a>""" scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
}" title="{etat_descr}">{etat_txt}</a>"""
H.append( H.append(
f"""</span></span></td> f"""</span></span></td>
</tr> </tr>
<tr class="{tr_class}"> <tr class="{tr_class} mievr_in">
<th class="moduleimpl_evaluations" colspan="2">&nbsp;</th> <th class="moduleimpl_evaluations" colspan="2">&nbsp;</th>
<th class="moduleimpl_evaluations">Durée</th> <th class="moduleimpl_evaluations">Durée</th>
<th class="moduleimpl_evaluations">Coef.</th> <th class="moduleimpl_evaluations">Coef.</th>
<th class="moduleimpl_evaluations">Notes</th> <th class="moduleimpl_evaluations">Notes</th>
<th class="moduleimpl_evaluations">Abs</th> <th class="moduleimpl_evaluations">Abs</th>
<th class="moduleimpl_evaluations">N</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>
<tr class="{tr_class}"> <tr class="{tr_class} mievr_in">
<td class="mievr">""" <td class="mievr">"""
) )
if can_edit_evals: if can_edit_evals:
@ -826,7 +844,7 @@ def _evaluation_poids_html(evaluation: Evaluation, max_poids: float = 0.0) -> st
+ "\n".join( + "\n".join(
[ [
f"""<div title="poids vers {ue.acronyme}: {poids:g}"> 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 ''} {'background-color: ' + ue.color + ';' if ue.color else ''}
"></div> "></div>
</div>""" </div>"""

View File

@ -36,7 +36,7 @@ import sqlalchemy as sa
from app import log from app import log
from app.auth.models import User 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.models import Adresse, EtudAnnotation, FormSemestre, Identite, ScoDocSiteConfig
from app.scodoc import ( from app.scodoc import (
codes_cursus, codes_cursus,
@ -445,6 +445,14 @@ def fiche_etud(etudid=None):
# Liens vers compétences BUT # Liens vers compétences BUT
if last_formsemestre and last_formsemestre.formation.is_apc(): if last_formsemestre and last_formsemestre.formation.is_apc():
but_cursus = cursus_but.EtudCursusBUT(etud, last_formsemestre.formation) 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[ info[
"but_cursus_mkup" "but_cursus_mkup"
] = f""" ] = f"""
@ -454,15 +462,20 @@ def fiche_etud(etudid=None):
cursus=but_cursus, cursus=but_cursus,
scu=scu, scu=scu,
)} )}
<div class="link_validation_rcues"> <div class="fiche_but_col2">
<a class="stdlink" href="{url_for("notes.validation_rcues", <div class="link_validation_rcues">
scodoc_dept=g.scodoc_dept, etudid=etudid, <a class="stdlink" href="{url_for("notes.validation_rcues",
formsemestre_id=last_formsemestre.id)}" scodoc_dept=g.scodoc_dept, etudid=etudid,
title="Visualiser les compétences BUT" 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> <img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="100px"/>
</a> <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>
</div> </div>
""" """

View File

@ -48,20 +48,17 @@ from wtforms import (
HiddenField, HiddenField,
SelectMultipleField, SelectMultipleField,
) )
from app.models import ModuleImpl from app.models import Evaluation, ModuleImpl
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import ScoValueError
from app.scodoc import html_sco_header, sco_preferences from app.scodoc import html_sco_header, sco_preferences
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_excel from app.scodoc import sco_excel
from app.scodoc.sco_excel import ScoExcelBook, COLORS from app.scodoc.sco_excel import ScoExcelBook, COLORS
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_etud from app.scodoc import sco_etud
import sco_version import sco_version
@ -138,11 +135,7 @@ class PlacementForm(FlaskForm):
def set_evaluation_infos(self, evaluation_id): def set_evaluation_infos(self, evaluation_id):
"""Initialise les données du formulaire avec les données de l'évaluation.""" """Initialise les données du formulaire avec les données de l'évaluation."""
eval_data = sco_evaluation_db.get_evaluations_dict( _ = Evaluation.get_evaluation(evaluation_id) # check exist ?
{"evaluation_id": evaluation_id}
)
if not eval_data:
raise ScoValueError("invalid evaluation_id")
self.groups_tree, self.has_groups, self.nb_groups = _get_group_info( self.groups_tree, self.has_groups, self.nb_groups = _get_group_info(
evaluation_id evaluation_id
) )
@ -239,14 +232,12 @@ class PlacementRunner:
self.groups_ids = [ self.groups_ids = [
gid if gid != TOUS else form.tous_id for gid in form["groups"].data gid if gid != TOUS else form.tous_id for gid in form["groups"].data
] ]
self.eval_data = sco_evaluation_db.get_evaluations_dict( self.evaluation = Evaluation.get_evaluation(self.evaluation_id)
{"evaluation_id": self.evaluation_id}
)[0]
self.groups = sco_groups.listgroups(self.groups_ids) self.groups = sco_groups.listgroups(self.groups_ids)
self.gr_title_filename = sco_groups.listgroups_filename(self.groups) self.gr_title_filename = sco_groups.listgroups_filename(self.groups)
# gr_title = sco_groups.listgroups_abbrev(d['groups']) # gr_title = sco_groups.listgroups_abbrev(d['groups'])
self.current_user = current_user 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) self.moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(self.moduleimpl_id)
# TODO: à revoir pour utiliser modèle ModuleImpl # TODO: à revoir pour utiliser modèle ModuleImpl
self.moduleimpl_data = sco_moduleimpl.moduleimpl_list( self.moduleimpl_data = sco_moduleimpl.moduleimpl_list(
@ -260,20 +251,25 @@ class PlacementRunner:
) )
self.evalname = "%s-%s" % ( self.evalname = "%s-%s" % (
self.module_data["code"] or "?", 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"]: if self.evaluation.description:
self.evaltitre = self.eval_data["description"] self.evaltitre = self.evaluation.description
else: 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 self.desceval = [ # une liste de chaines: description de l'evaluation
"%s" % self.sem["titreannee"], self.sem["titreannee"],
"Module : %s - %s" "Module : %s - %s"
% (self.module_data["code"] or "?", self.module_data["abbrev"] or ""), % (self.module_data["code"] or "?", self.module_data["abbrev"] or ""),
"Surveillants : %s" % self.surveillants, "Surveillants : %s" % self.surveillants,
"Batiment : %(batiment)s - Salle : %(salle)s" % self.__dict__, "Batiment : %(batiment)s - Salle : %(salle)s" % self.__dict__,
"Controle : %s (coef. %g)" "Controle : %s (coef. %g)" % (self.evaltitre, self.evaluation.coefficient),
% (self.evaltitre, self.eval_data["coefficient"]),
] ]
self.styles = None self.styles = None
self.plan = None self.plan = None
@ -339,10 +335,10 @@ class PlacementRunner:
def _production_pdf(self): def _production_pdf(self):
pdf_title = "<br>".join(self.desceval) pdf_title = "<br>".join(self.desceval)
pdf_title += ( pdf_title += f"""\nDate : {self.evaluation.date_debut.strftime("%d/%m/%Y")
"\nDate : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s" if self.evaluation.date_debut else '-'
% self.eval_data } - Horaire : {self.evaluation.heure_debut()} à {self.evaluation.heure_fin()
) }"""
filename = "placement_%(evalname)s_%(gr_title_filename)s" % self.__dict__ filename = "placement_%(evalname)s_%(gr_title_filename)s" % self.__dict__
titles = { titles = {
"nom": "Nom", "nom": "Nom",
@ -489,8 +485,10 @@ class PlacementRunner:
worksheet.append_blank_row() worksheet.append_blank_row()
worksheet.append_single_cell_row(desceval, self.styles["titres"]) worksheet.append_single_cell_row(desceval, self.styles["titres"])
worksheet.append_single_cell_row( worksheet.append_single_cell_row(
"Date : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s" f"""Date : {self.evaluation.date_debut.strftime("%d/%m/%Y")
% self.eval_data, if self.evaluation.date_debut else '-'
} - Horaire : {self.evaluation.heure_debut()} à {self.evaluation.heure_fin()
}""",
self.styles["titres"], self.styles["titres"],
) )

View File

@ -72,9 +72,7 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict:
moy_ues.append( moy_ues.append(
( (
ue["acronyme"], ue["acronyme"],
scu.fmt_note( scu.fmt_note(ue_status["moy"]),
nt.get_etud_ue_status(etudid, ue["ue_id"])["moy"]
),
) )
) )
else: else:

View File

@ -134,12 +134,12 @@ def _displayNote(val):
return val return val
def _check_notes(notes: list[(int, float)], evaluation: Evaluation): def _check_notes(notes: list[(int, float | str)], evaluation: Evaluation):
# XXX typehint : float or str
"""notes is a list of tuples (etudid, value) """notes is a list of tuples (etudid, value)
mod is the module (used to ckeck type, for malus) mod is the module (used to ckeck type, for malus)
returns list of valid notes (etudid, float value) 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 note_max = evaluation.note_max or 0.0
module: Module = evaluation.moduleimpl.module module: Module = evaluation.moduleimpl.module
@ -148,7 +148,10 @@ def _check_notes(notes: list[(int, float)], evaluation: Evaluation):
scu.ModuleType.RESSOURCE, scu.ModuleType.RESSOURCE,
scu.ModuleType.SAE, 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: elif module.module_type == ModuleType.MALUS:
note_min = -20.0 note_min = -20.0
else: else:
@ -881,7 +884,7 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
if evaluation.date_debut: if evaluation.date_debut:
indication_date = evaluation.date_debut.date().isoformat() indication_date = evaluation.date_debut.date().isoformat()
else: 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}" eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
date_str = ( date_str = (

View File

@ -35,7 +35,7 @@ from flask import g, url_for
from flask_login import current_user from flask_login import current_user
from app import db, log 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.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -94,6 +94,7 @@ def formsemestre_synchro_etuds(
que l'on va importer/inscrire que l'on va importer/inscrire
""" """
etuds = etuds or [] etuds = etuds or []
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
inscrits_without_key = inscrits_without_key or [] inscrits_without_key = inscrits_without_key or []
log(f"formsemestre_synchro_etuds: formsemestre_id={formsemestre_id}") log(f"formsemestre_synchro_etuds: formsemestre_id={formsemestre_id}")
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
@ -109,12 +110,13 @@ def formsemestre_synchro_etuds(
raise ScoValueError("opération impossible: semestre verrouille") raise ScoValueError("opération impossible: semestre verrouille")
if not sem["etapes"]: if not sem["etapes"]:
raise ScoValueError( raise ScoValueError(
"""opération impossible: ce semestre n'a pas de code étape f"""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>") (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() footer = html_sco_header.sco_footer()
base_url = url_for( base_url = url_for(
"notes.formsemestre_synchro_etuds", "notes.formsemestre_synchro_etuds",
@ -165,7 +167,13 @@ def formsemestre_synchro_etuds(
suffix=scu.XLSX_SUFFIX, 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: if not submitted:
H += _build_page( H += _build_page(
sem, sem,
@ -184,7 +192,7 @@ def formsemestre_synchro_etuds(
inscrits_without_key inscrits_without_key
) )
log("a_desinscrire_without_key=%s" % a_desinscrire_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) a_inscrire = a_inscrire.intersection(etuds_set)
if not dialog_confirmed: if not dialog_confirmed:
@ -205,10 +213,12 @@ def formsemestre_synchro_etuds(
a_inscrire_en_double = inscrits_ailleurs.intersection(a_inscrire) a_inscrire_en_double = inscrits_ailleurs.intersection(a_inscrire)
if a_inscrire_en_double: 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: for key in a_inscrire_en_double:
nom = f"""{etudsapo_ident[key]['nom']} {etudsapo_ident[key].get("prenom", "")}""" 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>") H.append("</ol>")
if a_desinscrire: if a_desinscrire:
@ -260,16 +270,26 @@ def formsemestre_synchro_etuds(
etudids_a_desinscrire = [nip2etudid(x) for x in a_desinscrire] etudids_a_desinscrire = [nip2etudid(x) for x in a_desinscrire]
etudids_a_desinscrire += a_desinscrire_without_key etudids_a_desinscrire += a_desinscrire_without_key
# #
# 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(): with sco_cache.DeferredSemCacheManager():
do_import_etuds_from_portal(sem, a_importer, etudsapo_ident) do_import_etuds_from_portal(formsemestre, a_importer, etudsapo_ident)
sco_inscr_passage.do_inscrit(sem, etudids_a_inscrire) sco_inscr_passage.do_inscrit(formsemestre, etudids_a_inscrire)
sco_inscr_passage.do_desinscrit(sem, etudids_a_desinscrire) sco_inscr_passage.do_desinscrit(
formsemestre, etudids_a_desinscrire, check_has_dec_jury=False
)
H.append( H.append(
"""<h3>Opération effectuée</h3> f"""<h3>Opération effectuée</h3>
<ul> <ul>
<li><a class="stdlink" href="formsemestre_synchro_etuds?formsemestre_id=%s">Continuer la synchronisation</a></li>""" <li><a class="stdlink" href="{
% formsemestre_id 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( partitions = sco_groups.get_partitions_list(
@ -279,8 +299,9 @@ def formsemestre_synchro_etuds(
H.append( H.append(
f"""<li><a class="stdlink" href="{ f"""<li><a class="stdlink" href="{
url_for("scolar.partition_editor", url_for("scolar.partition_editor",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
}">Répartir les groupes de {partitions[0]["partition_name"]}</a></li> )}">Répartir les groupes de {partitions[0]["partition_name"]}</a>
</li>
""" """
) )
@ -618,7 +639,7 @@ def get_annee_naissance(ddmmyyyyy: str) -> int:
return None 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.""" """Inscrit les etudiants Apogee dans ce semestre."""
log(f"do_import_etuds_from_portal: a_importer={a_importer}") log(f"do_import_etuds_from_portal: a_importer={a_importer}")
if not a_importer: if not a_importer:
@ -672,7 +693,7 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
# Inscription au semestre # Inscription au semestre
sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
sem["formsemestre_id"], formsemestre.id,
etud.id, etud.id,
etat=scu.INSCRIT, etat=scu.INSCRIT,
etape=args["etape"], etape=args["etape"],
@ -716,7 +737,7 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_INSCR, typ=ScolarNews.NEWS_INSCR,
text=f"Import Apogée de {len(created_etudids)} étudiants en ", text=f"Import Apogée de {len(created_etudids)} étudiants en ",
obj=sem["formsemestre_id"], obj=formsemestre.id,
) )

View File

@ -175,7 +175,7 @@ def external_ue_inscrit_et_note(
note_max=20.0, note_max=20.0,
coefficient=1.0, coefficient=1.0,
publish_incomplete=True, publish_incomplete=True,
evaluation_type=scu.EVALUATION_NORMALE, evaluation_type=Evaluation.EVALUATION_NORMALE,
visibulletin=False, visibulletin=False,
description="note externe", description="note externe",
) )

View File

@ -48,16 +48,15 @@ Opérations:
import datetime import datetime
from flask import request from flask import request
from app.models import FormSemestre from app.models import Evaluation, FormSemestre
from app.scodoc.intervals import intervalmap from app.scodoc.intervals import intervalmap
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_users from app.scodoc import sco_users
import sco_version
from app.scodoc.gen_tables import GenTable 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 # 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: # 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): def evaluation_list_operations(evaluation_id):
"""Page listing operations on evaluation""" """Page listing operations on evaluation"""
E = sco_evaluation_db.get_evaluations_dict({"evaluation_id": evaluation_id})[0] evaluation = Evaluation.get_evaluation(evaluation_id)
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] operations = list_operations(evaluation_id)
Ops = list_operations(evaluation_id)
columns_ids = ("datestr", "user_name", "nb_notes", "comment") columns_ids = ("datestr", "user_name", "nb_notes", "comment")
titles = { titles = {
@ -164,11 +161,14 @@ def evaluation_list_operations(evaluation_id):
tab = GenTable( tab = GenTable(
titles=titles, titles=titles,
columns_ids=columns_ids, columns_ids=columns_ids,
rows=Ops, rows=operations,
html_sortable=False, html_sortable=False,
html_title="<h2>Opérations sur l'évaluation %s du %s</h2>" html_title=f"""<h2>Opérations sur l'évaluation {evaluation.description} {
% (E["description"], E["jour"]), evaluation.date_debut.strftime("du %d/%m/%Y") if evaluation.date_debut else "(sans date)"
preferences=sco_preferences.SemPreferences(M["formsemestre_id"]), }</h2>""",
preferences=sco_preferences.SemPreferences(
evaluation.moduleimpl.formsemestre_id
),
) )
return tab.make_page() return tab.make_page()

View File

@ -454,10 +454,6 @@ NOTES_MENTIONS_LABS = (
"Excellent", "Excellent",
) )
EVALUATION_NORMALE = 0
EVALUATION_RATTRAPAGE = 1
EVALUATION_SESSION2 = 2
# Dates et années scolaires # Dates et années scolaires
# Ces dates "pivot" sont paramétrables dans les préférences générales # Ces dates "pivot" sont paramétrables dans les préférences générales
# on donne ici les valeurs par défaut. # on donne ici les valeurs par défaut.

View File

@ -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 from app.scodoc.sco_exceptions import ScoValueError
class ApoEtapeVDI(object): class ApoEtapeVDI(object):
"""Classe stockant le VDI avec le code étape (noms de fichiers maquettes et code semestres)"""
_ETAPE_VDI_SEP = "!" _ETAPE_VDI_SEP = "!"
def __init__(self, etape_vdi: str = None, etape: str = "", vdi: str = ""): def __init__(self, etape_vdi: str = None, etape: str = "", vdi: str = ""):
@ -110,7 +112,8 @@ class ApoEtapeVDI(object):
elif len(t) == 2: elif len(t) == 2:
etape, vdi = t etape, vdi = t
else: else:
raise ValueError("invalid code etape") # code étape invalide
etape, vdi = "", ""
return etape, vdi return etape, vdi
else: else:
return etape_vdi, "" return etape_vdi, ""

View File

@ -35,6 +35,11 @@
min-width: var(--sco-content-min-width); min-width: var(--sco-content-min-width);
max-width: var(--sco-content-max-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 { div.jury_but_box_title {
margin-bottom: 10px; margin-bottom: 10px;
} }

View File

@ -273,6 +273,10 @@ section>div:nth-child(1) {
min-width: 80px; min-width: 80px;
display: inline-block; display: inline-block;
} }
div.eval-bonus {
color: #197614;
background-color: pink;
}
.ueBonus, .ueBonus,
.ueBonus h3 { .ueBonus h3 {

View File

@ -962,10 +962,18 @@ td.fichetitre2 .fl {
div.section_but { div.section_but {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: flex-end;
justify-content: space-evenly; 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 { div.section_but > div.link_validation_rcues {
align-self: center; align-self: center;
text-align: center; text-align: center;
@ -1461,6 +1469,9 @@ span.eval_title {
font-size: 14pt; font-size: 14pt;
} }
#evaluation-edit-blocked td, #evaluation-edit-coef td {
padding-top: 24px;
}
/* #saisie_notes span.eval_title { /* #saisie_notes span.eval_title {
border-bottom: 1px solid rgb(100,100,100); border-bottom: 1px solid rgb(100,100,100);
} }
@ -1793,11 +1804,42 @@ table.formsemestre_status {
tr.formsemestre_status { tr.formsemestre_status {
background-color: rgb(90%, 90%, 90%); 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 { tr.formsemestre_status_green {
background-color: #eff7f2; 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 { tr.formsemestre_status_ue {
background-color: rgb(90%, 90%, 90%); background-color: rgb(90%, 90%, 90%);
} }
@ -2075,15 +2117,23 @@ th.moduleimpl_evaluations a:hover {
text-decoration: underline; 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 { tr.mievr {
background-color: #eeeeee; background-color: #eeeeee;
} }
tr.mievr_rattr { tr.mievr_rattr, tr.mievr_session2, tr.mievr_bonus {
background-color: #dddddd; background-color: #dddddd;
} }
span.mievr_rattr { span.mievr_rattr, span.mievr_session2, span.mievr_bonus {
display: inline-block; display: inline-block;
font-weight: bold; font-weight: bold;
font-size: 80%; 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 { tr.mievr th {
background-color: white; background-color: white;
} }
@ -2139,6 +2199,7 @@ tr.mievr td.mievr {
tr.mievr td.mievr_menu { tr.mievr td.mievr_menu {
width: 110px; width: 110px;
padding-bottom: 4px;
} }
tr.mievr td.mievr_dur { tr.mievr td.mievr_dur {
@ -2411,6 +2472,29 @@ div.formation_list_ues_titre {
color: #eee; 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_modules,
div.formation_list_ues { div.formation_list_ues {
border-radius: 18px; border-radius: 18px;
@ -2426,6 +2510,7 @@ div.formation_list_ues {
} }
div.formation_list_ues_content { div.formation_list_ues_content {
margin-top: 4px;
} }
div.formation_list_modules { div.formation_list_modules {
@ -2508,7 +2593,13 @@ div.formation_parcs > div {
opacity: 0.7; opacity: 0.7;
border-radius: 4px; border-radius: 4px;
text-align: center; 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 { div.formation_parcs > div.focus {
@ -3316,14 +3407,24 @@ li.tf-msg {
padding-bottom: 5px; padding-bottom: 5px;
} }
.warning { .warning, .warning-bloquant {
font-weight: bold;
color: red; color: red;
margin-left: 16px;
margin-bottom: 8px;
min-width: var(--sco-content-min-width);
max-width: var(--sco-content-max-width);
} }
.warning::before { .warning::before {
content: url(/ScoDoc/static/icons/warning_img.png); content:"";
vertical-align: -80%; 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 { .warning-light {
@ -3336,6 +3437,19 @@ li.tf-msg {
/* EMO_WARNING, "&#9888;&#65039;" */ /* EMO_WARNING, "&#9888;&#65039;" */
} }
.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 { p.error {
font-weight: bold; font-weight: bold;
color: red; color: red;
@ -3714,10 +3828,17 @@ span.sp_etape {
color: black; color: black;
} }
.inscrailleurs { .deja-inscrit {
font-weight: bold;
color: rgb(1, 76, 1) !important;
}
.inscrit-ailleurs {
font-weight: bold; font-weight: bold;
color: red !important; color: red !important;
} }
div.etuds_select_boxes {
margin-bottom: 16px;
}
span.paspaye, span.paspaye,
span.paspaye a { span.paspaye a {
@ -4682,6 +4803,10 @@ table.table_recap th.col_malus {
font-weight: bold; font-weight: bold;
color: rgb(165, 0, 0); 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 { table.table_recap tr.ects td {
color: rgb(160, 86, 3); color: rgb(160, 86, 3);

View File

@ -491,14 +491,15 @@ class releveBUT extends HTMLElement {
let output = ""; let output = "";
evaluations.forEach((evaluation) => { evaluations.forEach((evaluation) => {
output += ` output += `
<div class=eval> <div class="eval ${evaluation.evaluation_type == 3 ? "eval-bonus" : ""}">
<div>${this.URL(evaluation.url, evaluation.description || "Évaluation")}</div> <div>${this.URL(evaluation.url, evaluation.description || "Évaluation")}</div>
<div> <div>
${evaluation.note.value} ${evaluation.note.value}
<em>Coef.&nbsp;${evaluation.coef ?? "*"}</em> <em>${evaluation.evaluation_type == 0 ? "Coef." : evaluation.evaluation_type == 3 ? "Bonus" : ""
}&nbsp;${evaluation.coef ?? ""}</em>
</div> </div>
<div class=complement> <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>Max. promo.</div><div>${evaluation.note.max}</div>
<div>Moy. promo.</div><div>${evaluation.note.moy}</div> <div>Moy. promo.</div><div>${evaluation.note.moy}</div>
<div>Min. promo.</div><div>${evaluation.note.min}</div> <div>Min. promo.</div><div>${evaluation.note.min}</div>

View File

@ -13,7 +13,7 @@ import numpy as np
from app import db from app import db
from app.auth.models import User from app.auth.models import User
from app.comp.res_common import ResultatsSemestre 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.codes_cursus import UE_SPORT, DEF
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_groups from app.scodoc import sco_groups
@ -405,15 +405,22 @@ class TableRecap(tb.Table):
val = notes_db[etudid]["value"] val = notes_db[etudid]["value"]
else: else:
# Note manquante mais prise en compte immédiate: affiche ATT # 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) content = self.fmt_note(val)
classes = col_classes + [ if e.evaluation_type != Evaluation.EVALUATION_BONUS:
{ classes = col_classes + [
"ABS": "abs", {
"ATT": "att", "ABS": "abs",
"EXC": "exc", "ATT": "att",
}.get(content, "") "EXC": "exc",
] }.get(content, "")
]
else:
classes = col_classes + ["col_eval_bonus"]
row.add_cell( row.add_cell(
col_id, title, content, group="eval", classes=classes col_id, title, content, group="eval", classes=classes
) )
@ -450,7 +457,7 @@ class TableRecap(tb.Table):
row_descr_eval.add_cell( row_descr_eval.add_cell(
col_id, col_id,
None, None,
e.description or "", e.description,
target=url_for( target=url_for(
"notes.evaluation_listenotes", "notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,

View File

@ -20,7 +20,7 @@ Assiduité lors de l'évaluation
<a class="stdlink" href="{{ <a class="stdlink" href="{{
url_for('notes.evaluation_listenotes', url_for('notes.evaluation_listenotes',
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id) scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
}}"><em>{{evaluation.description or ''}}</em></a> }}"><em>{{evaluation.description}}</em></a>
{% endif %} {% endif %}
<a style="margin-left:32px;" href="{{request.url}}&fmt=xlsx">{{scu.ICON_XLS|safe}}</a> <a style="margin-left:32px;" href="{{request.url}}&fmt=xlsx">{{scu.ICON_XLS|safe}}</a>
</div> </div>

View File

@ -14,6 +14,7 @@
{%- block styles %} {%- block styles %}
<!-- Bootstrap --> <!-- Bootstrap -->
<link href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap.css" rel="stylesheet"> <link href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap.css" rel="stylesheet">
<link type="text/css" rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.css">
{%- endblock styles %} {%- endblock styles %}
{%- endblock head %} {%- endblock head %}
</head> </head>
@ -26,7 +27,14 @@
{% block scripts %} {% block scripts %}
<script src="{{scu.STATIC_DIR}}/jQuery/jquery.js"></script> <script src="{{scu.STATIC_DIR}}/jQuery/jquery.js"></script>
<script src="{{scu.STATIC_DIR}}/jQuery/jquery-migrate-1.2.0.min.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/jquery.field.min.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/bootstrap/js/bootstrap.min.js"></script> <script src="{{scu.STATIC_DIR}}/libjs/bootstrap/js/bootstrap.min.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/purl.js"></script>
<script> <script>
const SCO_TIMEZONE = "{{ scu.TIME_ZONE }}"; const SCO_TIMEZONE = "{{ scu.TIME_ZONE }}";
</script> </script>

View File

@ -4,6 +4,7 @@
<div class="formation_list_ues_titre">Unités d'Enseignement <div class="formation_list_ues_titre">Unités d'Enseignement
semestre {{semestre_idx}} &nbsp;-&nbsp; {{ects_by_sem[semestre_idx] | safe}} ECTS semestre {{semestre_idx}} &nbsp;-&nbsp; {{ects_by_sem[semestre_idx] | safe}} ECTS
</div> </div>
{{ html_ue_warning[semestre_idx] | safe }}
<div class="formation_list_ues_content"> <div class="formation_list_ues_content">
<ul class="apc_ue_list"> <ul class="apc_ue_list">
{% for ue in ues_by_sem[semestre_idx] %} {% for ue in ues_by_sem[semestre_idx] %}
@ -62,6 +63,8 @@
<div class="formation_parcs"> <div class="formation_parcs">
{% for parc in ue.parcours %} {% for parc in ue.parcours %}
<div>{{ parc.code }}</div> <div>{{ parc.code }}</div>
{% else %}
<div class="ue_tc" title="aucun parcours">Tronc Commun</div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}

View File

@ -43,13 +43,6 @@
{{ super() }} {{ super() }}
<script src="{{scu.STATIC_DIR}}/libjs/menu.js"></script> <script src="{{scu.STATIC_DIR}}/libjs/menu.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/bubble.js"></script> <script src="{{scu.STATIC_DIR}}/libjs/bubble.js"></script>
<script src="{{scu.STATIC_DIR}}/jQuery/jquery.js"></script>
<script src="{{scu.STATIC_DIR}}/jQuery/jquery-migrate-1.2.0.min.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/jquery.field.min.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<script src="{{scu.STATIC_DIR}}/js/scodoc.js"></script> <script src="{{scu.STATIC_DIR}}/js/scodoc.js"></script>
<script src="{{scu.STATIC_DIR}}/DataTables/datatables.min.js"></script> <script src="{{scu.STATIC_DIR}}/DataTables/datatables.min.js"></script>
<script> <script>

View File

@ -8,13 +8,15 @@
</p> </p>
{%if is_apc%} {%if is_apc%}
<p class="help help_but"> <p class="help help_but">
Dans le BUT, une évaluation peut évaluer différents apprentissages critiques... (à compléter) Dans le BUT, une évaluation peut évaluer différents apprentissages critiques,
Le coefficient est multiplié par les poids vers chaque UE. et les poids permettent de moduler l'importance de l'évaluation pour
chaque compétence (UE).
Le coefficient de l'évaluation est multiplié par les poids vers chaque UE.
</p> </p>
{%endif%} {%endif%}
<p class="help"> <p class="help">
Ne pas confondre ce coefficient avec le coefficient du module, qui est Ne pas confondre ce coefficient avec le coefficient du module, qui est
lui fixé par le programme pédagogique (le PPN pour les DUT) et pondère lui fixé par le programme pédagogique (le PN pour les BUT) et pondère
les moyennes de chaque module pour obtenir les moyennes d'UE et la les moyennes de chaque module pour obtenir les moyennes d'UE et la
moyenne générale. moyenne générale.
</p> </p>
@ -22,17 +24,31 @@
L'option <em>Visible sur bulletins</em> indique que la note sera L'option <em>Visible sur bulletins</em> indique que la note sera
reportée sur les bulletins en version dite "intermédiaire" (dans cette reportée sur les bulletins en version dite "intermédiaire" (dans cette
version, on peut ne faire apparaitre que certaines notes, en sus des version, on peut ne faire apparaitre que certaines notes, en sus des
moyennes de modules. Attention, cette option n'empêche pas la moyennes de modules). Attention, cette option n'empêche pas la
publication sur les bulletins en version "longue" (la note est donc publication sur les bulletins en version "longue" (la note est donc
visible par les étudiants sur le portail). visible par les étudiants sur le portail).
</p> </p>
<p class="help">
Les évaluations bonus sont particulières:
</p>
<ul>
<li>la valeur est ajoutée à la moyenne du module;</li>
<li>le bonus peut être négatif (malus);
</li>
<li>le bonus ne s'applique pas aux notes de rattrapage et deuxième session;
</li>
<li>le coefficient est ignoré, mais en BUT le bonus vers une UE est multiplié
par le poids correspondant (par défaut égal à 1);
</li>
<li>les notes de bonus sont prises en compte même si incomplètes.</li>
</ul>
<p class="help"> <p class="help">
Les modalités "rattrapage" et "deuxième session" définissent des Les modalités "rattrapage" et "deuxième session" définissent des
évaluations prises en compte de façon spéciale: évaluations prises en compte de façon spéciale:
</p> </p>
<ul> <ul>
<li>les notes d'une évaluation de "rattrapage" remplaceront les moyennes <li>les notes d'une évaluation de "rattrapage" remplaceront les moyennes
du module <em>si elles sont meilleures que celles calculées</em>. du module <em>si elles sont meilleures que celles calculées;</em>.
</li> </li>
<li>les notes de "deuxième session" remplacent, lorsqu'elles sont <li>les notes de "deuxième session" remplacent, lorsqu'elles sont
saisies, la moyenne de l'étudiant à ce module, même si la note de saisies, la moyenne de l'étudiant à ce module, même si la note de

View File

@ -58,7 +58,7 @@
{% if sco.etud_cur_sem %} {% if sco.etud_cur_sem %}
<span title="absences du {{ sco.etud_cur_sem['date_debut'] }} <span title="absences du {{ sco.etud_cur_sem['date_debut'] }}
au {{ sco.etud_cur_sem['date_fin'] }}">({{sco.prefs["assi_metrique"]}}) au {{ sco.etud_cur_sem['date_fin'] }}">({{sco.prefs["assi_metrique"]}})
<br />{{sco.nbabsjust}} J., {{sco.nbabsnj}} N.J.</span> <br />{{'%1.0f'|format(sco.nbabsjust)}} J., {{'%1.0f'|format(sco.nbabsnj)}} N.J.</span>
{% endif %} {% endif %}
<ul> <ul>
{% if current_user.has_permission(sco.Permission.AbsChange) %} {% if current_user.has_permission(sco.Permission.AbsChange) %}

View File

@ -279,6 +279,7 @@ def ajout_assiduite_etud() -> str | Response:
def _get_dates_from_assi_form( def _get_dates_from_assi_form(
form: AjoutAssiOrJustForm, form: AjoutAssiOrJustForm,
all_day: bool = False,
) -> tuple[ ) -> tuple[
bool, datetime.datetime | None, datetime.datetime | None, datetime.datetime | None bool, datetime.datetime | None, datetime.datetime | None, datetime.datetime | None
]: ]:
@ -308,13 +309,23 @@ def _get_dates_from_assi_form(
if date_fin: if date_fin:
# ignore les heures si plusieurs jours # ignore les heures si plusieurs jours
heure_debut = datetime.time.fromisoformat(debut_jour) # 0h
heure_fin = datetime.time.fromisoformat(fin_jour) # minuit # Assiduité : garde les heures inscritent dans le formulaire
# Justificatif : ignore les heures inscrites dans le formulaire (0h -> 23h59)
heure_debut = (
datetime.time.fromisoformat(debut_jour)
if not all_day
else datetime.time(0, 0, 0)
) # 0h ou ConfigAssiduite.MorningTime
heure_fin = (
datetime.time.fromisoformat(fin_jour)
if not all_day
else datetime.time(23, 59, 59)
) # 23h59 ou ConfigAssiduite.AfternoonTime
else: else:
try: try:
heure_debut = datetime.time.fromisoformat( heure_debut = datetime.time.fromisoformat(form.heure_debut.data or "00:00")
form.heure_debut.data or debut_jour
)
except ValueError: except ValueError:
form.set_error("heure début invalide", form.heure_debut) form.set_error("heure début invalide", form.heure_debut)
if bool(form.heure_debut.data) != bool(form.heure_fin.data): if bool(form.heure_debut.data) != bool(form.heure_fin.data):
@ -322,7 +333,7 @@ def _get_dates_from_assi_form(
"Les deux heures début et fin doivent être spécifiées, ou aucune" "Les deux heures début et fin doivent être spécifiées, ou aucune"
) )
try: try:
heure_fin = datetime.time.fromisoformat(form.heure_fin.data or fin_jour) heure_fin = datetime.time.fromisoformat(form.heure_fin.data or "23:59")
except ValueError: except ValueError:
form.set_error("heure fin invalide", form.heure_fin) form.set_error("heure fin invalide", form.heure_fin)
@ -694,7 +705,7 @@ def _record_justificatif_etud(
dt_debut_tz_server, dt_debut_tz_server,
dt_fin_tz_server, dt_fin_tz_server,
dt_entry_date_tz_server, dt_entry_date_tz_server,
) = _get_dates_from_assi_form(form) ) = _get_dates_from_assi_form(form, all_day=True)
if not ok: if not ok:
log("_record_justificatif_etud: dates invalides") log("_record_justificatif_etud: dates invalides")

View File

@ -90,7 +90,6 @@ from app.decorators import (
# --------------- # ---------------
from app.pe import pe_view # ne pas enlever, ajoute des vues from app.pe import pe_view # ne pas enlever, ajoute des vues
from app.scodoc import sco_bulletins_json, sco_utils as scu from app.scodoc import sco_bulletins_json, sco_utils as scu
from app.scodoc import notesdb as ndb
from app import log, send_scodoc_alarm from app import log, send_scodoc_alarm
from app.scodoc.sco_exceptions import ( from app.scodoc.sco_exceptions import (
@ -98,59 +97,62 @@ from app.scodoc.sco_exceptions import (
ScoValueError, ScoValueError,
ScoInvalidIdType, ScoInvalidIdType,
) )
from app.scodoc import html_sco_header from app.scodoc import (
from app.scodoc import sco_apogee_compare html_sco_header,
from app.scodoc import sco_archives_formsemestre sco_apogee_compare,
from app.scodoc import sco_assiduites sco_archives_formsemestre,
from app.scodoc import sco_bulletins sco_assiduites,
from app.scodoc import sco_bulletins_pdf sco_bulletins,
from app.scodoc import sco_cache sco_bulletins_pdf,
from app.scodoc import sco_cost_formation sco_cache,
from app.scodoc import sco_debouche sco_cost_formation,
from app.scodoc import sco_edit_apc sco_debouche,
from app.scodoc import sco_edit_formation sco_edit_apc,
from app.scodoc import sco_edit_matiere sco_edit_formation,
from app.scodoc import sco_edit_module sco_edit_matiere,
from app.scodoc import sco_edit_ue sco_edit_module,
from app.scodoc import sco_etape_apogee_view sco_edit_ue,
from app.scodoc import sco_etud sco_etape_apogee_view,
from app.scodoc import sco_evaluations sco_etud,
from app.scodoc import sco_evaluation_check_abs sco_evaluations,
from app.scodoc import sco_evaluation_db sco_evaluation_check_abs,
from app.scodoc import sco_evaluation_edit sco_evaluation_db,
from app.scodoc import sco_evaluation_recap sco_evaluation_edit,
from app.scodoc import sco_export_results sco_evaluation_recap,
from app.scodoc import sco_formations sco_export_results,
from app.scodoc import sco_formation_recap sco_formations,
from app.scodoc import sco_formation_versions sco_formation_recap,
from app.scodoc import sco_formsemestre sco_formation_versions,
from app.scodoc import sco_formsemestre_custommenu sco_formsemestre,
from app.scodoc import sco_formsemestre_edit sco_formsemestre_custommenu,
from app.scodoc import sco_formsemestre_exterieurs sco_formsemestre_edit,
from app.scodoc import sco_formsemestre_inscriptions sco_formsemestre_exterieurs,
from app.scodoc import sco_formsemestre_status sco_formsemestre_inscriptions,
from app.scodoc import sco_formsemestre_validation sco_formsemestre_status,
from app.scodoc import sco_inscr_passage sco_formsemestre_validation,
from app.scodoc import sco_liste_notes sco_groups_view,
from app.scodoc import sco_lycee sco_inscr_passage,
from app.scodoc import sco_moduleimpl sco_liste_notes,
from app.scodoc import sco_moduleimpl_inscriptions sco_lycee,
from app.scodoc import sco_moduleimpl_status sco_moduleimpl,
from app.scodoc import sco_placement sco_moduleimpl_inscriptions,
from app.scodoc import sco_poursuite_dut sco_moduleimpl_status,
from app.scodoc import sco_preferences sco_placement,
from app.scodoc import sco_prepajury sco_poursuite_dut,
from app.scodoc import sco_pv_forms sco_preferences,
from app.scodoc import sco_recapcomplet sco_prepajury,
from app.scodoc import sco_report sco_pv_forms,
from app.scodoc import sco_report_but sco_recapcomplet,
from app.scodoc import sco_saisie_notes sco_report,
from app.scodoc import sco_semset sco_report_but,
from app.scodoc import sco_synchro_etuds sco_saisie_notes,
from app.scodoc import sco_tag_module sco_semset,
from app.scodoc import sco_ue_external sco_synchro_etuds,
from app.scodoc import sco_undo_notes sco_tag_module,
from app.scodoc import sco_users sco_ue_external,
sco_undo_notes,
sco_users,
)
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_pv_dict import descr_autorisations from app.scodoc.sco_pv_dict import descr_autorisations
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@ -1640,7 +1642,7 @@ def evaluation_delete(evaluation_id):
.first_or_404() .first_or_404()
) )
tit = f"""Suppression de l'évaluation {evaluation.description or ""} ({evaluation.descr_date()})""" tit = f"""Suppression de l'évaluation {evaluation.description} ({evaluation.descr_date()})"""
etat = sco_evaluations.do_evaluation_etat(evaluation.id) etat = sco_evaluations.do_evaluation_etat(evaluation.id)
H = [ H = [
f""" f"""
@ -1844,10 +1846,20 @@ sco_publish(
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @scodoc7func
def formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"): def formsemestre_bulletins_pdf(
formsemestre_id,
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
version="selectedevals",
):
"Publie les bulletins dans un classeur PDF" "Publie les bulletins dans un classeur PDF"
# Informations sur les groupes à utiliser:
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids,
formsemestre_id=formsemestre_id,
select_all_when_unspecified=True,
)
pdfdoc, filename = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( pdfdoc, filename = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
formsemestre_id, version=version formsemestre_id, groups_infos=groups_infos, version=version
) )
return scu.sendPDFFile(pdfdoc, filename) return scu.sendPDFFile(pdfdoc, filename)
@ -1864,18 +1876,29 @@ _EXPL_BULL = """Versions des bulletins:
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @scodoc7func
def formsemestre_bulletins_pdf_choice(formsemestre_id, version=None): def formsemestre_bulletins_pdf_choice(
formsemestre_id,
version=None,
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
):
"""Choix version puis envoi classeur bulletins pdf""" """Choix version puis envoi classeur bulletins pdf"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# Informations sur les groupes à utiliser:
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids,
formsemestre_id=formsemestre_id,
select_all_when_unspecified=True,
)
if version: if version:
pdfdoc, filename = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( pdfdoc, filename = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
formsemestre_id, version=version formsemestre_id, groups_infos=groups_infos, version=version
) )
return scu.sendPDFFile(pdfdoc, filename) return scu.sendPDFFile(pdfdoc, filename)
return _formsemestre_bulletins_choice( return _formsemestre_bulletins_choice(
formsemestre, formsemestre,
title="Choisir la version des bulletins à générer",
explanation=_EXPL_BULL, explanation=_EXPL_BULL,
groups_infos=groups_infos,
title="Choisir la version des bulletins à générer",
) )
@ -1900,8 +1923,15 @@ def formsemestre_bulletins_mailetuds_choice(
version=None, version=None,
dialog_confirmed=False, dialog_confirmed=False,
prefer_mail_perso=0, prefer_mail_perso=0,
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
): ):
"""Choix version puis envoi classeur bulletins pdf""" """Choix version puis envoi classeur bulletins pdf"""
# Informations sur les groupes à utiliser:
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids,
formsemestre_id=formsemestre_id,
select_all_when_unspecified=True,
)
if version: if version:
return flask.redirect( return flask.redirect(
url_for( url_for(
@ -1909,8 +1939,9 @@ def formsemestre_bulletins_mailetuds_choice(
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
version=version, version=version,
dialog_confirmed=dialog_confirmed, dialog_confirmed=int(dialog_confirmed),
prefer_mail_perso=prefer_mail_perso, prefer_mail_perso=prefer_mail_perso,
group_ids=groups_infos.group_ids,
) )
) )
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
@ -1934,45 +1965,41 @@ def formsemestre_bulletins_mailetuds_choice(
</p><p>""" </p><p>"""
+ expl_bull, + expl_bull,
choose_mail=True, choose_mail=True,
groups_infos=groups_infos,
) )
# not published # not published
def _formsemestre_bulletins_choice( def _formsemestre_bulletins_choice(
formsemestre: FormSemestre, title="", explanation="", choose_mail=False formsemestre: FormSemestre,
title="",
explanation="",
choose_mail=False,
groups_infos=None,
): ):
"""Choix d'une version de bulletin""" """Choix d'une version de bulletin
versions = ( (pour envois mail ou génération classeur pdf)
"""
versions_bulletins = (
scu.BULLETINS_VERSIONS_BUT scu.BULLETINS_VERSIONS_BUT
if formsemestre.formation.is_apc() if formsemestre.formation.is_apc()
else scu.BULLETINS_VERSIONS else scu.BULLETINS_VERSIONS
) )
H = [
html_sco_header.html_sem_header(title),
f"""
<form name="f" method="GET" action="{request.base_url}">
<input type="hidden" name="formsemestre_id" value="{formsemestre.id}"></input>
""",
]
H.append("""<select name="version" class="noprint">""")
for version, description in versions.items():
H.append(f"""<option value="{version}">{description}</option>""")
H.append("""</select>&nbsp;&nbsp;<input type="submit" value="Générer"/>""") return render_template(
if choose_mail: "formsemestre/bulletins_choice.j2",
H.append( explanation=explanation,
"""<div> choose_mail=choose_mail,
<input type="checkbox" name="prefer_mail_perso" value="1" formsemestre=formsemestre,
/>Utiliser si possible les adresses personnelles menu_groups_choice=sco_groups_view.menu_groups_choice(groups_infos),
</div>""" sco=ScoData(formsemestre=formsemestre),
) sco_groups_view=sco_groups_view,
title=title,
H.append(f"""<p class="help">{explanation}</p>""") versions_bulletins=versions_bulletins,
)
return "\n".join(H) + html_sco_header.sco_footer()
@bp.route("/formsemestre_bulletins_mailetuds") @bp.route("/formsemestre_bulletins_mailetuds", methods=["GET", "POST"])
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @scodoc7func
@ -1981,16 +2008,23 @@ def formsemestre_bulletins_mailetuds(
version="long", version="long",
dialog_confirmed=False, dialog_confirmed=False,
prefer_mail_perso=0, prefer_mail_perso=0,
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
): ):
"""Envoie à chaque etudiant son bulletin """Envoie à chaque etudiant son bulletin
(inscrit non démissionnaire ni défaillant et ayant un mail renseigné dans ScoDoc) (inscrit non démissionnaire ni défaillant et ayant un mail renseigné dans ScoDoc)
""" """
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids,
formsemestre_id=formsemestre_id,
select_all_when_unspecified=True,
)
etudids = {m["etudid"] for m in groups_infos.members}
prefer_mail_perso = int(prefer_mail_perso) prefer_mail_perso = int(prefer_mail_perso)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
inscriptions = [ inscriptions = [
inscription inscription
for inscription in formsemestre.inscriptions for inscription in formsemestre.inscriptions
if inscription.etat == scu.INSCRIT if inscription.etat == scu.INSCRIT and inscription.etudid in etudids
] ]
# #
if not sco_bulletins.can_send_bulletin_by_mail(formsemestre_id): if not sco_bulletins.can_send_bulletin_by_mail(formsemestre_id):
@ -1998,7 +2032,7 @@ def formsemestre_bulletins_mailetuds(
# Confirmation dialog # Confirmation dialog
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
f"<h2>Envoyer les {len(inscriptions)} bulletins par e-mail aux étudiants inscrits ?", f"<h2>Envoyer les {len(inscriptions)} bulletins par e-mail aux étudiants inscrits sélectionnés ?",
dest_url="", dest_url="",
cancel_url=url_for( cancel_url=url_for(
"notes.formsemestre_status", "notes.formsemestre_status",
@ -2374,10 +2408,12 @@ def formsemestre_validation_but(
) )
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if len(deca.get_decisions_rcues_annee()) == 0: has_notes_en_attente = deca.has_notes_en_attente()
return jury_but_view.jury_but_semestriel( evaluations_a_debloquer = Evaluation.get_evaluations_blocked_for_etud(
formsemestre, etud, read_only, navigation_div=navigation_div formsemestre, etud
) )
if has_notes_en_attente or evaluations_a_debloquer:
read_only = True
if request.method == "POST": if request.method == "POST":
if not read_only: if not read_only:
deca.record_form(request.form) deca.record_form(request.form)
@ -2422,9 +2458,21 @@ def formsemestre_validation_but(
etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?") etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?")
warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_pair.semestre_id}</div>""" warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_pair.semestre_id}</div>"""
if deca.has_notes_en_attente(): if has_notes_en_attente:
warning += f"""<div class="warning">{etud.nomprenom} a des notes en ATTente. warning += f"""<div class="warning-bloquant">{etud.nomprenom} a des notes en ATTente.
Vous devriez régler cela avant de statuer en jury !</div>""" Vous devez régler cela avant de statuer en jury !</div>"""
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
]
warning += f"""<div class="warning-bloquant">Impossible de statuer sur cet étudiant:
il a des notes dans des évaluations qui seront débloquées plus tard:
voir {", ".join(links_evals)}
"""
H.append( H.append(
f""" f"""
<div> <div>
@ -2440,7 +2488,9 @@ def formsemestre_validation_but(
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a> }">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
</div> </div>
</div> </div>
<div class="jury_but_warning jury_but_box">
{warning} {warning}
</div>
</div> </div>
<form method="post" class="jury_but_box" id="jury_but"> <form method="post" class="jury_but_box" id="jury_but">

View File

@ -1,19 +1,20 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.6.940" SCOVERSION = "9.6.946"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"
SCONEWS = """ SCONEWS = """
<h4>Année 2023</h4> <h4>Année 2023-2024</h4>
<ul> <ul>
<li>ScoDoc 9.6 (juillet 2023)</li> <li>ScoDoc 9.6 (2023-2024)</li>
<ul> <ul>
<li>Nouveaux bulletins BUT compacts</li> <li>Nouveaux bulletins BUT compacts</li>
<li>Nouvelle gestion des absences et assiduité</li> <li>Nouvelle gestion des absences et assiduité</li>
<li>Mise à jour logiciels: Debian 12, Python 3.11, ...</li> <li>Mise à jour logiciels: Debian 12, Python 3.11, ...</li>
<li>Evaluations bonus</li>
</ul> </ul>
<li>ScoDoc 9.5 (juillet 2023)</li> <li>ScoDoc 9.5 (juillet 2023)</li>

View File

@ -59,6 +59,7 @@ cli.register(app)
@app.context_processor @app.context_processor
def inject_sco_utils(): def inject_sco_utils():
"Make scu available in all Jinja templates" "Make scu available in all Jinja templates"
# if modified, put the same in conftest.py#27
return dict(scu=scu) return dict(scu=scu)

View File

@ -1,17 +1,17 @@
import pytest import pytest
from flask import g from flask import g
from flask_login import login_user, logout_user, current_user from flask_login import login_user
from config import TestConfig from config import TestConfig
import app import app
from app import db, create_app from app import db, create_app
from app import initialize_scodoc_database, clear_scodoc_cache from app import initialize_scodoc_database, clear_scodoc_cache
from app import models from app import models
from app.auth.models import User, Role, UserRole, Permission from app.auth.models import User, Role
from app.auth.models import get_super_admin from app.auth.models import get_super_admin
from app.scodoc import sco_bulletins_standard
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
import app.scodoc.sco_utils as scu
RESOURCES_DIR = "/opt/scodoc/tests/ressources" RESOURCES_DIR = "/opt/scodoc/tests/ressources"
@ -23,6 +23,12 @@ def test_client():
# Run tests: # Run tests:
with apptest.test_client() as client: with apptest.test_client() as client:
with apptest.app_context(): with apptest.app_context():
@apptest.context_processor
def inject_sco_utils():
"Make scu available in all Jinja templates"
return dict(scu=scu)
with apptest.test_request_context(): with apptest.test_request_context():
# initialize scodoc "g": # initialize scodoc "g":
g.stored_get_formsemestre = {} g.stored_get_formsemestre = {}

View File

@ -1148,13 +1148,8 @@ def _setup_fake_db(
moduleimpls.append(ModuleImpl.query.filter_by(id=moduleimpl_id).first()) moduleimpls.append(ModuleImpl.query.filter_by(id=moduleimpl_id).first())
# Création de 3 étudiants # Création de x étudiants
etud_0 = g_fake.create_etud(prenom="etud0") etuds_dict: list[dict] = [
etud_1 = g_fake.create_etud(prenom="etud1")
etud_2 = g_fake.create_etud(prenom="etud2")
etuds_dict = [etud_0, etud_1, etud_2]
etud_dicts: list[dict] = [
g_fake.create_etud(prenom=f"etud{i}") for i in range(nb_etuds) g_fake.create_etud(prenom=f"etud{i}") for i in range(nb_etuds)
] ]
@ -1406,13 +1401,106 @@ def test_calcul_assiduites(test_client):
"total": {"journee": 11, "demi": 20, "heure": 81.0, "compte": 26}, "total": {"journee": 11, "demi": 20, "heure": 81.0, "compte": 26},
} }
for key in resultat_attendu: for key, value in resultat_attendu.items():
assert ( assert value["journee"] * 2 >= value["demi"], f"Trop de demi-journées [{key}]"
resultat_attendu[key]["journee"] * 2 >= resultat_attendu[key]["demi"]
), f"Trop de demi-journées [{key}]"
for key in resultat_attendu: for key, value in resultat_attendu.items():
for key2 in resultat_attendu[key]: for key2, value2 in value.items():
assert ( assert (
result[key][key2] == resultat_attendu[key][key2] result[key][key2] == value2
), f"Le calcul [{key}][{key2}] est faux (attendu > {resultat_attendu[key][key2]}{result[key][key2]} < obtenu)" ), f"Le calcul [{key}][{key2}] est faux (attendu > {value2}{result[key][key2]} < obtenu)"
def test_cas_justificatifs(test_client):
"""
Tests de certains cas particuliers des justificatifs
- Création du justificatif avant ou après assiduité
- Assiduité complétement couverte ou non
"""
data = _setup_fake_db(
[("2024-01-01", "2024-06-30")],
0,
1,
)
# <- Vérification justification si justif créé avant assi ->
# Période : 8h -> 10h le 01/01/2024
etud_1: Identite = data["etuds"][0]
justif_1: Justificatif = Justificatif.create_justificatif(
etudiant=etud_1,
date_debut=scu.is_iso_formated("2024-01-01T08:00:00+01:00", True),
date_fin=scu.is_iso_formated("2024-01-01T10:00:00+01:00", True),
etat=scu.EtatJustificatif.VALIDE,
)
assi_1: Assiduite = Assiduite.create_assiduite(
etud=etud_1,
date_debut=scu.is_iso_formated("2024-01-01T08:00:00+01:00", True),
date_fin=scu.is_iso_formated("2024-01-01T10:00:00+01:00", True),
etat=scu.EtatAssiduite.ABSENT,
)
assert assi_1.est_just is True, "Justification non prise en compte (a1)"
assert len(scass.justifies(justif_1)) == 1, "Justification non prise en compte (a2)"
# <- Vérification justification si justif créé après assi ->
# Période : 8h -> 10h le 02/01/2024
Assiduite.create_assiduite(
etud=etud_1,
date_debut=scu.is_iso_formated("2024-01-02T08:00:00+01:00", True),
date_fin=scu.is_iso_formated("2024-01-02T10:00:00+01:00", True),
etat=scu.EtatAssiduite.ABSENT,
)
justif_2: Justificatif = Justificatif.create_justificatif(
etudiant=etud_1,
date_debut=scu.is_iso_formated("2024-01-02T08:00:00+01:00", True),
date_fin=scu.is_iso_formated("2024-01-02T10:00:00+01:00", True),
etat=scu.EtatJustificatif.VALIDE,
)
compute_assiduites_justified(etud_1.etudid, [justif_2])
assert len(scass.justifies(justif_2)) == 1, "Justification non prise en compte (b1)"
# Ne fonctionne pas ⬇️
# assert assi_2.est_just is True, "Justification non prise en compte (b2)"
# <- Vérification assiduité complétement couverte ->
# Période : 12h -> 19h le 03/01/2024
Assiduite.create_assiduite(
etud=etud_1,
date_debut=scu.is_iso_formated("2024-01-03T12:00:00+01:00", True),
date_fin=scu.is_iso_formated("2024-01-03T19:00:00+01:00", True),
etat=scu.EtatAssiduite.ABSENT,
)
# Justification complète
justif_3: Justificatif = Justificatif.create_justificatif(
etudiant=etud_1,
date_debut=scu.is_iso_formated("2024-01-03T00:00:00", True),
date_fin=scu.is_iso_formated("2024-01-03T23:59:59", True),
etat=scu.EtatJustificatif.VALIDE,
)
# Justification incomplète
justif_4: Justificatif = Justificatif.create_justificatif(
etudiant=etud_1,
date_debut=scu.is_iso_formated("2024-01-03T08:00:00", True),
date_fin=scu.is_iso_formated("2024-01-03T18:00:00", True),
etat=scu.EtatJustificatif.VALIDE,
)
# Mise à jour de l'assiduité
compute_assiduites_justified(etud_1.etudid, [justif_3, justif_4])
assert (
len(scass.justifies(justif_3)) == 1
), "Justification complète non prise en compte (c1)"
assert (
len(scass.justifies(justif_4)) == 0
), "Justification complète non prise en compte (c2)"

View File

@ -1,5 +1,6 @@
"""Test calculs rattrapages """Test calculs rattrapages
""" """
import datetime import datetime
import app import app
@ -68,7 +69,7 @@ def test_notes_rattrapage(test_client):
date_debut=datetime.datetime(2020, 1, 2), date_debut=datetime.datetime(2020, 1, 2),
description="evaluation rattrapage", description="evaluation rattrapage",
coefficient=1.0, coefficient=1.0,
evaluation_type=scu.EVALUATION_RATTRAPAGE, evaluation_type=Evaluation.EVALUATION_RATTRAPAGE,
) )
etud = etuds[0] etud = etuds[0]
_, _, _ = G.create_note(evaluation_id=e["id"], etudid=etud["etudid"], note=12.0) _, _, _ = G.create_note(evaluation_id=e["id"], etudid=etud["etudid"], note=12.0)
@ -144,7 +145,7 @@ def test_notes_rattrapage(test_client):
date_debut=datetime.datetime(2020, 1, 2), date_debut=datetime.datetime(2020, 1, 2),
description="evaluation session 2", description="evaluation session 2",
coefficient=1.0, coefficient=1.0,
evaluation_type=scu.EVALUATION_SESSION2, evaluation_type=Evaluation.EVALUATION_SESSION2,
) )
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
@ -182,3 +183,43 @@ def test_notes_rattrapage(test_client):
) )
# Note moyenne: revient à note normale # Note moyenne: revient à note normale
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(10.0) assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(10.0)
# Supprime évaluation session 2
_, _, _ = G.create_note(
evaluation_id=e_session2["id"], etudid=etud["etudid"], note=scu.NOTES_SUPPRESS
)
evaluation = db.session.get(Evaluation, e_session2["id"])
assert evaluation
evaluation.delete()
#
# --- Evaluation bonus ---
#
# --- Création d'une évaluation "bonus"
e_bonus = G.create_evaluation(
moduleimpl_id=moduleimpl_id,
date_debut=datetime.datetime(2020, 1, 2),
description="evaluation bonus",
coefficient=1.0,
evaluation_type=Evaluation.EVALUATION_BONUS,
)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
# Note moyenne sans bonus
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(10.0)
# Saisie note bonus
_, _, _ = G.create_note(
evaluation_id=e_bonus["id"], etudid=etud["etudid"], note=1.0
)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
# Note moyenne sans bonus
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(11.0)
# Négatif, avec clip à zéro
_, _, _ = G.create_note(
evaluation_id=e_bonus["id"], etudid=etud["etudid"], note=-20.0
)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(0.0)

View File

@ -13,7 +13,7 @@ Au besoin, créer un base de test neuve:
""" """
import datetime import datetime
from app.models import FormSemestreInscription, Identite from app.models import Evaluation, FormSemestreInscription, Identite, ModuleImpl
from config import TestConfig from config import TestConfig
from tests.unit import sco_fake_gen from tests.unit import sco_fake_gen
@ -29,7 +29,6 @@ from app.scodoc import sco_bulletins
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_assiduites as scass from app.scodoc import sco_assiduites as scass
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre_validation from app.scodoc import sco_formsemestre_validation
from app.scodoc import sco_cursus_dut from app.scodoc import sco_cursus_dut
from app.scodoc import sco_saisie_notes from app.scodoc import sco_saisie_notes
@ -81,7 +80,7 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
module_id=module_id, module_id=module_id,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
) )
moduleimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
# --- Inscription des étudiants # --- Inscription des étudiants
for etud in etuds: for etud in etuds:
G.inscrit_etudiant(formsemestre_id, etud) G.inscrit_etudiant(formsemestre_id, etud)
@ -97,17 +96,18 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
assert ins.parcour is None assert ins.parcour is None
# --- Création évaluation # --- Création évaluation
e = G.create_evaluation( e1 = Evaluation.create(
moduleimpl_id=moduleimpl_id, moduleimpl=moduleimpl,
date_debut=datetime.datetime(2020, 1, 1), date_debut=datetime.datetime(2020, 1, 1),
description="evaluation test", description="evaluation test",
coefficient=1.0, coefficient=1.0,
) )
db.session.commit()
# --- Saisie toutes les notes de l'évaluation # --- Saisie toutes les notes de l'évaluation
for idx, etud in enumerate(etuds): for idx, etud in enumerate(etuds):
etudids_changed, nb_suppress, existing_decisions = G.create_note( etudids_changed, nb_suppress, existing_decisions = G.create_note(
evaluation_id=e["evaluation_id"], evaluation_id=e1.id,
etudid=etud["etudid"], etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)], note=NOTES_T[idx % len(NOTES_T)],
) )
@ -118,7 +118,7 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
# --- Vérifie que les notes sont prises en compte: # --- Vérifie que les notes sont prises en compte:
b = sco_bulletins.formsemestre_bulletinetud_dict(formsemestre_id, etud["etudid"]) b = sco_bulletins.formsemestre_bulletinetud_dict(formsemestre_id, etud["etudid"])
# Toute les notes sont saisies, donc eval complète # Toute les notes sont saisies, donc eval complète
etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"]) etat = sco_evaluations.do_evaluation_etat(e1.id)
assert etat["evalcomplete"] assert etat["evalcomplete"]
assert etat["nb_inscrits"] == len(etuds) assert etat["nb_inscrits"] == len(etuds)
assert etat["nb_notes"] == len(etuds) assert etat["nb_notes"] == len(etuds)
@ -131,30 +131,32 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
) )
# --- Une autre évaluation # --- Une autre évaluation
e2 = G.create_evaluation( e2 = Evaluation.create(
moduleimpl_id=moduleimpl_id, moduleimpl=moduleimpl,
date_debut=datetime.datetime(2020, 1, 2), date_debut=datetime.datetime(2020, 1, 2),
description="evaluation test 2", description="evaluation test 2",
coefficient=1.0, coefficient=1.0,
) )
db.session.commit()
# Saisie les notes des 5 premiers étudiants: # Saisie les notes des 5 premiers étudiants:
for idx, etud in enumerate(etuds[:5]): for idx, etud in enumerate(etuds[:5]):
etudids_changed, nb_suppress, existing_decisions = G.create_note( etudids_changed, nb_suppress, existing_decisions = G.create_note(
evaluation_id=e2["evaluation_id"], evaluation_id=e2.id,
etudid=etud["etudid"], etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)], note=NOTES_T[idx % len(NOTES_T)],
) )
# Cette éval n'est pas complète # Cette éval n'est pas complète
etat = sco_evaluations.do_evaluation_etat(e2["evaluation_id"]) etat = sco_evaluations.do_evaluation_etat(e2.id)
assert etat["evalcomplete"] is False assert etat["evalcomplete"] is False
# la première éval est toujours complète: # la première éval est toujours complète:
etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"]) etat = sco_evaluations.do_evaluation_etat(e1.id)
assert etat["evalcomplete"] assert etat["evalcomplete"]
# Modifie l'évaluation 2 pour "prise en compte immédiate" # Modifie l'évaluation 2 pour "prise en compte immédiate"
e2["publish_incomplete"] = True e2.publish_incomplete = True
sco_evaluation_db.do_evaluation_edit(e2) db.session.add(e2)
etat = sco_evaluations.do_evaluation_etat(e2["evaluation_id"]) db.session.flush()
etat = sco_evaluations.do_evaluation_etat(e2.id)
assert etat["evalcomplete"] is False assert etat["evalcomplete"] is False
assert etat["nb_att"] == 0 # il n'y a pas de notes (explicitement) en attente assert etat["nb_att"] == 0 # il n'y a pas de notes (explicitement) en attente
assert etat["evalattente"] # mais l'eval est en attente (prise en compte immédiate) assert etat["evalattente"] # mais l'eval est en attente (prise en compte immédiate)
@ -162,26 +164,26 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
# Saisie des notes qui manquent: # Saisie des notes qui manquent:
for idx, etud in enumerate(etuds[5:]): for idx, etud in enumerate(etuds[5:]):
etudids_changed, nb_suppress, existing_decisions = G.create_note( etudids_changed, nb_suppress, existing_decisions = G.create_note(
evaluation_id=e2["evaluation_id"], evaluation_id=e2.id,
etudid=etud["etudid"], etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)], note=NOTES_T[idx % len(NOTES_T)],
) )
etat = sco_evaluations.do_evaluation_etat(e2["evaluation_id"]) etat = sco_evaluations.do_evaluation_etat(e2.id)
assert etat["evalcomplete"] assert etat["evalcomplete"]
assert etat["nb_att"] == 0 assert etat["nb_att"] == 0
assert not etat["evalattente"] # toutes les notes sont présentes assert not etat["evalattente"] # toutes les notes sont présentes
# --- Suppression des notes # --- Suppression des notes
sco_saisie_notes.evaluation_suppress_alln(e["evaluation_id"], dialog_confirmed=True) sco_saisie_notes.evaluation_suppress_alln(e1.id, dialog_confirmed=True)
etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"]) etat = sco_evaluations.do_evaluation_etat(e1.id)
assert etat["nb_notes"] == 0 assert etat["nb_notes"] == 0
assert not etat["evalcomplete"] assert not etat["evalcomplete"]
# --- Saisie des notes manquantes # --- Saisie des notes manquantes
ans = sco_saisie_notes.do_evaluation_set_missing( ans = sco_saisie_notes.do_evaluation_set_missing(
e["evaluation_id"], 12.34, dialog_confirmed=True e1.id, 12.34, dialog_confirmed=True
) )
assert f'{etat["nb_inscrits"]} notes changées' in ans assert f'{etat["nb_inscrits"]} notes changées' in ans
etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"]) etat = sco_evaluations.do_evaluation_etat(e1.id)
assert etat["evalcomplete"] assert etat["evalcomplete"]
# ----------------------- # -----------------------

View File

@ -78,6 +78,13 @@ def import_formation(dept_id: int) -> Formation:
) )
formation.referentiel_competence_id = ref_comp.id formation.referentiel_competence_id = ref_comp.id
db.session.add(formation) db.session.add(formation)
# --- Association niveaux de compétences aux UE de S1:
niveaux = ref_comp.get_niveaux_by_parcours(1)[1]["TC"]
ues = formation.ues.filter_by(semestre_idx=1).all()
assert len(niveaux) == len(ues) # le ref comp et les formation doivent correspondre
for ue, niveau in zip(ues, niveaux):
ue.niveau_competence = niveau
db.session.add(ue)
db.session.commit() db.session.commit()
return formation return formation