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

View File

@ -24,7 +24,7 @@ from reportlab.lib.colors import blue
from reportlab.lib.units import cm, mm
from reportlab.platypus import Paragraph, Spacer
from app.models import ScoDocSiteConfig
from app.models import Evaluation, ScoDocSiteConfig
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
from app.scodoc import gen_tables
from app.scodoc.codes_cursus import UE_SPORT
@ -422,7 +422,11 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
def evaluations_rows(self, rows, evaluations: list[dict], ue_acros=()):
"lignes des évaluations"
for e in evaluations:
coef = e["coef"] if e["evaluation_type"] == scu.EVALUATION_NORMALE else "*"
coef = (
e["coef"]
if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE
else "*"
)
t = {
"titre": f"{e['description'] or ''}",
"moyenne": e["note"]["value"],
@ -431,7 +435,10 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
),
"coef": coef,
"_coef_pdf": Paragraph(
f"<para align=right fontSize={self.small_fontsize}><i>{coef}</i></para>"
f"""<para align=right fontSize={self.small_fontsize}><i>{
coef if e["evaluation_type"] != Evaluation.EVALUATION_BONUS
else "bonus"
}</i></para>"""
),
"_pdf_style": [
(

View File

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

View File

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

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:
"""Section html pour fiche etudiant

View File

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

View File

@ -205,11 +205,12 @@ class ResultatsSemestre(ResultatsCache):
"coefficient" : float, # 0 si None
"description" : str, # de l'évaluation, "" si None
"etat" {
"blocked" : bool, # vrai si prise en compte bloquée
"evalcomplete" : bool,
"last_modif" : datetime.datetime | None, # saisie de note la plus récente
"nb_notes" : int, # nb notes d'étudiants inscrits
},
"evaluatiuon_id" : int,
"evaluation_id" : int,
"jour" : datetime.datetime, # e.date_debut or datetime.datetime(1900, 1, 1)
"publish_incomplete" : bool,
}
@ -230,15 +231,16 @@ class ResultatsSemestre(ResultatsCache):
date_modif = cursor.one_or_none()
last_modif = date_modif[0] if date_modif else None
return {
"coefficient": evaluation.coefficient or 0.0,
"description": evaluation.description or "",
"evaluation_id": evaluation.id,
"jour": evaluation.date_debut or datetime.datetime(1900, 1, 1),
"coefficient": evaluation.coefficient,
"description": evaluation.description,
"etat": {
"blocked": evaluation.is_blocked(),
"evalcomplete": etat.is_complete,
"nb_notes": etat.nb_notes,
"last_modif": last_modif,
},
"evaluation_id": evaluation.id,
"jour": evaluation.date_debut or datetime.datetime(1900, 1, 1),
"publish_incomplete": evaluation.publish_incomplete,
}
@ -432,9 +434,24 @@ class ResultatsSemestre(ResultatsCache):
ue_cap_dict["compense_formsemestre_id"] = None
return ue_cap_dict
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict | None:
"""L'état de l'UE pour cet étudiant.
Result: dict, ou None si l'UE n'est pas dans ce semestre.
{
"is_capitalized": # vrai si la version capitalisée est celle prise en compte (meilleure)
"was_capitalized":# si elle a été capitalisée (meilleure ou pas)
"is_external": # si UE externe
"coef_ue": 0.0,
"cur_moy_ue": 0.0, # moyenne de l'UE courante
"moy": 0.0, # moyenne prise en compte
"event_date": # date de la capiltalisation éventuelle (ou None)
"ue": ue_dict, # l'UE, comme un dict
"formsemestre_id": None,
"capitalized_ue_id": None, # l'id de l'UE capitalisée, ou None
"ects_pot": 0.0, # deprecated (les ECTS liés à cette UE)
"ects": 0.0, # les ECTS acquis grace à cette UE
"ects_ue": # les ECTS liés à cette UE
}
"""
ue: UniteEns = db.session.get(UniteEns, ue_id)
ue_dict = ue.to_dict()
@ -455,7 +472,7 @@ class ResultatsSemestre(ResultatsCache):
"ects": 0.0,
"ects_ue": ue.ects,
}
if not ue_id in self.etud_moy_ue:
if not ue_id in self.etud_moy_ue or not etudid in self.etud_moy_ue[ue_id]:
return None
if not self.validations:
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
@ -512,11 +529,13 @@ class ResultatsSemestre(ResultatsCache):
"is_external": ue_cap["is_external"] if is_capitalized else ue.is_external,
"coef_ue": coef_ue,
"ects_pot": ue.ects or 0.0,
"ects": self.validations.decisions_jury_ues.get(etudid, {})
.get(ue.id, {})
.get("ects", 0.0)
if self.validations.decisions_jury_ues
else 0.0,
"ects": (
self.validations.decisions_jury_ues.get(etudid, {})
.get(ue.id, {})
.get("ects", 0.0)
if self.validations.decisions_jury_ues
else 0.0
),
"ects_ue": ue.ects,
"cur_moy_ue": cur_moy_ue,
"moy": moy_ue,

View File

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

View File

@ -10,6 +10,7 @@ from flask_login import current_user
import sqlalchemy as sa
from app import db, log
from app import models
from app.models.etudiants import Identite
from app.models.events import ScolarNews
from app.models.notes import NotesNotes
@ -23,10 +24,8 @@ MAX_EVALUATION_DURATION = datetime.timedelta(days=365)
NOON = datetime.time(12, 00)
DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
VALID_EVALUATION_TYPES = {0, 1, 2}
class Evaluation(db.Model):
class Evaluation(models.ScoDocModel):
"""Evaluation (contrôle, examen, ...)"""
__tablename__ = "notes_evaluation"
@ -38,9 +37,9 @@ class Evaluation(db.Model):
)
date_debut = db.Column(db.DateTime(timezone=True), nullable=True)
date_fin = db.Column(db.DateTime(timezone=True), nullable=True)
description = db.Column(db.Text)
note_max = db.Column(db.Float)
coefficient = db.Column(db.Float)
description = db.Column(db.Text, nullable=False)
note_max = db.Column(db.Float, nullable=False)
coefficient = db.Column(db.Float, nullable=False)
visibulletin = db.Column(
db.Boolean, nullable=False, default=True, server_default="true"
)
@ -48,15 +47,30 @@ class Evaluation(db.Model):
publish_incomplete = db.Column(
db.Boolean, nullable=False, default=False, server_default="false"
)
# type d'evaluation: 0 normale, 1 rattrapage, 2 "2eme session"
"prise en compte immédiate"
evaluation_type = db.Column(
db.Integer, nullable=False, default=0, server_default="0"
)
"type d'evaluation: 0 normale, 1 rattrapage, 2 2eme session, 3 bonus"
blocked_until = db.Column(db.DateTime(timezone=True), nullable=True)
"date de prise en compte"
BLOCKED_FOREVER = datetime.datetime(2666, 12, 31, tzinfo=scu.TIME_ZONE)
# ordre de presentation (par défaut, le plus petit numero
# est la plus ancienne eval):
numero = db.Column(db.Integer, nullable=False, default=0)
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
EVALUATION_NORMALE = 0 # valeurs stockées en base, ne pas changer !
EVALUATION_RATTRAPAGE = 1
EVALUATION_SESSION2 = 2
EVALUATION_BONUS = 3
VALID_EVALUATION_TYPES = {
EVALUATION_NORMALE,
EVALUATION_RATTRAPAGE,
EVALUATION_SESSION2,
EVALUATION_BONUS,
}
def __repr__(self):
return f"""<Evaluation {self.id} {
self.date_debut.isoformat() if self.date_debut else ''} "{
@ -70,13 +84,14 @@ class Evaluation(db.Model):
date_fin: datetime.datetime = None,
description=None,
note_max=None,
blocked_until=None,
coefficient=None,
visibulletin=None,
publish_incomplete=None,
evaluation_type=None,
numero=None,
**kw, # ceci pour absorber les éventuel arguments excedentaires
):
) -> "Evaluation":
"""Create an evaluation. Check permission and all arguments.
Ne crée pas les poids vers les UEs.
Add to session, do not commit.
@ -88,7 +103,7 @@ class Evaluation(db.Model):
args = locals()
del args["cls"]
del args["kw"]
check_convert_evaluation_args(moduleimpl, args)
check_and_convert_evaluation_args(args, moduleimpl)
# Check numeros
Evaluation.moduleimpl_evaluation_renumber(moduleimpl, only_if_unumbered=True)
if not "numero" in args or args["numero"] is None:
@ -199,6 +214,10 @@ class Evaluation(db.Model):
def to_dict_api(self) -> dict:
"Représentation dict pour API JSON"
return {
"blocked": self.is_blocked(),
"blocked_until": (
self.blocked_until.isoformat() if self.blocked_until else ""
),
"coefficient": self.coefficient,
"date_debut": self.date_debut.isoformat() if self.date_debut else "",
"date_fin": self.date_fin.isoformat() if self.date_fin else "",
@ -235,15 +254,6 @@ class Evaluation(db.Model):
return e_dict
def from_dict(self, data):
"""Set evaluation attributes from given dict values."""
check_convert_evaluation_args(self.moduleimpl, data)
if data.get("numero") is None:
data["numero"] = Evaluation.get_max_numero(self.moduleimpl.id) + 1
for k in self.__dict__:
if k != "_sa_instance_state" and k != "id" and k in data:
setattr(self, k, data[k])
@classmethod
def get_evaluation(
cls, evaluation_id: int | str, dept_id: int = None
@ -361,19 +371,6 @@ class Evaluation(db.Model):
Chaine vide si non renseignée."""
return self.date_fin.time().isoformat("minutes") if self.date_fin else ""
def clone(self, not_copying=()):
"""Clone, not copying the given attrs
Attention: la copie n'a pas d'id avant le prochain commit
"""
d = dict(self.__dict__)
d.pop("id") # get rid of id
d.pop("_sa_instance_state") # get rid of SQLAlchemy special attr
for k in not_copying:
d.pop(k)
copy = self.__class__(**d)
db.session.add(copy)
return copy
def is_matin(self) -> bool:
"Evaluation commençant le matin (faux si pas de date)"
if not self.date_debut:
@ -386,6 +383,14 @@ class Evaluation(db.Model):
return False
return self.date_debut.time() >= NOON
def is_blocked(self, now=None) -> bool:
"True si prise en compte bloquée"
if self.blocked_until is None:
return False
if now is None:
now = datetime.datetime.now(scu.TIME_ZONE)
return self.blocked_until > now
def set_default_poids(self) -> bool:
"""Initialize les poids vers les UE à leurs valeurs par défaut
C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.
@ -474,6 +479,29 @@ class Evaluation(db.Model):
"""
return NotesNotes.query.filter_by(etudid=etud.id, evaluation_id=self.id).first()
@classmethod
def get_evaluations_blocked_for_etud(
cls, formsemestre, etud: Identite
) -> list["Evaluation"]:
"""Liste des évaluations de ce semestre avec note pour cet étudiant et date blocage
et date blocage < FOREVER.
Si non vide, une note apparaitra dans le futur pour cet étudiant: il faut
donc interdire la saisie du jury.
"""
now = datetime.datetime.now(scu.TIME_ZONE)
return (
Evaluation.query.filter(
Evaluation.blocked_until != None, # pylint: disable=C0121
Evaluation.blocked_until >= now,
)
.join(ModuleImpl)
.filter_by(formsemestre_id=formsemestre.id)
.join(ModuleImplInscription)
.filter_by(etudid=etud.id)
.join(NotesNotes)
.all()
)
class EvaluationUEPoids(db.Model):
"""Poids des évaluations (BUT)
@ -531,7 +559,7 @@ def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
return e_dict
def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
"""Check coefficient, dates and duration, raises exception if invalid.
Convert date and time strings to date and time objects.
@ -546,7 +574,7 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
# --- evaluation_type
try:
data["evaluation_type"] = int(data.get("evaluation_type", 0) or 0)
if not data["evaluation_type"] in VALID_EVALUATION_TYPES:
if not data["evaluation_type"] in Evaluation.VALID_EVALUATION_TYPES:
raise ScoValueError("invalid evaluation_type value")
except ValueError as exc:
raise ScoValueError("invalid evaluation_type value") from exc
@ -571,7 +599,7 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
if coef < 0:
raise ScoValueError("invalid coefficient value (must be positive or null)")
data["coefficient"] = coef
# --- date de l'évaluation
# --- date de l'évaluation dans le semestre ?
formsemestre = moduleimpl.formsemestre
date_debut = data.get("date_debut", None)
if date_debut:
@ -612,6 +640,8 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
"Heures de l'évaluation incohérentes !",
dest_url="javascript:history.back();",
)
if "blocked_until" in data:
data["blocked_until"] = data["blocked_until"] or None
def heure_to_time(heure: str) -> datetime.time:
@ -641,3 +671,6 @@ def _moduleimpl_evaluation_insert_before(
db.session.add(e)
db.session.commit()
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"
)
"Si vrai, la moyenne générale indicative BUT n'est pas calculée"
mode_calcul_moyennes = db.Column(
db.Integer, nullable=False, default=0, server_default="0"
)
"pour usage futur"
gestion_semestrielle = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)

