forked from ScoDoc/DocScoDoc
Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into table
This commit is contained in:
commit
83b47db3dc
@ -550,3 +550,22 @@ def scodoc_flash_status_messages():
|
||||
f"Mode test: mails redirigés vers {email_test_mode_address}",
|
||||
category="warning",
|
||||
)
|
||||
|
||||
|
||||
def critical_error(msg):
|
||||
"""Handle a critical error: flush all caches, display message to the user"""
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
log(f"\n*** CRITICAL ERROR: {msg}")
|
||||
send_scodoc_alarm(f"CRITICAL ERROR: {msg}", msg)
|
||||
clear_scodoc_cache()
|
||||
raise ScoValueError(
|
||||
f"""
|
||||
Une erreur est survenue.
|
||||
|
||||
Si le problème persiste, merci de contacter le support ScoDoc via
|
||||
{scu.SCO_DISCORD_ASSISTANCE}
|
||||
|
||||
{msg}
|
||||
"""
|
||||
)
|
||||
|
@ -19,6 +19,7 @@ from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||
from app.models import GroupDescr, Partition
|
||||
from app.models.groups import group_membership
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
@ -170,24 +171,15 @@ def set_etud_group(etudid: int, group_id: int):
|
||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if etud.id not in {e.id for e in group.partition.formsemestre.etuds}:
|
||||
return json_error(404, "etud non inscrit au formsemestre du groupe")
|
||||
groups = (
|
||||
GroupDescr.query.filter_by(partition_id=group.partition.id)
|
||||
.join(group_membership)
|
||||
.filter_by(etudid=etudid)
|
||||
|
||||
sco_groups.change_etud_group_in_partition(
|
||||
etudid, group_id, group.partition.to_dict()
|
||||
)
|
||||
ok = False
|
||||
for other_group in groups:
|
||||
if other_group.id == group_id:
|
||||
ok = True
|
||||
else:
|
||||
other_group.etuds.remove(etud)
|
||||
if not ok:
|
||||
group.etuds.append(etud)
|
||||
log(f"set_etud_group({etud}, {group})")
|
||||
db.session.commit()
|
||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
||||
|
||||
return jsonify({"group_id": group_id, "etudid": etudid})
|
||||
|
||||
|
||||
@ -207,6 +199,8 @@ def group_remove_etud(group_id: int, etudid: int):
|
||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if etud in group.etuds:
|
||||
group.etuds.remove(etud)
|
||||
db.session.commit()
|
||||
@ -232,6 +226,8 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
groups = (
|
||||
GroupDescr.query.filter_by(partition_id=partition_id)
|
||||
.join(group_membership)
|
||||
@ -262,8 +258,10 @@ def group_create(partition_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not partition.groups_editable:
|
||||
return json_error(404, "partition non editable")
|
||||
return json_error(403, "partition non editable")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
group_name = data.get("group_name")
|
||||
if group_name is None:
|
||||
@ -294,8 +292,10 @@ def group_delete(group_id: int):
|
||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group: GroupDescr = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not group.partition.groups_editable:
|
||||
return json_error(404, "partition non editable")
|
||||
return json_error(403, "partition non editable")
|
||||
formsemestre_id = group.partition.formsemestre_id
|
||||
log(f"deleting {group}")
|
||||
db.session.delete(group)
|
||||
@ -318,8 +318,10 @@ def group_edit(group_id: int):
|
||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group: GroupDescr = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not group.partition.groups_editable:
|
||||
return json_error(404, "partition non editable")
|
||||
return json_error(403, "partition non editable")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
group_name = data.get("group_name")
|
||||
if group_name is not None:
|
||||
@ -358,6 +360,8 @@ def partition_create(formsemestre_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
if not formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
partition_name = data.get("partition_name")
|
||||
if partition_name is None:
|
||||
@ -406,6 +410,8 @@ def formsemestre_order_partitions(formsemestre_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
if not formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
partition_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if not isinstance(partition_ids, int) and not all(
|
||||
isinstance(x, int) for x in partition_ids
|
||||
@ -443,6 +449,8 @@ def partition_order_groups(partition_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
group_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if not isinstance(group_ids, int) and not all(
|
||||
isinstance(x, int) for x in group_ids
|
||||
@ -484,6 +492,8 @@ def partition_edit(partition_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
modified = False
|
||||
partition_name = data.get("partition_name")
|
||||
@ -542,6 +552,8 @@ def partition_delete(partition_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not partition.partition_name:
|
||||
return json_error(404, "ne peut pas supprimer la partition par défaut")
|
||||
is_parcours = partition.is_parcours()
|
||||
|
@ -102,7 +102,7 @@ class EtudCursusBUT:
|
||||
)
|
||||
"Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
|
||||
self.parcour: ApcParcours = self.inscriptions[-1].parcour
|
||||
"Le parcour à valider: celui du DERNIER semestre suivi (peut être None)"
|
||||
"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] = {}
|
||||
@ -142,7 +142,7 @@ class EtudCursusBUT:
|
||||
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_ORDERED[validation_rcue.code]
|
||||
|
@ -693,20 +693,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
|
||||
db.session.commit()
|
||||
|
||||
def record(self, code: str, no_overwrite=False):
|
||||
def record(self, code: str, no_overwrite=False) -> bool:
|
||||
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
|
||||
Si no_overwrite, ne fait rien si un code est déjà enregistré.
|
||||
Si l'étudiant est DEM ou DEF, ne fait rien.
|
||||
"""
|
||||
if self.inscription_etat != scu.INSCRIT:
|
||||
return
|
||||
return False
|
||||
if code and not code in self.codes:
|
||||
raise ScoValueError(
|
||||
f"code annee <tt>{html.escape(code)}</tt> invalide pour formsemestre {html.escape(self.formsemestre)}"
|
||||
)
|
||||
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
|
||||
self.recorded = True
|
||||
return # no change
|
||||
return False # no change
|
||||
if self.validation:
|
||||
db.session.delete(self.validation)
|
||||
db.session.commit()
|
||||
@ -746,9 +746,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
next_semestre_id,
|
||||
)
|
||||
|
||||
self.recorded = True
|
||||
db.session.commit()
|
||||
self.recorded = True
|
||||
self.invalidate_formsemestre_cache()
|
||||
return True
|
||||
|
||||
def invalidate_formsemestre_cache(self):
|
||||
"invalide le résultats des deux formsemestres"
|
||||
@ -759,13 +760,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
if self.formsemestre_pair is not None:
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id)
|
||||
|
||||
def record_all(self, no_overwrite: bool = True):
|
||||
def record_all(
|
||||
self, no_overwrite: bool = True, only_validantes: bool = False
|
||||
) -> bool:
|
||||
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
|
||||
et sont donc en mode "automatique".
|
||||
- Si "à cheval", ne modifie pas les codes UE de l'année scolaire précédente.
|
||||
- Pour les RCUE: n'enregistre que si la nouvelle décision est plus favorable que l'ancienne.
|
||||
|
||||
Si only_validantes, n'enregistre que des décisions "validantes" de droit: ADM ou CMP.
|
||||
|
||||
Return: True si au moins un code modifié et enregistré.
|
||||
"""
|
||||
# Toujours valider dans l'ordre UE, RCUE, Année:
|
||||
modif = False
|
||||
# Toujours valider dans l'ordre UE, RCUE, Année
|
||||
annee_scolaire = self.formsemestre.annee_scolaire()
|
||||
# UEs
|
||||
for dec_ue in self.decisions_ues.values():
|
||||
@ -774,25 +782,40 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
) and dec_ue.formsemestre.annee_scolaire() == annee_scolaire:
|
||||
# rappel: le code par défaut est en tête
|
||||
code = dec_ue.codes[0] if dec_ue.codes else None
|
||||
# enregistre le code jury seulement s'il n'y a pas déjà de code
|
||||
# (no_overwrite=True) sauf en mode test yaml
|
||||
dec_ue.record(code, no_overwrite=no_overwrite)
|
||||
# RCUE : enregistre seulement si pas déjà validé "mieux"
|
||||
if (not only_validantes) or code in sco_codes.CODES_UE_VALIDES_DE_DROIT:
|
||||
# enregistre le code jury seulement s'il n'y a pas déjà de code
|
||||
# (no_overwrite=True) sauf en mode test yaml
|
||||
modif |= dec_ue.record(code, no_overwrite=no_overwrite)
|
||||
# RCUE :
|
||||
for dec_rcue in self.decisions_rcue_by_niveau.values():
|
||||
code = dec_rcue.codes[0] if dec_rcue.codes else None
|
||||
if (not dec_rcue.recorded) and (
|
||||
(not dec_rcue.validation)
|
||||
or BUT_CODES_ORDERED.get(dec_rcue.validation.code, 0)
|
||||
< BUT_CODES_ORDERED.get(code, 0)
|
||||
if (
|
||||
(not dec_rcue.recorded)
|
||||
and ( # enregistre seulement si pas déjà validé "mieux"
|
||||
(not dec_rcue.validation)
|
||||
or BUT_CODES_ORDERED.get(dec_rcue.validation.code, 0)
|
||||
< BUT_CODES_ORDERED.get(code, 0)
|
||||
)
|
||||
and ( # décision validante de droit ?
|
||||
(
|
||||
(not only_validantes)
|
||||
or code in sco_codes.CODES_RCUE_VALIDES_DE_DROIT
|
||||
)
|
||||
)
|
||||
):
|
||||
dec_rcue.record(code, no_overwrite=no_overwrite)
|
||||
modif |= dec_rcue.record(code, no_overwrite=no_overwrite)
|
||||
# Année:
|
||||
if not self.recorded:
|
||||
# rappel: le code par défaut est en tête
|
||||
code = self.codes[0] if self.codes else None
|
||||
# enregistre le code jury seulement s'il n'y a pas déjà de code
|
||||
# (no_overwrite=True) sauf en mode test yaml
|
||||
self.record(code, no_overwrite=no_overwrite)
|
||||
if (
|
||||
not only_validantes
|
||||
) or code in sco_codes.CODES_ANNEE_BUT_VALIDES_DE_DROIT:
|
||||
modif |= self.record(code, no_overwrite=no_overwrite)
|
||||
|
||||
return modif
|
||||
|
||||
def erase(self, only_one_sem=False):
|
||||
"""Efface les décisions de jury de cet étudiant
|
||||
@ -1005,23 +1028,23 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
return f"""<{self.__class__.__name__} rcue={self.rcue} valid={self.code_valide
|
||||
} codes={self.codes} explanation={self.explanation}"""
|
||||
|
||||
def record(self, code: str, no_overwrite=False):
|
||||
def record(self, code: str, no_overwrite=False) -> bool:
|
||||
"""Enregistre le code RCUE.
|
||||
Note:
|
||||
- si le RCUE est ADJ, les UE non validées sont passées à ADJ
|
||||
XXX on pourra imposer ici d'autres règles de cohérence
|
||||
"""
|
||||
if self.rcue is None:
|
||||
return # pas de RCUE a enregistrer
|
||||
return False # pas de RCUE a enregistrer
|
||||
if self.inscription_etat != scu.INSCRIT:
|
||||
return
|
||||
return False
|
||||
if code and not code in self.codes:
|
||||
raise ScoValueError(
|
||||
f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}"
|
||||
)
|
||||
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
|
||||
self.recorded = True
|
||||
return # no change
|
||||
return False # no change
|
||||
parcours_id = self.parcour.id if self.parcour is not None else None
|
||||
if self.validation:
|
||||
db.session.delete(self.validation)
|
||||
@ -1057,7 +1080,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
flash(
|
||||
f"""UEs du RCUE "{dec_ue.ue.niveau_competence.competence.titre}" passées en ADJR"""
|
||||
)
|
||||
dec_ue.record("ADJR")
|
||||
dec_ue.record(sco_codes.ADJR)
|
||||
|
||||
# Valide les niveaux inférieurs de la compétence (code ADSUP)
|
||||
# TODO
|
||||
@ -1072,6 +1095,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
)
|
||||
self.code_valide = code # mise à jour état
|
||||
self.recorded = True
|
||||
return True
|
||||
|
||||
def erase(self):
|
||||
"""Efface la décision de jury de cet étudiant pour cet RCUE"""
|
||||
@ -1203,9 +1227,10 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes
|
||||
self.explanation = "notes insuffisantes"
|
||||
|
||||
def record(self, code: str, no_overwrite=False):
|
||||
def record(self, code: str, no_overwrite=False) -> bool:
|
||||
"""Enregistre le code jury pour cette UE.
|
||||
Si no_overwrite, n'enregistre pas s'il y a déjà un code.
|
||||
Return: True si code enregistré (modifié)
|
||||
"""
|
||||
if code and not code in self.codes:
|
||||
raise ScoValueError(
|
||||
@ -1213,7 +1238,7 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
)
|
||||
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
|
||||
self.recorded = True
|
||||
return # no change
|
||||
return False # no change
|
||||
self.erase()
|
||||
if code is None:
|
||||
self.validation = None
|
||||
@ -1244,6 +1269,7 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
|
||||
self.code_valide = code # mise à jour
|
||||
self.recorded = True
|
||||
return True
|
||||
|
||||
def erase(self):
|
||||
"""Efface la décision de jury de cet étudiant pour cette UE"""
|
||||
|
@ -284,6 +284,10 @@ class RowCollector:
|
||||
self["_nom_disp_order"] = etud.sort_key
|
||||
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
|
||||
self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court")
|
||||
self["_nom_short_data"] = {
|
||||
"etudid": etud.id,
|
||||
"nomprenom": etud.nomprenom,
|
||||
}
|
||||
if with_links:
|
||||
self["_nom_short_order"] = etud.sort_key
|
||||
self["_nom_short_target"] = url_for(
|
||||
@ -368,10 +372,6 @@ class RowCollector:
|
||||
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
|
||||
"col_rcue col_rcues_validables" + klass,
|
||||
)
|
||||
self["_rcues_validables_data"] = {
|
||||
"etudid": deca.etud.id,
|
||||
"nomprenom": deca.etud.nomprenom,
|
||||
}
|
||||
if len(deca.rcues_annee) > 0:
|
||||
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair
|
||||
if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen:
|
||||
|
@ -18,29 +18,29 @@ from app.scodoc.sco_exceptions import ScoValueError
|
||||
def formsemestre_validation_auto_but(
|
||||
formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True
|
||||
) -> int:
|
||||
"""Calcul automatique des décisions de jury sur une année BUT.
|
||||
Ne modifie jamais de décisions de l'année scolaire précédente, même
|
||||
"""Calcul automatique des décisions de jury sur une "année" BUT.
|
||||
|
||||
- N'enregistre jamais de décisions de l'année scolaire précédente, même
|
||||
si on a des RCUE "à cheval".
|
||||
Normalement, only_adm est True et on n'enregistre que les décisions ADM (de droit).
|
||||
Si only_adm est faux, on enregistre la première décision proposée par ScoDoc
|
||||
(mode à n'utiliser que pour les tests)
|
||||
- Ne modifie jamais de décisions déjà enregistrées (sauf si no_overwrite est faux,
|
||||
ce qui est utilisé pour certains tests unitaires).
|
||||
- Normalement, only_adm est True et on n'enregistre que les décisions validantes
|
||||
de droit: ADM ou CMP.
|
||||
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
|
||||
(mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys)
|
||||
|
||||
Si no_overwrite est vrai (défaut), ne ré-écrit jamais les codes déjà enregistrés
|
||||
(utiliser faux pour certains tests)
|
||||
|
||||
Returns: nombre d'étudiants "admis"
|
||||
Returns: nombre d'étudiants pour lesquels on a enregistré au moins un code.
|
||||
"""
|
||||
if not formsemestre.formation.is_apc():
|
||||
raise ScoValueError("fonction réservée aux formations BUT")
|
||||
nb_admis = 0
|
||||
nb_etud_modif = 0
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
for etudid in formsemestre.etuds_inscriptions:
|
||||
etud: Identite = Identite.query.get(etudid)
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||
if deca.admis: # année réussie
|
||||
nb_admis += 1
|
||||
if deca.admis or not only_adm:
|
||||
deca.record_all(no_overwrite=no_overwrite)
|
||||
nb_etud_modif += deca.record_all(
|
||||
no_overwrite=no_overwrite, only_validantes=only_adm
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
return nb_admis
|
||||
return nb_etud_modif
|
||||
|
@ -39,6 +39,7 @@ from dataclasses import dataclass
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.models import Evaluation, EvaluationUEPoids, ModuleImpl
|
||||
from app.scodoc import sco_cache
|
||||
@ -484,7 +485,8 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
||||
if nb_etuds == 0:
|
||||
return pd.Series()
|
||||
evals_coefs = self.get_evaluations_coefs(modimpl).reshape(-1)
|
||||
assert evals_coefs.shape == (nb_evals,)
|
||||
if evals_coefs.shape != (nb_evals,):
|
||||
app.critical_error("compute_module_moy: vals_coefs.shape != nb_evals")
|
||||
evals_notes_20 = self.get_eval_notes_sur_20(modimpl)
|
||||
# Les coefs des évals pour chaque étudiant: là où il a des notes
|
||||
# non neutralisées
|
||||
|
@ -553,7 +553,11 @@ class ResultatsSemestre(ResultatsCache):
|
||||
"Nom",
|
||||
etud.nom_short,
|
||||
"identite_court",
|
||||
data={"order": etud.sort_key},
|
||||
data={
|
||||
"order": etud.sort_key,
|
||||
"etudid": etud.id,
|
||||
"nomprenom": etud.nomprenom,
|
||||
},
|
||||
target=url_bulletin,
|
||||
target_attrs=f'class="etudinfo" id="{etudid}"',
|
||||
)
|
||||
|
@ -71,6 +71,11 @@ class ApcValidationRCUE(db.Model):
|
||||
<em>enregistrée le {self.date.strftime("%d/%m/%Y")}
|
||||
à {self.date.strftime("%Hh%M")}</em>"""
|
||||
|
||||
def annee(self) -> str:
|
||||
"""l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """
|
||||
niveau = self.niveau()
|
||||
return niveau.annee if niveau else None
|
||||
|
||||
def niveau(self) -> ApcNiveau:
|
||||
"""Le niveau de compétence associé à cet RCUE."""
|
||||
# Par convention, il est donné par la seconde UE
|
||||
|
@ -63,51 +63,51 @@ class FormSemestre(db.Model):
|
||||
"False si verrouillé"
|
||||
modalite = db.Column(
|
||||
db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite")
|
||||
) # "FI", "FAP", "FC", ...
|
||||
# gestion compensation sem DUT:
|
||||
)
|
||||
"Modalité de formation: 'FI', 'FAP', 'FC', ..."
|
||||
gestion_compensation = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# ne publie pas le bulletin XML ou JSON:
|
||||
"gestion compensation sem DUT (inutilisé en APC)"
|
||||
bul_hide_xml = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# Bloque le calcul des moyennes (générale et d'UE)
|
||||
"ne publie pas le bulletin XML ou JSON"
|
||||
block_moyennes = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# Bloque le calcul de la moyenne générale (utile pour BUT)
|
||||
"Bloque le calcul des moyennes (générale et d'UE)"
|
||||
block_moyenne_generale = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
"Si vrai, la moyenne générale indicative BUT n'est pas calculée"
|
||||
# semestres decales (pour gestion jurys):
|
||||
gestion_semestrielle = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# couleur fond bulletins HTML:
|
||||
"Semestres décalés (pour gestion jurys DUT, pas implémenté ou utile en BUT)"
|
||||
bul_bgcolor = db.Column(
|
||||
db.String(SHORT_STR_LEN),
|
||||
default="white",
|
||||
server_default="white",
|
||||
nullable=False,
|
||||
)
|
||||
# autorise resp. a modifier semestre:
|
||||
"couleur fond bulletins HTML"
|
||||
resp_can_edit = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# autorise resp. a modifier slt les enseignants:
|
||||
"autorise resp. à modifier le formsemestre"
|
||||
resp_can_change_ens = db.Column(
|
||||
db.Boolean(), nullable=False, default=True, server_default="true"
|
||||
)
|
||||
# autorise les ens a creer des evals:
|
||||
"autorise resp. a modifier slt les enseignants"
|
||||
ens_can_edit_eval = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="False"
|
||||
)
|
||||
# code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'
|
||||
"autorise les enseignants à créer des évals dans leurs modimpls"
|
||||
elt_sem_apo = db.Column(db.Text()) # peut être fort long !
|
||||
# code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'
|
||||
"code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'"
|
||||
elt_annee_apo = db.Column(db.Text())
|
||||
"code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'"
|
||||
|
||||
# Relations:
|
||||
etapes = db.relationship(
|
||||
|
@ -87,6 +87,7 @@ class Partition(db.Model):
|
||||
def to_dict(self, with_groups=False) -> dict:
|
||||
"""as a dict, with or without groups"""
|
||||
d = dict(self.__dict__)
|
||||
d["partition_id"] = self.id
|
||||
d.pop("_sa_instance_state", None)
|
||||
d.pop("formsemestre", None)
|
||||
|
||||
|
@ -219,6 +219,8 @@ class UniteEns(db.Model):
|
||||
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
# Invalidation du cache
|
||||
self.formation.invalidate_cached_sems()
|
||||
log(f"ue.set_niveau_competence( {self}, {niveau} )")
|
||||
|
||||
def set_parcour(self, parcour: ApcParcours):
|
||||
@ -246,6 +248,8 @@ class UniteEns(db.Model):
|
||||
self.niveau_competence = None
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
# Invalidation du cache
|
||||
self.formation.invalidate_cached_sems()
|
||||
log(f"ue.set_parcour( {self}, {parcour} )")
|
||||
|
||||
|
||||
|
@ -459,8 +459,7 @@ class JuryPE(object):
|
||||
etud = self.get_cache_etudInfo_d_un_etudiant(etudid)
|
||||
(_, parcours) = sco_report.get_codeparcoursetud(etud)
|
||||
if (
|
||||
len(set(sco_codes_parcours.CODES_SEM_REO.keys()) & set(parcours.values()))
|
||||
> 0
|
||||
len(sco_codes_parcours.CODES_SEM_REO & set(parcours.values())) > 0
|
||||
): # Eliminé car NAR apparait dans le parcours
|
||||
reponse = True
|
||||
if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
|
||||
@ -563,9 +562,8 @@ class JuryPE(object):
|
||||
dec = nt.get_etud_decision_sem(
|
||||
etudid
|
||||
) # quelle est la décision du jury ?
|
||||
if dec and dec["code"] in list(
|
||||
sco_codes_parcours.CODES_SEM_VALIDES.keys()
|
||||
): # isinstance( sesMoyennes[i+1], float) and
|
||||
if dec and (dec["code"] in sco_codes_parcours.CODES_SEM_VALIDES):
|
||||
# isinstance( sesMoyennes[i+1], float) and
|
||||
# mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide"
|
||||
leFid = sem["formsemestre_id"]
|
||||
else:
|
||||
|
@ -187,20 +187,23 @@ CODES_EXPL = {
|
||||
|
||||
# Les codes de semestres:
|
||||
CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT}
|
||||
CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé
|
||||
CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente
|
||||
CODES_SEM_VALIDES_DE_DROIT = {ADM, ADC}
|
||||
CODES_SEM_VALIDES = CODES_SEM_VALIDES_DE_DROIT | {ADJ} # semestre validé
|
||||
CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente
|
||||
|
||||
CODES_SEM_REO = {NAR: 1} # reorientation
|
||||
CODES_SEM_REO = {NAR} # reorientation
|
||||
|
||||
CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True, ADJR: True}
|
||||
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
|
||||
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR}
|
||||
"UE validée"
|
||||
|
||||
CODES_RCUE_VALIDES = {ADM, CMP, ADJ}
|
||||
CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP}
|
||||
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ}
|
||||
"Niveau RCUE validé"
|
||||
|
||||
# Pour le BUT:
|
||||
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
|
||||
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
||||
CODES_RCUE = {ADM, AJ, CMP}
|
||||
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
|
||||
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
|
||||
BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE
|
||||
@ -230,17 +233,17 @@ BUT_CODES_ORDERED = {
|
||||
|
||||
def code_semestre_validant(code: str) -> bool:
|
||||
"Vrai si ce CODE entraine la validation du semestre"
|
||||
return CODES_SEM_VALIDES.get(code, False)
|
||||
return code in CODES_SEM_VALIDES
|
||||
|
||||
|
||||
def code_semestre_attente(code: str) -> bool:
|
||||
"Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)"
|
||||
return CODES_SEM_ATTENTES.get(code, False)
|
||||
return code in CODES_SEM_ATTENTES
|
||||
|
||||
|
||||
def code_ue_validant(code: str) -> bool:
|
||||
"Vrai si ce code d'UE est validant (ie attribue les ECTS)"
|
||||
return CODES_UE_VALIDES.get(code, False)
|
||||
return code in CODES_UE_VALIDES
|
||||
|
||||
|
||||
DEVENIR_EXPL = {
|
||||
|
@ -890,7 +890,7 @@ def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite)
|
||||
car ils ne dépendent que de la note d'UE et de la validation ou non du semestre.
|
||||
Les UE des semestres NON ASSIDUS ne sont jamais validées (code AJ).
|
||||
"""
|
||||
valid_semestre = CODES_SEM_VALIDES.get(code_etat_sem, False)
|
||||
valid_semestre = code_etat_sem in CODES_SEM_VALIDES
|
||||
cnx = ndb.GetDBConnexion()
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
@ -1185,7 +1185,10 @@ def do_formsemestre_clone(
|
||||
"""Clone a semestre: make copy, same modules, same options, same resps, same partitions.
|
||||
New dates, responsable_id
|
||||
"""
|
||||
log("cloning %s" % orig_formsemestre_id)
|
||||
log(f"cloning orig_formsemestre_id")
|
||||
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
|
||||
@ -1196,7 +1199,8 @@ def do_formsemestre_clone(
|
||||
args["date_fin"] = date_fin
|
||||
args["etat"] = 1 # non verrouillé
|
||||
formsemestre_id = sco_formsemestre.do_formsemestre_create(args)
|
||||
log("created formsemestre %s" % formsemestre_id)
|
||||
log(f"created formsemestre {formsemestre_id}")
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
# 2- create moduleimpls
|
||||
mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id)
|
||||
for mod_orig in mods_orig:
|
||||
@ -1258,7 +1262,12 @@ def do_formsemestre_clone(
|
||||
args["formsemestre_id"] = formsemestre_id
|
||||
_ = sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, args)
|
||||
|
||||
# 5- Copy partitions and groups
|
||||
# 6- Copie les parcours
|
||||
formsemestre.parcours = formsemestre_orig.parcours
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
|
||||
# 7- Copy partitions and groups
|
||||
if clone_partitions:
|
||||
sco_groups_copy.clone_partitions_and_groups(
|
||||
orig_formsemestre_id, formsemestre_id
|
||||
|
@ -643,12 +643,12 @@ def formsemestre_description_table(
|
||||
titles = {title: title for title in columns_ids}
|
||||
titles.update({f"ue_{ue.id}": ue.acronyme for ue in ues})
|
||||
titles["ects"] = "ECTS"
|
||||
titles["jour"] = "Evaluation"
|
||||
titles["jour"] = "Évaluation"
|
||||
titles["description"] = ""
|
||||
titles["coefficient"] = "Coef. éval."
|
||||
titles["evalcomplete_str"] = "Complète"
|
||||
titles["parcours"] = "Parcours"
|
||||
titles["publish_incomplete_str"] = "Toujours Utilisée"
|
||||
titles["publish_incomplete_str"] = "Toujours utilisée"
|
||||
title = f"{parcours.SESSION_NAME.capitalize()} {formsemestre.titre_mois()}"
|
||||
|
||||
R = []
|
||||
@ -727,6 +727,8 @@ def formsemestre_description_table(
|
||||
evals.reverse() # ordre chronologique
|
||||
# Ajoute etat:
|
||||
for e in evals:
|
||||
e["_jour_order"] = e["jour"].isoformat()
|
||||
e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
|
||||
e["UE"] = l["UE"]
|
||||
e["_UE_td_attrs"] = l["_UE_td_attrs"]
|
||||
e["Code"] = l["Code"]
|
||||
|
@ -781,8 +781,8 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
|
||||
)
|
||||
|
||||
# Choix code semestre:
|
||||
codes = list(sco_codes_parcours.CODES_JURY_SEM)
|
||||
codes.sort() # fortuitement, cet ordre convient bien !
|
||||
codes = sorted(sco_codes_parcours.CODES_JURY_SEM)
|
||||
# fortuitement, cet ordre convient bien !
|
||||
|
||||
H.append(
|
||||
'<tr><td>Code semestre: </td><td><select name="code_etat"><option value="" selected>Choisir...</option>'
|
||||
|
@ -664,8 +664,10 @@ def set_group(etudid: int, group_id: int) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def change_etud_group_in_partition(etudid, group_id, partition=None):
|
||||
"""Inscrit etud au groupe de cette partition, et le desinscrit d'autres groupes de cette partition."""
|
||||
def change_etud_group_in_partition(etudid: int, group_id: int, partition: dict = None):
|
||||
"""Inscrit etud au groupe de cette partition,
|
||||
et le desinscrit d'autres groupes de cette partition.
|
||||
"""
|
||||
log("change_etud_group_in_partition: etudid=%s group_id=%s" % (etudid, group_id))
|
||||
|
||||
# 0- La partition
|
||||
@ -706,7 +708,7 @@ def change_etud_group_in_partition(etudid, group_id, partition=None):
|
||||
cnx.commit()
|
||||
|
||||
# 5- Update parcours
|
||||
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre.update_inscriptions_parcours_from_groups()
|
||||
|
||||
# 6- invalidate cache
|
||||
@ -1558,11 +1560,14 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
|
||||
|
||||
|
||||
def do_evaluation_listeetuds_groups(
|
||||
evaluation_id, groups=None, getallstudents=False, include_demdef=False
|
||||
):
|
||||
"""Donne la liste des etudids inscrits a cette evaluation dans les
|
||||
evaluation_id: int,
|
||||
groups=None,
|
||||
getallstudents: bool = False,
|
||||
include_demdef: bool = False,
|
||||
) -> list[tuple[int, str]]:
|
||||
"""Donne la liste non triée des etudids inscrits à cette évaluation dans les
|
||||
groupes indiqués.
|
||||
Si getallstudents==True, donne tous les etudiants inscrits a cette
|
||||
Si getallstudents==True, donne tous les étudiants inscrits à cette
|
||||
evaluation.
|
||||
Si include_demdef, compte aussi les etudiants démissionnaires et défaillants
|
||||
(sinon, par défaut, seulement les 'I')
|
||||
|
@ -36,6 +36,7 @@ from flask import url_for, g, request
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.models import FormSemestre
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_codes_parcours
|
||||
@ -175,6 +176,8 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
|
||||
(la liste doit avoir été vérifiée au préalable)
|
||||
En option: inscrit aux mêmes groupes que dans le semestre origine
|
||||
"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(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(
|
||||
@ -190,7 +193,6 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
|
||||
# du nom de la partition: évidemment, cela ne marche pas si on a les
|
||||
# même noms de groupes dans des partitions différentes)
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
log("cherche groupes de %(nom)s" % etud)
|
||||
|
||||
# recherche le semestre origine (il serait plus propre de l'avoir conservé!)
|
||||
if len(etud["sems"]) < 2:
|
||||
@ -201,13 +203,11 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
|
||||
prev_formsemestre["formsemestre_id"] if prev_formsemestre else None,
|
||||
)
|
||||
|
||||
cursem_groups_by_name = dict(
|
||||
[
|
||||
(g["group_name"], g)
|
||||
for g in sco_groups.get_sem_groups(sem["formsemestre_id"])
|
||||
if g["group_name"]
|
||||
]
|
||||
)
|
||||
cursem_groups_by_name = {
|
||||
g["group_name"]: g
|
||||
for g in sco_groups.get_sem_groups(sem["formsemestre_id"])
|
||||
if g["group_name"]
|
||||
}
|
||||
|
||||
# forme la liste des groupes présents dans les deux semestres:
|
||||
partition_groups = [] # [ partition+group ] (ds nouveau sem.)
|
||||
@ -217,14 +217,13 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
|
||||
new_group = cursem_groups_by_name[prev_group_name]
|
||||
partition_groups.append(new_group)
|
||||
|
||||
# inscrit aux groupes
|
||||
# Inscrit aux groupes
|
||||
for partition_group in partition_groups:
|
||||
if partition_group["groups_editable"]:
|
||||
sco_groups.change_etud_group_in_partition(
|
||||
etudid,
|
||||
partition_group["group_id"],
|
||||
partition_group,
|
||||
)
|
||||
sco_groups.change_etud_group_in_partition(
|
||||
etudid,
|
||||
partition_group["group_id"],
|
||||
partition_group,
|
||||
)
|
||||
|
||||
|
||||
def do_desinscrit(sem, etudids):
|
||||
@ -481,11 +480,12 @@ def build_page(
|
||||
|
||||
|
||||
def formsemestre_inscr_passage_help(sem):
|
||||
return (
|
||||
"""<div class="pas_help"><h3><a name="help">Explications</a></h3>
|
||||
return f"""<div class="pas_help"><h3><a name="help">Explications</a></h3>
|
||||
<p>Cette page permet d'inscrire des étudiants dans le semestre destination
|
||||
<a class="stdlink"
|
||||
href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titreannee)s</a>,
|
||||
href="{
|
||||
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=sem["formsemestre_id"] )
|
||||
}">{sem['titreannee']}</a>,
|
||||
et d'en désincrire si besoin.
|
||||
</p>
|
||||
<p>Les étudiants sont groupés par semestres d'origines. Ceux qui sont en caractères
|
||||
@ -495,10 +495,13 @@ def formsemestre_inscr_passage_help(sem):
|
||||
<p>Au départ, les étudiants déjà inscrits sont sélectionnés; vous pouvez ajouter d'autres
|
||||
étudiants à inscrire dans le semestre destination.</p>
|
||||
<p>Si vous dé-selectionnez un étudiant déjà inscrit (en gras), il sera désinscrit.</p>
|
||||
<p>Le bouton <em>inscrire aux mêmes groupes</em> ne prend en compte que les groupes qui existent
|
||||
dans les deux semestres: pensez à créer les partitions et groupes que vous souhaitez conserver
|
||||
<b>avant</b> d'inscrire les étudiants.
|
||||
</p>
|
||||
<p class="help">Aucune action ne sera effectuée si vous n'appuyez pas sur le bouton "Appliquer les modifications" !</p>
|
||||
</div>"""
|
||||
% sem
|
||||
)
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def etuds_select_boxes(
|
||||
@ -574,13 +577,13 @@ def etuds_select_boxes(
|
||||
if with_checkbox:
|
||||
H.append(
|
||||
""" (Select.
|
||||
<a href="#" onclick="sem_select('%(id)s', true);">tous</a>
|
||||
<a href="#" onclick="sem_select('%(id)s', false );">aucun</a>""" # "
|
||||
<a href="#" class="stdlink" onclick="sem_select('%(id)s', true);">tous</a>
|
||||
<a href="#" class="stdlink" onclick="sem_select('%(id)s', false );">aucun</a>""" # "
|
||||
% infos
|
||||
)
|
||||
if sel_inscrits:
|
||||
H.append(
|
||||
"""<a href="#" onclick="sem_select_inscrits('%(id)s');">inscrits</a>"""
|
||||
"""<a href="#" class="stdlink" onclick="sem_select_inscrits('%(id)s');">inscrits</a>"""
|
||||
% infos
|
||||
)
|
||||
if with_checkbox or sel_inscrits:
|
||||
|
@ -41,6 +41,7 @@ from app import log
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
@ -459,7 +459,7 @@ def ficheEtud(etudid=None):
|
||||
# XXX dev
|
||||
info["but_cursus_mkup"] = ""
|
||||
if info["sems"]:
|
||||
last_sem = FormSemestre.query.get_or_404(info["sems"][-1]["formsemestre_id"])
|
||||
last_sem = FormSemestre.query.get_or_404(info["sems"][0]["formsemestre_id"])
|
||||
if last_sem.formation.is_apc():
|
||||
but_cursus = cursus_but.EtudCursusBUT(etud, last_sem.formation)
|
||||
info["but_cursus_mkup"] = render_template(
|
||||
|
@ -967,31 +967,35 @@ def has_existing_decision(M, E, etudid):
|
||||
# Nouveau formulaire saisie notes (2016)
|
||||
|
||||
|
||||
def saisie_notes(evaluation_id, group_ids=[]):
|
||||
def saisie_notes(evaluation_id: int, group_ids: list = None):
|
||||
"""Formulaire saisie notes d'une évaluation pour un groupe"""
|
||||
if not isinstance(evaluation_id, int):
|
||||
raise ScoInvalidParamError()
|
||||
group_ids = [int(group_id) for group_id in group_ids]
|
||||
group_ids = [int(group_id) for group_id in (group_ids or [])]
|
||||
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
if not evals:
|
||||
raise ScoValueError("évaluation inexistante")
|
||||
E = evals[0]
|
||||
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
formsemestre_id = M["formsemestre_id"]
|
||||
moduleimpl_status_url = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=E["moduleimpl_id"],
|
||||
)
|
||||
# Check access
|
||||
# (admin, respformation, and responsable_id)
|
||||
if not sco_permissions_check.can_edit_notes(current_user, E["moduleimpl_id"]):
|
||||
return (
|
||||
html_sco_header.sco_header()
|
||||
+ "<h2>Modification des notes impossible pour %s</h2>"
|
||||
% current_user.user_name
|
||||
+ """<p>(vérifiez que le semestre n'est pas verrouillé et que vous
|
||||
return f"""
|
||||
{html_sco_header.sco_header()}
|
||||
<h2>Modification des notes impossible pour {current_user.user_name}</h2>
|
||||
|
||||
<p>(vérifiez que le semestre n'est pas verrouillé et que vous
|
||||
avez l'autorisation d'effectuer cette opération)</p>
|
||||
<p><a href="moduleimpl_status?moduleimpl_id=%s">Continuer</a></p>
|
||||
"""
|
||||
% E["moduleimpl_id"]
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
<p><a href="{ moduleimpl_status_url }">Continuer</a>
|
||||
</p>
|
||||
{html_sco_header.sco_footer()}
|
||||
"""
|
||||
|
||||
# Informations sur les groupes à afficher:
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
||||
@ -1049,8 +1053,14 @@ def saisie_notes(evaluation_id, group_ids=[]):
|
||||
alone=True,
|
||||
)
|
||||
)
|
||||
H.append("""</td><td style="padding-left: 35px;"><button class="btn_masquer_DEM">Masquer les DEM</button></td></tr></table></div>""")
|
||||
H.append("""<style>
|
||||
H.append(
|
||||
"""
|
||||
</td>
|
||||
<td style="padding-left: 35px;"><button class="btn_masquer_DEM">Masquer les DEM</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<style>
|
||||
.btn_masquer_DEM{
|
||||
font-size: 12px;
|
||||
}
|
||||
@ -1061,19 +1071,14 @@ def saisie_notes(evaluation_id, group_ids=[]):
|
||||
body.masquer_DEM .etud_dem{
|
||||
display: none !important;
|
||||
}
|
||||
</style>""")
|
||||
|
||||
# Le formulaire de saisie des notes:
|
||||
destination = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=E["moduleimpl_id"],
|
||||
</style>
|
||||
"""
|
||||
)
|
||||
|
||||
form = _form_saisie_notes(E, M, groups_infos, destination=destination)
|
||||
# Le formulaire de saisie des notes:
|
||||
form = _form_saisie_notes(E, M, groups_infos, destination=moduleimpl_status_url)
|
||||
if form is None:
|
||||
log(f"redirecting to {destination}")
|
||||
return flask.redirect(destination)
|
||||
return flask.redirect(moduleimpl_status_url)
|
||||
H.append(form)
|
||||
#
|
||||
H.append("</div>") # /saisie_notes
|
||||
@ -1104,6 +1109,9 @@ def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int):
|
||||
for etudid in etudids:
|
||||
# infos identite etudiant
|
||||
e = sco_etud.etudident_list(cnx, {"etudid": etudid})[0]
|
||||
etud: Identite = Identite.query.get(etudid)
|
||||
# TODO: refactor et eliminer etudident_list.
|
||||
e["etud"] = etud # utilisé seulement pour le tri -- a refactorer
|
||||
sco_etud.format_etud_ident(e)
|
||||
etuds.append(e)
|
||||
# infos inscription dans ce semestre
|
||||
@ -1155,7 +1163,7 @@ def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int):
|
||||
e["val"] = "DEM"
|
||||
e["explanation"] = "Démission"
|
||||
|
||||
etuds.sort(key=lambda x: (x["nom"], x["prenom"]))
|
||||
etuds.sort(key=lambda x: x["etud"].sort_key)
|
||||
|
||||
return etuds
|
||||
|
||||
|
@ -26,8 +26,10 @@ function change_menu_code(elt) {
|
||||
let ue_selects = elt.parentElement.parentElement.parentElement.querySelectorAll(
|
||||
"select.ue_rcue_" + elt.dataset.niveau_id);
|
||||
ue_selects.forEach(select => {
|
||||
select.value = "ADJR";
|
||||
change_menu_code(select); // pour changer les styles
|
||||
if (select.value != "ADM") {
|
||||
select.value = "ADJR";
|
||||
change_menu_code(select); // pour changer les styles
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -219,11 +219,11 @@ $(function () {
|
||||
localStorage.setItem(order_info_key, order_info);
|
||||
}
|
||||
let etudids = [];
|
||||
document.querySelectorAll("td.col_rcues_validables").forEach(e => {
|
||||
document.querySelectorAll("td.identite_court").forEach(e => {
|
||||
etudids.push(e.dataset.etudid);
|
||||
});
|
||||
let noms = [];
|
||||
document.querySelectorAll("td.col_rcues_validables").forEach(e => {
|
||||
document.querySelectorAll("td.identite_court").forEach(e => {
|
||||
noms.push(e.dataset.nomprenom);
|
||||
});
|
||||
const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]);
|
||||
|
@ -8,14 +8,22 @@
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<h2>Calcul automatique des décisions de jury annuelle BUT</h2>
|
||||
<h2>Calcul automatique des décisions de jury du BUT</h2>
|
||||
<ul>
|
||||
<li>Seuls les étudiants qui valident l'année seront affectés:
|
||||
tous les niveaux de compétences (RCUE) validables
|
||||
(moyenne annuelle au dessus de 10);
|
||||
<li>N'enregistre jamais de décisions de l'année scolaire précédente, même
|
||||
si on a des RCUE "à cheval" sur deux années.
|
||||
</li>
|
||||
<li>Ne modifie jamais de décisions déjà enregistrées.
|
||||
</li>
|
||||
<li>N'enregistre que les décisions <b>validantes de droit: ADM ou CMP</b>.
|
||||
</li>
|
||||
<li>L'assiduité n'est <b>pas</b> prise en compte.
|
||||
</li>
|
||||
<li>l'assiduité n'est <b>pas</b> prise en compte;</li>
|
||||
</ul>
|
||||
<p>
|
||||
En conséquence, saisir ensuite <b>manuellement les décisions manquantes</b>,
|
||||
notamment sur les UEs en dessous de 10.
|
||||
</p>
|
||||
<p class="warning">
|
||||
Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !
|
||||
</p>
|
||||
|
@ -2350,7 +2350,7 @@ def formsemestre_validation_but(
|
||||
etud: Identite = Identite.query.filter_by(
|
||||
id=etudid, dept_id=g.scodoc_dept_id
|
||||
).first_or_404()
|
||||
|
||||
nb_etuds = formsemestre.etuds.count()
|
||||
# la route ne donne pas le type d'etudid pour pouvoir construire des URLs
|
||||
# provisoires avec NEXT et PREV
|
||||
try:
|
||||
@ -2360,16 +2360,24 @@ def formsemestre_validation_but(
|
||||
read_only = not sco_permissions_check.can_validate_sem(formsemestre_id)
|
||||
|
||||
# --- Navigation
|
||||
prev_lnk = f"""{scu.EMO_PREV_ARROW} <a href="{url_for(
|
||||
prev_lnk = (
|
||||
f"""{scu.EMO_PREV_ARROW} <a href="{url_for(
|
||||
"notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id, etudid="PREV"
|
||||
)}" class="stdlink"">précédent</a>
|
||||
"""
|
||||
next_lnk = f"""<a href="{url_for(
|
||||
if nb_etuds > 1
|
||||
else ""
|
||||
)
|
||||
next_lnk = (
|
||||
f"""<a href="{url_for(
|
||||
"notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id, etudid="NEXT"
|
||||
)}" class="stdlink"">suivant</a> {scu.EMO_NEXT_ARROW}
|
||||
"""
|
||||
if nb_etuds > 1
|
||||
else ""
|
||||
)
|
||||
navigation_div = f"""
|
||||
<div class="but_navigation">
|
||||
<div class="prev">
|
||||
@ -2548,10 +2556,10 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None):
|
||||
form = jury_but_forms.FormSemestreValidationAutoBUTForm()
|
||||
if request.method == "POST":
|
||||
if not form.cancel.data:
|
||||
nb_admis = jury_but_validation_auto.formsemestre_validation_auto_but(
|
||||
nb_etud_modif = jury_but_validation_auto.formsemestre_validation_auto_but(
|
||||
formsemestre
|
||||
)
|
||||
flash(f"Décisions enregistrées ({nb_admis} admis)")
|
||||
flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)")
|
||||
return redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_saisie_jury",
|
||||
@ -2563,7 +2571,7 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None):
|
||||
"but/formsemestre_validation_auto_but.html",
|
||||
form=form,
|
||||
sco=ScoData(formsemestre=formsemestre),
|
||||
title=f"Calcul automatique jury BUT",
|
||||
title="Calcul automatique jury BUT",
|
||||
)
|
||||
|
||||
|
||||
@ -2641,7 +2649,17 @@ def formsemestre_validation_auto(formsemestre_id):
|
||||
message="<p>Opération non autorisée pour %s</h2>" % current_user,
|
||||
dest_url=scu.ScoURL(),
|
||||
)
|
||||
|
||||
formsemestre: FormSemestre = FormSemestre.query.filter_by(
|
||||
id=formsemestre_id, dept_id=g.scodoc_dept_id
|
||||
).first_or_404()
|
||||
if formsemestre.formation.is_apc():
|
||||
return redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_validation_auto_but",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
)
|
||||
)
|
||||
return sco_formsemestre_validation.formsemestre_validation_auto(formsemestre_id)
|
||||
|
||||
|
||||
|
@ -935,7 +935,7 @@ def partition_editor(formsemestre_id: int):
|
||||
def create_partition_parcours(formsemestre_id):
|
||||
"""Création d'une partitions nommée "Parcours" (PARTITION_PARCOURS)
|
||||
avec un groupe par parcours."""
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre.setup_parcours_groups()
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
|
@ -4,4 +4,5 @@ markers =
|
||||
but_gb
|
||||
lemans
|
||||
lyon
|
||||
test_test
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.4.29"
|
||||
SCOVERSION = "9.4.31"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
@ -25,7 +25,7 @@ from app import models
|
||||
|
||||
from app.auth.models import User, Role, UserRole
|
||||
from app.entreprises.models import entreprises_reset_database
|
||||
from app.models import departements
|
||||
from app.models import Departement, departements
|
||||
from app.models import Formation, UniteEns, Matiere, Module
|
||||
from app.models import FormSemestre, FormSemestreInscription
|
||||
from app.models import GroupDescr
|
||||
@ -73,6 +73,7 @@ def make_shell_context():
|
||||
"ctx": app.test_request_context(),
|
||||
"current_app": flask.current_app,
|
||||
"current_user": current_user,
|
||||
"Departement": Departement,
|
||||
"db": db,
|
||||
"Evaluation": Evaluation,
|
||||
"flask": flask,
|
||||
|
@ -34,7 +34,11 @@ def test_list_users(api_admin_headers):
|
||||
|
||||
# Tous les utilisateurs, vus par SuperAdmin:
|
||||
users = GET("/users/query", headers=admin_h)
|
||||
|
||||
assert len(users) > 2
|
||||
# Les utilisateurs du dept. TAPI
|
||||
users_TAPI = GET("/users/query?departement=TAPI", headers=admin_h)
|
||||
nb_TAPI = len(users_TAPI)
|
||||
assert nb_TAPI > 1
|
||||
# Les utilisateurs de chaque département (+ ceux sans département)
|
||||
all_users = []
|
||||
for acronym in [dept["acronym"] for dept in depts] + [""]:
|
||||
@ -59,9 +63,8 @@ def test_list_users(api_admin_headers):
|
||||
for i, u in enumerate(u for u in u_users if u["dept"] != "TAPI"):
|
||||
headers = get_auth_headers(u["user_name"], "test")
|
||||
users_by_u = GET("/users/query", headers=headers)
|
||||
assert len(users_by_u) == 4 + i
|
||||
# explication: tous ont le droit de voir les 3 users de TAPI
|
||||
# (test, other et u_TAPI)
|
||||
assert len(users_by_u) == nb_TAPI + 1 + i
|
||||
# explication: tous ont le droit de voir les users de TAPI
|
||||
# plus l'utilisateur de chaque département jusqu'au leur
|
||||
# (u_AA voit AA, u_BB voit AA et BB, etc)
|
||||
|
||||
@ -90,6 +93,10 @@ def test_edit_users(api_admin_headers):
|
||||
)
|
||||
assert user["dept"] == "TAPI"
|
||||
assert user["active"] is False
|
||||
user = GET(f"/user/{user['id']}", headers=admin_h)
|
||||
assert user["nom"] == "Toto"
|
||||
assert user["dept"] == "TAPI"
|
||||
assert user["active"] is False
|
||||
|
||||
|
||||
def test_roles(api_admin_headers):
|
||||
@ -229,3 +236,10 @@ def test_modif_users_depts(api_admin_headers):
|
||||
ok = True
|
||||
assert ok
|
||||
# Nettoyage:
|
||||
# on ne peut pas supprimer l'utilisateur lambda, mais on
|
||||
# le rend inactif et on le retire de son département
|
||||
u = POST_JSON(
|
||||
f"/user/{u_lambda['id']}/edit",
|
||||
{"active": False, "dept": None},
|
||||
headers=admin_h,
|
||||
)
|
||||
|
47
tests/api/test_test.py
Normal file
47
tests/api/test_test.py
Normal file
@ -0,0 +1,47 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
|
||||
"""Unit tests for... tests
|
||||
|
||||
Ensure test DB is in the expected initial state.
|
||||
|
||||
Usage: pytest tests/unit/test_test.py
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.api.setup_test_api import (
|
||||
api_headers,
|
||||
GET,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.test_test
|
||||
def test_test_db(api_headers):
|
||||
"""Check that we indeed have: 2 users, 1 dept, 3 formsemestres.
|
||||
Juste après init, les ensembles seront ceux donnés ci-dessous.
|
||||
Les autres tests peuvent ajouter des éléments, c'edt pourquoi on utilise issubset().
|
||||
"""
|
||||
headers = api_headers
|
||||
assert {
|
||||
"admin_api",
|
||||
"admin",
|
||||
"lecteur_api",
|
||||
"other",
|
||||
"test",
|
||||
"u_AA",
|
||||
"u_BB",
|
||||
"u_CC",
|
||||
"u_DD",
|
||||
"u_TAPI",
|
||||
}.issubset({u["user_name"] for u in GET("/users/query", headers=headers)})
|
||||
assert {
|
||||
"AA",
|
||||
"BB",
|
||||
"CC",
|
||||
"DD",
|
||||
"TAPI",
|
||||
}.issubset({d["acronym"] for d in GET("/departements", headers=headers)})
|
||||
assert 1 in (
|
||||
formsemestre["semestre_id"]
|
||||
for formsemestre in GET("/formsemestres/query", headers=headers)
|
||||
)
|
@ -129,7 +129,7 @@ FormSemestres:
|
||||
|
||||
|
||||
Etudiants:
|
||||
Aaaaa:
|
||||
Aïaaa: # avec un i trema
|
||||
prenom: Étudiant_SEE
|
||||
civilite: M
|
||||
formsemestres:
|
||||
@ -196,7 +196,7 @@ Etudiants:
|
||||
|
||||
S3:
|
||||
parcours: SEE
|
||||
Bbbbb:
|
||||
Azbbbb: # Az devrait être trié après Aï.
|
||||
prenom: Étudiante_BMB
|
||||
civilite: F
|
||||
formsemestres:
|
||||
|
Loading…
Reference in New Issue
Block a user