Merge branch 'offSco' into assiduites_fixes

This commit is contained in:
Iziram 2024-06-19 19:51:52 +02:00
commit 72d705d90d
29 changed files with 681 additions and 439 deletions

View File

@ -29,7 +29,7 @@ from app.models.but_refcomp import (
ApcReferentielCompetences,
)
from app.models.ues import UEParcours
from app.models.but_validations import ApcValidationRCUE
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
from app.models.etudiants import Identite
from app.models.formations import Formation
from app.models.formsemestre import FormSemestre
@ -42,7 +42,7 @@ from app.scodoc import sco_cursus_dut
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
"""Pour compat ScoDoc 7: à revoir pour le BUT"""
"""Pour compat ScoDoc 7"""
def __init__(self, etud: Identite, formsemestre_id: int, res: ResultatsSemestreBUT):
super().__init__(etud, formsemestre_id, res)
@ -54,8 +54,22 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
return False
def parcours_validated(self):
"True si le parcours est validé"
return False # XXX TODO
"True si le parcours (ici diplôme BUT) est validé"
return but_parcours_validated(
self.etud.id, self.cur_sem.formation.referentiel_competence_id
)
def but_parcours_validated(etudid: int, referentiel_competence_id: int) -> bool:
"""Détermine si le parcours BUT est validé:
ne regarde que si une validation BUT3 est enregistrée
"""
return any(
sco_codes.code_annee_validant(v.code)
for v in ApcValidationAnnee.query.filter_by(
etudid=etudid, ordre=3, referentiel_competence_id=referentiel_competence_id
)
)
class EtudCursusBUT:
@ -287,81 +301,81 @@ class FormSemestreCursusBUT:
)
return niveaux_by_annee
def get_etud_validation_par_competence_et_annee(self, etud: Identite):
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
validation_par_competence_et_annee = {}
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
# On s'assurer qu'elle concerne notre cursus !
ue = validation_rcue.ue2
if ue.id not in self.ue_ids:
if (
ue.formation.referentiel_competences_id
== self.referentiel_competences_id
):
self.ue_ids = ue.id
else:
continue # skip this validation
niveau = validation_rcue.niveau()
if not niveau.competence.id in validation_par_competence_et_annee:
validation_par_competence_et_annee[niveau.competence.id] = {}
previous_validation = validation_par_competence_et_annee.get(
niveau.competence.id
).get(validation_rcue.annee())
# prend la "meilleure" validation
if (not previous_validation) or (
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
):
self.validation_par_competence_et_annee[niveau.competence.id][
niveau.annee
] = validation_rcue
return validation_par_competence_et_annee
# def get_etud_validation_par_competence_et_annee(self, etud: Identite):
# """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
# validation_par_competence_et_annee = {}
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
# # On s'assurer qu'elle concerne notre cursus !
# ue = validation_rcue.ue2
# if ue.id not in self.ue_ids:
# if (
# ue.formation.referentiel_competences_id
# == self.referentiel_competences_id
# ):
# self.ue_ids = ue.id
# else:
# continue # skip this validation
# niveau = validation_rcue.niveau()
# if not niveau.competence.id in validation_par_competence_et_annee:
# validation_par_competence_et_annee[niveau.competence.id] = {}
# previous_validation = validation_par_competence_et_annee.get(
# niveau.competence.id
# ).get(validation_rcue.annee())
# # prend la "meilleure" validation
# if (not previous_validation) or (
# sco_codes.BUT_CODES_ORDER[validation_rcue.code]
# > sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
# ):
# self.validation_par_competence_et_annee[niveau.competence.id][
# niveau.annee
# ] = validation_rcue
# return validation_par_competence_et_annee
def list_etud_inscriptions(self, etud: Identite):
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
self.niveaux_by_annee = {}
"{ annee : liste des niveaux à valider }"
self.niveaux: dict[int, ApcNiveau] = {}
"cache les niveaux"
for annee in (1, 2, 3):
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
annee, [self.parcour] if self.parcour else None # XXX WIP
)[1]
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
niveaux_d[self.parcour.id] if self.parcour else []
)
self.niveaux.update(
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
)
# def list_etud_inscriptions(self, etud: Identite):
# "Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
# self.niveaux_by_annee = {}
# "{ annee : liste des niveaux à valider }"
# self.niveaux: dict[int, ApcNiveau] = {}
# "cache les niveaux"
# for annee in (1, 2, 3):
# niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
# annee, [self.parcour] if self.parcour else None # XXX WIP
# )[1]
# # groupe les niveaux de tronc commun et ceux spécifiques au parcour
# self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
# niveaux_d[self.parcour.id] if self.parcour else []
# )
# self.niveaux.update(
# {niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
# )
self.validation_par_competence_et_annee = {}
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
niveau = validation_rcue.niveau()
if not niveau.competence.id in self.validation_par_competence_et_annee:
self.validation_par_competence_et_annee[niveau.competence.id] = {}
previous_validation = self.validation_par_competence_et_annee.get(
niveau.competence.id
).get(validation_rcue.annee())
# prend la "meilleure" validation
if (not previous_validation) or (
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
):
self.validation_par_competence_et_annee[niveau.competence.id][
niveau.annee
] = validation_rcue
# self.validation_par_competence_et_annee = {}
# """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
# niveau = validation_rcue.niveau()
# if not niveau.competence.id in self.validation_par_competence_et_annee:
# self.validation_par_competence_et_annee[niveau.competence.id] = {}
# previous_validation = self.validation_par_competence_et_annee.get(
# niveau.competence.id
# ).get(validation_rcue.annee())
# # prend la "meilleure" validation
# if (not previous_validation) or (
# sco_codes.BUT_CODES_ORDER[validation_rcue.code]
# > sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
# ):
# self.validation_par_competence_et_annee[niveau.competence.id][
# niveau.annee
# ] = validation_rcue
self.competences = {
competence.id: competence
for competence in (
self.parcour.query_competences()
if self.parcour
else self.formation.referentiel_competence.get_competences_tronc_commun()
)
}
"cache { competence_id : competence }"
# self.competences = {
# competence.id: competence
# for competence in (
# self.parcour.query_competences()
# if self.parcour
# else self.formation.referentiel_competence.get_competences_tronc_commun()
# )
# }
# "cache { competence_id : competence }"
def but_ects_valides(etud: Identite, referentiel_competence_id: int) -> float:

View File

@ -1034,8 +1034,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
return messages
def valide_diplome(self) -> bool:
"Vrai si l'étudiant à validé son diplôme"
return False # TODO XXX
"Vrai si l'étudiant a validé son diplôme (décision enregistrée)"
return self.annee_but == 3 and sco_codes.code_annee_validant(self.code_valide)
def list_ue_parcour_etud(

View File

@ -155,6 +155,7 @@ def pvjury_table_but(
deca = None
ects_but_valides = but_ects_valides(etud, referentiel_competence_id)
has_diplome = deca.valide_diplome()
row = {
"nom_pv": (
etud.code_ine or etud.code_nip or etud.id
@ -181,10 +182,15 @@ def pvjury_table_but(
),
"decision_but": deca.code_valide if deca else "",
"devenir": (
"Diplôme obtenu"
if has_diplome
else (
", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
if deca
else ""
)
),
"diplome": "ADM" if has_diplome else "",
# pour exports excel seulement:
"civilite": etud.civilite_etat_civil_str,
"nom": etud.nom,

View File

@ -219,6 +219,7 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
dec_rcue["code"]}"""
)
decisions["descr_decisions_rcue"] = ", ".join(titres_rcues)
decisions["descr_decisions_rcue_list"] = titres_rcues
decisions["descr_decisions_niveaux"] = (
"Niveaux de compétences: " + decisions["descr_decisions_rcue"]
)

View File

@ -36,6 +36,7 @@ from app.models.config import ScoDocSiteConfig
from app.models.departements import Departement
from app.models.etudiants import Identite
from app.models.evaluations import Evaluation
from app.models.events import ScolarNews
from app.models.formations import Formation
from app.models.groups import GroupDescr, Partition
from app.models.moduleimpls import (
@ -207,6 +208,70 @@ class FormSemestre(models.ScoDocModel):
).first_or_404()
return cls.query.filter_by(id=formsemestre_id).first_or_404()
@classmethod
def create_formsemestre(cls, args: dict, silent=False) -> "FormSemestre":
"""Création d'un formsemestre, avec toutes les valeurs par défaut
et notification (sauf si silent).
Crée la partition par défaut.
"""
# was sco_formsemestre.do_formsemestre_create
if "dept_id" not in args:
args["dept_id"] = g.scodoc_dept_id
formsemestre: "FormSemestre" = cls.create_from_dict(args)
db.session.flush()
for etape in args["etapes"]:
formsemestre.add_etape(etape)
db.session.commit()
for u in args["responsables"]:
formsemestre.responsables.append(u)
# create default partition
partition = Partition(
formsemestre=formsemestre, partition_name=None, numero=1000000
)
db.session.add(partition)
partition.create_group(default=True)
db.session.commit()
if not silent:
url = url_for(
"notes.formsemestre_status",
scodoc_dept=formsemestre.departement.acronym,
formsemestre_id=formsemestre.id,
)
ScolarNews.add(
typ=ScolarNews.NEWS_SEM,
text=f"""Création du semestre <a href="{url}">{formsemestre.titre}</a>""",
url=url,
max_frequency=0,
)
return formsemestre
@classmethod
def convert_dict_fields(cls, args: dict) -> dict:
"""Convert fields in the given dict.
args: dict with args in application.
returns: dict to store in model's db.
"""
if "date_debut" in args:
args["date_debut"] = scu.convert_fr_date(args["date_debut"])
if "date_fin" in args:
args["date_fin"] = scu.convert_fr_date(args["date_debut"])
if "etat" in args:
args["etat"] = bool(args["etat"])
if "bul_bgcolor" in args:
args["bul_bgcolor"] = args.get("bul_bgcolor") or "white"
if "titre" in args:
args["titre"] = args.get("titre") or "sans titre"
return args
@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 'etapes' to excluded."""
# on ne peut pas affecter directement etapes
return super().filter_model_attributes(data, (excluded or set()) | {"etapes"})
def sort_key(self) -> tuple:
"""clé pour tris par ordre de date_debut, le plus ancien en tête
(pour avoir le plus récent d'abord, sort avec reverse=True)"""
@ -729,7 +794,7 @@ class FormSemestre(models.ScoDocModel):
FormSemestre.titre,
)
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
def etapes_apo_vdi(self) -> list["ApoEtapeVDI"]:
"Liste des vdis"
# was read_formsemestre_etapes
return [e.as_apovdi() for e in self.etapes if e.etape_apo]
@ -742,9 +807,9 @@ class FormSemestre(models.ScoDocModel):
return ""
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape]))
def add_etape(self, etape_apo: str):
def add_etape(self, etape_apo: str | ApoEtapeVDI):
"Ajoute une étape"
etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=etape_apo)
etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=str(etape_apo))
db.session.add(etape)
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
@ -973,7 +1038,7 @@ class FormSemestre(models.ScoDocModel):
def etudids_actifs(self) -> tuple[list[int], set[int]]:
"""Liste les etudids inscrits (incluant DEM et DEF),
qui ser al'index des dataframes de notes
qui sera l'index des dataframes de notes
et donne l'ensemble des inscrits non DEM ni DEF.
"""
return [inscr.etudid for inscr in self.inscriptions], {
@ -1271,7 +1336,7 @@ class FormSemestreEtape(db.Model):
def __str__(self):
return self.etape_apo or ""
def as_apovdi(self) -> ApoEtapeVDI:
def as_apovdi(self) -> "ApoEtapeVDI":
return ApoEtapeVDI(self.etape_apo)

View File

@ -93,6 +93,10 @@ class Partition(ScoDocModel):
):
group.remove_etud(etud)
def is_default(self) -> bool:
"vrai si partition par défault (tous les étudiants)"
return not self.partition_name
def is_parcours(self) -> bool:
"Vrai s'il s'agit de la partition de parcours"
return self.partition_name == scu.PARTITION_PARCOURS

View File