View File

@ -6,7 +6,12 @@ from flask import current_app, g
from app import db
from app import models
from app.models import APO_CODE_STR_LEN
from app.models.but_refcomp import ApcParcours, app_critiques_modules, parcours_modules
from app.models.but_refcomp import (
ApcParcours,
ApcReferentielCompetences,
app_critiques_modules,
parcours_modules,
)
from app.scodoc import sco_utils as scu
from app.scodoc.codes_cursus import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError
@ -100,6 +105,33 @@ class Module(models.ScoDocModel):
return args_dict
@classmethod
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded.
Add 'id' to excluded."""
# on ne peut pas affecter directement parcours
return super().filter_model_attributes(data, (excluded or set()) | {"parcours"})
@classmethod
def create_from_dict(cls, data: dict) -> "Module":
"""Create from given dict, add parcours"""
mod = super().create_from_dict(data)
for p in data.get("parcours", []) or []:
if isinstance(p, ApcParcours):
parcour: ApcParcours = p
else:
pid = int(p)
query = ApcParcours.query.filter_by(id=pid)
if g.scodoc_dept:
query = query.join(ApcReferentielCompetences).filter_by(
dept_id=g.scodoc_dept_id
)
parcour: ApcParcours = query.first()
if parcour is None:
raise ScoValueError("Parcours invalide")
mod.parcours.append(parcour)
return mod
def clone(self):
"""Create a new copy of this module."""
mod = Module(

View File

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

View File

@ -85,17 +85,6 @@ UE_ELECTIVE = 4 # UE "élective" dans certains cursus (UCAC?, ISCID)
UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...)
UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...)
def ue_is_fondamentale(ue_type):
return ue_type in (UE_STANDARD, UE_STAGE_LP, UE_PROFESSIONNELLE)
def ue_is_professionnelle(ue_type):
return (
ue_type == UE_PROFESSIONNELLE
) # NB: les UE_PROFESSIONNELLE sont à la fois fondamentales et pro
UE_TYPE_NAME = {
UE_STANDARD: "Standard",
UE_SPORT: "Sport/Culture (points bonus)",
@ -104,8 +93,6 @@ UE_TYPE_NAME = {
UE_ELECTIVE: "Elective (ISCID)",
UE_PROFESSIONNELLE: "Professionnelle (ISCID)",
UE_OPTIONNELLE: "Optionnelle",
# UE_FONDAMENTALE : '"Fondamentale" (eg UCAC)',
# UE_OPTIONNELLE : '"Optionnelle" (UCAC)'
}
# Couleurs RGB (dans [0.,1.]) des UE pour les bulletins:

View File

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

View File

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

View File

@ -34,7 +34,7 @@ Il suffit d'appeler abs_notify() après chaque ajout d'absence.
import datetime
from typing import Optional
from flask import g, url_for
from flask import flash, g, url_for
from flask_mail import Message
from app import db
@ -46,7 +46,6 @@ from app.models.etudiants import Identite
from app.models.events import Scolog
from app.models.formsemestre import FormSemestre
import app.scodoc.notesdb as ndb
from app.scodoc import sco_etud
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
@ -283,10 +282,17 @@ def abs_notification_message(
)
template = prefs["abs_notification_mail_tmpl"]
txt = ""
if template:
txt = prefs["abs_notification_mail_tmpl"] % values
try:
txt = prefs["abs_notification_mail_tmpl"] % values
except KeyError:
flash("Mail non envoyé: format invalide (voir paramétrage)")
log("abs_notification_message: invalid key in abs_notification_mail_tmpl")
txt = ""
else:
log("abs_notification_message: empty template, not sending message")
if not txt:
return None
subject = f"""[ScoDoc] Trop d'absences pour {etud.nomprenom}"""

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -28,7 +28,6 @@
from flask.templating import render_template
from app import db
from app.but import apc_edit_ue
from app.models import UniteEns, Matiere, Module, FormSemestre, ModuleImpl
from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import codes_cursus
@ -48,6 +47,8 @@ def html_edit_formation_apc(
- Les ressources
- Les SAÉs
"""
from app.but import cursus_but
cursus = formation.get_cursus()
assert cursus.APC_SAE
@ -101,18 +102,26 @@ def html_edit_formation_apc(
),
}
html_ue_warning = {
semestre_idx: cursus_but.formation_semestre_niveaux_warning(
formation, semestre_idx
)
for semestre_idx in semestre_ids
}
H = [
render_template(
"pn/form_ues.j2",
formation=formation,
semestre_ids=semestre_ids,
editable=editable,
tag_editable=tag_editable,
icons=icons,
ues_by_sem=ues_by_sem,
ects_by_sem=ects_by_sem,
scu=scu,
codes_cursus=codes_cursus,
ects_by_sem=ects_by_sem,
editable=editable,
formation=formation,
html_ue_warning=html_ue_warning,
icons=icons,
scu=scu,
semestre_ids=semestre_ids,
tag_editable=tag_editable,
ues_by_sem=ues_by_sem,
),
]
for semestre_idx in semestre_ids:

View File

@ -682,8 +682,11 @@ def module_edit(
"input_type": "checkbox",
"vertical": True,
"dom_id": "tf_module_parcours",
"labels": [parcour.libelle for parcour in ref_comp.parcours]
+ ["Tous (tronc commun)"],
"labels": [
f"&nbsp; {parcour.libelle} (<b>{parcour.code}</b>)"
for parcour in ref_comp.parcours
]
+ ["&nbsp; Tous (tronc commun)"],
"allowed_values": [
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>
<input type="checkbox" class="sco_tag_checkbox"
{'checked' if show_tags else ''}
> Montrer les tags des modules voire en ajouter <i>(ceux correspondant aux titres des compétences étant ajoutés par défaut)</i></input>
> Montrer les tags des modules voire en ajouter
<i>(ceux correspondant aux titres des compétences étant ajoutés par défaut)</i>
</input>
</form>
"""
)

View File

