Jurys BUT: modif. autorisations passage. Cosmetic css.

This commit is contained in:
Emmanuel Viennet 2023-01-26 10:49:04 -03:00
parent fca090649c
commit 63ea4f31f2
9 changed files with 312 additions and 144 deletions

View File

@ -630,19 +630,38 @@ class DecisionsProposeesAnnee(DecisionsProposees):
d[dec_rcue.rcue.ue_2.id] = dec_rcue
return d
def next_annee_semestre_id(self, code: str) -> int:
"""L'indice du semestre dans lequel l'étudiant est autorisé à
poursuivre l'année suivante. None si aucun."""
if self.formsemestre_pair is None:
return None # seulement sur année
if code == RED:
return self.formsemestre_pair.semestre_id - 1
elif (
code in sco_codes.BUT_CODES_PASSAGE
def next_semestre_ids(self, code: str) -> set[int]:
"""Les indices des semestres dans lequels l'étudiant est autorisé
à poursuivre après le semestre courant.
"""
ids = set()
# La poursuite d'études dans un semestre pair dune même année
# est de droit pour tout étudiant:
if (self.formsemestre.semestre_id % 2) and sco_codes.ParcoursBUT.NB_SEM:
ids.add(self.formsemestre.semestre_id + 1)
# La poursuite détudes dans un semestre impair est possible si
# et seulement si létudiant a obtenu :
# - la moyenne à plus de la moitié des regroupements cohérents dUE ;
# - et une moyenne égale ou supérieure à 8 sur 20 à chaque RCUE.
#
# La condition a paru trop stricte à de nombreux collègues.
# ScoDoc ne contraint donc pas à la respecter strictement.
# Si le code est dans BUT_CODES_PASSAGE (ADM, ADJ, PASD, PAS1NCI, ATJ),
# autorise à passer dans le semestre suivant
if (
self.jury_annuel
and code in sco_codes.BUT_CODES_PASSAGE
and self.formsemestre_pair.semestre_id < sco_codes.ParcoursBUT.NB_SEM
):
return self.formsemestre_pair.semestre_id + 1
return None
ids.add(self.formsemestre.semestre_id + 1)
if code == RED:
ids.add(
self.formsemestre.semestre_id - (self.formsemestre.semestre_id + 1) % 2
)
return ids
def record_form(self, form: dict):
"""Enregistre les codes de jury en base
@ -704,47 +723,43 @@ class DecisionsProposeesAnnee(DecisionsProposees):
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 False # no change
if self.validation:
db.session.delete(self.validation)
db.session.commit()
if code is None:
self.validation = None
else:
self.validation = ApcValidationAnnee(
etudid=self.etud.id,
formsemestre=self.formsemestre_impair,
ordre=self.annee_but,
annee_scolaire=self.annee_scolaire(),
code=code,
)
db.session.add(self.validation)
db.session.commit()
log(f"Recording {self}: {code}")
Scolog.logdb(
method="jury_but",
etudid=self.etud.id,
msg=f"Validation année BUT{self.annee_but}: {code}",
)
if code != self.code_valide and (self.code_valide is None or not no_overwrite):
# Enregistrement du code annuel BUT
if self.validation:
db.session.delete(self.validation)
db.session.commit()
if code is None:
self.validation = None
else:
self.validation = ApcValidationAnnee(
etudid=self.etud.id,
formsemestre=self.formsemestre_impair,
ordre=self.annee_but,
annee_scolaire=self.annee_scolaire(),
code=code,
)
db.session.add(self.validation)
db.session.commit()
log(f"Recording {self}: {code}")
Scolog.logdb(
method="jury_but",
etudid=self.etud.id,
msg=f"Validation année BUT{self.annee_but}: {code}",
)
# --- Autorisation d'inscription dans semestre suivant ?
if self.formsemestre_pair is not None:
if code is None:
ScolarAutorisationInscription.delete_autorisation_etud(
etudid=self.etud.id,
origin_formsemestre_id=self.formsemestre_pair.id,
)
else:
next_semestre_id = self.next_annee_semestre_id(code)
if next_semestre_id is not None:
ScolarAutorisationInscription.autorise_etud(
self.etud.id,
self.formsemestre_pair.formation.formation_code,
self.formsemestre_pair.id,
next_semestre_id,
)
ScolarAutorisationInscription.delete_autorisation_etud(
etudid=self.etud.id,
origin_formsemestre_id=self.formsemestre.id,
)
for next_semestre_id in self.next_semestre_ids(code):
ScolarAutorisationInscription.autorise_etud(
self.etud.id,
self.formsemestre.formation.formation_code,
self.formsemestre.id,
next_semestre_id,
)
db.session.commit()
self.recorded = True
@ -872,18 +887,18 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.invalidate_formsemestre_cache()
def get_autorisations_passage(self) -> list[int]:
"""Les liste des indices de semestres auxquels on est autorisé à
s'inscrire depuis cette année"""
formsemestre = self.formsemestre_pair or self.formsemestre_impair
if not formsemestre:
return []
return [
a.semestre_id
for a in ScolarAutorisationInscription.query.filter_by(
etudid=self.etud.id,
origin_formsemestre_id=formsemestre.id,
)
]
"""Liste des indices de semestres auxquels on est autorisé à
s'inscrire depuis le semestre courant.
"""
return sorted(
[
a.semestre_id
for a in ScolarAutorisationInscription.query.filter_by(
etudid=self.etud.id,
origin_formsemestre_id=self.formsemestre.id,
)
]
)
def descr_niveaux_validation(self, line_sep: str = "\n") -> str:
"""Description textuelle des niveaux validés (enregistrés)

View File

@ -43,21 +43,20 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
"""
H = []
H.append("""<div class="but_section_annee">""")
H.append(
f"""
<div>
<b>Décision de jury pour l'année :</b> {
_gen_but_select("code_annee", deca.codes, deca.code_valide,
disabled=True, klass="manual")
}
<span>({deca.code_valide or 'non'} enregistrée)</span>
if deca.jury_annuel:
H.append(
f"""
<div class="but_section_annee">
<div>
<b>Décision de jury pour l'année :</b> {
_gen_but_select("code_annee", deca.codes, deca.code_valide,
disabled=True, klass="manual")
}
<span>({deca.code_valide or 'non'} enregistrée)</span>
</div>
</div>
"""
)
div_explanation = f"""<div class="but_explanation">{deca.explanation}</div>"""
H.append("""</div>""")
)
formsemestre_1 = deca.formsemestre_impair
formsemestre_2 = deca.formsemestre_pair
@ -74,7 +73,7 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
<div class="titre_niveaux">
<b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b>
</div>
{div_explanation}
<div class="but_explanation">{deca.explanation}</div>
<div class="but_annee">
<div class="titre"></div>
<div class="titre">{"S" +str(formsemestre_1.semestre_id)
@ -285,7 +284,7 @@ def jury_but_semestriel(
read_only: bool,
navigation_div: str = "",
) -> str:
"""Formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel)"""
"""Page: formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel)."""
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res)
inscription_etat = etud.inscription_etat(formsemestre.id)
@ -374,20 +373,20 @@ def jury_but_semestriel(
f"""
<div class="jury_but">
<div>
<div class="bull_head">
<div>
<div class="titre_parcours">Jury BUT S{formsemestre.id}
- Parcours {(parcour.libelle if parcour else False) or "non spécifié"}
<div class="bull_head">
<div>
<div class="titre_parcours">Jury BUT S{formsemestre.id}
- Parcours {(parcour.libelle if parcour else False) or "non spécifié"}
</div>
<div class="nom_etud">{etud.nomprenom}</div>
</div>
<div class="bull_photo"><a href="{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
</div>
</div>
<div class="nom_etud">{etud.nomprenom}</div>
</div>
<div class="bull_photo"><a href="{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
</div>
</div>
<h3>Jury sur un semestre BUT isolé (ne concerne que les UEs)</h3>
{warning}
<h3>Jury sur un semestre BUT isolé (ne concerne que les UEs)</h3>
{warning}
</div>
<form method="post" id="jury_but">
@ -450,24 +449,35 @@ def jury_but_semestriel(
)
H.append("</div>") # but_annee
div_autorisations_passage = (
f"""
<div class="but_autorisations_passage">
<span>Autorisé à passer en&nbsp;:</span>
{ ", ".join( ["S" + str(a.semestre_id or '') for a in autorisations_passage ] )}
</div>
"""
if autorisations_passage
else """<div class="but_autorisations_passage but_explanation">pas d'autorisations de passage enregistrées.</div>"""
)
H.append(div_autorisations_passage)
if read_only:
H.append(
"""<div class="but_explanation">
Vous n'avez pas la permission de modifier ces décisions.
Les champs entourés en vert sont enregistrés.</div>"""
Les champs entourés en vert sont enregistrés.
</div>
"""
)
else:
if formsemestre.semestre_id < formsemestre.formation.get_parcours().NB_SEM:
H.append(
f"""
<div class="but_settings">
<input type="checkbox" name="autorisation_passage" value="1" {
"checked" if est_autorise_a_passer else ""}>
<em>autoriser à passer dans le semestre S{formsemestre.semestre_id+1}</em>
{("(autorisations enregistrées: " + ' '.join(
'S' + str(a.semestre_id or '') for a in autorisations_passage) + ")"
) if autorisations_passage else ""}
</input>
<input type="checkbox" name="autorisation_passage" value="1" {
"checked" if est_autorise_a_passer else ""}>
<em>autoriser à passer dans le semestre S{formsemestre.semestre_id+1}</em>
</input>
</div>
"""
)

View File

@ -544,7 +544,9 @@ def _ligne_evaluation(
if not first_eval:
H.append("""<tr><td colspan="8">&nbsp;</td></tr>""")
tr_class_1 += " mievr_spaced"
H.append(f"""<tr class="{tr_class_1}"><td class="mievr_tit" colspan="8">""")
H.append(
f"""<tr class="{tr_class_1} mievr_tit"><td class="mievr_tit" colspan="8">"""
)
coef = evaluation.coefficient
if is_apc:
if not evaluation.get_ue_poids_dict():
@ -588,7 +590,9 @@ def _ligne_evaluation(
)
#
H.append(
f"""<div class="evaluation_order">
f"""
</td>
<td class="evaluation_order">
<span class="evalindex" title="Indice dans les vecteurs (formules)">{
eval_index:2}</span>
<span class="eval_arrows_chld">
@ -612,20 +616,6 @@ def _ligne_evaluation(
else:
H.append(arrow_none)
H.append(
f"""</span></span></td>
</div>
</tr>
<tr class="{tr_class}">
<th class="moduleimpl_evaluations" colspan="2">&nbsp;</th>
<th class="moduleimpl_evaluations">Durée</th>
<th class="moduleimpl_evaluations">Coef.</th>
<th class="moduleimpl_evaluations">Notes</th>
<th class="moduleimpl_evaluations">Abs</th>
<th class="moduleimpl_evaluations">N</th>
<th class="moduleimpl_evaluations">Moyenne """
)
if etat["evalcomplete"]:
etat_txt = """(prise en compte)"""
etat_descr = "notes utilisées dans les moyennes"
@ -648,9 +638,19 @@ def _ligne_evaluation(
}" title="{etat_descr}">{etat_txt}</a>"""
H.append(
f"""{etat_txt}</th>
</tr>
<tr class="{tr_class}"><td class="mievr">"""
f"""</span></span></td>
</tr>
<tr class="{tr_class}">
<th class="moduleimpl_evaluations" colspan="2">&nbsp;</th>
<th class="moduleimpl_evaluations">Durée</th>
<th class="moduleimpl_evaluations">Coef.</th>
<th class="moduleimpl_evaluations">Notes</th>
<th class="moduleimpl_evaluations">Abs</th>
<th class="moduleimpl_evaluations">N</th>
<th class="moduleimpl_evaluations" colspan="2">Moyenne {etat_txt}</th>
</tr>
<tr class="{tr_class}">
<td class="mievr">"""
)
if can_edit_evals:
H.append(
@ -726,7 +726,7 @@ def _ligne_evaluation(
<td class="rightcell mievr_nbnotes">{etat["nb_notes"]} / {etat["nb_inscrits"]}</td>
<td class="rightcell mievr_coef">{etat["nb_abs"]}</td>
<td class="rightcell mievr_coef">{etat["nb_neutre"]}</td>
<td class="rightcell">"""
<td class="rightcell" colspan="2">"""
% etat
)
if etat["moy"]:
@ -750,11 +750,11 @@ def _ligne_evaluation(
H.append(f"""<tr class="{tr_class}"><td></td>""")
if modimpl.module.is_apc():
H.append(
f"""<td colspan="7" class="eval_poids">{
f"""<td colspan="8" class="eval_poids">{
evaluation.get_ue_poids_str()}</td>"""
)
else:
H.append('<td colspan="7"></td>')
H.append('<td colspan="8"></td>')
H.append("""</tr>""")
else: # il y a deja des notes saisies
gr_moyennes = etat["gr_moyennes"]
@ -773,7 +773,10 @@ def _ligne_evaluation(
name = "Tous" # tous
else:
name = f"""Groupe {gr_moyenne["group_name"]}"""
H.append(f"""<td colspan="2" class="mievr_grtit">{name} &nbsp;</td><td>""")
H.append(
f"""<td colspan="2" class="mievr_grtit">{name} &nbsp;</td>
<td colspan="2">"""
)
if gr_moyenne["gr_nb_notes"] > 0:
H.append(
f"""{gr_moyenne["gr_moy"]}&nbsp; (<a href="{

View File

@ -1,5 +1,10 @@
/* Saisie décision de jury BUT */
:root {
--color-recorded: rgb(3, 157, 3);
--color-explanation: blueviolet;
}
.jury_but {
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
@ -18,6 +23,19 @@
margin-top: 0px;
}
form#jury_but {
margin: 0px 16px 16px 16px;
background-color: rgb(253, 253, 231);
border: 2px solid rgb(4, 4, 118);
border-radius: 4px;
padding-top: 8px;
padding-left: 16px;
padding-right: 16px;
padding-bottom: 16px;
min-width: var(--sco-content-min-width);
max-width: var(--sco-content-max-width);
}
.but_annee {
margin-left: 32px;
display: inline-grid;
@ -112,11 +130,10 @@ div.but_settings {
}
.but_explanation {
color: blueviolet;
color: var(--color-explanation);
font-style: italic;
padding-top: 4px;
padding-bottom: 12px;
;
}
select:disabled {
@ -130,7 +147,7 @@ select:invalid {
select.but_code option.recorded,
div.but_code recorded {
color: rgb(3, 157, 3);
color: var(--color-recorded);
font-weight: bold;
}
@ -143,14 +160,14 @@ div.but_code {
div.but_niveau_ue.recorded,
div.but_niveau_rcue.recorded {
border-color: rgb(0, 169, 0);
border-color: var(--color-recorded);
border-width: 3px;
}
div.but_niveau_ue.recorded_different,
div.but_niveau_rcue.recorded_different {
box-shadow: 0 0 0 3px red;
outline: dashed 3px rgb(0, 169, 0);
outline: dashed 3px var(--color-recorded);
}
div.but_niveau_ue.annee_prec {
@ -174,6 +191,8 @@ div.but_buttons span {
div.but_doc_codes {
margin: 16px;
min-width: var(--sco-content-min-width);
max-width: var(--sco-content-max-width);
background-color: rgb(227, 254, 254);
font-size: 75%;
border: 2px solid rgb(4, 4, 118);
@ -227,4 +246,16 @@ div.but_doc table tr td.amue {
.but_niveau_rcue .scoplement {
font-weight: normal;
}
.but_autorisations_passage {
margin-top: 12px;
margin-left: 32px;
font-weight: bold;
color: var(--color-recorded);
}
.but_autorisations_passage.but_explanation {
font-weight: normal;
color: var(--color-explanation);
}

View File

@ -1,7 +1,11 @@
/* # -*- mode: css -*-
ScoDoc, (c) Emmanuel Viennet 1998 - 2021
/* ScoDoc, (c) Emmanuel Viennet 1998 - 2023
*/
:root {
--sco-content-min-width: 600px;
--sco-content-max-width: 1024px;
}
html,
body {
margin: 0;
@ -1728,6 +1732,8 @@ ul.ue_inscr_list li.etud {
div.moduleimpl_tableaubord {
padding: 7px;
border: 2px solid gray;
min-width: var(--sco-content-min-width);
max-width: var(--sco-content-max-width);
}
div.moduleimpl_type_sae {
@ -1855,11 +1861,19 @@ span.mievr_rattr {
padding: 1px 3px 1px 3px;
}
tr.mievr td.mievr_tit {
tr.mievr_tit td.mievr_tit {
font-weight: bold;
background-color: #cccccc;
border-top-left-radius: 8px;
}
tr.mievr.mievr_tit td {
background-color: #e1e1e1;
}
tr.mievr_tit td:last-child {
border-top-right-radius: 8px;
text-align: right;
padding-right: 8px;
}
tr.mievr td {
@ -1922,11 +1936,6 @@ span.eval_warning_coef {
background-color: rgb(255, 225, 0);
}
div.evaluation_order {
position: absolute;
right: 1em;
}
span.evalindex {
font-weight: normal;
font-size: 80%;
@ -2580,11 +2589,13 @@ div.bull_head {
display: grid;
justify-content: space-between;
grid-template-columns: auto auto;
min-width: 600px;
max-width: 1072px;
}
div.bull_photo {
display: inline-block;
margin-right: 10px;
margin-right: 8px;
}
span.bulletin_menubar_but {

View File

@ -2499,11 +2499,26 @@ def formsemestre_validation_but(
H.append(jury_but_view.show_etud(deca, read_only=read_only))
autorisations_idx = deca.get_autorisations_passage()
div_autorisations_passage = (
f"""
<div class="but_autorisations_passage">
<span>Autorisé à passer en&nbsp;:</span>
{ ", ".join( ["S" + str(i) for i in autorisations_idx ] )}
</div>
"""
if autorisations_idx
else """<div class="but_autorisations_passage but_explanation">pas d'autorisations de passage enregistrées.</div>"""
)
H.append(div_autorisations_passage)
if read_only:
H.append(
"""<div class="but_explanation">
"""
<div class="but_explanation">
Vous n'avez pas la permission de modifier ces décisions.
Les champs entourés en vert sont enregistrés.</div>"""
Les champs entourés en vert sont enregistrés.
</div>"""
)
else:
erase_span = f"""<a href="{
@ -2513,9 +2528,11 @@ def formsemestre_validation_but(
H.append(
f"""<div class="but_settings">
<input type="checkbox" onchange="enable_manual_codes(this)">
<em>permettre la saisie manuelles des codes d'année et de niveaux.
Dans ce cas, il vous revient de vous assurer de la cohérence entre
vos codes d'UE/RCUE/Année !</em>
<em>permettre la saisie manuelles des codes
{"d'année et " if deca.jury_annuel else ""}
de niveaux.
Dans ce cas, assurez-vous de la cohérence entre les codes d'UE/RCUE/Année !
</em>
</input>
</div>

View File

@ -32,7 +32,7 @@ from app.models import GroupDescr
from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription
from app.models import Partition
from app.models import ScolarFormSemestreValidation
from app.models import ScolarAutorisationInscription, ScolarFormSemestreValidation
from app.models.but_refcomp import (
ApcCompetence,
ApcNiveau,
@ -101,6 +101,7 @@ def make_shell_context():
"ResultatsSemestreBUT": ResultatsSemestreBUT,
"Role": Role,
"scolar": scolar,
"ScolarAutorisationInscription": ScolarAutorisationInscription,
"ScolarFormSemestreValidation": ScolarFormSemestreValidation,
"ScolarNews": models.ScolarNews,
"scu": scu,

View File

@ -1022,3 +1022,84 @@ Etudiants:
moy_ue: 9.5000
moy_ue_with_cap: 12.7600
decision_annee: AJ
geii1000:
prenom: etugeii1000
civilite: M
formsemestres:
S1:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 12.0000
"S1.2": 9.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
codes: [ "ADM", "..." ]
code_valide: ADM
moy_ue: 12.0000
"UE12":
codes: [ "AJ", "..." ]
code_valide: AJ
decision_jury: AJ
moy_ue: 9.0000
S2:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S2.1": 12.0000
"S2.2": 10.50 # capitalise mais ne compense pas
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 2
valide_moitie_rcue: False
codes: [ "RED", "..." ]
decisions_ues:
"UE21":
codes: [ "ADM", "..." ]
code_valide: ADM
moy_ue: 12.0000
"UE22":
code_valide: ADM
moy_ue: 10.5
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: ADM
decision_jury: ADM
rcue:
moy_rcue: 12.0000
est_compensable: False
"UE12":
code_valide: AJ
decision_jury: AJ
rcue:
moy_rcue: 9.75
est_compensable: False
decision_annee: RED
S1-red:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 5.0000
"S1.2": 12.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
code_valide: AJ
moy_ue: 5. # LA MOYENNE COURANTE
moy_ue_with_cap: 12.0000
"UE12":
code_valide: ADM
decision_jury: ADM
moy_ue: 12.0000
# RCUE inter-annuel
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE12":
code_valide: ADM
rcue:
moy_rcue: 11.25
est_compensable: False

View File

@ -33,7 +33,7 @@ DEPT = TestConfig.DEPT_TEST
@pytest.mark.but_gb
def test_but_jury_GB(test_client):
"""Tests sur un cursus GB
- construction des semestres et de leurs étudianst à partir du yaml
- construction des semestres et de leurs étudiants à partir du yaml
- vérification jury de S1
- vérification jury de S2
- vérification jury de S3
@ -96,7 +96,6 @@ def test_but_jury_GEII_lyon(test_client):
# Construit la base de test GB une seule fois
# puis lance les tests de jury
doc = yaml_setup.setup_from_yaml("tests/unit/cursus_but_geii_lyon.yaml")
formsemestres = FormSemestre.query.order_by(
FormSemestre.date_debut, FormSemestre.semestre_id
).all()