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

This commit is contained in:
Emmanuel Viennet 2023-01-27 04:01:28 -03:00
commit 569df98e25
29 changed files with 536 additions and 243 deletions

View File

@ -630,19 +630,38 @@ class DecisionsProposeesAnnee(DecisionsProposees):
d[dec_rcue.rcue.ue_2.id] = dec_rcue d[dec_rcue.rcue.ue_2.id] = dec_rcue
return d return d
def next_annee_semestre_id(self, code: str) -> int: def next_semestre_ids(self, code: str) -> set[int]:
"""L'indice du semestre dans lequel l'étudiant est autorisé à """Les indices des semestres dans lequels l'étudiant est autorisé
poursuivre l'année suivante. None si aucun.""" à poursuivre après le semestre courant.
if self.formsemestre_pair is None: """
return None # seulement sur année ids = set()
if code == RED: # La poursuite d'études dans un semestre pair dune même année
return self.formsemestre_pair.semestre_id - 1 # est de droit pour tout étudiant:
elif ( if (self.formsemestre.semestre_id % 2) and sco_codes.ParcoursBUT.NB_SEM:
code in sco_codes.BUT_CODES_PASSAGE 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 and self.formsemestre_pair.semestre_id < sco_codes.ParcoursBUT.NB_SEM
): ):
return self.formsemestre_pair.semestre_id + 1 ids.add(self.formsemestre.semestre_id + 1)
return None
if code == RED:
ids.add(
self.formsemestre.semestre_id - (self.formsemestre.semestre_id + 1) % 2
)
return ids
def record_form(self, form: dict): def record_form(self, form: dict):
"""Enregistre les codes de jury en base """Enregistre les codes de jury en base
@ -704,47 +723,43 @@ class DecisionsProposeesAnnee(DecisionsProposees):
raise ScoValueError( raise ScoValueError(
f"code annee <tt>{html.escape(code)}</tt> invalide pour formsemestre {html.escape(self.formsemestre)}" 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 if code != self.code_valide and (self.code_valide is None or not no_overwrite):
return False # no change # Enregistrement du code annuel BUT
if self.validation: if self.validation:
db.session.delete(self.validation) db.session.delete(self.validation)
db.session.commit() db.session.commit()
if code is None: if code is None:
self.validation = None self.validation = None
else: else:
self.validation = ApcValidationAnnee( self.validation = ApcValidationAnnee(
etudid=self.etud.id, etudid=self.etud.id,
formsemestre=self.formsemestre_impair, formsemestre=self.formsemestre_impair,
ordre=self.annee_but, ordre=self.annee_but,
annee_scolaire=self.annee_scolaire(), annee_scolaire=self.annee_scolaire(),
code=code, code=code,
) )
db.session.add(self.validation) db.session.add(self.validation)
db.session.commit() db.session.commit()
log(f"Recording {self}: {code}") log(f"Recording {self}: {code}")
Scolog.logdb( Scolog.logdb(
method="jury_but", method="jury_but",
etudid=self.etud.id, etudid=self.etud.id,
msg=f"Validation année BUT{self.annee_but}: {code}", msg=f"Validation année BUT{self.annee_but}: {code}",
) )
# --- Autorisation d'inscription dans semestre suivant ? # --- Autorisation d'inscription dans semestre suivant ?
if self.formsemestre_pair is not None: ScolarAutorisationInscription.delete_autorisation_etud(
if code is None: etudid=self.etud.id,
ScolarAutorisationInscription.delete_autorisation_etud( origin_formsemestre_id=self.formsemestre.id,
etudid=self.etud.id, )
origin_formsemestre_id=self.formsemestre_pair.id, for next_semestre_id in self.next_semestre_ids(code):
) ScolarAutorisationInscription.autorise_etud(
else: self.etud.id,
next_semestre_id = self.next_annee_semestre_id(code) self.formsemestre.formation.formation_code,
if next_semestre_id is not None: self.formsemestre.id,
ScolarAutorisationInscription.autorise_etud( next_semestre_id,
self.etud.id, )
self.formsemestre_pair.formation.formation_code,
self.formsemestre_pair.id,
next_semestre_id,
)
db.session.commit() db.session.commit()
self.recorded = True self.recorded = True
@ -872,18 +887,18 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.invalidate_formsemestre_cache() self.invalidate_formsemestre_cache()
def get_autorisations_passage(self) -> list[int]: def get_autorisations_passage(self) -> list[int]:
"""Les liste des indices de semestres auxquels on est autorisé à """Liste des indices de semestres auxquels on est autorisé à
s'inscrire depuis cette année""" s'inscrire depuis le semestre courant.
formsemestre = self.formsemestre_pair or self.formsemestre_impair """
if not formsemestre: return sorted(
return [] [
return [ a.semestre_id
a.semestre_id for a in ScolarAutorisationInscription.query.filter_by(
for a in ScolarAutorisationInscription.query.filter_by( etudid=self.etud.id,
etudid=self.etud.id, origin_formsemestre_id=self.formsemestre.id,
origin_formsemestre_id=formsemestre.id, )
) ]
] )
def descr_niveaux_validation(self, line_sep: str = "\n") -> str: def descr_niveaux_validation(self, line_sep: str = "\n") -> str:
"""Description textuelle des niveaux validés (enregistrés) """Description textuelle des niveaux validés (enregistrés)

View File

@ -11,7 +11,7 @@ import re
import numpy as np import numpy as np
import flask import flask
from flask import flash, url_for from flask import flash, render_template, url_for
from flask import g, request from flask import g, request
from app import db from app import db
@ -32,8 +32,10 @@ from app.models import (
ScolarAutorisationInscription, ScolarAutorisationInscription,
ScolarFormSemestreValidation, ScolarFormSemestreValidation,
) )
from app.models.config import ScoDocSiteConfig
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -43,21 +45,20 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
""" """
H = [] H = []
H.append("""<div class="but_section_annee">""") if deca.jury_annuel:
H.append( H.append(
f""" f"""
<div> <div class="but_section_annee">
<b>Décision de jury pour l'année :</b> { <div>
_gen_but_select("code_annee", deca.codes, deca.code_valide, <b>Décision de jury pour l'année :</b> {
disabled=True, klass="manual") _gen_but_select("code_annee", deca.codes, deca.code_valide,
} disabled=True, klass="manual")
<span>({deca.code_valide or 'non'} enregistrée)</span> }
<span>({deca.code_valide or 'non'} enregistrée)</span>
</div>
</div> </div>
""" """
) )
div_explanation = f"""<div class="but_explanation">{deca.explanation}</div>"""
H.append("""</div>""")
formsemestre_1 = deca.formsemestre_impair formsemestre_1 = deca.formsemestre_impair
formsemestre_2 = deca.formsemestre_pair formsemestre_2 = deca.formsemestre_pair
@ -74,7 +75,7 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
<div class="titre_niveaux"> <div class="titre_niveaux">
<b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b> <b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b>
</div> </div>
{div_explanation} <div class="but_explanation">{deca.explanation}</div>
<div class="but_annee"> <div class="but_annee">
<div class="titre"></div> <div class="titre"></div>
<div class="titre">{"S" +str(formsemestre_1.semestre_id) <div class="titre">{"S" +str(formsemestre_1.semestre_id)
@ -285,7 +286,7 @@ def jury_but_semestriel(
read_only: bool, read_only: bool,
navigation_div: str = "", navigation_div: str = "",
) -> 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) res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res) parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res)
inscription_etat = etud.inscription_etat(formsemestre.id) inscription_etat = etud.inscription_etat(formsemestre.id)
@ -374,20 +375,20 @@ def jury_but_semestriel(
f""" f"""
<div class="jury_but"> <div class="jury_but">
<div> <div>
<div class="bull_head"> <div class="bull_head">
<div> <div>
<div class="titre_parcours">Jury BUT S{formsemestre.id} <div class="titre_parcours">Jury BUT S{formsemestre.id}
- Parcours {(parcour.libelle if parcour else False) or "non spécifié"} - 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>
<div class="nom_etud">{etud.nomprenom}</div> <h3>Jury sur un semestre BUT isolé (ne concerne que les UEs)</h3>
</div> {warning}
<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}
</div> </div>
<form method="post" id="jury_but"> <form method="post" id="jury_but">
@ -450,24 +451,37 @@ def jury_but_semestriel(
) )
H.append("</div>") # but_annee 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: if read_only:
H.append( H.append(
"""<div class="but_explanation"> f"""<div class="but_explanation">
Vous n'avez pas la permission de modifier ces décisions. {"Vous n'avez pas la permission de modifier ces décisions."
Les champs entourés en vert sont enregistrés.</div>""" if formsemestre.etat
else "Semestre verrouillé."}
Les champs entourés en vert sont enregistrés.
</div>
"""
) )
else: else:
if formsemestre.semestre_id < formsemestre.formation.get_parcours().NB_SEM: if formsemestre.semestre_id < formsemestre.formation.get_parcours().NB_SEM:
H.append( H.append(
f""" f"""
<div class="but_settings"> <div class="but_settings">
<input type="checkbox" name="autorisation_passage" value="1" { <input type="checkbox" name="autorisation_passage" value="1" {
"checked" if est_autorise_a_passer else ""}> "checked" if est_autorise_a_passer else ""}>
<em>autoriser à passer dans le semestre S{formsemestre.semestre_id+1}</em> <em>autoriser à passer dans le semestre S{formsemestre.semestre_id+1}</em>
{("(autorisations enregistrées: " + ' '.join( </input>
'S' + str(a.semestre_id or '') for a in autorisations_passage) + ")"
) if autorisations_passage else ""}
</input>
</div> </div>
""" """
) )
@ -481,7 +495,19 @@ def jury_but_semestriel(
</div> </div>
""" """
) )
H.append(navigation_div)
H.append(navigation_div)
H.append("</div>")
H.append(
render_template(
"but/documentation_codes_jury.html",
nom_univ=f"""Export {sco_preferences.get_preference("InstituteName")
or sco_preferences.get_preference("UnivName")
or "Apogée"}""",
codes=ScoDocSiteConfig.get_codes_apo_dict(),
)
)
return "\n".join(H) return "\n".join(H)

View File

@ -740,7 +740,8 @@ class ResultatsSemestre(ResultatsCache):
f"""<a href="{url_for('notes.formsemestre_validation_etud_form', f"""<a href="{url_for('notes.formsemestre_validation_etud_form',
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid
) )
}">{"saisir" if not jury_code_sem else "modifier"} décision</a>""", }">{("saisir" if not jury_code_sem else "modifier")
if self.formsemestre.etat else "voir"} décisions</a>""",
"col_jury_link", "col_jury_link",
) )

View File

@ -55,7 +55,8 @@ class Formation(db.Model):
modules = db.relationship("Module", lazy="dynamic", backref="formation") modules = db.relationship("Module", lazy="dynamic", backref="formation")
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme!r}')>" return f"""<{self.__class__.__name__}(id={self.id}, dept_id={
self.dept_id}, acronyme={self.acronyme!r}, version={self.version})>"""
def to_html(self) -> str: def to_html(self) -> str:
"titre complet pour affichage" "titre complet pour affichage"

View File

@ -111,6 +111,7 @@ class UniteEns(db.Model):
e["ects"] = e["ects"] e["ects"] = e["ects"]
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0 e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
e["code_apogee"] = e["code_apogee"] or "" # pas de None e["code_apogee"] = e["code_apogee"] or "" # pas de None
e["parcour"] = self.parcour.to_dict() if self.parcour else None
if with_module_ue_coefs: if with_module_ue_coefs:
if convert_objects: if convert_objects:
e["module_ue_coefs"] = [ e["module_ue_coefs"] = [

View File

@ -99,7 +99,7 @@ def html_edit_formation_apc(
H = [ H = [
render_template( render_template(
"pn/form_ues.html", "pn/form_ues.j2",
formation=formation, formation=formation,
semestre_ids=semestre_ids, semestre_ids=semestre_ids,
editable=editable, editable=editable,
@ -122,7 +122,7 @@ def html_edit_formation_apc(
).first() ).first()
H += [ H += [
render_template( render_template(
"pn/form_mods.html", "pn/form_mods.j2",
formation=formation, formation=formation,
titre=f"Ressources du S{semestre_idx}", titre=f"Ressources du S{semestre_idx}",
create_element_msg="créer une nouvelle ressource", create_element_msg="créer une nouvelle ressource",
@ -138,7 +138,7 @@ def html_edit_formation_apc(
if ues_by_sem[semestre_idx].count() > 0 if ues_by_sem[semestre_idx].count() > 0
else "", else "",
render_template( render_template(
"pn/form_mods.html", "pn/form_mods.j2",
formation=formation, formation=formation,
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}", titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
create_element_msg="créer une nouvelle SAÉ", create_element_msg="créer une nouvelle SAÉ",
@ -154,7 +154,7 @@ def html_edit_formation_apc(
if ues_by_sem[semestre_idx].count() > 0 if ues_by_sem[semestre_idx].count() > 0
else "", else "",
render_template( render_template(
"pn/form_mods.html", "pn/form_mods.j2",
formation=formation, formation=formation,
titre=f"Autres modules (non BUT) du S{semestre_idx}", titre=f"Autres modules (non BUT) du S{semestre_idx}",
create_element_msg="créer un nouveau module", create_element_msg="créer un nouveau module",
@ -196,7 +196,7 @@ def html_ue_infos(ue):
and ue.matieres.count() == 0 and ue.matieres.count() == 0
) )
return render_template( return render_template(
"pn/ue_infos.html", "pn/ue_infos.j2",
titre=f"UE {ue.acronyme} {ue.titre}", titre=f"UE {ue.acronyme} {ue.titre}",
ue=ue, ue=ue,
formsemestres=formsemestres, formsemestres=formsemestres,

View File

@ -723,7 +723,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
"libjs/jQuery-tagEditor/jquery.caret.min.js", "libjs/jQuery-tagEditor/jquery.caret.min.js",
"js/module_tag_editor.js", "js/module_tag_editor.js",
], ],
page_title=f"Programme {formation.acronyme}", page_title=f"Programme {formation.acronyme} v{formation.version}",
), ),
f"""<h2>{formation.to_html()} {lockicon} f"""<h2>{formation.to_html()} {lockicon}
</h2> </h2>
@ -765,7 +765,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
# Description de la formation # Description de la formation
H.append( H.append(
render_template( render_template(
"pn/form_descr.html", "pn/form_descr.j2",
formation=formation, formation=formation,
parcours=parcours, parcours=parcours,
editable=editable, editable=editable,
@ -913,8 +913,12 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<li><a class="stdlink" href="{ <li><a class="stdlink" href="{
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept, url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
formation_id=formation_id, format='xml') formation_id=formation_id, format='xml')
}">Export XML de la formation</a> }">Export XML de la formation</a> ou
(permet de la sauvegarder pour l'échanger avec un autre site) <a class="stdlink" href="{
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
formation_id=formation_id, format='xml', export_codes_apo=0)
}">sans codes Apogée</a>
(permet de l'enregistrer pour l'échanger avec un autre site)
</li> </li>
<li><a class="stdlink" href="{ <li><a class="stdlink" href="{

View File

@ -109,6 +109,7 @@ def formation_export(
export_ids=False, export_ids=False,
export_tags=True, export_tags=True,
export_external_ues=False, export_external_ues=False,
export_codes_apo=True,
format=None, format=None,
): ):
"""Get a formation, with UE, matieres, modules """Get a formation, with UE, matieres, modules
@ -116,30 +117,45 @@ def formation_export(
""" """
formation: Formation = Formation.query.get_or_404(formation_id) formation: Formation = Formation.query.get_or_404(formation_id)
f_dict = formation.to_dict(with_refcomp_attrs=True) f_dict = formation.to_dict(with_refcomp_attrs=True)
selector = {"formation_id": formation_id} if not export_ids:
del f_dict["formation_id"]
del f_dict["dept_id"]
ues = formation.ues
if not export_external_ues: if not export_external_ues:
selector["is_external"] = False ues = ues.filter_by(is_external=False)
ues = sco_edit_ue.ue_list(selector) ues = ues.all()
f_dict["ue"] = ues ues.sort(key=lambda u: (u.semestre_idx or 0, u.numero or 0, u.acronyme))
for ue_dict in ues: f_dict["ue"] = []
ue_id = ue_dict["ue_id"] for ue in ues:
ue_dict = ue.to_dict()
f_dict["ue"].append(ue_dict)
ue_dict.pop("module_ue_coefs", None)
if formation.is_apc(): if formation.is_apc():
# BUT: indique niveau de compétence associé à l'UE # BUT: indique niveau de compétence associé à l'UE
ue = UniteEns.query.get(ue_id)
if ue.niveau_competence: if ue.niveau_competence:
ue_dict["apc_niveau_libelle"] = ue.niveau_competence.libelle ue_dict["apc_niveau_libelle"] = ue.niveau_competence.libelle
ue_dict["apc_niveau_annee"] = ue.niveau_competence.annee ue_dict["apc_niveau_annee"] = ue.niveau_competence.annee
ue_dict["apc_niveau_ordre"] = ue.niveau_competence.ordre ue_dict["apc_niveau_ordre"] = ue.niveau_competence.ordre
ue_dict["reference"] = ue_id # pour les coefficients # Et le parcour:
if ue.parcour:
ue_dict["parcour"] = [ue.parcour.to_dict(with_annees=False)]
ue_dict["reference"] = ue.id # pour les coefficients
if not export_ids: if not export_ids:
del ue_dict["id"] for id_id in (
del ue_dict["ue_id"] "id",
del ue_dict["formation_id"] "ue_id",
if "niveau_competence_id" in ue_dict: "formation_id",
del ue_dict["niveau_competence_id"] "parcour_id",
"niveau_competence_id",
):
ue_dict.pop(id_id, None)
if not export_codes_apo:
ue_dict.pop("code_apogee", None)
if ue_dict["ects"] is None: if ue_dict["ects"] is None:
del ue_dict["ects"] del ue_dict["ects"]
mats = sco_edit_matiere.matiere_list({"ue_id": ue_id}) mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
mats.sort(key=lambda m: m["numero"] or 0)
ue_dict["matiere"] = mats ue_dict["matiere"] = mats
for mat in mats: for mat in mats:
matiere_id = mat["matiere_id"] matiere_id = mat["matiere_id"]
@ -148,6 +164,7 @@ def formation_export(
del mat["matiere_id"] del mat["matiere_id"]
del mat["ue_id"] del mat["ue_id"]
mods = sco_edit_module.module_list({"matiere_id": matiere_id}) mods = sco_edit_module.module_list({"matiere_id": matiere_id})
mods.sort(key=lambda m: (m["numero"] or 0, m["code"]))
mat["module"] = mods mat["module"] = mods
for mod in mods: for mod in mods:
module_id = mod["module_id"] module_id = mod["module_id"]
@ -183,6 +200,8 @@ def formation_export(
del mod["matiere_id"] del mod["matiere_id"]
del mod["module_id"] del mod["module_id"]
del mod["formation_id"] del mod["formation_id"]
if not export_codes_apo:
del mod["code_apogee"]
if mod["ects"] is None: if mod["ects"] is None:
del mod["ects"] del mod["ects"]
@ -323,14 +342,30 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
referentiel_competence_id, ue_info[1] referentiel_competence_id, ue_info[1]
) )
ue_id = sco_edit_ue.do_ue_create(ue_info[1]) ue_id = sco_edit_ue.do_ue_create(ue_info[1])
ue: UniteEns = UniteEns.query.get(ue_id)
assert ue
if xml_ue_id: if xml_ue_id:
ues_old2new[xml_ue_id] = ue_id ues_old2new[xml_ue_id] = ue_id
# élément optionnel présent dans les exports BUT: # élément optionnel présent dans les exports BUT:
ue_reference = ue_info[1].get("reference") ue_reference = ue_info[1].get("reference")
if ue_reference: if ue_reference:
ue_reference_to_id[int(ue_reference)] = ue_id ue_reference_to_id[int(ue_reference)] = ue_id
# -- create matieres # -- create matieres
for mat_info in ue_info[2]: for mat_info in ue_info[2]:
if mat_info[0] == "parcour":
# Parcours (BUT)
code_parcours = mat_info[1]["code"]
parcour = ApcParcours.query.filter_by(
code=code_parcours,
referentiel_id=referentiel_competence_id,
).first()
if parcour:
ue.parcour = parcour
db.session.add(ue)
else:
log(f"Warning: parcours {code_parcours} inexistant !")
continue
assert mat_info[0] == "matiere" assert mat_info[0] == "matiere"
mat_info[1]["ue_id"] = ue_id mat_info[1]["ue_id"] = ue_id
mat_id = sco_edit_matiere.do_matiere_create(mat_info[1]) mat_id = sco_edit_matiere.do_matiere_create(mat_info[1])
@ -382,12 +417,12 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
# associe les parcours de ce module (BUT) # associe les parcours de ce module (BUT)
if referentiel_competence_id is not None: if referentiel_competence_id is not None:
code_parcours = child[1]["code"] code_parcours = child[1]["code"]
parcours = ApcParcours.query.filter_by( parcour = ApcParcours.query.filter_by(
code=code_parcours, code=code_parcours,
referentiel_id=referentiel_competence_id, referentiel_id=referentiel_competence_id,
).first() ).first()
if parcours: if parcour:
module.parcours.append(parcours) module.parcours.append(parcour)
db.session.add(module) db.session.add(module)
else: else:
log( log(

View File

@ -556,9 +556,10 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
str(parcour.id) for parcour in ref_comp.parcours str(parcour.id) for parcour in ref_comp.parcours
], ],
"explanation": """Parcours proposés dans ce semestre. "explanation": """Parcours proposés dans ce semestre.
S'il s'agit d'un semestre de "tronc commun", ne pas indiquer de parcours. Cocher tous les parcours est exactement équivalent à n'en cocher aucun:
Attention, si aucun parcours n'est coché, toutes les UEs du par exemple, pour un semestre de "tronc commun", on peut ne pas indiquer de parcours.
programme seront considérées, quel que soit leur parcours. Si aucun parcours n'est coché, toutes les UEs du
programme seront donc considérées, quel que soit leur parcours.
""", """,
}, },
) )

View File

@ -602,9 +602,21 @@ def formsemestre_recap_parcours_table(
<td class="datedebut">{sem['mois_debut']}</td> <td class="datedebut">{sem['mois_debut']}</td>
<td class="rcp_titre_sem"><a class="formsemestre_status_link" <td class="rcp_titre_sem"><a class="formsemestre_status_link"
href="{a_url}formsemestre_bulletinetud?formsemestre_id={formsemestre.id}&etudid={etudid}" href="{a_url}formsemestre_bulletinetud?formsemestre_id={formsemestre.id}&etudid={etudid}"
title="Bulletin de notes">{formsemestre.titre_annee()}{parcours_name}</a></td> title="Bulletin de notes">{formsemestre.titre_annee()}{parcours_name}</a>
""" """
) )
if nt.is_apc:
H.append(
f"""<a class="stdlink jury_link" title="Validations du semestre BUT"
href="{ url_for("notes.formsemestre_validation_but",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
etudid=etudid,
)
}">jury</a>"""
)
H.append("""</td>""")
if nt.is_apc: if nt.is_apc:
H.append('<td class="rcp_but">BUT</td>') H.append('<td class="rcp_but">BUT</td>')
elif decision_sem: elif decision_sem:

View File

@ -39,21 +39,22 @@ from app import log
from app.models import FormSemestre from app.models import FormSemestre
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_preferences from app.scodoc import sco_etud
from app.scodoc import sco_pvjury from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formations
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_etud from app.scodoc import sco_preferences
from app.scodoc import sco_pvjury
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False): def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False):
"""Liste des etudiants autorisés à s'inscrire dans sem. """Liste des etudiants autorisés à s'inscrire dans sem.
delai = nb de jours max entre la date de l'autorisation et celle de debut du semestre cible. delai = nb de jours max entre la date de l'autorisation et celle de debut du semestre cible.
ignore_jury: si vrai, considère tous les étudiants comem autorisés, même ignore_jury: si vrai, considère tous les étudiants comme autorisés, même
s'ils n'ont pas de décision de jury. s'ils n'ont pas de décision de jury.
""" """
src_sems = list_source_sems(sem, delai=delai) src_sems = list_source_sems(sem, delai=delai)
@ -276,8 +277,8 @@ def formsemestre_inscr_passage(
submitted=False, submitted=False,
dialog_confirmed=False, dialog_confirmed=False,
ignore_jury=False, ignore_jury=False,
): ) -> str:
"""Form. pour inscription des etudiants d'un semestre dans un autre """Page Form. pour inscription des etudiants d'un semestre dans un autre
(donné par formsemestre_id). (donné par formsemestre_id).
Permet de selectionner parmi les etudiants autorisés à s'inscrire. Permet de selectionner parmi les etudiants autorisés à s'inscrire.
Principe: Principe:
@ -285,8 +286,8 @@ def formsemestre_inscr_passage(
- afficher chaque semestre "boites" avec cases à cocher - afficher chaque semestre "boites" avec cases à cocher
- si l'étudiant est déjà inscrit, le signaler (gras, nom de groupes): il peut être désinscrit - si l'étudiant est déjà inscrit, le signaler (gras, nom de groupes): il peut être désinscrit
- on peut choisir les groupes TD, TP, TA - on peut choisir les groupes TD, TP, TA
- seuls les etudiants non inscrits changent (de groupe) - seuls les étudiants non inscrits changent (de groupe)
- les etudiants inscrit qui se trouvent décochés sont désinscrits - les étudiants inscrit qui se trouvent décochés sont désinscrits
- Confirmation: indiquer les étudiants inscrits et ceux désinscrits, le total courant. - Confirmation: indiquer les étudiants inscrits et ceux désinscrits, le total courant.
""" """
@ -326,11 +327,9 @@ def formsemestre_inscr_passage(
a_desinscrire = inscrits_set - etuds_set a_desinscrire = inscrits_set - etuds_set
else: else:
a_inscrire = a_desinscrire = [] a_inscrire = a_desinscrire = []
# log('formsemestre_inscr_passage: a_inscrire=%s' % str(a_inscrire) )
# log('formsemestre_inscr_passage: a_desinscrire=%s' % str(a_desinscrire) )
if not submitted: if not submitted:
H += build_page( H += _build_page(
sem, sem,
auth_etuds_by_sem, auth_etuds_by_sem,
inscrits, inscrits,
@ -343,7 +342,7 @@ def formsemestre_inscr_passage(
if not dialog_confirmed: if not dialog_confirmed:
# Confirmation # Confirmation
if a_inscrire: if a_inscrire:
H.append("<h3>Etudiants à inscrire</h3><ol>") H.append("<h3>Étudiants à inscrire</h3><ol>")
for etud in set_to_sorted_etud_list(a_inscrire): for etud in set_to_sorted_etud_list(a_inscrire):
H.append("<li>%(nomprenom)s</li>" % etud) H.append("<li>%(nomprenom)s</li>" % etud)
H.append("</ol>") H.append("</ol>")
@ -354,7 +353,7 @@ def formsemestre_inscr_passage(
H.append('<li class="inscrailleurs">%(nomprenom)s</li>' % etud) H.append('<li class="inscrailleurs">%(nomprenom)s</li>' % etud)
H.append("</ul>") H.append("</ul>")
if a_desinscrire: if a_desinscrire:
H.append("<h3>Etudiants à désinscrire</h3><ol>") H.append("<h3>Étudiants à désinscrire</h3><ol>")
for etudid in a_desinscrire: for etudid in a_desinscrire:
H.append( H.append(
'<li class="desinscription">%(nomprenom)s</li>' '<li class="desinscription">%(nomprenom)s</li>'
@ -384,21 +383,29 @@ def formsemestre_inscr_passage(
) )
) )
else: else:
# Inscription des étudiants au nouveau semestre: with sco_cache.DeferredSemCacheManager():
do_inscrit( # Inscription des étudiants au nouveau semestre:
sem, do_inscrit(
a_inscrire, sem,
inscrit_groupes=inscrit_groupes, a_inscrire,
) inscrit_groupes=inscrit_groupes,
)
# Desincriptions: # Désinscriptions:
do_desinscrit(sem, a_desinscrire) do_desinscrit(sem, a_desinscrire)
H.append( H.append(
"""<h3>Opération effectuée</h3> f"""<h3>Opération effectuée</h3>
<ul><li><a class="stdlink" href="formsemestre_inscr_passage?formsemestre_id=%s">Continuer les inscriptions</a></li> <ul>
<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%s">Tableau de bord du semestre</a></li>""" <li><a class="stdlink" href="{
% (formsemestre_id, formsemestre_id) url_for("notes.formsemestre_inscr_passage",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">Continuer les inscriptions</a>
</li>
<li><a class="stdlink" href="{
url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">Tableau de bord du semestre</a>
</li>"""
) )
partition = sco_groups.formsemestre_get_main_partition(formsemestre_id) partition = sco_groups.formsemestre_get_main_partition(formsemestre_id)
if ( if (
@ -410,7 +417,7 @@ def formsemestre_inscr_passage(
H.append( H.append(
f"""<li><a class="stdlink" href="{ f"""<li><a class="stdlink" href="{
url_for("scolar.affect_groups", url_for("scolar.affect_groups",
scodoc_dept=g.scodoc_dept, partition_id=partition["partition_id"]) scodoc_dept=g.scodoc_dept, partition_id=partition["partition_id"])
}">Répartir les groupes de {partition["partition_name"]}</a></li> }">Répartir les groupes de {partition["partition_name"]}</a></li>
""" """
) )
@ -420,7 +427,7 @@ def formsemestre_inscr_passage(
return "\n".join(H) return "\n".join(H)
def build_page( def _build_page(
sem, sem,
auth_etuds_by_sem, auth_etuds_by_sem,
inscrits, inscrits,
@ -429,6 +436,7 @@ def build_page(
inscrit_groupes=False, inscrit_groupes=False,
ignore_jury=False, ignore_jury=False,
): ):
formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
inscrit_groupes = int(inscrit_groupes) inscrit_groupes = int(inscrit_groupes)
ignore_jury = int(ignore_jury) ignore_jury = int(ignore_jury)
if inscrit_groupes: if inscrit_groupes:
@ -443,27 +451,36 @@ def build_page(
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
"Passages dans le semestre", with_page_header=False "Passages dans le semestre", with_page_header=False
), ),
"""<form name="f" method="post" action="%s">""" % request.base_url, f"""<form name="f" method="post" action="{request.base_url}">
"""<input type="hidden" name="formsemestre_id" value="%(formsemestre_id)s"/>
<input type="submit" name="submitted" value="Appliquer les modifications"/> <input type="hidden" name="formsemestre_id" value="{sem['formsemestre_id']}"/>
&nbsp;<a href="#help">aide</a>
""" <input type="submit" name="submitted" value="Appliquer les modifications"/>
% sem, # " &nbsp;<a href="#help">aide</a>
"""<input name="inscrit_groupes" type="checkbox" value="1" %s>inscrire aux mêmes groupes</input>"""
% inscrit_groupes_checked, <input name="inscrit_groupes" type="checkbox" value="1"
"""<input name="ignore_jury" type="checkbox" value="1" onchange="document.f.submit()" %s>inclure tous les étudiants (même sans décision de jury)</input>""" {inscrit_groupes_checked}>inscrire aux mêmes groupes</input>
% ignore_jury_checked,
"""<div class="pas_recap">Actuellement <span id="nbinscrits">%s</span> inscrits <input name="ignore_jury" type="checkbox" value="1" onchange="document.f.submit()"
et %d candidats supplémentaires {ignore_jury_checked}>inclure tous les étudiants (même sans décision de jury)</input>
</div>"""
% (len(inscrits), len(candidats_non_inscrits)), <div class="pas_recap">Actuellement <span id="nbinscrits">{len(inscrits)}</span> inscrits
etuds_select_boxes(auth_etuds_by_sem, inscrits_ailleurs), et {len(candidats_non_inscrits)} candidats supplémentaires
"""<p/><input type="submit" name="submitted" value="Appliquer les modifications"/>""", </div>
formsemestre_inscr_passage_help(sem),
"""</form>""", <div>{scu.EMO_WARNING} <em>Seuls les semestres dont la date de fin est antérieure à la date de début
de ce semestre ({formsemestre.date_debut.strftime("%d/%m/%Y")}) sont pris en compte.</em></div>
{etuds_select_boxes(auth_etuds_by_sem, inscrits_ailleurs)}
<input type="submit" name="submitted" value="Appliquer les modifications"/>
{formsemestre_inscr_passage_help(sem)}
</form>
""",
] ]
# Semestres sans etudiants autorisés # Semestres sans étudiants autorisés
empty_sems = [] empty_sems = []
for formsemestre_id in auth_etuds_by_sem.keys(): for formsemestre_id in auth_etuds_by_sem.keys():
if not auth_etuds_by_sem[formsemestre_id]["etuds"]: if not auth_etuds_by_sem[formsemestre_id]["etuds"]:
@ -473,7 +490,10 @@ def build_page(
"""<div class="pas_empty_sems"><h3>Autres semestres sans candidats :</h3><ul>""" """<div class="pas_empty_sems"><h3>Autres semestres sans candidats :</h3><ul>"""
) )
for infos in empty_sems: for infos in empty_sems:
H.append("""<li><a href="%(title_target)s">%(title)s</a></li>""" % infos) H.append(
"""<li><a class="stdlink" href="%(title_target)s">%(title)s</a></li>"""
% infos
)
H.append("""</ul></div>""") H.append("""</ul></div>""")
return H return H

View File

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

View File

@ -200,7 +200,7 @@ def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small") ->
return abort(404, "etudiant inconnu") return abort(404, "etudiant inconnu")
etud = etuds[0] etud = etuds[0]
else: else:
raise ValueError("etud_photo_html: either etud or etudid must be specified") abort(404, "etud_photo_html: either etud or etudid must be specified")
photo_url = etud_photo_url(etud, size=size) photo_url = etud_photo_url(etud, size=size)
nom = etud.get("nomprenom", etud["nom_disp"]) nom = etud.get("nomprenom", etud["nom_disp"])
if title is None: if title is None:
@ -244,7 +244,7 @@ def photo_pathname(photo_filename: str, size="orig"):
elif size == "orig": elif size == "orig":
version = "" version = ""
else: else:
raise ValueError("invalid size parameter for photo") abort(404, "invalid size parameter for photo")
if not photo_filename: if not photo_filename:
return False return False
path = os.path.join(PHOTO_DIR, photo_filename) + version + IMAGE_EXT path = os.path.join(PHOTO_DIR, photo_filename) + version + IMAGE_EXT

View File

@ -1309,7 +1309,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
H = [] H = []
if nb_decisions > 0: if nb_decisions > 0:
H.append( H.append(
"""<div class="saisie_warn"> f"""<div class="saisie_warn">
<ul class="tf-msg"> <ul class="tf-msg">
<li class="tf-msg">Attention: il y a déjà des <b>décisions de jury</b> enregistrées pour <li class="tf-msg">Attention: il y a déjà des <b>décisions de jury</b> enregistrées pour
{nb_decisions} étudiants. Après changement des notes, vérifiez la situation !</li> {nb_decisions} étudiants. Après changement des notes, vérifiez la situation !</li>

View File

@ -1,5 +1,10 @@
/* Saisie décision de jury BUT */ /* Saisie décision de jury BUT */
:root {
--color-recorded: rgb(3, 157, 3);
--color-explanation: blueviolet;
}
.jury_but { .jury_but {
font-family: Verdana, Geneva, Tahoma, sans-serif; font-family: Verdana, Geneva, Tahoma, sans-serif;
} }
@ -18,6 +23,19 @@
margin-top: 0px; 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 { .but_annee {
margin-left: 32px; margin-left: 32px;
display: inline-grid; display: inline-grid;
@ -112,11 +130,10 @@ div.but_settings {
} }
.but_explanation { .but_explanation {
color: blueviolet; color: var(--color-explanation);
font-style: italic; font-style: italic;
padding-top: 4px; padding-top: 4px;
padding-bottom: 12px; padding-bottom: 12px;
;
} }
select:disabled { select:disabled {
@ -130,7 +147,7 @@ select:invalid {
select.but_code option.recorded, select.but_code option.recorded,
div.but_code recorded { div.but_code recorded {
color: rgb(3, 157, 3); color: var(--color-recorded);
font-weight: bold; font-weight: bold;
} }
@ -143,14 +160,14 @@ div.but_code {
div.but_niveau_ue.recorded, div.but_niveau_ue.recorded,
div.but_niveau_rcue.recorded { div.but_niveau_rcue.recorded {
border-color: rgb(0, 169, 0); border-color: var(--color-recorded);
border-width: 3px; border-width: 3px;
} }
div.but_niveau_ue.recorded_different, div.but_niveau_ue.recorded_different,
div.but_niveau_rcue.recorded_different { div.but_niveau_rcue.recorded_different {
box-shadow: 0 0 0 3px red; 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 { div.but_niveau_ue.annee_prec {
@ -172,8 +189,16 @@ div.but_buttons span {
margin-right: 16px; margin-right: 16px;
} }
div.but_doc_codes.but_warning_rcue_cap {
padding-top: 8px;
font-size: 100%;
font-style: italic;
}
div.but_doc_codes { div.but_doc_codes {
margin: 16px; margin: 16px;
min-width: var(--sco-content-min-width);
max-width: var(--sco-content-max-width);
background-color: rgb(227, 254, 254); background-color: rgb(227, 254, 254);
font-size: 75%; font-size: 75%;
border: 2px solid rgb(4, 4, 118); border: 2px solid rgb(4, 4, 118);
@ -227,4 +252,16 @@ div.but_doc table tr td.amue {
.but_niveau_rcue .scoplement { .but_niveau_rcue .scoplement {
font-weight: normal; 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 - 2023
ScoDoc, (c) Emmanuel Viennet 1998 - 2021
*/ */
:root {
--sco-content-min-width: 600px;
--sco-content-max-width: 1024px;
}
html, html,
body { body {
margin: 0; margin: 0;
@ -1657,6 +1661,11 @@ td.formsemestre_status_inscrits {
text-align: center; text-align: center;
} }
td.rcp_titre_sem a.jury_link {
margin-left: 8px;
color: red;
}
td.formsemestre_status_cell { td.formsemestre_status_cell {
white-space: nowrap; white-space: nowrap;
} }
@ -1728,6 +1737,8 @@ ul.ue_inscr_list li.etud {
div.moduleimpl_tableaubord { div.moduleimpl_tableaubord {
padding: 7px; padding: 7px;
border: 2px solid gray; border: 2px solid gray;
min-width: var(--sco-content-min-width);
max-width: var(--sco-content-max-width);
} }
div.moduleimpl_type_sae { div.moduleimpl_type_sae {
@ -1855,11 +1866,19 @@ span.mievr_rattr {
padding: 1px 3px 1px 3px; padding: 1px 3px 1px 3px;
} }
tr.mievr td.mievr_tit { tr.mievr_tit td.mievr_tit {
font-weight: bold; font-weight: bold;
background-color: #cccccc;
border-top-left-radius: 8px; border-top-left-radius: 8px;
}
tr.mievr.mievr_tit td {
background-color: #e1e1e1;
}
tr.mievr_tit td:last-child {
border-top-right-radius: 8px; border-top-right-radius: 8px;
text-align: right;
padding-right: 8px;
} }
tr.mievr td { tr.mievr td {
@ -1922,11 +1941,6 @@ span.eval_warning_coef {
background-color: rgb(255, 225, 0); background-color: rgb(255, 225, 0);
} }
div.evaluation_order {
position: absolute;
right: 1em;
}
span.evalindex { span.evalindex {
font-weight: normal; font-weight: normal;
font-size: 80%; font-size: 80%;
@ -2580,11 +2594,13 @@ div.bull_head {
display: grid; display: grid;
justify-content: space-between; justify-content: space-between;
grid-template-columns: auto auto; grid-template-columns: auto auto;
min-width: 600px;
max-width: 1072px;
} }
div.bull_photo { div.bull_photo {
display: inline-block; display: inline-block;
margin-right: 10px; margin-right: 8px;
} }
span.bulletin_menubar_but { span.bulletin_menubar_but {

View File

@ -78,7 +78,10 @@ $(function () {
} else { } else {
document.querySelector("div.next").innerHTML = ""; document.querySelector("div.next").innerHTML = "";
} }
} else {
// Supprime les liens de navigation
document.querySelector("div.prev").innerHTML = "";
document.querySelector("div.next").innerHTML = "";
} }
}); });

View File

@ -262,10 +262,11 @@
<b>Textes de référence:</b> <b>Textes de référence:</b>
<ul> <ul>
<li><a href="https://www.enseignementsup-recherche.gouv.fr/fr/bo/21/Special4/ESRS2114777A.htm">Bulletin <li><a href="https://www.enseignementsup-recherche.gouv.fr/fr/bo/21/Special4/ESRS2114777A.htm">Bulletin
officiel spécial n°4 du 17 juin 2021</a></li> officiel spécial n°4 du 26 mai 2022 </a></li>
<li><a <li><a
href="https://cache.media.enseignementsup-recherche.gouv.fr//file/SPE4-MESRI-17-6-2021/19/4/SP4_ESR_17_6_2021_1413194.pdf">Version href="https://cache.media.education.gouv.fr/file/SP4-MESRI-26-5-2022/10/0/spe617_annexe1_1426100.pdf">Version
pdf complète</a></li> pdf de l'arrêté de mai 2022 sur la licence professionnelle « bachelor universitaire de technologie
»</a></li>
</ul> </ul>
</div> </div>

View File

@ -49,7 +49,8 @@
<span class="formation_module_ue">(<a title="UE de rattachement">{{mod.ue.acronyme}}</a>)</span>, <span class="formation_module_ue">(<a title="UE de rattachement">{{mod.ue.acronyme}}</a>)</span>,
{% endif %} {% endif %}
parcours <b>{{ mod.get_parcours()|map(attribute="code")|join("</b>, <b>")|default('tronc commun', true)|safe - parcours <b>{{ mod.get_parcours()|map(attribute="code")|join("</b>, <b>")|default('tronc commun',
true)|safe
}}</b> }}</b>
{% if mod.heures_cours or mod.heures_td or mod.heures_tp %} {% if mod.heures_cours or mod.heures_td or mod.heures_tp %}
({{mod.heures_cours|default("&nbsp;",true)|safe}}/{{mod.heures_td|default("&nbsp;",true)|safe}}/{{mod.heures_tp|default("&nbsp;",true)|safe}}, ({{mod.heures_cours|default("&nbsp;",true)|safe}}/{{mod.heures_td|default("&nbsp;",true)|safe}}/{{mod.heures_tp|default("&nbsp;",true)|safe}},

View File

@ -704,10 +704,15 @@ def formation_list(format=None, formation_id=None, args={}):
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @scodoc7func
def formation_export(formation_id, export_ids=False, format=None): def formation_export(
formation_id, export_ids=False, format=None, export_codes_apo=True
):
"Export de la formation au format indiqué (xml ou json)" "Export de la formation au format indiqué (xml ou json)"
return sco_formations.formation_export( return sco_formations.formation_export(
formation_id, export_ids=export_ids, format=format formation_id,
export_ids=export_ids,
format=format,
export_codes_apo=export_codes_apo,
) )
@ -2494,11 +2499,31 @@ def formsemestre_validation_but(
H.append(jury_but_view.show_etud(deca, read_only=read_only)) 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: if read_only:
H.append( H.append(
"""<div class="but_explanation"> f"""
Vous n'avez pas la permission de modifier ces décisions. <div class="but_explanation">
Les champs entourés en vert sont enregistrés.</div>""" {"Vous n'avez pas la permission de modifier ces décisions."
if formsemestre.etat
else "Semestre verrouillé."}
Les champs entourés en vert sont enregistrés.
</div>"""
) )
else: else:
erase_span = f"""<a href="{ erase_span = f"""<a href="{
@ -2508,9 +2533,11 @@ def formsemestre_validation_but(
H.append( H.append(
f"""<div class="but_settings"> f"""<div class="but_settings">
<input type="checkbox" onchange="enable_manual_codes(this)"> <input type="checkbox" onchange="enable_manual_codes(this)">
<em>permettre la saisie manuelles des codes d'année et de niveaux. <em>permettre la saisie manuelles des codes
Dans ce cas, il vous revient de vous assurer de la cohérence entre {"d'année et " if deca.jury_annuel else ""}
vos codes d'UE/RCUE/Année !</em> de niveaux.
Dans ce cas, assurez-vous de la cohérence entre les codes d'UE/RCUE/Année !
</em>
</input> </input>
</div> </div>
@ -2522,7 +2549,15 @@ def formsemestre_validation_but(
) )
H.append(navigation_div) H.append(navigation_div)
H.append("</form>") H.append("</form>")
if deca.a_cheval:
H.append(
f"""<div class="but_doc_codes but_warning_rcue_cap">
{scu.EMO_WARNING} Rappel: pour les redoublants, seules les UE <b>capitalisées</b> (note > 10)
lors d'une année précédente peuvent être prise en compte pour former
un RCUE (associé à un niveau de compétence du BUT).
</div>
"""
)
H.append( H.append(
render_template( render_template(
"but/documentation_codes_jury.html", "but/documentation_codes_jury.html",

View File

@ -216,7 +216,7 @@ def edit_modules_ue_coefs():
</h2> </h2>
""", """,
render_template( render_template(
"pn/form_modules_ue_coefs.html", "pn/form_modules_ue_coefs.j2",
formation=formation, formation=formation,
data_source=url_for( data_source=url_for(
"notes.table_modules_ue_coefs", "notes.table_modules_ue_coefs",

View File

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

View File

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

View File

@ -1022,3 +1022,84 @@ Etudiants:
moy_ue: 9.5000 moy_ue: 9.5000
moy_ue_with_cap: 12.7600 moy_ue_with_cap: 12.7600
decision_annee: AJ 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 @pytest.mark.but_gb
def test_but_jury_GB(test_client): def test_but_jury_GB(test_client):
"""Tests sur un cursus GB """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 S1
- vérification jury de S2 - vérification jury de S2
- vérification jury de S3 - 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 # Construit la base de test GB une seule fois
# puis lance les tests de jury # puis lance les tests de jury
doc = yaml_setup.setup_from_yaml("tests/unit/cursus_but_geii_lyon.yaml") doc = yaml_setup.setup_from_yaml("tests/unit/cursus_but_geii_lyon.yaml")
formsemestres = FormSemestre.query.order_by( formsemestres = FormSemestre.query.order_by(
FormSemestre.date_debut, FormSemestre.semestre_id FormSemestre.date_debut, FormSemestre.semestre_id
).all() ).all()