@ -31,96 +31,15 @@
import flask
from flask import url_for, g
from flask_login import current_user
import sqlalchemy as sa
from app import db, log
from app.models import Evaluation
from app.models.evaluations import check_convert_evaluation_args
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import AccessDenied
from app.scodoc import sco_cache
from app.scodoc import sco_moduleimpl
_evaluationEditor = ndb.EditableTable(
"notes_evaluation",
"evaluation_id",
(
"evaluation_id",
"moduleimpl_id",
"date_debut",
"date_fin",
"description",
"note_max",
"coefficient",
"visibulletin",
"publish_incomplete",
"evaluation_type",
"numero",
),
sortkey="numero, date_debut desc", # plus recente d'abord
output_formators={
"numero": ndb.int_null_is_zero,
},
input_formators={
"visibulletin": bool,
"publish_incomplete": bool,
"evaluation_type": int,
},
)
def get_evaluations_dict(args: dict) -> list[dict]:
"""Liste evaluations, triées numero (or most recent date first).
Fonction de transition pour ancien code ScoDoc7.
Ajoute les champs:
'duree' : '2h30'
'matin' : 1 (commence avant 12:00) ou 0
'apresmidi' : 1 (termine après 12:00) ou 0
'descrheure' : ' de 15h00 à 16h30'
"""
# calcule duree (chaine de car.) de chaque evaluation et ajoute jour_iso, matin, apresmidi
return [
e.to_dict()
for e in Evaluation.query.filter_by(**args).order_by(
sa.desc(Evaluation.numero), sa.desc(Evaluation.date_debut)
)
]
def do_evaluation_list_in_formsemestre(formsemestre_id):
"list evaluations in this formsemestre"
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
evals = []
for modimpl in mods:
evals += get_evaluations_dict(args={"moduleimpl_id": modimpl["moduleimpl_id"]})
return evals
def do_evaluation_edit(args):
"edit an evaluation"
evaluation_id = args["evaluation_id"]
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
if evaluation is None:
raise ValueError("evaluation inexistante !")
if not evaluation.moduleimpl.can_edit_evaluation(current_user):
raise AccessDenied(
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
)
args["moduleimpl_id"] = evaluation.moduleimpl.id
check_convert_evaluation_args(evaluation.moduleimpl, args)
cnx = ndb.GetDBConnexion()
_evaluationEditor.edit(cnx, args)
# inval cache pour ce semestre
sco_cache.invalidate_formsemestre(
formsemestre_id=evaluation.moduleimpl.formsemestre_id
)
# ancien _notes_getall

View File

