Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into table

This commit is contained in:
Emmanuel Viennet 2023-01-24 19:07:23 -03:00
commit 83b47db3dc
34 changed files with 373 additions and 180 deletions

View File

@ -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}
"""
)

View File

@ -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()

View File

@ -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]

View File

@ -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
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
dec_ue.record(code, no_overwrite=no_overwrite)
# RCUE : enregistre seulement si pas déjà validé "mieux"
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 (
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"""

View File

@ -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:

View File

@ -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 -é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

View File

@ -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

View File

@ -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}"',
)

View File

@ -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

View File

@ -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(

View File

@ -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)

View File

@ -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} )")

View File

@ -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:

View File

@ -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 = {

View File

@ -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)

View File

@ -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

View File

@ -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"]

View File

@ -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>'

View File

@ -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')

View File

@ -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)
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,9 +217,8 @@ 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"],
@ -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 -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:

View File

@ -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

View File

@ -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(

View File

@ -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>
<p><a href="{ moduleimpl_status_url }">Continuer</a>
</p>
{html_sco_header.sco_footer()}
"""
% E["moduleimpl_id"]
+ 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

View File

@ -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 => {
if (select.value != "ADM") {
select.value = "ADJR";
change_menu_code(select); // pour changer les styles
}
});
}
}

View File

@ -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]);

View File

@ -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>

View File

@ -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}&nbsp;<a href="{url_for(
prev_lnk = (
f"""{scu.EMO_PREV_ARROW}&nbsp;<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>&nbsp;{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)

View File

@ -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(

View File

@ -4,4 +4,5 @@ markers =
but_gb
lemans
lyon
test_test

View File

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

View File

@ -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,

View File

@ -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
View 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)
)

View File

@ -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: