Diverses améliorations pour faciliter la config BUT. Voir #862

This commit is contained in:
Emmanuel Viennet 2024-02-20 21:30:08 +01:00
parent 56aa5fbba3
commit fae9fbdd09
9 changed files with 239 additions and 109 deletions

View File

@ -119,6 +119,12 @@ def _build_bulletin_but_infos(
refcomp = formsemestre.formation.referentiel_competence
if refcomp is None:
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
warn_html = cursus_but.formsemestre_warning_apc_setup(
formsemestre, bulletins_sem.res
)
if warn_html:
raise ScoValueError("<b>Formation mal configurée pour le BUT</b>" + warn_html)
ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau(
refcomp, etud
)

View File

@ -23,29 +23,21 @@ from app.comp.res_but import ResultatsSemestreBUT
from app.comp.res_compat import NotesTableCompat
from app.models.but_refcomp import (
ApcAnneeParcours,
ApcCompetence,
ApcNiveau,
ApcParcours,
ApcParcoursNiveauCompetence,
ApcReferentielCompetences,
)
from app.models import Scolog, ScolarAutorisationInscription
from app.models.but_validations import (
ApcValidationAnnee,
ApcValidationRCUE,
)
from app.models.ues import UEParcours
from app.models.but_validations import ApcValidationRCUE
from app.models.etudiants import Identite
from app.models.formations import Formation
from app.models.formsemestre import FormSemestre, FormSemestreInscription
from app.models.formsemestre import FormSemestre
from app.models.ues import UniteEns
from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import codes_cursus as sco_codes
from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES, UE_STANDARD
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
from app.scodoc import sco_cursus_dut
@ -440,11 +432,16 @@ def formsemestre_warning_apc_setup(
"""
if not formsemestre.formation.is_apc():
return ""
url_formation = url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=formsemestre.formation.id,
semestre_idx=formsemestre.semestre_id,
)
if formsemestre.formation.referentiel_competence is None:
return f"""<div class="formsemestre_status_warning">
La <a class="stdlink" href="{
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)
}">formation n'est pas associée à un référentiel de compétence.</a>
La <a class="stdlink" href="{url_formation}">formation
n'est pas associée à un référentiel de compétence.</a>
</div>
"""
H = []
@ -462,7 +459,9 @@ def formsemestre_warning_apc_setup(
)
if nb_ues_sans_parcours != nb_ues_tot:
H.append(
f"""Le semestre n'est associé à aucun parcours, mais les UEs de la formation ont des parcours"""
"""Le semestre n'est associé à aucun parcours,
mais les UEs de la formation ont des parcours
"""
)
# Vérifie les niveaux de chaque parcours
for parcour in formsemestre.parcours or [None]:
@ -489,7 +488,8 @@ def formsemestre_warning_apc_setup(
if not H:
return ""
return f"""<div class="formsemestre_status_warning">
Problème dans la configuration de la formation:
Problème dans la
<a class="stdlink" href="{url_formation}">configuration de la formation</a>:
<ul>
<li>{ '</li><li>'.join(H) }</li>
</ul>
@ -502,6 +502,76 @@ def formsemestre_warning_apc_setup(
"""
def formation_semestre_niveaux_warning(formation: Formation, semestre_idx: int) -> str:
"""Vérifie que tous les niveaux de compétences de cette année de formation
ont bien des UEs.
Afin de ne pas générer trop de messages, on ne considère que les parcours
du référentiel de compétences pour lesquels au moins une UE a été associée.
Renvoie fragment de html
"""
annee = (semestre_idx - 1) // 2 + 1 # année BUT
ref_comp: ApcReferentielCompetences = formation.referentiel_competence
niveaux_sans_ue_by_parcour = {} # { parcour.code : [ ue ... ] }
parcours_ids = {
uep.parcours_id
for uep in UEParcours.query.join(UniteEns).filter_by(
formation_id=formation.id, type=UE_STANDARD
)
}
for parcour in ref_comp.parcours:
if parcour.id not in parcours_ids:
continue # saute parcours associés à aucune UE (tous semestres)
niveaux_sans_ue = []
niveaux = ApcNiveau.niveaux_annee_de_parcours(parcour, annee, ref_comp)
# print(f"\n# Parcours {parcour.code} : {len(niveaux)} niveaux")
for niveau in niveaux:
ues = [ue for ue in formation.ues if ue.niveau_competence_id == niveau.id]
if not ues:
niveaux_sans_ue.append(niveau)
# print( niveau.competence.titre + " " + str(niveau.ordre) + "\t" + str(ue) )
if niveaux_sans_ue:
niveaux_sans_ue_by_parcour[parcour.code] = niveaux_sans_ue
#
H = []
for parcour_code, niveaux in niveaux_sans_ue_by_parcour.items():
H.append(
f"""<li>Parcours {parcour_code} : {
len(niveaux)} niveaux sans UEs
<span>
{ ', '.join( f'{niveau.competence.titre} {niveau.ordre}'
for niveau in niveaux
)
}
</span>
</li>
"""
)
# Combien de compétences de tronc commun ?
_, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee)
nb_niveaux_tc = len(niveaux_by_parcours["TC"])
nb_ues_tc = len(
formation.query_ues_parcour(None)
.filter(UniteEns.semestre_idx == semestre_idx)
.all()
)
if nb_niveaux_tc != nb_ues_tc:
H.append(
f"""<li>{nb_niveaux_tc} niveaux de compétences de tronc commun,
mais {nb_ues_tc} UEs de tronc commun !</li>"""
)
if H:
return f"""<div class="formation_semestre_niveaux_warning">
<div>Problèmes détectés à corriger :</div>
<ul>
{"".join(H)}
</ul>
</div>
"""
return "" # no problem detected
def ue_associee_au_niveau_du_parcours(
ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S"
) -> UniteEns:

View File

@ -50,14 +50,11 @@ import traceback
import reportlab
from reportlab.platypus import (
SimpleDocTemplate,
DocIf,
Paragraph,
Spacer,
Frame,
PageBreak,
)
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
from reportlab.platypus import Table, KeepInFrame
from flask import request
from flask_login import current_user
@ -213,26 +210,26 @@ class BulletinGenerator:
story.append(PageBreak()) # insert page break at end
return story
else:
# Generation du document PDF
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
report = io.BytesIO() # in-memory document, no disk file
document = sco_pdf.BaseDocTemplate(report)
document.addPageTemplates(
sco_pdf.ScoDocPageTemplate(
document,
author="%s %s (E. Viennet) [%s]"
% (sco_version.SCONAME, sco_version.SCOVERSION, self.description),
title=f"""Bulletin {sem["titremois"]} de {etat_civil}""",
subject="Bulletin de note",
margins=self.margins,
server_name=self.server_name,
filigranne=self.filigranne,
preferences=sco_preferences.SemPreferences(formsemestre_id),
)
# Generation du document PDF
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
report = io.BytesIO() # in-memory document, no disk file
document = sco_pdf.BaseDocTemplate(report)
document.addPageTemplates(
sco_pdf.ScoDocPageTemplate(
document,
author=f"""{sco_version.SCONAME} {
sco_version.SCOVERSION} (E. Viennet) [{self.description}]""",
title=f"""Bulletin {sem["titremois"]} de {etat_civil}""",
subject="Bulletin de note",
margins=self.margins,
server_name=self.server_name,
filigranne=self.filigranne,
preferences=sco_preferences.SemPreferences(formsemestre_id),
)
document.build(story)
data = report.getvalue()
)
document.build(story)
data = report.getvalue()
return data
def buildTableObject(self, P, pdfTableStyle, colWidths):

View File

@ -28,7 +28,7 @@
from flask.templating import render_template
from app import db
from app.but import apc_edit_ue
from app.but import cursus_but
from app.models import UniteEns, Matiere, Module, FormSemestre, ModuleImpl
from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import codes_cursus
@ -101,18 +101,26 @@ def html_edit_formation_apc(
),
}
html_ue_warning = {
semestre_idx: cursus_but.formation_semestre_niveaux_warning(
formation, semestre_idx
)
for semestre_idx in semestre_ids
}
H = [
render_template(
"pn/form_ues.j2",
formation=formation,
semestre_ids=semestre_ids,
editable=editable,
tag_editable=tag_editable,
icons=icons,
ues_by_sem=ues_by_sem,
ects_by_sem=ects_by_sem,
scu=scu,
codes_cursus=codes_cursus,
ects_by_sem=ects_by_sem,
editable=editable,
formation=formation,
html_ue_warning=html_ue_warning,
icons=icons,
scu=scu,
semestre_ids=semestre_ids,
tag_editable=tag_editable,
ues_by_sem=ues_by_sem,
),
]
for semestre_idx in semestre_ids:

View File

@ -892,7 +892,9 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<form>
<input type="checkbox" class="sco_tag_checkbox"
{'checked' if show_tags else ''}
> Montrer les tags des modules voire en ajouter <i>(ceux correspondant aux titres des compétences étant ajoutés par défaut)</i></input>
> Montrer les tags des modules voire en ajouter
<i>(ceux correspondant aux titres des compétences étant ajoutés par défaut)</i>
</input>
</form>
"""
)

View File

@ -211,8 +211,6 @@ def do_formsemestre_desinscription(
"""Désinscription d'un étudiant.
Si semestre extérieur et dernier inscrit, suppression de ce semestre.
"""
from app.scodoc import sco_formsemestre_edit
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
etud = Identite.get_etud(etudid)
# -- check lock
@ -258,17 +256,14 @@ def do_formsemestre_desinscription(
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
# --- Semestre extérieur
if formsemestre.modalite == "EXT":
inscrits = do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id}
)
nbinscrits = len(inscrits)
if nbinscrits == 0:
if 0 == len(formsemestre.inscriptions):
log(
f"""do_formsemestre_desinscription:
suppression du semestre extérieur {formsemestre}"""
)
flash("Semestre exterieur supprimé")
sco_formsemestre_edit.do_formsemestre_delete(formsemestre_id)
db.session.delete(formsemestre)
db.session.commit()
flash(f"Semestre extérieur supprimé: {formsemestre.titre_annee()}")
logdb(
cnx,
@ -587,26 +582,29 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
ue_id = ue.id
ue_descr = ue.acronyme
if ue.type != UE_STANDARD:
ue_descr += " <em>%s</em>" % UE_TYPE_NAME[ue.type]
ue_descr += f" <em>{UE_TYPE_NAME[ue.type]}</em>"
ue_status = nt.get_etud_ue_status(etudid, ue_id)
if ue_status and ue_status["is_capitalized"]:
sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"])
ue_descr += (
' <a class="discretelink" href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" title="%s">(capitalisée le %s)'
% (
sem_origin["formsemestre_id"],
etudid,
sem_origin["titreannee"],
ndb.DateISOtoDMY(ue_status["event_date"]),
)
)
ue_descr += f"""
<a class="discretelink" href="{ url_for(
'notes.formsemestre_bulletinetud', scodoc_dept=g.scodoc_dept,
formsemestre_id=sem_origin["formsemestre_id"],
etudid = etudid
)}" title="{sem_origin['titreannee']}">(capitalisée le {
ndb.DateISOtoDMY(ue_status["event_date"])
})
"""
descr.append(
(
"sec_%s" % ue_id,
f"sec_{ue_id}",
{
"input_type": "separator",
"title": """<b>%s :</b> <a href="#" onclick="chkbx_select('%s', true);">inscrire</a> | <a href="#" onclick="chkbx_select('%s', false);">désinscrire</a> à tous les modules"""
% (ue_descr, ue_id, ue_id),
"title": f"""<b>{ue_descr} :</b>
<a href="#" onclick="chkbx_select('{ue_id}', true);">inscrire</a> | <a
href="#" onclick="chkbx_select('{ue_id}', false);">désinscrire</a>
à tous les modules
""",
},
)
)
@ -776,9 +774,7 @@ def do_moduleimpl_incription_options(
# verifie que ce module existe bien
mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
if len(mods) != 1:
raise ScoValueError(
"inscription: invalid moduleimpl_id: %s" % moduleimpl_id
)
raise ScoValueError(f"inscription: invalid moduleimpl_id: {moduleimpl_id}")
mod = mods[0]
sco_moduleimpl.do_moduleimpl_inscription_create(
{"moduleimpl_id": moduleimpl_id, "etudid": etudid},
@ -790,7 +786,7 @@ def do_moduleimpl_incription_options(
mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
if len(mods) != 1:
raise ScoValueError(
"desinscription: invalid moduleimpl_id: %s" % moduleimpl_id
f"desinscription: invalid moduleimpl_id: {moduleimpl_id}"
)
mod = mods[0]
inscr = sco_moduleimpl.do_moduleimpl_inscription_list(
@ -798,8 +794,7 @@ def do_moduleimpl_incription_options(
)
if not inscr:
raise ScoValueError(
"pas inscrit a ce module ! (etudid=%s, moduleimpl_id=%s)"
% (etudid, moduleimpl_id)
f"pas inscrit a ce module ! (etudid={etudid}, moduleimpl_id={moduleimpl_id})"
)
oid = inscr[0]["moduleimpl_inscription_id"]
sco_moduleimpl.do_moduleimpl_inscription_delete(
@ -808,11 +803,13 @@ def do_moduleimpl_incription_options(
H = [
html_sco_header.sco_header(),
"""<h3>Modifications effectuées</h3>
<p><a class="stdlink" href="%s">
Retour à la fiche étudiant</a></p>
"""
% url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
f"""<h3>Modifications effectuées</h3>
<p><a class="stdlink" href="{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">
Retour à la fiche étudiant</a>
</p>
""",
html_sco_header.sco_footer(),
]
return "\n".join(H)
@ -856,49 +853,59 @@ def formsemestre_inscrits_ailleurs(formsemestre_id):
"""Page listant les étudiants inscrits dans un autre semestre
dont les dates recouvrent le semestre indiqué.
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header(
"Inscriptions multiples parmi les étudiants du semestre ",
init_qtip=True,
javascripts=["js/etud_info.js"],
)
]
insd = list_inscrits_ailleurs(formsemestre_id)
# liste ordonnée par nom
etudlist = [
sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
for etudid in insd.keys()
if insd[etudid]
]
etudlist.sort(key=lambda x: x["nom"])
etudlist = [Identite.get_etud(etudid) for etudid, sems in insd.items() if sems]
etudlist.sort(key=lambda x: x.sort_key)
if etudlist:
H.append("<ul>")
for etud in etudlist:
H.append(
'<li><a href="%s" class="discretelink">%s</a> : '
% (
f"""<li><a id="{etud.id}" class="discretelink etudinfo"
href={
url_for(
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"],
),
etud["nomprenom"],
)
etudid=etud.id,
)
}
>{etud.nomprenom}</a> :
"""
)
l = []
for s in insd[etud["etudid"]]:
for s in insd[etud.id]:
l.append(
'<a class="discretelink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a>'
% s
f"""<a class="discretelink" href="{
url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
)}">{s['titremois']}</a>"""
)
H.append(", ".join(l))
H.append("</li>")
H.append("</ul>")
H.append("<p>Total: %d étudiants concernés.</p>" % len(etudlist))
H.append(
"""<p class="help">Ces étudiants sont inscrits dans le semestre sélectionné et aussi dans d'autres semestres qui se déroulent en même temps ! <br>Sauf exception, cette situation est anormale:</p>
f"""
</ul>
<p><b>Total: {len(etudlist)} étudiants concernés.</b></p>
<p class="help">Ces étudiants sont inscrits dans le semestre sélectionné et aussi
dans d'autres semestres qui se déroulent en même temps !
</p>
<p>
<b>Sauf exception, cette situation est anormale:</b>
</p>
<ul>
<li>vérifier que les dates des semestres se suivent sans se chevaucher</li>
<li>ou si besoin désinscrire le(s) étudiant(s) de l'un des semestres (via leurs fiches individuelles).</li>
<li>vérifier que les dates des semestres se suivent <em>sans se chevaucher</em>
</li>
<li>ou bien si besoin désinscrire le(s) étudiant(s) de l'un des semestres
(via leurs fiches individuelles).
</li>
</ul>
"""
)

View File

@ -110,12 +110,13 @@ def formsemestre_synchro_etuds(
raise ScoValueError("opération impossible: semestre verrouille")
if not sem["etapes"]:
raise ScoValueError(
"""opération impossible: ce semestre n'a pas de code étape
(voir "<a href="formsemestre_editwithmodules?formsemestre_id=%(formsemestre_id)s">Modifier ce semestre</a>")
f"""opération impossible: ce semestre n'a pas de code étape
(voir <a class="stdlink" href="{
url_for('notes.formsemestre_editwithmodules',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">Modifier ce semestre</a>)
"""
% sem
)
header = html_sco_header.sco_header(page_title="Synchronisation étudiants")
footer = html_sco_header.sco_footer()
base_url = url_for(
"notes.formsemestre_synchro_etuds",
@ -166,7 +167,13 @@ def formsemestre_synchro_etuds(
suffix=scu.XLSX_SUFFIX,
)
H = [header]
H = [
html_sco_header.sco_header(
page_title="Synchronisation étudiants",
init_qtip=True,
javascripts=["js/etud_info.js"],
)
]
if not submitted:
H += _build_page(
sem,

View File

@ -2411,6 +2411,29 @@ div.formation_list_ues_titre {
color: #eee;
}
div.formation_semestre_niveaux_warning {
font-weight: bold;
color: red;
padding: 4px;
margin-top: 8px;
margin-left: 24px;
margin-right: 24px;
background-color: yellow;
border-radius: 8px;
}
div.formation_semestre_niveaux_warning div {
color: black;
font-size: 110%;
}
div.formation_semestre_niveaux_warning ul {
list-style-type: none;
padding-left: 0;
}
div.formation_semestre_niveaux_warning ul li:before {
content: '⚠️';
margin-right: 10px; /* Adjust space between emoji and text */
}
div.formation_list_modules,
div.formation_list_ues {
border-radius: 18px;
@ -2426,6 +2449,7 @@ div.formation_list_ues {
}
div.formation_list_ues_content {
margin-top: 4px;
}
div.formation_list_modules {
@ -2508,7 +2532,13 @@ div.formation_parcs > div {
opacity: 0.7;
border-radius: 4px;
text-align: center;
padding: 4px 8px;
padding: 2px 6px;
margin-top: 8px;
margin-bottom: 2px;
}
div.formation_parcs > div.ue_tc {
color: black;
font-style: italic;
}
div.formation_parcs > div.focus {

View File

@ -4,6 +4,7 @@
<div class="formation_list_ues_titre">Unités d'Enseignement
semestre {{semestre_idx}} &nbsp;-&nbsp; {{ects_by_sem[semestre_idx] | safe}} ECTS
</div>
{{ html_ue_warning[semestre_idx] | safe }}
<div class="formation_list_ues_content">
<ul class="apc_ue_list">
{% for ue in ues_by_sem[semestre_idx] %}
@ -62,6 +63,8 @@
<div class="formation_parcs">
{% for parc in ue.parcours %}
<div>{{ parc.code }}</div>
{% else %}
<div class="ue_tc" title="aucun parcours">Tronc Commun</div>
{% endfor %}
</div>
{% endif %}