@ -31,14 +31,12 @@ import datetime
import time
import flask
from flask import url_for, render_template
from flask import g
from flask import g, render_template, request, url_for
from flask_login import current_user
from flask import request
from app import db
from app.models import Evaluation, Module, ModuleImpl
from app.models.evaluations import heure_to_time
from app.models.evaluations import heure_to_time, check_and_convert_evaluation_args
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
@ -108,7 +106,7 @@ def evaluation_create_form(
raise ValueError("missing evaluation_id parameter")
initvalues = evaluation.to_dict()
moduleimpl_id = initvalues["moduleimpl_id"]
submitlabel = "Modifier les données"
submitlabel = "Modifier l'évaluation"
action = "Modification d'une évaluation"
link = ""
# Note maximale actuelle dans cette éval ?
@ -142,6 +140,15 @@ def evaluation_create_form(
else:
poids = 0.0
initvalues[f"poids_{ue.id}"] = poids
# Blocage
if edit:
initvalues["blocked"] = evaluation.is_blocked()
initvalues["blocked_until"] = (
evaluation.blocked_until.strftime("%d/%m/%Y")
if evaluation.blocked_until
and evaluation.blocked_until < Evaluation.BLOCKED_FOREVER
else ""
)
#
form = [
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
@ -183,7 +190,8 @@ def evaluation_create_form(
{
"size": 6,
"type": "float", # peut être négatif (!)
"explanation": "coef. dans le module (choisi librement par l'enseignant, non utilisé pour rattrapage et 2ème session)",
"explanation": """coef. dans le module (choisi librement par
l'enseignant, non utilisé pour rattrapage, 2ème session et bonus)""",
"allow_null": False,
},
)
@ -195,7 +203,7 @@ def evaluation_create_form(
"size": 4,
"type": "float",
"title": "Notes de 0 à",
"explanation": f"barème (note max actuelle: {min_note_max_str})",
"explanation": f"""barème (note max actuelle: {min_note_max_str}).""",
"allow_null": False,
"max_value": scu.NOTES_MAX,
"min_value": min_note_max,
@ -206,7 +214,8 @@ def evaluation_create_form(
{
"size": 36,
"type": "text",
"explanation": """type d'évaluation, apparait sur le bulletins longs. Exemples: "contrôle court", "examen de TP", "examen final".""",
"explanation": """type d'évaluation, apparait sur le bulletins longs.
Exemples: "contrôle court", "examen de TP", "examen final".""",
},
),
(
@ -230,16 +239,20 @@ def evaluation_create_form(
{
"input_type": "menu",
"title": "Modalité",
"allowed_values": (
scu.EVALUATION_NORMALE,
scu.EVALUATION_RATTRAPAGE,
scu.EVALUATION_SESSION2,
),
"allowed_values": Evaluation.VALID_EVALUATION_TYPES,
"type": "int",
"labels": (
"Normale",
"Rattrapage (remplace si meilleure note)",
"Deuxième session (remplace toujours)",
(
"Bonus "
+ (
"(pondéré par poids et ajouté aux moyennes de ce module)"
if is_apc
else "(ajouté à la moyenne de ce module)"
)
),
),
},
),
@ -251,8 +264,10 @@ def evaluation_create_form(
{
"size": 6,
"type": "float",
"explanation": "importance de l'évaluation (multiplie les poids ci-dessous)",
"explanation": """importance de l'évaluation (multiplie les poids ci-dessous).
Non utilisé pour les bonus.""",
"allow_null": False,
"dom_id": "evaluation-edit-coef",
},
),
]
@ -294,6 +309,28 @@ def evaluation_create_form(
},
),
)
# Bloquage / date prise en compte
form += [
(
"blocked",
{
"input_type": "boolcheckbox",
"title": "Bloquer la prise en compte",
"explanation": """empêche la prise en compte
(ne sera pas visible sur les bulletins ni dans les tableaux)""",
"dom_id": "evaluation-edit-blocked",
},
),
(
"blocked_until",
{
"input_type": "datedmy",
"title": "Date déblocage",
"size": 12,
"explanation": "sera débloquée à partir de cette date",
},
),
]
tf = TrivialFormulator(
request.base_url,
vals,
@ -324,7 +361,9 @@ def evaluation_create_form(
+ "\n".join(H)
+ "\n"
+ tf[1]
+ render_template("scodoc/help/evaluations.j2", is_apc=is_apc)
+ render_template(
"scodoc/help/evaluations.j2", is_apc=is_apc, modimpl=modimpl
)
+ render_template("sco_timepicker.j2")
+ html_sco_header.sco_footer()
)
@ -342,6 +381,8 @@ def evaluation_create_form(
raise ScoValueError("Date (j/m/a) invalide") from exc
else:
date_debut = None
args["date_debut"] = date_debut
args["date_fin"] = date_debut # même jour
args.pop("jour", None)
if date_debut and args.get("heure_debut"):
try:
@ -350,7 +391,8 @@ def evaluation_create_form(
raise ScoValueError("Heure début invalide") from exc
args["date_debut"] = datetime.datetime.combine(date_debut, heure_debut)
args.pop("heure_debut", None)
# note: ce formulaire ne permet de créer que des évaluation avec debut et fin sur le même jour.
# note: ce formulaire ne permet de créer que des évaluations
# avec debut et fin sur le même jour.
if date_debut and args.get("heure_fin"):
try:
heure_fin = heure_to_time(args["heure_fin"])
@ -358,8 +400,22 @@ def evaluation_create_form(
raise ScoValueError("Heure fin invalide") from exc
args["date_fin"] = datetime.datetime.combine(date_debut, heure_fin)
args.pop("heure_fin", None)
# Blocage:
if args.get("blocked"):
if args.get("blocked_until"):
try:
args["blocked_until"] = datetime.datetime.strptime(
args["blocked_until"], "%d/%m/%Y"
)
except ValueError as exc:
raise ScoValueError("Date déblocage (j/m/a) invalide") from exc
else: # bloquage coché sans date
args["blocked_until"] = Evaluation.BLOCKED_FOREVER
else: # si pas coché, efface date déblocage
args["blocked_until"] = None
#
if edit:
check_and_convert_evaluation_args(args, modimpl)
evaluation.from_dict(args)
else:
# création d'une evaluation

View File

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

View File

@ -534,7 +534,7 @@ def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, line
# description evaluation
ws.append_single_cell_row(scu.unescape_html(description), style_titres)
ws.append_single_cell_row(
f"Evaluation {evaluation.descr_date()} (coef. {(evaluation.coefficient or 0.0):g})",
f"Evaluation {evaluation.descr_date()} (coef. {(evaluation.coefficient):g})",
style,
)
# ligne blanche

View File

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

View File

@ -31,6 +31,7 @@ import flask
from flask import url_for, flash, redirect
from flask import g, request
from flask_login import current_user
import sqlalchemy as sa
from app import db
from app.auth.models import User
@ -63,8 +64,6 @@ from app.scodoc import html_sco_header
from app.scodoc import codes_cursus
from app.scodoc import sco_compute_moy
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups_copy
from app.scodoc import sco_modalites
@ -1113,7 +1112,8 @@ def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
f"""<b>impossible de supprimer {module.code} ({module.titre or ""})
car il y a {nb_evals} évaluations définies
(<a href="{
url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
}" class="stdlink">supprimez-les d\'abord</a>)</b>"""
]
ok = False
@ -1233,7 +1233,11 @@ def formsemestre_clone(formsemestre_id):
return "".join(H) + msg + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1: # cancel
return flask.redirect(
"formsemestre_status?formsemestre_id=%s" % formsemestre_id
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
else:
resp = User.get_user_from_nomplogin(tf[2]["responsable_id"])
@ -1356,9 +1360,9 @@ def do_formsemestre_clone(
return formsemestre_id
def formsemestre_delete(formsemestre_id):
def formsemestre_delete(formsemestre_id: int) -> str | flask.Response:
"""Delete a formsemestre (affiche avertissements)"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header("Suppression du semestre"),
"""<div class="ue_warning"><span>Attention !</span>
@ -1376,17 +1380,18 @@ Ceci n'est possible que si :
</ol>
</div>""",
]
evals = sco_evaluation_db.do_evaluation_list_in_formsemestre(formsemestre_id)
if evals:
evaluations = (
Evaluation.query.join(ModuleImpl)
.filter_by(formsemestre_id=formsemestre.id)
.all()
)
if evaluations:
H.append(
f"""<p class="warning">Attention: il y a {len(evals)} évaluations
f"""<p class="warning">Attention: il y a {len(evaluations)} évaluations
dans ce semestre
(sa suppression entrainera l'effacement définif des notes) !</p>"""
)
submit_label = (
f"Confirmer la suppression (du semestre et des {len(evals)} évaluations !)"
)
submit_label = f"Confirmer la suppression (du semestre et des {len(evaluations)} évaluations !)"
else:
submit_label = "Confirmer la suppression du semestre"
tf = TrivialFormulator(
@ -1413,8 +1418,10 @@ Ceci n'est possible que si :
)
else:
H.append(tf[1])
return "\n".join(H) + html_sco_header.sco_footer()
elif tf[0] == -1: # cancel
if tf[0] == -1: # cancel
return flask.redirect(
url_for(
"notes.formsemestre_status",
@ -1422,10 +1429,9 @@ Ceci n'est possible que si :
formsemestre_id=formsemestre_id,
)
)
else:
return flask.redirect(
"formsemestre_delete2?formsemestre_id=" + str(formsemestre_id)
)
return flask.redirect(
"formsemestre_delete2?formsemestre_id=" + str(formsemestre_id)
)
def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
@ -1486,106 +1492,165 @@ def formsemestre_has_decisions_or_compensations(
return False, ""
def do_formsemestre_delete(formsemestre_id):
def do_formsemestre_delete(formsemestre_id: int):
"""delete formsemestre, and all its moduleimpls.
No checks, no warnings: erase all !
"""
cnx = ndb.GetDBConnexion()
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sco_cache.EvaluationCache.invalidate_sem(formsemestre_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
sco_cache.EvaluationCache.invalidate_sem(formsemestre.id)
titre_sem = formsemestre.titre_annee()
# --- Destruction des modules de ce semestre
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
for mod in mods:
for modimpl in formsemestre.modimpls:
# evaluations
evals = sco_evaluation_db.get_evaluations_dict(
args={"moduleimpl_id": mod["moduleimpl_id"]}
)
for e in evals:
ndb.SimpleQuery(
"DELETE FROM notes_notes WHERE evaluation_id=%(evaluation_id)s",
e,
for e in modimpl.evaluations:
db.session.execute(
sa.text(
"""DELETE FROM notes_notes WHERE evaluation_id=:evaluation_id"""
),
{"evaluation_id": e.id},
)
ndb.SimpleQuery(
"DELETE FROM notes_notes_log WHERE evaluation_id=%(evaluation_id)s",
e,
)
ndb.SimpleQuery(
"DELETE FROM notes_evaluation WHERE id=%(evaluation_id)s",
e,
db.session.execute(
sa.text(
"""DELETE FROM notes_notes_log WHERE evaluation_id=:evaluation_id"""
),
{"evaluation_id": e.id},
)
sco_moduleimpl.do_moduleimpl_delete(
mod["moduleimpl_id"], formsemestre_id=formsemestre_id
)
db.session.delete(e)
db.session.delete(modimpl)
# --- Desinscription des etudiants
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
req = "DELETE FROM notes_formsemestre_inscription WHERE formsemestre_id=%(formsemestre_id)s"
cursor.execute(req, {"formsemestre_id": formsemestre_id})
db.session.execute(
sa.text(
"DELETE FROM notes_formsemestre_inscription WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des evenements
req = "DELETE FROM scolar_events WHERE formsemestre_id=%(formsemestre_id)s"
cursor.execute(req, {"formsemestre_id": formsemestre_id})
db.session.execute(
sa.text("DELETE FROM scolar_events WHERE formsemestre_id=:formsemestre_id"),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des appreciations
req = "DELETE FROM notes_appreciations WHERE formsemestre_id=%(formsemestre_id)s"
cursor.execute(req, {"formsemestre_id": formsemestre_id})
db.session.execute(
sa.text(
"DELETE FROM notes_appreciations WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Supression des validations (!!!)
req = "DELETE FROM scolar_formsemestre_validation WHERE formsemestre_id=%(formsemestre_id)s"
cursor.execute(req, {"formsemestre_id": formsemestre_id})
db.session.execute(
sa.text(
"DELETE FROM scolar_formsemestre_validation WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Supression des references a ce semestre dans les compensations:
req = "UPDATE scolar_formsemestre_validation SET compense_formsemestre_id=NULL WHERE compense_formsemestre_id=%(formsemestre_id)s"
cursor.execute(req, {"formsemestre_id": formsemestre_id})
db.session.execute(
sa.text(
"""UPDATE scolar_formsemestre_validation
SET compense_formsemestre_id=NULL
WHERE compense_formsemestre_id=:formsemestre_id"""
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des autorisations
req = "DELETE FROM scolar_autorisation_inscription WHERE origin_formsemestre_id=%(formsemestre_id)s"
cursor.execute(req, {"formsemestre_id": formsemestre_id})
db.session.execute(
sa.text(
"DELETE FROM scolar_autorisation_inscription WHERE origin_formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des coefs d'UE capitalisées
req = "DELETE FROM notes_formsemestre_uecoef WHERE formsemestre_id=%(formsemestre_id)s"
cursor.execute(req, {"formsemestre_id": formsemestre_id})
db.session.execute(
sa.text(
"DELETE FROM notes_formsemestre_uecoef WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des item du menu custom
req = "DELETE FROM notes_formsemestre_custommenu WHERE formsemestre_id=%(formsemestre_id)s"
cursor.execute(req, {"formsemestre_id": formsemestre_id})
db.session.execute(
sa.text(
"DELETE FROM notes_formsemestre_custommenu WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des formules
req = "DELETE FROM notes_formsemestre_ue_computation_expr WHERE formsemestre_id=%(formsemestre_id)s"
cursor.execute(req, {"formsemestre_id": formsemestre_id})
db.session.execute(
sa.text(
"DELETE FROM notes_formsemestre_ue_computation_expr WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des preferences
req = "DELETE FROM sco_prefs WHERE formsemestre_id=%(formsemestre_id)s"
cursor.execute(req, {"formsemestre_id": formsemestre_id})
db.session.execute(
sa.text("DELETE FROM sco_prefs WHERE formsemestre_id=:formsemestre_id"),
{"formsemestre_id": formsemestre_id},
)
# --- Suppression des groupes et partitions
req = """DELETE FROM group_membership
db.session.execute(
sa.text(
"""
DELETE FROM group_membership
WHERE group_id IN
(SELECT gm.group_id FROM group_membership gm, partition p, group_descr gd
WHERE gm.group_id = gd.id AND gd.partition_id = p.id
AND p.formsemestre_id=%(formsemestre_id)s)
AND p.formsemestre_id=:formsemestre_id)
"""
cursor.execute(req, {"formsemestre_id": formsemestre_id})
req = """DELETE FROM group_descr
),
{"formsemestre_id": formsemestre_id},
)
db.session.execute(
sa.text(
"""
DELETE FROM group_descr
WHERE id IN
(SELECT gd.id FROM group_descr gd, partition p
WHERE gd.partition_id = p.id
AND p.formsemestre_id=%(formsemestre_id)s)
AND p.formsemestre_id=:formsemestre_id)
"""
cursor.execute(req, {"formsemestre_id": formsemestre_id})
req = "DELETE FROM partition WHERE formsemestre_id=%(formsemestre_id)s"
cursor.execute(req, {"formsemestre_id": formsemestre_id})
),
{"formsemestre_id": formsemestre_id},
)
db.session.execute(
sa.text("DELETE FROM partition WHERE formsemestre_id=:formsemestre_id"),
{"formsemestre_id": formsemestre_id},
)
# --- Responsables
req = """DELETE FROM notes_formsemestre_responsables
WHERE formsemestre_id=%(formsemestre_id)s"""
cursor.execute(req, {"formsemestre_id": formsemestre_id})
db.session.execute(
sa.text(
"DELETE FROM notes_formsemestre_responsables WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Etapes
req = """DELETE FROM notes_formsemestre_etapes
WHERE formsemestre_id=%(formsemestre_id)s"""
cursor.execute(req, {"formsemestre_id": formsemestre_id})
db.session.execute(
sa.text(
"DELETE FROM notes_formsemestre_etapes WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- SemSets
db.session.execute(
sa.text(
"DELETE FROM notes_semset_formsemestre WHERE formsemestre_id=:formsemestre_id"
),
{"formsemestre_id": formsemestre_id},
)
# --- Dispenses d'UE
req = """DELETE FROM "dispenseUE" WHERE formsemestre_id=%(formsemestre_id)s"""
cursor.execute(req, {"formsemestre_id": formsemestre_id})
db.session.execute(
sa.text("""DELETE FROM "dispenseUE" WHERE formsemestre_id=:formsemestre_id"""),
{"formsemestre_id": formsemestre_id},
)
# --- Destruction du semestre
sco_formsemestre._formsemestreEditor.delete(cnx, formsemestre_id)
db.session.delete(formsemestre)
# news
ScolarNews.add(
typ=ScolarNews.NEWS_SEM,
obj=formsemestre_id,
text="Suppression du semestre %(titre)s" % sem,
text=f"Suppression du semestre {titre_sem}",
max_frequency=0,
)
@ -1678,7 +1743,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
sum_coefs_by_ue_id = {}
for ue in ues:
sum_coefs_by_ue_id[ue.id] = sum(
modimpl.module.coefficient
modimpl.module.coefficient or 0.0
for modimpl in modimpls
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
def do_formsemestre_desinscription(etudid, formsemestre_id):
def check_if_has_decision_jury(
formsemestre: FormSemestre, etudids: list[int] | set[int]
):
"raise exception if one of the etuds has a decision in formsemestre"
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
for etudid in etudids:
if nt.etud_has_decision(etudid):
etud = Identite.query.get(etudid)
raise ScoValueError(
f"""désinscription impossible: l'étudiant {etud.nomprenom} a
une décision de jury (la supprimer avant si nécessaire)"""
)
def do_formsemestre_desinscription(
etudid, formsemestre_id: int, check_has_dec_jury=True
):
"""Désinscription d'un étudiant.
Si semestre extérieur et dernier inscrit, suppression de ce semestre.
"""
from app.scodoc import sco_formsemestre_edit
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
etud = Identite.get_etud(etudid)
# -- check lock
@ -204,13 +218,8 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
raise ScoValueError("désinscription impossible: semestre verrouille")
# -- Si decisions de jury, désinscription interdite
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
if nt.etud_has_decision(etudid):
raise ScoValueError(
f"""désinscription impossible: l'étudiant {etud.nomprenom} a
une décision de jury (la supprimer avant si nécessaire)"""
)
if check_has_dec_jury:
check_if_has_decision_jury(formsemestre, [etudid])
insem = do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id, "etudid": etudid}
@ -247,17 +256,14 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
# --- Semestre extérieur
if formsemestre.modalite == "EXT":
inscrits = do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id}
)
nbinscrits = len(inscrits)
if nbinscrits == 0:
if 0 == len(formsemestre.inscriptions):
log(
f"""do_formsemestre_desinscription:
suppression du semestre extérieur {formsemestre}"""
)
flash("Semestre exterieur supprimé")
sco_formsemestre_edit.do_formsemestre_delete(formsemestre_id)
db.session.delete(formsemestre)
db.session.commit()
flash(f"Semestre extérieur supprimé: {formsemestre.titre_annee()}")
logdb(
cnx,
@ -576,26 +582,29 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
ue_id = ue.id
ue_descr = ue.acronyme
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)
if ue_status and ue_status["is_capitalized"]:
sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"])
ue_descr += (
' <a class="discretelink" href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" title="%s">(capitalisée le %s)'
% (
sem_origin["formsemestre_id"],
etudid,
sem_origin["titreannee"],
ndb.DateISOtoDMY(ue_status["event_date"]),
)
)
ue_descr += f"""
<a class="discretelink" href="{ url_for(
'notes.formsemestre_bulletinetud', scodoc_dept=g.scodoc_dept,
formsemestre_id=sem_origin["formsemestre_id"],
etudid = etudid
)}" title="{sem_origin['titreannee']}">(capitalisée le {
ndb.DateISOtoDMY(ue_status["event_date"])
})
"""
descr.append(
(
"sec_%s" % ue_id,
f"sec_{ue_id}",
{
"input_type": "separator",
"title": """<b>%s :</b> <a href="#" onclick="chkbx_select('%s', true);">inscrire</a> | <a href="#" onclick="chkbx_select('%s', false);">désinscrire</a> à tous les modules"""
% (ue_descr, ue_id, ue_id),
"title": f"""<b>{ue_descr} :</b>
<a href="#" onclick="chkbx_select('{ue_id}', true);">inscrire</a> | <a
href="#" onclick="chkbx_select('{ue_id}', false);">désinscrire</a>
à tous les modules
""",
},
)
)
@ -765,9 +774,7 @@ def do_moduleimpl_incription_options(
# verifie que ce module existe bien
mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
if len(mods) != 1:
raise ScoValueError(
"inscription: invalid moduleimpl_id: %s" % moduleimpl_id
)
raise ScoValueError(f"inscription: invalid moduleimpl_id: {moduleimpl_id}")
mod = mods[0]
sco_moduleimpl.do_moduleimpl_inscription_create(
{"moduleimpl_id": moduleimpl_id, "etudid": etudid},
@ -779,7 +786,7 @@ def do_moduleimpl_incription_options(
mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
if len(mods) != 1:
raise ScoValueError(
"desinscription: invalid moduleimpl_id: %s" % moduleimpl_id
f"desinscription: invalid moduleimpl_id: {moduleimpl_id}"
)
mod = mods[0]
inscr = sco_moduleimpl.do_moduleimpl_inscription_list(
@ -787,8 +794,7 @@ def do_moduleimpl_incription_options(
)
if not inscr:
raise ScoValueError(
"pas inscrit a ce module ! (etudid=%s, moduleimpl_id=%s)"
% (etudid, moduleimpl_id)
f"pas inscrit a ce module ! (etudid={etudid}, moduleimpl_id={moduleimpl_id})"
)
oid = inscr[0]["moduleimpl_inscription_id"]
sco_moduleimpl.do_moduleimpl_inscription_delete(
@ -797,11 +803,13 @@ def do_moduleimpl_incription_options(
H = [
html_sco_header.sco_header(),
"""<h3>Modifications effectuées</h3>
<p><a class="stdlink" href="%s">
Retour à la fiche étudiant</a></p>
"""
% url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
f"""<h3>Modifications effectuées</h3>
<p><a class="stdlink" href="{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">
Retour à la fiche étudiant</a>
</p>
""",
html_sco_header.sco_footer(),
]
return "\n".join(H)
@ -845,49 +853,59 @@ def formsemestre_inscrits_ailleurs(formsemestre_id):
"""Page listant les étudiants inscrits dans un autre semestre
dont les dates recouvrent le semestre indiqué.
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header(
"Inscriptions multiples parmi les étudiants du semestre ",
init_qtip=True,
javascripts=["js/etud_info.js"],
)
]
insd = list_inscrits_ailleurs(formsemestre_id)
# liste ordonnée par nom
etudlist = [
sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
for etudid in insd.keys()
if insd[etudid]
]
etudlist.sort(key=lambda x: x["nom"])
etudlist = [Identite.get_etud(etudid) for etudid, sems in insd.items() if sems]
etudlist.sort(key=lambda x: x.sort_key)
if etudlist:
H.append("<ul>")
for etud in etudlist:
H.append(
'<li><a href="%s" class="discretelink">%s</a> : '
% (
f"""<li><a id="{etud.id}" class="discretelink etudinfo"
href={
url_for(
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"],
),
etud["nomprenom"],
)
etudid=etud.id,
)
}
>{etud.nomprenom}</a> :
"""
)
l = []
for s in insd[etud["etudid"]]:
for s in insd[etud.id]:
l.append(
'<a class="discretelink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a>'
% s
f"""<a class="discretelink" href="{
url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
)}">{s['titremois']}</a>"""
)
H.append(", ".join(l))
H.append("</li>")
H.append("</ul>")
H.append("<p>Total: %d étudiants concernés.</p>" % len(etudlist))
H.append(
"""<p class="help">Ces étudiants sont inscrits dans le semestre sélectionné et aussi dans d'autres semestres qui se déroulent en même temps ! <br>Sauf exception, cette situation est anormale:</p>
f"""
</ul>
<p><b>Total: {len(etudlist)} étudiants concernés.</b></p>
<p class="help">Ces étudiants sont inscrits dans le semestre sélectionné et aussi
dans d'autres semestres qui se déroulent en même temps !
</p>
<p>
<b>Sauf exception, cette situation est anormale:</b>
</p>
<ul>
<li>vérifier que les dates des semestres se suivent sans se chevaucher</li>
<li>ou si besoin désinscrire le(s) étudiant(s) de l'un des semestres (via leurs fiches individuelles).</li>
<li>vérifier que les dates des semestres se suivent <em>sans se chevaucher</em>
</li>
<li>ou bien si besoin désinscrire le(s) étudiant(s) de l'un des semestres
(via leurs fiches individuelles).
</li>
</ul>
"""
)

View File

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

View File

@ -34,7 +34,7 @@ from flask import url_for, flash, g, request
from flask_login import current_user
import sqlalchemy as sa
from app.models.etudiants import Identite
from app.models import Identite, Evaluation
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import db, log
@ -232,7 +232,9 @@ def formsemestre_validation_etud_form(
H.append(
tf_error_message(
f"""Impossible de statuer sur cet étudiant: il a des notes en
attente dans des évaluations de ce semestre (voir <a href="{
attente dans des évaluations de ce semestre (voir
<a class="stdlink"
href="{
url_for( "notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">tableau de bord</a>)
@ -241,6 +243,26 @@ def formsemestre_validation_etud_form(
)
return "\n".join(H + footer)
evaluations_a_debloquer = Evaluation.get_evaluations_blocked_for_etud(
formsemestre, etud
)
if evaluations_a_debloquer:
links_evals = [
f"""<a class="stdlink" href="{url_for(
'notes.evaluation_listenotes', scodoc_dept=g.scodoc_dept, evaluation_id=e.id
)}">{e.description} en {e.moduleimpl.module.code}</a>"""
for e in evaluations_a_debloquer
]
H.append(
tf_error_message(
f"""Impossible de statuer sur cet étudiant:
il a des notes dans des évaluations qui seront débloquées plus tard:
voir {", ".join(links_evals)}
"""
)
)
return "\n".join(H + footer)
# Infos si pas de semestre précédent
if not Se.prev:
if Se.sem["semestre_id"] == 1:
@ -1399,7 +1421,7 @@ def check_formation_ues(formation: Formation) -> tuple[str, dict[int, list[Unite
if (
len(semestre_ids) > 1
): # plusieurs semestres d'indices differents dans le cursus
ue_multiples[ue["ue_id"]] = sems
ue_multiples[ue.id] = sems
if not ue_multiples:
return "", {}
@ -1423,12 +1445,12 @@ def check_formation_ues(formation: Formation) -> tuple[str, dict[int, list[Unite
]
slist = ", ".join(
[
"""%(titreannee)s (<em>semestre <b class="fontred">%(semestre_id)s</b></em>)"""
% s
f"""{s['titreannee']
} (<em>semestre <b class="fontred">{s['semestre_id']}</b></em>)"""
for s in sems
]
)
H.append("<li><b>%s</b> : %s</li>" % (ue.acronyme, slist))
H.append(f"<li><b>{ue.acronyme}</b> : {slist}</li>")
H.append("</ul></div>")
return "\n".join(H), ue_multiples

View File

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

View File

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

View File

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

View File

@ -91,7 +91,9 @@ def do_moduleimpl_delete(oid, formsemestre_id=None):
) # > moduleimpl_delete
def moduleimpl_list(moduleimpl_id=None, formsemestre_id=None, module_id=None):
def moduleimpl_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None
) -> list[dict]:
"list moduleimpls"
args = locals()
cnx = ndb.GetDBConnexion()

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
mais permet de "dispenser" un étudiant de suivre certaines UEs de son parcours.
</p>
<p>Il peut s'agit d'étudiants redoublants ayant déjà acquis l'UE, ou d'une UE
<p>Il peut s'agir d'étudiants redoublants ayant déjà acquis l'UE, ou d'une UE
présente dans le semestre mais pas dans le parcours de l'étudiant, ou bien d'autres
cas particuliers.
</p>

View File

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

View File

@ -36,7 +36,7 @@ import sqlalchemy as sa
from app import log
from app.auth.models import User
from app.but import cursus_but
from app.but import cursus_but, validations_view
from app.models import Adresse, EtudAnnotation, FormSemestre, Identite, ScoDocSiteConfig
from app.scodoc import (
codes_cursus,
@ -445,6 +445,14 @@ def fiche_etud(etudid=None):
# Liens vers compétences BUT
if last_formsemestre and last_formsemestre.formation.is_apc():
but_cursus = cursus_but.EtudCursusBUT(etud, last_formsemestre.formation)
refcomp = last_formsemestre.formation.referentiel_competence
if refcomp:
ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau(
refcomp, etud
)
ects_total = sum((v.ects() for v in ue_validation_by_niveau.values()))
else:
ects_total = ""
info[
"but_cursus_mkup"
] = f"""
@ -454,15 +462,20 @@ def fiche_etud(etudid=None):
cursus=but_cursus,
scu=scu,
)}
<div class="link_validation_rcues">
<a class="stdlink" href="{url_for("notes.validation_rcues",
scodoc_dept=g.scodoc_dept, etudid=etudid,
formsemestre_id=last_formsemestre.id)}"
title="Visualiser les compétences BUT"
>
<img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="100px"/>
<div>Compétences BUT</div>
</a>
<div class="fiche_but_col2">
<div class="link_validation_rcues">
<a class="stdlink" href="{url_for("notes.validation_rcues",
scodoc_dept=g.scodoc_dept, etudid=etudid,
formsemestre_id=last_formsemestre.id)}"
title="Visualiser les compétences BUT"
>
<img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="100px"/>
<div style="text-align: center;">Compétences BUT</div>
</a>
</div>
<div class="fiche_total_etcs">
Total ECTS BUT: {ects_total:g}
</div>
</div>
</div>
"""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -48,16 +48,15 @@ Opérations:
import datetime
from flask import request
from app.models import FormSemestre
from app.models import Evaluation, FormSemestre
from app.scodoc.intervals import intervalmap
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences
from app.scodoc import sco_users
import sco_version
from app.scodoc.gen_tables import GenTable
import sco_version
# deux notes (de même uid) sont considérées comme de la même opération si
# elles sont séparées de moins de 2*tolerance:
@ -149,10 +148,8 @@ def list_operations(evaluation_id):
def evaluation_list_operations(evaluation_id):
"""Page listing operations on evaluation"""
E = sco_evaluation_db.get_evaluations_dict({"evaluation_id": evaluation_id})[0]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
Ops = list_operations(evaluation_id)
evaluation = Evaluation.get_evaluation(evaluation_id)
operations = list_operations(evaluation_id)
columns_ids = ("datestr", "user_name", "nb_notes", "comment")
titles = {
@ -164,11 +161,14 @@ def evaluation_list_operations(evaluation_id):
tab = GenTable(
titles=titles,
columns_ids=columns_ids,
rows=Ops,
rows=operations,
html_sortable=False,
html_title="<h2>Opérations sur l'évaluation %s du %s</h2>"
% (E["description"], E["jour"]),
preferences=sco_preferences.SemPreferences(M["formsemestre_id"]),
html_title=f"""<h2>Opérations sur l'évaluation {evaluation.description} {
evaluation.date_debut.strftime("du %d/%m/%Y") if evaluation.date_debut else "(sans date)"
}</h2>""",
preferences=sco_preferences.SemPreferences(
evaluation.moduleimpl.formsemestre_id
),
)
return tab.make_page()

View File

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

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

View File

@ -35,6 +35,11 @@
min-width: var(--sco-content-min-width);
max-width: var(--sco-content-max-width);
}
div.jury_but_warning {
background-color: yellow;
border-color: red;
padding-bottom: 4px;
}
div.jury_but_box_title {
margin-bottom: 10px;
}

View File

@ -273,6 +273,10 @@ section>div:nth-child(1) {
min-width: 80px;
display: inline-block;
}
div.eval-bonus {
color: #197614;
background-color: pink;
}
.ueBonus,
.ueBonus h3 {
@ -280,7 +284,7 @@ section>div:nth-child(1) {
color: #000 !important;
}
/* UE Capitalisée */
.synthese .ue.capitalisee,
.synthese .ue.capitalisee,
.ue.capitalisee>h3{
background: var(--couleurFondTitresUECapitalisee);;
}

View File

@ -962,10 +962,18 @@ td.fichetitre2 .fl {
div.section_but {
display: flex;
flex-direction: row;
align-items: center;
align-items: flex-end;
justify-content: space-evenly;
}
div.fiche_but_col2 {
display: flex;
flex-direction: column;
justify-content: space-between;
}
div.fiche_total_etcs {
font-weight: bold;
margin-top: 16px;
}
div.section_but > div.link_validation_rcues {
align-self: center;
text-align: center;
@ -1461,6 +1469,9 @@ span.eval_title {
font-size: 14pt;
}
#evaluation-edit-blocked td, #evaluation-edit-coef td {
padding-top: 24px;
}
/* #saisie_notes span.eval_title {
border-bottom: 1px solid rgb(100,100,100);
}
@ -1793,11 +1804,42 @@ table.formsemestre_status {
tr.formsemestre_status {
background-color: rgb(90%, 90%, 90%);
}
table.formsemestre_status tr td:first-child {
padding-left: 4px;
}
table.formsemestre_status tr td:last-child {
padding-right: 8px;
}
tr.formsemestre_status_green {
background-color: #eff7f2;
}
tr.modimpl_non_conforme td {
background-color: #ffc458;
}
tr.modimpl_non_conforme td, tr.modimpl_attente td {
padding-top: 4px;
padding-bottom: 4px;
}
tr.modimpl_has_blocked span.nb_evals_blocked, tr span.evals_attente {
background-color: yellow;
border-radius: 4px;
font-weight: bold;
margin-left: 8px;
padding-left: 4px;
padding-right: 4px;
}
tr.modimpl_has_blocked span.nb_evals_blocked {
color: red;
}
tr span.evals_attente {
background-color: orange;
color: green;
}
table.formsemestre_status a.redlink {
text-decoration: none;
}
tr.formsemestre_status_ue {
background-color: rgb(90%, 90%, 90%);
}
@ -2075,15 +2117,23 @@ th.moduleimpl_evaluations a:hover {
text-decoration: underline;
}
tr.mievr_in.evaluation_blocked th.moduleimpl_evaluation_moy span, tr.evaluation_blocked th.moduleimpl_evaluation_moy a {
font-weight: bold;
color: red;
background-color: yellow;
padding: 2px;
border-radius: 2px;
}
tr.mievr {
background-color: #eeeeee;
}
tr.mievr_rattr {
tr.mievr_rattr, tr.mievr_session2, tr.mievr_bonus {
background-color: #dddddd;
}
span.mievr_rattr {
span.mievr_rattr, span.mievr_session2, span.mievr_bonus {
display: inline-block;
font-weight: bold;
font-size: 80%;
@ -2129,6 +2179,16 @@ tr.mievr.non_visible_inter th {
);
}
tr.mievr_tit.evaluation_blocked td,tr.mievr_tit.evaluation_blocked th {
background-image: radial-gradient(#bd7777 1px, transparent 1px);
background-size: 10px 10px;
}
tr.mievr_in.evaluation_blocked td, tr.mievr_in.evaluation_blocked th {
background-color: rgb(195, 235, 255);
padding-top: 4px;
}
tr.mievr th {
background-color: white;
}
@ -2139,6 +2199,7 @@ tr.mievr td.mievr {
tr.mievr td.mievr_menu {
width: 110px;
padding-bottom: 4px;
}
tr.mievr td.mievr_dur {
@ -2411,6 +2472,29 @@ div.formation_list_ues_titre {
color: #eee;
}
div.formation_semestre_niveaux_warning {
font-weight: bold;
color: red;
padding: 4px;
margin-top: 8px;
margin-left: 24px;
margin-right: 24px;
background-color: yellow;
border-radius: 8px;
}
div.formation_semestre_niveaux_warning div {
color: black;
font-size: 110%;
}
div.formation_semestre_niveaux_warning ul {
list-style-type: none;
padding-left: 0;
}
div.formation_semestre_niveaux_warning ul li:before {
content: '⚠️';
margin-right: 10px; /* Adjust space between emoji and text */
}
div.formation_list_modules,
div.formation_list_ues {
border-radius: 18px;
@ -2426,6 +2510,7 @@ div.formation_list_ues {
}
div.formation_list_ues_content {
margin-top: 4px;
}
div.formation_list_modules {
@ -2508,7 +2593,13 @@ div.formation_parcs > div {
opacity: 0.7;
border-radius: 4px;
text-align: center;
padding: 4px 8px;
padding: 2px 6px;
margin-top: 8px;
margin-bottom: 2px;
}
div.formation_parcs > div.ue_tc {
color: black;
font-style: italic;
}
div.formation_parcs > div.focus {
@ -3316,14 +3407,24 @@ li.tf-msg {
padding-bottom: 5px;
}
.warning {
font-weight: bold;
.warning, .warning-bloquant {
color: red;
margin-left: 16px;
margin-bottom: 8px;
min-width: var(--sco-content-min-width);
max-width: var(--sco-content-max-width);
}
.warning::before {
content: url(/ScoDoc/static/icons/warning_img.png);
vertical-align: -80%;
content:"";
margin-right: 8px;
height:32px;
width: 32px;
background-size: 32px 32px;
background-image: url(/ScoDoc/static/icons/warning_std.svg);
background-repeat: no-repeat;
display: inline-block;
vertical-align: -40%;
}
.warning-light {
@ -3336,6 +3437,19 @@ li.tf-msg {
/* 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 {
font-weight: bold;
color: red;
@ -3714,10 +3828,17 @@ span.sp_etape {
color: black;
}
.inscrailleurs {
.deja-inscrit {
font-weight: bold;
color: rgb(1, 76, 1) !important;
}
.inscrit-ailleurs {
font-weight: bold;
color: red !important;
}
div.etuds_select_boxes {
margin-bottom: 16px;
}
span.paspaye,
span.paspaye a {
@ -4682,6 +4803,10 @@ table.table_recap th.col_malus {
font-weight: bold;
color: rgb(165, 0, 0);
}
table.table_recap td.col_eval_bonus,
table.table_recap th.col_eval_bonus {
color: #90c;
}
table.table_recap tr.ects td {
color: rgb(160, 86, 3);

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@
{%- block styles %}
<!-- Bootstrap -->
<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 head %}
</head>
@ -26,7 +27,14 @@
{% block scripts %}
<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-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>
const SCO_TIMEZONE = "{{ scu.TIME_ZONE }}";
</script>

View File

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

View File

@ -43,13 +43,6 @@
{{ super() }}
<script src="{{scu.STATIC_DIR}}/libjs/menu.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}}/DataTables/datatables.min.js"></script>
<script>

View File

@ -8,13 +8,15 @@
</p>
{%if is_apc%}
<p class="help help_but">
Dans le BUT, une évaluation peut évaluer différents apprentissages critiques... (à compléter)
Le coefficient est multiplié par les poids vers chaque UE.
Dans le BUT, une évaluation peut évaluer différents apprentissages critiques,
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>
{%endif%}
<p class="help">
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
moyenne générale.
</p>
@ -22,17 +24,31 @@
L'option <em>Visible sur bulletins</em> indique que la note sera
reportée sur les bulletins en version dite "intermédiaire" (dans cette
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
visible par les étudiants sur le portail).
</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">
Les modalités "rattrapage" et "deuxième session" définissent des
évaluations prises en compte de façon spéciale:
</p>
<ul>
<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>les notes de "deuxième session" remplacent, lorsqu'elles sont
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 %}
<span title="absences du {{ sco.etud_cur_sem['date_debut'] }}
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 %}
<ul>
{% 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(
form: AjoutAssiOrJustForm,
all_day: bool = False,
) -> tuple[
bool, datetime.datetime | None, datetime.datetime | None, datetime.datetime | None
]:
@ -308,13 +309,23 @@ def _get_dates_from_assi_form(
if date_fin:
# 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:
try:
heure_debut = datetime.time.fromisoformat(
form.heure_debut.data or debut_jour
)
heure_debut = datetime.time.fromisoformat(form.heure_debut.data or "00:00")
except ValueError:
form.set_error("heure début invalide", form.heure_debut)
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"
)
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:
form.set_error("heure fin invalide", form.heure_fin)
@ -694,7 +705,7 @@ def _record_justificatif_etud(
dt_debut_tz_server,
dt_fin_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:
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.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.scodoc.sco_exceptions import (
@ -98,59 +97,62 @@ from app.scodoc.sco_exceptions import (
ScoValueError,
ScoInvalidIdType,
)
from app.scodoc import html_sco_header
from app.scodoc import sco_apogee_compare
from app.scodoc import sco_archives_formsemestre
from app.scodoc import sco_assiduites
from app.scodoc import sco_bulletins
from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_cache
from app.scodoc import sco_cost_formation
from app.scodoc import sco_debouche
from app.scodoc import sco_edit_apc
from app.scodoc import sco_edit_formation
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_etape_apogee_view
from app.scodoc import sco_etud
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_check_abs
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_evaluation_edit
from app.scodoc import sco_evaluation_recap
from app.scodoc import sco_export_results
from app.scodoc import sco_formations
from app.scodoc import sco_formation_recap
from app.scodoc import sco_formation_versions
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_custommenu
from app.scodoc import sco_formsemestre_edit
from app.scodoc import sco_formsemestre_exterieurs
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_formsemestre_validation
from app.scodoc import sco_inscr_passage
from app.scodoc import sco_liste_notes
from app.scodoc import sco_lycee
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_moduleimpl_inscriptions
from app.scodoc import sco_moduleimpl_status
from app.scodoc import sco_placement
from app.scodoc import sco_poursuite_dut
from app.scodoc import sco_preferences
from app.scodoc import sco_prepajury
from app.scodoc import sco_pv_forms
from app.scodoc import sco_recapcomplet
from app.scodoc import sco_report
from app.scodoc import sco_report_but
from app.scodoc import sco_saisie_notes
from app.scodoc import sco_semset
from app.scodoc import sco_synchro_etuds
from app.scodoc import sco_tag_module
from app.scodoc import sco_ue_external
from app.scodoc import sco_undo_notes
from app.scodoc import sco_users
from app.scodoc import (
html_sco_header,
sco_apogee_compare,
sco_archives_formsemestre,
sco_assiduites,
sco_bulletins,
sco_bulletins_pdf,
sco_cache,
sco_cost_formation,
sco_debouche,
sco_edit_apc,
sco_edit_formation,
sco_edit_matiere,
sco_edit_module,
sco_edit_ue,
sco_etape_apogee_view,
sco_etud,
sco_evaluations,
sco_evaluation_check_abs,
sco_evaluation_db,
sco_evaluation_edit,
sco_evaluation_recap,
sco_export_results,
sco_formations,
sco_formation_recap,
sco_formation_versions,
sco_formsemestre,
sco_formsemestre_custommenu,
sco_formsemestre_edit,
sco_formsemestre_exterieurs,
sco_formsemestre_inscriptions,
sco_formsemestre_status,
sco_formsemestre_validation,
sco_groups_view,
sco_inscr_passage,
sco_liste_notes,
sco_lycee,
sco_moduleimpl,
sco_moduleimpl_inscriptions,
sco_moduleimpl_status,
sco_placement,
sco_poursuite_dut,
sco_preferences,
sco_prepajury,
sco_pv_forms,
sco_recapcomplet,
sco_report,
sco_report_but,
sco_saisie_notes,
sco_semset,
sco_synchro_etuds,
sco_tag_module,
sco_ue_external,
sco_undo_notes,
sco_users,
)
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_pv_dict import descr_autorisations
from app.scodoc.sco_permissions import Permission
@ -1640,7 +1642,7 @@ def evaluation_delete(evaluation_id):
.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)
H = [
f"""
@ -1844,10 +1846,20 @@ sco_publish(
@scodoc
@permission_required(Permission.ScoView)
@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"
# 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(
formsemestre_id, version=version
formsemestre_id, groups_infos=groups_infos, version=version
)
return scu.sendPDFFile(pdfdoc, filename)
@ -1864,18 +1876,29 @@ _EXPL_BULL = """Versions des bulletins:
@scodoc
@permission_required(Permission.ScoView)
@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"""
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:
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 _formsemestre_bulletins_choice(
formsemestre,
title="Choisir la version des bulletins à générer",
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,
dialog_confirmed=False,
prefer_mail_perso=0,
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
):
"""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:
return flask.redirect(
url_for(
@ -1909,8 +1939,9 @@ def formsemestre_bulletins_mailetuds_choice(
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
version=version,
dialog_confirmed=dialog_confirmed,
dialog_confirmed=int(dialog_confirmed),
prefer_mail_perso=prefer_mail_perso,
group_ids=groups_infos.group_ids,
)
)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
@ -1934,45 +1965,41 @@ def formsemestre_bulletins_mailetuds_choice(
</p><p>"""
+ expl_bull,
choose_mail=True,
groups_infos=groups_infos,
)
# not published
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"""
versions = (
"""Choix d'une version de bulletin
(pour envois mail ou génération classeur pdf)
"""
versions_bulletins = (
scu.BULLETINS_VERSIONS_BUT
if formsemestre.formation.is_apc()
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"/>""")
if choose_mail:
H.append(
"""<div>
<input type="checkbox" name="prefer_mail_perso" value="1"
/>Utiliser si possible les adresses personnelles
</div>"""
)
H.append(f"""<p class="help">{explanation}</p>""")
return "\n".join(H) + html_sco_header.sco_footer()
return render_template(
"formsemestre/bulletins_choice.j2",
explanation=explanation,
choose_mail=choose_mail,
formsemestre=formsemestre,
menu_groups_choice=sco_groups_view.menu_groups_choice(groups_infos),
sco=ScoData(formsemestre=formsemestre),
sco_groups_view=sco_groups_view,
title=title,
versions_bulletins=versions_bulletins,
)
@bp.route("/formsemestre_bulletins_mailetuds")
@bp.route("/formsemestre_bulletins_mailetuds", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
@ -1981,16 +2008,23 @@ def formsemestre_bulletins_mailetuds(
version="long",
dialog_confirmed=False,
prefer_mail_perso=0,
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
):
"""Envoie à chaque etudiant son bulletin
(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)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
inscriptions = [
inscription
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):
@ -1998,7 +2032,7 @@ def formsemestre_bulletins_mailetuds(
# Confirmation dialog
if not dialog_confirmed:
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="",
cancel_url=url_for(
"notes.formsemestre_status",
@ -2374,10 +2408,12 @@ def formsemestre_validation_but(
)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if len(deca.get_decisions_rcues_annee()) == 0:
return jury_but_view.jury_but_semestriel(
formsemestre, etud, read_only, navigation_div=navigation_div
)
has_notes_en_attente = deca.has_notes_en_attente()
evaluations_a_debloquer = Evaluation.get_evaluations_blocked_for_etud(
formsemestre, etud
)
if has_notes_en_attente or evaluations_a_debloquer:
read_only = True
if request.method == "POST":
if not read_only:
deca.record_form(request.form)
@ -2422,9 +2458,21 @@ def formsemestre_validation_but(
etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?")
warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_pair.semestre_id}</div>"""
if deca.has_notes_en_attente():
warning += f"""<div class="warning">{etud.nomprenom} a des notes en ATTente.
Vous devriez régler cela avant de statuer en jury !</div>"""
if has_notes_en_attente:
warning += f"""<div class="warning-bloquant">{etud.nomprenom} a des notes en ATTente.
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(
f"""
<div>
@ -2440,7 +2488,9 @@ def formsemestre_validation_but(
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
</div>
</div>
<div class="jury_but_warning jury_but_box">
{warning}
</div>
</div>
<form method="post" class="jury_but_box" id="jury_but">

View File

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

View File

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

View File

@ -1,17 +1,17 @@
import pytest
from flask import g
from flask_login import login_user, logout_user, current_user
from flask_login import login_user
from config import TestConfig
import app
from app import db, create_app
from app import initialize_scodoc_database, clear_scodoc_cache
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.scodoc import sco_bulletins_standard
from app.scodoc import notesdb as ndb
import app.scodoc.sco_utils as scu
RESOURCES_DIR = "/opt/scodoc/tests/ressources"
@ -23,6 +23,12 @@ def test_client():
# Run tests:
with apptest.test_client() as client:
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():
# initialize scodoc "g":
g.stored_get_formsemestre = {}

View File

@ -1148,13 +1148,8 @@ def _setup_fake_db(
moduleimpls.append(ModuleImpl.query.filter_by(id=moduleimpl_id).first())
# Création de 3 étudiants
etud_0 = g_fake.create_etud(prenom="etud0")
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] = [
# Création de x étudiants
etuds_dict: list[dict] = [
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},
}
for key in resultat_attendu:
assert (
resultat_attendu[key]["journee"] * 2 >= resultat_attendu[key]["demi"]
), f"Trop de demi-journées [{key}]"
for key, value in resultat_attendu.items():
assert value["journee"] * 2 >= value["demi"], f"Trop de demi-journées [{key}]"
for key in resultat_attendu:
for key2 in resultat_attendu[key]:
for key, value in resultat_attendu.items():
for key2, value2 in value.items():
assert (
result[key][key2] == resultat_attendu[key][key2]
), f"Le calcul [{key}][{key2}] est faux (attendu > {resultat_attendu[key][key2]}{result[key][key2]} < obtenu)"
result[key][key2] == value2
), 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
"""
import datetime
import app
@ -68,7 +69,7 @@ def test_notes_rattrapage(test_client):
date_debut=datetime.datetime(2020, 1, 2),
description="evaluation rattrapage",
coefficient=1.0,
evaluation_type=scu.EVALUATION_RATTRAPAGE,
evaluation_type=Evaluation.EVALUATION_RATTRAPAGE,
)
etud = etuds[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),
description="evaluation session 2",
coefficient=1.0,
evaluation_type=scu.EVALUATION_SESSION2,
evaluation_type=Evaluation.EVALUATION_SESSION2,
)
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
@ -182,3 +183,43 @@ def test_notes_rattrapage(test_client):
)
# Note moyenne: revient à note normale
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
from app.models import FormSemestreInscription, Identite
from app.models import Evaluation, FormSemestreInscription, Identite, ModuleImpl
from config import TestConfig
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 sco_assiduites as scass
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_cursus_dut
from app.scodoc import sco_saisie_notes
@ -81,7 +80,7 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
module_id=module_id,
formsemestre_id=formsemestre_id,
)
moduleimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
# --- Inscription des étudiants
for etud in etuds:
G.inscrit_etudiant(formsemestre_id, etud)
@ -97,17 +96,18 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
assert ins.parcour is None
# --- Création évaluation
e = G.create_evaluation(
moduleimpl_id=moduleimpl_id,
e1 = Evaluation.create(
moduleimpl=moduleimpl,
date_debut=datetime.datetime(2020, 1, 1),
description="evaluation test",
coefficient=1.0,
)
db.session.commit()
# --- Saisie toutes les notes de l'évaluation
for idx, etud in enumerate(etuds):
etudids_changed, nb_suppress, existing_decisions = G.create_note(
evaluation_id=e["evaluation_id"],
evaluation_id=e1.id,
etudid=etud["etudid"],
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:
b = sco_bulletins.formsemestre_bulletinetud_dict(formsemestre_id, etud["etudid"])
# 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["nb_inscrits"] == len(etuds)
assert etat["nb_notes"] == len(etuds)
@ -131,30 +131,32 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
)
# --- Une autre évaluation
e2 = G.create_evaluation(
moduleimpl_id=moduleimpl_id,
e2 = Evaluation.create(
moduleimpl=moduleimpl,
date_debut=datetime.datetime(2020, 1, 2),
description="evaluation test 2",
coefficient=1.0,
)
db.session.commit()
# Saisie les notes des 5 premiers étudiants:
for idx, etud in enumerate(etuds[:5]):
etudids_changed, nb_suppress, existing_decisions = G.create_note(
evaluation_id=e2["evaluation_id"],
evaluation_id=e2.id,
etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)],
)
# 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
# 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"]
# Modifie l'évaluation 2 pour "prise en compte immédiate"
e2["publish_incomplete"] = True
sco_evaluation_db.do_evaluation_edit(e2)
etat = sco_evaluations.do_evaluation_etat(e2["evaluation_id"])
e2.publish_incomplete = True
db.session.add(e2)
db.session.flush()
etat = sco_evaluations.do_evaluation_etat(e2.id)
assert etat["evalcomplete"] is False
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)
@ -162,26 +164,26 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
# Saisie des notes qui manquent:
for idx, etud in enumerate(etuds[5:]):
etudids_changed, nb_suppress, existing_decisions = G.create_note(
evaluation_id=e2["evaluation_id"],
evaluation_id=e2.id,
etudid=etud["etudid"],
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["nb_att"] == 0
assert not etat["evalattente"] # toutes les notes sont présentes
# --- Suppression des notes
sco_saisie_notes.evaluation_suppress_alln(e["evaluation_id"], dialog_confirmed=True)
etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"])
sco_saisie_notes.evaluation_suppress_alln(e1.id, dialog_confirmed=True)
etat = sco_evaluations.do_evaluation_etat(e1.id)
assert etat["nb_notes"] == 0
assert not etat["evalcomplete"]
# --- Saisie des notes manquantes
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
etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"])
etat = sco_evaluations.do_evaluation_etat(e1.id)
assert etat["evalcomplete"]
# -----------------------

View File

@ -78,6 +78,13 @@ def import_formation(dept_id: int) -> Formation:
)
formation.referentiel_competence_id = ref_comp.id
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()
return formation