@ -313,9 +313,12 @@ class GenTable:
T.append(l + [self.bottom_titles.get(cid, "") for cid in self.columns_ids])
return T
def get_titles_list(self):
def get_titles_list(self, with_lines_titles=True):
"list of titles"
return [self.titles.get(cid, "") for cid in self.columns_ids]
titles = [self.titles.get(cid, "") for cid in self.columns_ids]
if with_lines_titles:
titles.insert(0, "")
return titles
def gen(self, fmt="html", columns_ids=None):
"""Build representation of the table in the specified format.

View File

@ -569,8 +569,7 @@ class ApoEtud(dict):
# prend le plus récent avec décision
for formsemestre in cur_formsemestres:
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
has_decision = res.etud_has_decision(self.etud.id)
if has_decision:
if apo_data.export_res_sdj or res.etud_has_decision(self.etud.id):
cur_formsemestre = formsemestre
self.cur_res = res
break
@ -639,7 +638,7 @@ class ApoEtud(dict):
has_decision = res.etud_has_decision(self.etud.id)
else:
has_decision = res.get_etud_decision_sem(self.etud.id)
if has_decision:
if has_decision or apo_data.export_res_sdj:
autre_formsemestre = formsemestre
break
if autre_formsemestre is None:

View File

@ -1056,10 +1056,10 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
if current_user.has_permission(Permission.EditFormSemestre):
H.append(
f"""<ul>
<li><a class="stdlink" href="{
<li><b><a class="stdlink" href="{
url_for('notes.formsemestre_createwithmodules', scodoc_dept=g.scodoc_dept,
formation_id=formation_id, semestre_id=1)
}">Mettre en place un nouveau semestre de formation {formation.acronyme}</a>
}">Mettre en place un nouveau semestre de formation {formation.acronyme}</a></b>
</li>
</ul>"""
)
@ -1514,7 +1514,7 @@ def edit_ue_set_code_apogee(id=None, value=None):
ue_id = id
value = value.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
log("edit_ue_set_code_apogee: ue_id=%s code_apogee=%s" % (ue_id, value))
log(f"edit_ue_set_code_apogee: ue_id={ue_id} code_apogee={value}")
ues = ue_list(args={"ue_id": ue_id})
if not ues:

View File

@ -229,11 +229,14 @@ def etapes_apo_str(etapes):
return ", ".join([str(x) for x in etapes])
def do_formsemestre_create(args, silent=False):
def do_formsemestre_create( # DEPRECATED, use FormSemestre.create_formsemestre()
args, silent=False
):
"create a formsemestre"
from app.models import ScolarNews
from app.scodoc import sco_groups
log("Warning: do_formsemestre_create is deprecated")
cnx = ndb.GetDBConnexion()
formsemestre_id = _formsemestreEditor.create(cnx, args)
if args["etapes"]:

View File

@ -37,16 +37,17 @@ from app import db
from app.auth.models import User
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
from app.models import (
Module,
ModuleImpl,
Evaluation,
UniteEns,
ScoDocSiteConfig,
ScolarFormSemestreValidation,
ScolarAutorisationInscription,
ApcValidationAnnee,
ApcValidationRCUE,
Evaluation,
FormSemestreUECoef,
Module,
ModuleImpl,
ScoDocSiteConfig,
ScolarAutorisationInscription,
ScolarFormSemestreValidation,
ScolarNews,
UniteEns,
)
from app.models.formations import Formation
from app.models.formsemestre import FormSemestre
@ -439,12 +440,13 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
{
"size": 32,
"title": "Element(s) Apogée sem.:",
"explanation": "associé(s) au résultat du semestre (ex: VRTW1). Inutile en BUT. Séparés par des virgules.",
"allow_null": not sco_preferences.get_preference(
"always_require_apo_sem_codes"
)
"explanation": """associé(s) au résultat du semestre (ex: VRTW1).
Inutile en BUT. Séparés par des virgules.""",
"allow_null": (
not sco_preferences.get_preference("always_require_apo_sem_codes")
or (formsemestre and formsemestre.modalite == "EXT")
or (formsemestre.formation.is_apc()),
or (formsemestre and formsemestre.formation.is_apc())
),
},
)
)
@ -1250,7 +1252,7 @@ def formsemestre_clone(formsemestre_id):
raise ScoValueError("id responsable invalide")
new_formsemestre_id = do_formsemestre_clone(
formsemestre_id,
resp.id,
resp,
tf[2]["date_debut"],
tf[2]["date_fin"],
clone_evaluations=tf[2]["clone_evaluations"],
@ -1268,7 +1270,7 @@ def formsemestre_clone(formsemestre_id):
def do_formsemestre_clone(
orig_formsemestre_id,
responsable_id, # new resp.
responsable: User, # new resp.
date_debut,
date_fin, # 'dd/mm/yyyy'
clone_evaluations=False,
@ -1281,49 +1283,63 @@ def do_formsemestre_clone(
formsemestre_orig: FormSemestre = FormSemestre.query.get_or_404(
orig_formsemestre_id
)
orig_sem = sco_formsemestre.get_formsemestre(orig_formsemestre_id)
cnx = ndb.GetDBConnexion()
# 1- create sem
args = orig_sem.copy()
args = formsemestre_orig.to_dict()
del args["formsemestre_id"]
args["responsables"] = [responsable_id]
del args["id"]
del args["parcours"] # copiés ensuite
args["responsables"] = [responsable]
args["date_debut"] = date_debut
args["date_fin"] = date_fin
args["etat"] = 1 # non verrouillé
formsemestre_id = sco_formsemestre.do_formsemestre_create(args)
log(f"created formsemestre {formsemestre_id}")
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
formsemestre = FormSemestre.create_formsemestre(args)
log(f"created formsemestre {formsemestre}")
# 2- create moduleimpls
modimpl_orig: ModuleImpl
for modimpl_orig in formsemestre_orig.modimpls:
assert isinstance(modimpl_orig, ModuleImpl)
assert isinstance(modimpl_orig.id, int)
log(f"cloning {modimpl_orig}")
args = modimpl_orig.to_dict(with_module=False)
args["formsemestre_id"] = formsemestre_id
args["formsemestre_id"] = formsemestre.id
modimpl_new = ModuleImpl.create_from_dict(args)
log(f"created ModuleImpl from {args}")
db.session.flush()
# copy enseignants
for ens in modimpl_orig.enseignants:
modimpl_new.enseignants.append(ens)
db.session.add(modimpl_new)
db.session.flush()
log(f"new moduleimpl.id = {modimpl_new.id}")
# optionally, copy evaluations
if clone_evaluations:
e: Evaluation
for e in Evaluation.query.filter_by(moduleimpl_id=modimpl_orig.id):
log(f"cloning evaluation {e.id}")
# copie en enlevant la date
new_eval = e.clone(
not_copying=("date_debut", "date_fin", "moduleimpl_id")
)
new_eval.moduleimpl_id = modimpl_new.id
args = dict(e.__dict__)
args.pop("_sa_instance_state")
args.pop("id")
args["moduleimpl_id"] = modimpl_new.id
new_eval = Evaluation(**args)
db.session.add(new_eval)
db.session.commit()
# Copie les poids APC de l'évaluation
new_eval.set_ue_poids_dict(e.get_ue_poids_dict())
db.session.commit()
# 3- copy uecoefs
objs = sco_formsemestre.formsemestre_uecoef_list(
cnx, args={"formsemestre_id": orig_formsemestre_id}
for ue_coef in FormSemestreUECoef.query.filter_by(
formsemestre_id=formsemestre_orig.id
):
new_ue_coef = FormSemestreUECoef(
formsemestre_id=formsemestre.id,
ue_id=ue_coef.ue_id,
coefficient=ue_coef.coefficient,
)
for obj in objs:
args = obj.copy()
args["formsemestre_id"] = formsemestre_id
_ = sco_formsemestre.formsemestre_uecoef_create(cnx, args)
db.session.add(new_ue_coef)
db.session.flush()
# NB: don't copy notes_formsemestre_custommenu (usually specific)
@ -1335,11 +1351,11 @@ def do_formsemestre_clone(
if not prefs.is_global(pname):
pvalue = prefs[pname]
try:
prefs.base_prefs.set(formsemestre_id, pname, pvalue)
prefs.base_prefs.set(formsemestre.id, pname, pvalue)
except ValueError:
log(
"do_formsemestre_clone: ignoring old preference %s=%s for %s"
% (pname, pvalue, formsemestre_id)
f"""do_formsemestre_clone: ignoring old preference {
pname}={pvalue} for {formsemestre}"""
)
# 5- Copie les parcours
@ -1350,10 +1366,10 @@ def do_formsemestre_clone(
# 6- Copy partitions and groups
if clone_partitions:
sco_groups_copy.clone_partitions_and_groups(
orig_formsemestre_id, formsemestre_id
orig_formsemestre_id, formsemestre.id
)
return formsemestre_id
return formsemestre.id
def formsemestre_delete(formsemestre_id: int) -> str | flask.Response:

View File

@ -794,7 +794,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
<div class="sem-groups-partition-titre">{
'Groupes de ' + partition.partition_name
if partition.partition_name else
'Tous les étudiants'}
('aucun étudiant inscrit' if partition_is_empty else 'Tous les étudiants')}
</div>
<div class="sem-groups-partition-titre">{
"Assiduité" if not partition_is_empty else ""
@ -885,7 +885,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
)
H.append("</div>") # /sem-groups-assi
if partition_is_empty:
if partition_is_empty and not partition.is_default():
H.append(
'<div class="help sem-groups-none">Aucun groupe peuplé dans cette partition'
)

View File

@ -28,7 +28,6 @@
""" Importation des étudiants à partir de fichiers CSV
"""
import collections
import io
import os
import re
@ -64,6 +63,7 @@ import app.scodoc.sco_utils as scu
FORMAT_FILE = "format_import_etudiants.txt"
# Champs modifiables via "Import données admission"
# (nom/prénom modifiables en mode "avec etudid")
ADMISSION_MODIFIABLE_FIELDS = (
"code_nip",
"code_ine",
@ -132,19 +132,27 @@ def sco_import_format(with_codesemestre=True):
return r
def sco_import_format_dict(with_codesemestre=True):
def sco_import_format_dict(with_codesemestre=True, use_etudid: bool = False):
"""Attribut: { 'type': , 'table', 'allow_nulls' , 'description' }"""
fmt = sco_import_format(with_codesemestre=with_codesemestre)
R = collections.OrderedDict()
formats = {}
for l in fmt:
R[l[0]] = {
formats[l[0]] = {
"type": l[1],
"table": l[2],
"allow_nulls": l[3],
"description": l[4],
"aliases": l[5],
}
return R
if use_etudid:
formats["etudid"] = {
"type": "int",
"table": "identite",
"allow_nulls": False,
"description": "",
"aliases": ["etudid", "id"],
}
return formats
def sco_import_generate_excel_sample(
@ -188,8 +196,7 @@ def sco_import_generate_excel_sample(
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
members = groups_infos.members
log(
"sco_import_generate_excel_sample: group_ids=%s %d members"
% (group_ids, len(members))
f"sco_import_generate_excel_sample: group_ids={group_ids}, {len(members)} members"
)
titles = ["etudid"] + titles
titles_styles = [style] + titles_styles
@ -234,21 +241,26 @@ def students_import_excel(
exclude_cols=["photo_filename"],
)
if return_html:
if formsemestre_id:
dest = url_for(
dest_url = (
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
else:
dest = url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
if formsemestre_id
else url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
)
H = [html_sco_header.sco_header(page_title="Import etudiants")]
H.append("<ul>")
for d in diag:
H.append("<li>%s</li>" % d)
H.append("</ul>")
H.append("<p>Import terminé !</p>")
H.append('<p><a class="stdlink" href="%s">Continuer</a></p>' % dest)
H.append(f"<li>{d}</li>")
H.append(
f"""
</ul>)
<p>Import terminé !</p>
<p><a class="stdlink" href="{dest_url}">Continuer</a></p>
"""
)
return "\n".join(H) + html_sco_header.sco_footer()
@ -308,13 +320,13 @@ def scolars_import_excel_file(
titleslist = []
for t in fs:
if t not in titles:
raise ScoValueError('Colonne invalide: "%s"' % t)
raise ScoValueError(f'Colonne invalide: "{t}"')
titleslist.append(t) #
# ok, same titles
# Start inserting data, abort whole transaction in case of error
created_etudids = []
np_imported_homonyms = 0
GroupIdInferers = {}
group_id_inferer = {}
try: # --- begin DB transaction
linenum = 0
for line in data[1:]:
@ -429,7 +441,7 @@ def scolars_import_excel_file(
_import_one_student(
formsemestre_id,
values,
GroupIdInferers,
group_id_inferer,
annee_courante,
created_etudids,
linenum,
@ -496,13 +508,14 @@ def scolars_import_excel_file(
def students_import_admission(
csvfile, type_admission="", formsemestre_id=None, return_html=True
):
csvfile, type_admission="", formsemestre_id=None, return_html=True, use_etudid=False
) -> str:
"import donnees admission from Excel file (v2016)"
diag = scolars_import_admission(
csvfile,
formsemestre_id=formsemestre_id,
type_admission=type_admission,
use_etudid=use_etudid,
)
if return_html:
H = [html_sco_header.sco_header(page_title="Import données admissions")]
@ -524,6 +537,7 @@ def students_import_admission(
)
return "\n".join(H) + html_sco_header.sco_footer()
return ""
def _import_one_student(
@ -599,13 +613,15 @@ def _is_new_ine(cnx, code_ine):
# ------ Fonction ré-écrite en nov 2016 pour lire des fichiers sans etudid (fichiers APB)
def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None):
def scolars_import_admission(
datafile, formsemestre_id=None, type_admission=None, use_etudid=False
):
"""Importe données admission depuis un fichier Excel quelconque
par exemple ceux utilisés avec APB
par exemple ceux utilisés avec APB, avec ou sans etudid
Cherche dans ce fichier les étudiants qui correspondent à des inscrits du
semestre formsemestre_id.
Le fichier n'a pas l'INE ni le NIP ni l'etudid, la correspondance se fait
Si le fichier n'a pas d'etudid (use_etudid faux), la correspondance se fait
via les noms/prénoms qui doivent être égaux (la casse, les accents et caractères spéciaux
étant ignorés).
@ -617,20 +633,21 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
dans le fichier importé) du champ type_admission.
Si une valeur existe ou est présente dans le fichier importé, ce paramètre est ignoré.
TODO:
- choix onglet du classeur
"""
log(f"scolars_import_admission: formsemestre_id={formsemestre_id}")
diag: list[str] = []
members = sco_groups.get_group_members(
sco_groups.get_default_group(formsemestre_id)
)
etuds_by_nomprenom = {} # { nomprenom : etud }
diag = []
etuds_by_etudid = {} # { etudid : etud }
if use_etudid:
etuds_by_etudid = {m["etudid"]: m for m in members}
else:
for m in members:
np = (adm_normalize_string(m["nom"]), adm_normalize_string(m["prenom"]))
if np in etuds_by_nomprenom:
msg = "Attention: hononymie pour %s %s" % (m["nom"], m["prenom"])
msg = f"""Attention: hononymie pour {m["nom"]} {m["prenom"]}"""
log(msg)
diag.append(msg)
etuds_by_nomprenom[np] = m
@ -644,19 +661,29 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
titles = data[0]
# idx -> ('field', convertor)
fields = adm_get_fields(titles, formsemestre_id)
idx_nom = None
idx_prenom = None
fields = adm_get_fields(titles, formsemestre_id, use_etudid=use_etudid)
idx_nom = idx_prenom = idx_etudid = None
for idx, field in fields.items():
if field[0] == "nom":
match field[0]:
case "nom":
idx_nom = idx
if field[0] == "prenom":
case "prenom":
idx_prenom = idx
if (idx_nom is None) or (idx_prenom is None):
case "etudid":
idx_etudid = idx
if (not use_etudid and ((idx_nom is None) or (idx_prenom is None))) or (
use_etudid and idx_etudid is None
):
log("fields indices=" + ", ".join([str(x) for x in fields]))
log("fields titles =" + ", ".join([fields[x][0] for x in fields]))
log("fields titles =" + ", ".join([x[0] for x in fields.values()]))
raise ScoFormatError(
"scolars_import_admission: colonnes nom et prenom requises",
(
"""colonne etudid requise
(si l'option "Utiliser l'identifiant d'étudiant ScoDoc" est cochée)"""
if use_etudid
else "colonnes nom et prenom requises"
),
dest_url=url_for(
"scolar.form_students_import_infos_admissions",
scodoc_dept=g.scodoc_dept,
@ -665,18 +692,31 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
)
modifiable_fields = set(ADMISSION_MODIFIABLE_FIELDS)
if use_etudid:
modifiable_fields |= {"nom", "prenom"}
nline = 2 # la premiere ligne de donnees du fichier excel est 2
n_import = 0
for line in data[1:]:
if use_etudid:
try:
etud = etuds_by_etudid.get(int(line[idx_etudid]))
except ValueError:
etud = None
if not etud:
msg = f"""Étudiant avec code etudid=<b>{line[idx_etudid]}</b> inexistant"""
diag.append(msg)
else:
# Retrouve l'étudiant parmi ceux du semestre par (nom, prenom)
nom = adm_normalize_string(line[idx_nom])
prenom = adm_normalize_string(line[idx_prenom])
if (nom, prenom) not in etuds_by_nomprenom:
msg = f"""Étudiant <b>{line[idx_nom]} {line[idx_prenom]} inexistant</b>"""
etud = etuds_by_nomprenom.get((nom, prenom))
if not etud:
msg = (
f"""Étudiant <b>{line[idx_nom]} {line[idx_prenom]}</b> inexistant"""
)
diag.append(msg)
else:
etud = etuds_by_nomprenom[(nom, prenom)]
if etud:
cur_adm = sco_etud.admission_list(cnx, args={"id": etud["admission_id"]})[0]
# peuple les champs presents dans le tableau
args = {}
@ -758,19 +798,19 @@ def adm_normalize_string(s):
)
def adm_get_fields(titles, formsemestre_id):
def adm_get_fields(titles, formsemestre_id: int, use_etudid: bool = False):
"""Cherche les colonnes importables dans les titres (ligne 1) du fichier excel
return: { idx : (field_name, convertor) }
"""
format_dict = sco_import_format_dict()
format_dict = sco_import_format_dict(use_etudid=use_etudid)
fields = {}
idx = 0
for title in titles:
title_n = adm_normalize_string(title)
for k in format_dict:
for v in format_dict[k]["aliases"]:
for k, fmt in format_dict.items():
for v in fmt["aliases"]:
if adm_normalize_string(v) == title_n:
typ = format_dict[k]["type"]
typ = fmt["type"]
if typ == "real":
convertor = adm_convert_real
elif typ == "integer" or typ == "int":

View File

@ -140,7 +140,7 @@ def read_users_excel_file(datafile, titles=TITLES) -> list[dict]:
for line in data[1:]:
d = {}
for i, field in enumerate(xls_titles):
d[field] = line[i]
d[field] = (line[i] or "").strip()
users.append(d)
return users

View File

@ -70,13 +70,21 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
menu_eval = [
{
"title": "Saisir notes",
"title": "Saisir les notes",
"endpoint": "notes.saisie_notes",
"args": {
"evaluation_id": evaluation_id,
},
"enabled": can_edit_notes_ens,
},
{
"title": "Saisir par fichier tableur",
"id": "menu_saisie_tableur",
"endpoint": "notes.saisie_notes_tableur",
"args": {
"evaluation_id": evaluation.id,
},
},
{
"title": "Modifier évaluation",
"endpoint": "notes.evaluation_edit",

View File

@ -257,7 +257,9 @@ def pdf_lettre_individuelle(sem, decision, etud: Identite, params, signature=Non
else:
params["autorisations_txt"] = ""
if decision["decision_sem"] and situation_etud.parcours_validated():
if (
formsemestre.formation.is_apc() or decision["decision_sem"]
) and situation_etud.parcours_validated():
params["diplome_txt"] = (
"""Vous avez donc obtenu le diplôme : <b>%(titre_formation)s</b>""" % params
)
@ -357,5 +359,8 @@ def add_apc_infos(formsemestre: FormSemestre, params: dict, decision: dict):
params[
"decision_ue_txt"
] = f"""{params["decision_ue_txt"]}<br/>
<b>Niveaux de compétences:</b><br/> {decision.get("descr_decisions_rcue") or ""}
<b>Niveaux de compétences:</b>
<br/>&nbsp;&nbsp;&nbsp;&nbsp;- {
'<br/>&nbsp;&nbsp;&nbsp;&nbsp;- '.join( decision.get("descr_decisions_rcue_list", []) )
}
"""

View File

@ -248,8 +248,6 @@ def formsemestre_report(
result="codedecision",
category_name="",
result_name="",
title="Statistiques",
only_primo=None,
):
"""
Tableau sur résultats (result) par type de category bac
@ -277,9 +275,6 @@ def formsemestre_report(
f"Répartition des résultats par {category_name}, semestre {sem['titreannee']}"
)
tab.html_caption = f"Répartition des résultats par {category_name}."
tab.base_url = "%s?formsemestre_id=%s" % (request.base_url, formsemestre_id)
if only_primo:
tab.base_url += "&only_primo=on"
return tab
@ -326,8 +321,15 @@ def formsemestre_report_counts(
category=category,
result=result,
category_name=category_name,
title=title,
only_primo=only_primo,
)
tab.base_url = url_for(
"notes.formsemestre_report_counts",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
category=category,
only_primo=int(bool(only_primo)),
result=result,
group_ids=group_ids,
)
if len(formsemestre.inscriptions) == 0:
F = ["""<p><em>Aucun étudiant</em></p>"""]

View File

@ -29,12 +29,15 @@
Formulaire revu en juillet 2016
"""
import html
import time
import psycopg2
import flask
from flask import g, url_for, request
from flask_login import current_user
from flask_sqlalchemy.query import Query
import psycopg2
from app import db, log
from app.auth.models import User
@ -75,8 +78,6 @@ import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import json_error
from app.scodoc.sco_utils import ModuleType
from flask_sqlalchemy.query import Query
def convert_note_from_string(
note: str,
@ -115,7 +116,7 @@ def convert_note_from_string(
return note_value, invalid
def _displayNote(val):
def _display_note(val):
"""Convert note from DB to viewable string.
Utilisé seulement pour I/O vers formulaires (sans perte de precision)
(Utiliser fmt_note pour les affichages)
@ -272,7 +273,7 @@ def do_evaluation_upload_xls():
diag.append("Notes invalides pour: " + ", ".join(etudsnames))
raise InvalidNoteValue()
else:
etudids_changed, nb_suppress, etudids_with_decisions = notes_add(
etudids_changed, nb_suppress, etudids_with_decisions, messages = notes_add(
current_user, evaluation_id, valid_notes, comment
)
# news
@ -292,9 +293,19 @@ def do_evaluation_upload_xls():
max_frequency=30 * 60, # 30 minutes
)
msg = f"""<p>{len(etudids_changed)} notes changées ({len(withoutnotes)} sans notes, {
len(absents)} absents, {nb_suppress} note supprimées)
msg = f"""<p>
{len(etudids_changed)} notes changées ({len(withoutnotes)} sans notes,
{len(absents)} absents, {nb_suppress} note supprimées)
</p>"""
if messages:
msg += f"""<div class="warning">Attention&nbsp;:
<ul>
<li>{
'</li><li>'.join(messages)
}
</li>
</ul>
</div>"""
if etudids_with_decisions:
msg += """<p class="warning">Important: il y avait déjà des décisions de jury
enregistrées, qui sont peut-être à revoir suite à cette modification !</p>
@ -322,7 +333,7 @@ def do_evaluation_set_etud_note(evaluation: Evaluation, etud: Identite, value) -
# Convert and check value
L, invalids, _, _, _ = _check_notes([(etud.id, value)], evaluation)
if len(invalids) == 0:
etudids_changed, _, _ = notes_add(
etudids_changed, _, _, _ = notes_add(
current_user, evaluation.id, L, "Initialisation notes"
)
if len(etudids_changed) == 1:
@ -398,7 +409,9 @@ def do_evaluation_set_missing(
)
# ok
comment = "Initialisation notes manquantes"
etudids_changed, _, _ = notes_add(current_user, evaluation_id, valid_notes, comment)
etudids_changed, _, _, _ = notes_add(
current_user, evaluation_id, valid_notes, comment
)
# news
url = url_for(
"notes.moduleimpl_status",
@ -456,7 +469,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
)
if not dialog_confirmed:
etudids_changed, nb_suppress, existing_decisions = notes_add(
etudids_changed, nb_suppress, existing_decisions, _ = notes_add(
current_user, evaluation_id, notes, do_it=False, check_inscription=False
)
msg = f"""<p>Confirmer la suppression des {nb_suppress} notes ?
@ -477,7 +490,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
)
# modif
etudids_changed, nb_suppress, existing_decisions = notes_add(
etudids_changed, nb_suppress, existing_decisions, _ = notes_add(
current_user,
evaluation_id,
notes,
@ -519,7 +532,7 @@ def notes_add(
comment=None,
do_it=True,
check_inscription=True,
) -> tuple[list[int], int, list[int]]:
) -> tuple[list[int], int, list[int], list[str]]:
"""
Insert or update notes
notes is a list of tuples (etudid,value)
@ -528,30 +541,48 @@ def notes_add(
Nota:
- si la note existe deja avec valeur distincte, ajoute une entree au log (notes_notes_log)
Return: tuple (etudids_changed, nb_suppress, etudids_with_decision)
Return: tuple (etudids_changed, nb_suppress, etudids_with_decision, messages)
messages = list de messages d'avertissement/information pour l'utilisateur
"""
evaluation = Evaluation.get_evaluation(evaluation_id)
now = psycopg2.Timestamp(*time.localtime()[:6])
# Vérifie inscription et valeur note
inscrits = {
messages = []
# Vérifie inscription au module (même DEM/DEF)
etudids_inscrits_mod = {
x[0]
for x in sco_groups.do_evaluation_listeetuds_groups(
evaluation_id, getallstudents=True, include_demdef=True
)
}
# Les étudiants inscrits au semestre ni DEM ni DEF
_, etudids_actifs = evaluation.moduleimpl.formsemestre.etudids_actifs()
# Les étudiants inscrits au semestre et ceux "actifs" (ni DEM ni DEF)
etudids_inscrits_sem, etudids_actifs = (
evaluation.moduleimpl.formsemestre.etudids_actifs()
)
for etudid, value in notes:
if check_inscription and (
(etudid not in inscrits) or (etudid not in etudids_actifs)
):
if check_inscription:
msg_err, msg_warn = "", ""
if etudid not in etudids_inscrits_sem:
msg_err = "non inscrit au semestre"
elif etudid not in etudids_inscrits_mod:
msg_err = "non inscrit au module"
elif etudid not in etudids_actifs:
# DEM ou DEF
msg_warn = "démissionnaire ou défaillant (note enregistrée)"
if msg_err or msg_warn:
etud = Identite.query.get(etudid) if isinstance(etudid, int) else None
msg = f"étudiant {etud.nomprenom if etud else etudid} {msg_err or msg_warn}"
if msg_err:
log(f"notes_add: {etudid} non inscrit ou DEM/DEF: aborting")
raise NoteProcessError(f"étudiant {etudid} non inscrit dans ce module")
raise NoteProcessError(msg)
if msg_warn:
messages.append(msg)
if (value is not None) and not isinstance(value, float):
log(f"notes_add: {etudid} valeur de note invalide ({value}): aborting")
etud = Identite.query.get(etudid) if isinstance(etudid, int) else None
raise NoteProcessError(
f"etudiant {etudid}: valeur de note invalide ({value})"
f"etudiant {etud.nomprenom if etud else etudid}: valeur de note invalide ({value})"
)
# Recherche notes existantes
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
@ -566,20 +597,66 @@ def notes_add(
etudids_with_decision = []
try:
for etudid, value in notes:
changed = False
if etudid not in notes_db:
# nouvelle note
if value != scu.NOTES_SUPPRESS:
changed, suppressed = _record_note(
cursor,
notes_db,
etudid,
evaluation_id,
value,
comment=comment,
user=user,
date=now,
do_it=do_it,
)
if suppressed:
nb_suppress += 1
if changed:
etudids_changed.append(etudid)
if res.etud_has_decision(etudid, include_rcues=False):
etudids_with_decision.append(etudid)
except Exception as exc:
log("*** exception in notes_add")
if do_it:
cnx.rollback() # abort
# inval cache
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
sco_cache.EvaluationCache.delete(evaluation_id)
raise ScoException from exc
if do_it:
cnx.commit()
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
sco_cache.EvaluationCache.delete(evaluation_id)
return etudids_changed, nb_suppress, etudids_with_decision, messages
def _record_note(
cursor,
notes_db,
etudid: int,
evaluation_id: int,
value: float,
comment: str = "",
user: User = None,
date=None,
do_it=False,
):
"Enregistrement de la note en base"
changed = False
suppressed = False
args = {
"etudid": etudid,
"evaluation_id": evaluation_id,
"value": value,
"comment": comment,
# convention scodoc7 quote comment:
"comment": (html.escape(comment) if isinstance(comment, str) else comment),
"uid": user.id,
"date": now,
"date": date,
}
ndb.quote_dict(args)
if etudid not in notes_db:
# nouvelle note
if value != scu.NOTES_SUPPRESS:
if do_it:
# Note: le conflit ci-dessous peut arriver si un autre thread
# a modifié la base après qu'on ait lu notes_db
cursor.execute(
@ -600,9 +677,7 @@ def notes_add(
oldval = notes_db[etudid]["value"]
if type(value) != type(oldval):
changed = True
elif isinstance(value, float) and (
abs(value - oldval) > scu.NOTES_PRECISION
):
elif isinstance(value, float) and (abs(value - oldval) > scu.NOTES_PRECISION):
changed = True
elif value != oldval:
changed = True
@ -617,17 +692,8 @@ def notes_add(
WHERE etudid=%(etudid)s
and evaluation_id=%(evaluation_id)s
""",
{"etudid": etudid, "evaluation_id": evaluation_id},
args,
)
args = {
"etudid": etudid,
"evaluation_id": evaluation_id,
"value": value,
"date": now,
"comment": comment,
"uid": user.id,
}
ndb.quote_dict(args)
if value != scu.NOTES_SUPPRESS:
if do_it:
cursor.execute(
@ -661,24 +727,8 @@ def notes_add(
""",
args,
)
nb_suppress += 1
if changed:
etudids_changed.append(etudid)
if res.etud_has_decision(etudid, include_rcues=False):
etudids_with_decision.append(etudid)
except Exception as exc:
log("*** exception in notes_add")
if do_it:
cnx.rollback() # abort
# inval cache
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
sco_cache.EvaluationCache.delete(evaluation_id)
raise ScoException from exc
if do_it:
cnx.commit()
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
sco_cache.EvaluationCache.delete(evaluation_id)
return etudids_changed, nb_suppress, etudids_with_decision
suppressed = True
return changed, suppressed
def saisie_notes_tableur(evaluation_id, group_ids=()):
@ -703,7 +753,7 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
)
page_title = "Saisie des notes" + (
f"""de {evaluation.description}""" if evaluation.description else ""
f""" de {evaluation.description}""" if evaluation.description else ""
)
# Informations sur les groupes à afficher:
@ -797,9 +847,13 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
}">
Revenir au tableau de bord du module</a>
&nbsp;&nbsp;&nbsp;
<a class="stdlink" href="{url_for("notes.saisie_notes_tableur",
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
}">Charger un autre fichier de notes</a>
&nbsp;&nbsp;&nbsp;
<a class="stdlink" href="{url_for("notes.saisie_notes",
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
}">Charger d'autres notes dans cette évaluation</a>
}">Formulaire de saisie des notes</a>
</p>"""
)
else:
@ -1015,7 +1069,7 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
"Autres opérations",
[
{
"title": "Saisie par fichier tableur",
"title": "Saisir par fichier tableur",
"id": "menu_saisie_tableur",
"endpoint": "notes.saisie_notes_tableur",
"args": {
@ -1126,7 +1180,7 @@ def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: in
# Note actuelle de l'étudiant:
if etudid in notes_db:
e["val"] = _displayNote(notes_db[etudid]["value"])
e["val"] = _display_note(notes_db[etudid]["value"])
comment = notes_db[etudid]["comment"]
if comment is None:
comment = ""
@ -1368,7 +1422,7 @@ def save_notes(
#
valid_notes, _, _, _, _ = _check_notes(notes, evaluation)
if valid_notes:
etudids_changed, _, etudids_with_decision = notes_add(
etudids_changed, _, etudids_with_decision, messages = notes_add(
current_user, evaluation.id, valid_notes, comment=comment, do_it=True
)
ScolarNews.add(
@ -1386,12 +1440,14 @@ def save_notes(
etudid: get_note_history_menu(evaluation.id, etudid)
for etudid in etudids_changed
},
"messages": messages,
}
else:
result = {
"etudids_changed": [],
"etudids_with_decision": [],
"history_menu": [],
"messages": [],
}
return result
@ -1420,7 +1476,7 @@ def get_note_history_menu(evaluation_id: int, etudid: int) -> str:
first = True
for i in history:
jt = i["date"].strftime("le %d/%m/%Y à %H:%M") + " (%s)" % i["user_name"]
dispnote = _displayNote(i["value"])
dispnote = _display_note(i["value"])
if first:
nv = "" # ne repete pas la valeur de la note courante
else:

View File

@ -180,7 +180,7 @@ def external_ue_inscrit_et_note(
description="note externe",
)
# Saisie des notes
_, _, _ = sco_saisie_notes.notes_add(
_, _, _, _ = sco_saisie_notes.notes_add(
current_user,
evaluation.id,
list(notes_etuds.items()),

View File

@ -35,7 +35,7 @@ from flask import url_for, g, request
from flask_login import current_user
from app import db, Departement
from app import Departement
from app.auth.models import Permission, Role, User, UserRole
from app.models import ScoDocSiteConfig, USERNAME_STR_LEN

View File

@ -109,13 +109,17 @@ ETATS_INSCRIPTION = {
}
def convert_fr_date(date_str: str, allow_iso=True) -> datetime.datetime:
def convert_fr_date(
date_str: str | datetime.datetime, allow_iso=True
) -> datetime.datetime:
"""Converti une date saisie par un humain français avant 2070
en un objet datetime.
12/2/1972 => 1972-02-12, 12/2/72 => 1972-02-12, mais 12/2/24 => 2024-02-12
Le pivot est 70.
ScoValueError si date invalide.
"""
if isinstance(date_str, datetime.datetime):
return date_str
try:
return datetime.datetime.strptime(date_str, DATE_FMT)
except ValueError:

View File

@ -30,7 +30,7 @@
from app.scodoc.sco_exceptions import ScoValueError
class ApoEtapeVDI(object):
class ApoEtapeVDI:
"""Classe stockant le VDI avec le code étape (noms de fichiers maquettes et code semestres)"""
_ETAPE_VDI_SEP = "!"
@ -118,7 +118,7 @@ class ApoEtapeVDI(object):
else:
return etape_vdi, ""
def concat_etape_vdi(self, etape, vdi=""):
def concat_etape_vdi(self, etape: str, vdi: str = "") -> str:
if vdi:
return self._ETAPE_VDI_SEP.join([etape, vdi])
else:

View File

@ -84,6 +84,7 @@ class TableJury(TableRecap):
autorisations = res.get_autorisations_inscription()
if res.is_apc:
validations_annee = res.get_validations_annee()
for row in self.rows:
etud = row.etud
if not res.is_apc:
@ -103,7 +104,20 @@ class TableJury(TableRecap):
self.foot_title_row.cells["jury_code_sem"].target_attrs[
"title"
] = """Code jury sur le semestre"""
# Autorisations inscription
# Autorisations inscription ou diplôme BUT S6
if res.is_apc and res.formsemestre.semestre_id == 6:
# on ne vérifie le diplôme que dans ce cas pour ne pas ralentir
if cursus_but.but_parcours_validated(
etud.id, res.formsemestre.formation.referentiel_competence_id
):
row.add_cell(
"autorisations_inscription",
"Passage",
"Diplôme obtenu",
group="jury_code_sem",
classes=["recorded_code"],
)
else:
row.add_cell(
"autorisations_inscription",
"Passage",

View File

@ -52,10 +52,12 @@
else '<span class="missing_ue_ects">aucun</span>'|safe
}} ECTS
{%- endif -%}
{%- if ue.code_apogee -%}
{{ virg() }} Apo {{ue.code_apogee}}
{%- endif -%}
)
{{ virg() }} Apo:
<span class="{% if editable %}span_apo_edit{% endif %}"
data-url="edit_ue_set_code_apogee"
id="{{ue.id}}" data-placeholder="{{scu.APO_MISSING_CODE_STR}}">
{{ue.code_apogee or ''
}}</span>)
</span>
</span>

View File

@ -2365,17 +2365,24 @@ def form_students_import_infos_admissions(formsemestre_id=None):
Les données sont affichées sur les fiches individuelles des étudiants.
</p>
</div>
<div class="help">
<p>
Importer ici la feuille excel utilisée pour envoyer le classement Parcoursup.
Vous pouvez importer ici la feuille excel utilisée pour envoyer
le classement Parcoursup.
Seuls les étudiants actuellement inscrits dans ce semestre ScoDoc seront affectés,
les autres lignes de la feuille seront ignorées.
Et seules les colonnes intéressant ScoDoc
seront importées: il est inutile d'éliminer les autres.
<br>
</p>
<p>
<em>Seules les données "admission" seront modifiées
(et pas l'identité de l'étudiant).</em>
<br>
<em>Les colonnes "nom" et "prenom" sont requises, ou bien une colonne "etudid".</em>
</p>
<p>
<em>Les colonnes "nom" et "prenom" sont requises,
ou bien une colonne "etudid" si la case
"Utiliser l'identifiant d'étudiant ScoDoc" est cochée.
</em>
</p>
<p>
Avant d'importer vos données, il est recommandé d'enregistrer
@ -2386,6 +2393,7 @@ def form_students_import_infos_admissions(formsemestre_id=None):
}">exporter les données actuelles de ScoDoc</a>
(ce fichier peut être -importé après d'éventuelles modifications)
</p>
</div>
""",
]
@ -2397,6 +2405,15 @@ def form_students_import_infos_admissions(formsemestre_id=None):
"csvfile",
{"title": "Fichier Excel:", "input_type": "file", "size": 40},
),
(
"use_etudid",
{
"input_type": "boolcheckbox",
"title": "Utiliser l'identifiant d'étudiant ScoDoc (<tt>etudid</tt>)",
"explanation": """si cochée, utilise le code pour retrouver dans ScoDoc
les étudiants du fichier excel. Sinon, utilise les noms/prénoms.""",
},
),
(
"type_admission",
{
@ -2436,6 +2453,7 @@ def form_students_import_infos_admissions(formsemestre_id=None):
tf[2]["csvfile"],
type_admission=tf[2]["type_admission"],
formsemestre_id=formsemestre_id,
use_etudid=tf[2]["use_etudid"],
)

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.6.975"
SCOVERSION = "9.6.978"
SCONAME = "ScoDoc"

View File

@ -2,6 +2,7 @@
Vérif moyennes de modules des bulletins
et aussi moyennes modules et UE internes (via nt)
"""
import datetime
import numpy as np
from flask import g
@ -107,16 +108,16 @@ def test_notes_modules(test_client):
# --- Notes ordinaires
note_1 = 12.0
note_2 = 13.0
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e1["evaluation_id"], etudid=etuds[0]["etudid"], note=note_1
)
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e2["evaluation_id"], etudid=etuds[0]["etudid"], note=note_2
)
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e1["evaluation_id"], etudid=etuds[1]["etudid"], note=note_1 / 2
)
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e2["evaluation_id"], etudid=etuds[1]["etudid"], note=note_2 / 3
)
b = sco_bulletins.formsemestre_bulletinetud_dict(
@ -139,22 +140,20 @@ def test_notes_modules(test_client):
)
# Absence à une évaluation
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e1["evaluation_id"], etudid=etudid, note=None
) # abs
_, _, _ = G.create_note(
evaluation_id=e2["evaluation_id"], etudid=etudid, note=note_2
)
_ = G.create_note(evaluation_id=e2["evaluation_id"], etudid=etudid, note=note_2)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
note_th = (coef_1 * 0.0 + coef_2 * note_2) / (coef_1 + coef_2)
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_th)
# Absences aux deux évaluations
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e1["evaluation_id"], etudid=etudid, note=None
) # abs
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e2["evaluation_id"], etudid=etudid, note=None
) # abs
b = sco_bulletins.formsemestre_bulletinetud_dict(
@ -171,10 +170,8 @@ def test_notes_modules(test_client):
)
# Note excusée EXC <-> scu.NOTES_NEUTRALISE
_, _, _ = G.create_note(
evaluation_id=e1["evaluation_id"], etudid=etudid, note=note_1
)
_, _, _ = G.create_note(
_ = G.create_note(evaluation_id=e1["evaluation_id"], etudid=etudid, note=note_1)
_ = G.create_note(
evaluation_id=e2["evaluation_id"], etudid=etudid, note=scu.NOTES_NEUTRALISE
) # EXC
b = sco_bulletins.formsemestre_bulletinetud_dict(
@ -190,10 +187,8 @@ def test_notes_modules(test_client):
expected_moy_ue=note_1,
)
# Note en attente ATT <-> scu.NOTES_ATTENTE
_, _, _ = G.create_note(
evaluation_id=e1["evaluation_id"], etudid=etudid, note=note_1
)
_, _, _ = G.create_note(
_ = G.create_note(evaluation_id=e1["evaluation_id"], etudid=etudid, note=note_1)
_ = G.create_note(
evaluation_id=e2["evaluation_id"], etudid=etudid, note=scu.NOTES_ATTENTE
) # ATT
b = sco_bulletins.formsemestre_bulletinetud_dict(
@ -209,10 +204,10 @@ def test_notes_modules(test_client):
expected_moy_ue=note_1,
)
# Neutralisation (EXC) des 2 évals
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e1["evaluation_id"], etudid=etudid, note=scu.NOTES_NEUTRALISE
) # EXC
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e2["evaluation_id"], etudid=etudid, note=scu.NOTES_NEUTRALISE
) # EXC
b = sco_bulletins.formsemestre_bulletinetud_dict(
@ -228,10 +223,10 @@ def test_notes_modules(test_client):
expected_moy_ue=np.nan,
)
# Attente (ATT) sur les 2 evals
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e1["evaluation_id"], etudid=etudid, note=scu.NOTES_ATTENTE
) # ATT
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e2["evaluation_id"], etudid=etudid, note=scu.NOTES_ATTENTE
) # ATT
b = sco_bulletins.formsemestre_bulletinetud_dict(
@ -290,7 +285,7 @@ def test_notes_modules(test_client):
{"etudid": etudid, "moduleimpl_id": moduleimpl_id},
formsemestre_id=formsemestre_id,
)
_, _, _ = G.create_note(evaluation_id=e1["evaluation_id"], etudid=etudid, note=12.5)
_ = G.create_note(evaluation_id=e1["evaluation_id"], etudid=etudid, note=12.5)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
mod_stats = nt.get_mod_stats(moduleimpl_id)
@ -318,9 +313,7 @@ def test_notes_modules(test_client):
description="evaluation mod 2",
coefficient=1.0,
)
_, _, _ = G.create_note(
evaluation_id=e_m2["evaluation_id"], etudid=etudid, note=19.5
)
_ = G.create_note(evaluation_id=e_m2["evaluation_id"], etudid=etudid, note=19.5)
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
ue_status = nt.get_etud_ue_status(etudid, ue_id)
@ -328,22 +321,20 @@ def test_notes_modules(test_client):
# Moyenne d'UE si l'un des modules est EXC ("NA")
# 2 modules, notes EXC dans le premier, note valide n dans le second
# la moyenne de l'UE doit être n
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e1["evaluation_id"], etudid=etudid, note=scu.NOTES_NEUTRALISE
) # EXC
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e2["evaluation_id"], etudid=etudid, note=scu.NOTES_NEUTRALISE
) # EXC
_, _, _ = G.create_note(
evaluation_id=e_m2["evaluation_id"], etudid=etudid, note=12.5
)
_, _, _ = G.create_note(
_ = G.create_note(evaluation_id=e_m2["evaluation_id"], etudid=etudid, note=12.5)
_ = G.create_note(
evaluation_id=e1["evaluation_id"], etudid=etuds[1]["etudid"], note=11.0
)
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e2["evaluation_id"], etudid=etuds[1]["etudid"], note=11.0
)
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e_m2["evaluation_id"], etudid=etuds[1]["etudid"], note=11.0
)
@ -407,12 +398,12 @@ def test_notes_modules_att_dem(test_client):
coefficient=coef_1,
)
# Attente (ATT) sur les 2 evals
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e1["evaluation_id"],
etudid=etuds[0]["etudid"],
note=scu.NOTES_ATTENTE,
) # ATT
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e1["evaluation_id"],
etudid=etuds[1]["etudid"],
note=scu.NOTES_ATTENTE,
@ -455,7 +446,7 @@ def test_notes_modules_att_dem(test_client):
assert note_e1 == scu.NOTES_ATTENTE # XXXX un peu contestable
# Saisie note ABS pour le deuxième etud
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e1["evaluation_id"], etudid=etuds[1]["etudid"], note=None
)
nt = check_nt(

View File

@ -72,8 +72,8 @@ def test_notes_rattrapage(test_client):
evaluation_type=Evaluation.EVALUATION_RATTRAPAGE,
)
etud = etuds[0]
_, _, _ = G.create_note(evaluation_id=e["id"], etudid=etud["etudid"], note=12.0)
_, _, _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=11.0)
_ = G.create_note(evaluation_id=e["id"], etudid=etud["etudid"], note=12.0)
_ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=11.0)
# --- Vérifications internes structures ScoDoc
formsemestre = db.session.get(FormSemestre, formsemestre_id)
@ -98,23 +98,21 @@ def test_notes_rattrapage(test_client):
# Note moyenne: ici le ratrapage est inférieur à la note:
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(12.0)
# rattrapage > moyenne:
_, _, _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=18.0)
_ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=18.0)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(18.0)
# rattrapage vs absences
_, _, _ = G.create_note(
evaluation_id=e["id"], etudid=etud["etudid"], note=None
) # abs
_, _, _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=17.0)
_ = G.create_note(evaluation_id=e["id"], etudid=etud["etudid"], note=None) # abs
_ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=17.0)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(17.0)
# et sans note de rattrapage
_, _, _ = G.create_note(evaluation_id=e["id"], etudid=etud["etudid"], note=10.0)
_, _, _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=None)
_ = G.create_note(evaluation_id=e["id"], etudid=etud["etudid"], note=10.0)
_ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=None)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
@ -159,18 +157,14 @@ def test_notes_rattrapage(test_client):
assert len(mod_res.get_evaluations_completes(moduleimpl)) == 2
# Saisie note session 2:
_, _, _ = G.create_note(
evaluation_id=e_session2["id"], etudid=etud["etudid"], note=5.0
)
_ = G.create_note(evaluation_id=e_session2["id"], etudid=etud["etudid"], note=5.0)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
# Note moyenne: utilise session 2 même si inférieure
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(5.0)
_, _, _ = G.create_note(
evaluation_id=e_session2["id"], etudid=etud["etudid"], note=20.0
)
_ = G.create_note(evaluation_id=e_session2["id"], etudid=etud["etudid"], note=20.0)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
@ -178,16 +172,14 @@ def test_notes_rattrapage(test_client):
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(20.0)
# Met la note session2 à ABS (None)
_, _, _ = G.create_note(
evaluation_id=e_session2["id"], etudid=etud["etudid"], note=None
)
_ = G.create_note(evaluation_id=e_session2["id"], etudid=etud["etudid"], note=None)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
# Note moyenne: zéro car ABS
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(0.0)
# Supprime note session 2
_, _, _ = G.create_note(
_ = G.create_note(
evaluation_id=e_session2["id"], etudid=etud["etudid"], note=scu.NOTES_SUPPRESS
)
b = sco_bulletins.formsemestre_bulletinetud_dict(
@ -216,18 +208,14 @@ def test_notes_rattrapage(test_client):
# 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
)
_ = 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
)
_ = 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"]
)

View File

@ -105,7 +105,7 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
# --- Saisie toutes les notes de l'évaluation
for idx, etud in enumerate(etuds):
etudids_changed, nb_suppress, existing_decisions = G.create_note(
etudids_changed, nb_suppress, existing_decisions, messages = G.create_note(
evaluation_id=e1.id,
etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)],
@ -113,6 +113,7 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
assert not existing_decisions
assert nb_suppress == 0
assert len(etudids_changed) == 1
assert messages == []
# --- Vérifie que les notes sont prises en compte:
b = sco_bulletins.formsemestre_bulletinetud_dict(formsemestre_id, etud["etudid"])
@ -139,11 +140,12 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
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(
etudids_changed, nb_suppress, existing_decisions, messages = G.create_note(
evaluation_id=e2.id,
etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)],
)
assert messages == []
# Cette éval n'est pas complète
etat = sco_evaluations.do_evaluation_etat(e2.id)
assert etat["evalcomplete"] is False
@ -162,11 +164,12 @@ 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(
etudids_changed, nb_suppress, existing_decisions, messages = G.create_note(
evaluation_id=e2.id,
etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)],
)
assert messages == []
etat = sco_evaluations.do_evaluation_etat(e2.id)
assert etat["evalcomplete"]
assert etat["nb_att"] == 0