From fae9fbdd09777ba0ab6af096a7c284e805d85687 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 20 Feb 2024 21:30:08 +0100 Subject: [PATCH] =?UTF-8?q?Diverses=20am=C3=A9liorations=20pour=20facilite?= =?UTF-8?q?r=20la=20config=20BUT.=20Voir=20#862?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/bulletin_but_court.py | 6 + app/but/cursus_but.py | 102 ++++++++++++++--- app/scodoc/sco_bulletins_generator.py | 43 ++++---- app/scodoc/sco_edit_apc.py | 26 +++-- app/scodoc/sco_edit_ue.py | 4 +- app/scodoc/sco_formsemestre_inscriptions.py | 115 +++++++++++--------- app/scodoc/sco_synchro_etuds.py | 17 ++- app/static/css/scodoc.css | 32 +++++- app/templates/pn/form_ues.j2 | 3 + 9 files changed, 239 insertions(+), 109 deletions(-) diff --git a/app/but/bulletin_but_court.py b/app/but/bulletin_but_court.py index 7fb389a5c..a4fc6ec13 100644 --- a/app/but/bulletin_but_court.py +++ b/app/but/bulletin_but_court.py @@ -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("Formation mal configurée pour le BUT" + warn_html) ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau( refcomp, etud ) diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index 1f453882e..4a16bf05d 100644 --- a/app/but/cursus_but.py +++ b/app/but/cursus_but.py @@ -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"""
- La formation n'est pas associée à un référentiel de compétence. + La formation + n'est pas associée à un référentiel de compétence.
""" 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"""
- Problème dans la configuration de la formation: + Problème dans la + configuration de la formation: @@ -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"""
  • Parcours {parcour_code} : { + len(niveaux)} niveaux sans UEs + + { ', '.join( f'{niveau.competence.titre} {niveau.ordre}' + for niveau in niveaux + ) + } + +
  • + """ + ) + # 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"""
  • {nb_niveaux_tc} niveaux de compétences de tronc commun, + mais {nb_ues_tc} UEs de tronc commun !
  • """ + ) + + if H: + return f"""
    +
    Problèmes détectés à corriger :
    +
      + {"".join(H)} +
    +
    + """ + return "" # no problem detected + + def ue_associee_au_niveau_du_parcours( ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S" ) -> UniteEns: diff --git a/app/scodoc/sco_bulletins_generator.py b/app/scodoc/sco_bulletins_generator.py index bda2567e9..b8a00f9bf 100644 --- a/app/scodoc/sco_bulletins_generator.py +++ b/app/scodoc/sco_bulletins_generator.py @@ -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): diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index a90d37e57..b56a93939 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -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: diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index fa40ad552..7e9543ce2 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -892,7 +892,9 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
    Montrer les tags des modules voire en ajouter (ceux correspondant aux titres des compétences étant ajoutés par défaut) + > Montrer les tags des modules voire en ajouter + (ceux correspondant aux titres des compétences étant ajoutés par défaut) +
    """ ) diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py index 7ee6439a7..f8ddc30d5 100644 --- a/app/scodoc/sco_formsemestre_inscriptions.py +++ b/app/scodoc/sco_formsemestre_inscriptions.py @@ -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 += " %s" % UE_TYPE_NAME[ue.type] + ue_descr += f" {UE_TYPE_NAME[ue.type]}" 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 += ( - ' (capitalisée le %s)' - % ( - sem_origin["formsemestre_id"], - etudid, - sem_origin["titreannee"], - ndb.DateISOtoDMY(ue_status["event_date"]), - ) - ) + ue_descr += f""" + (capitalisée le { + ndb.DateISOtoDMY(ue_status["event_date"]) + }) + """ descr.append( ( - "sec_%s" % ue_id, + f"sec_{ue_id}", { "input_type": "separator", - "title": """%s : inscrire | désinscrire à tous les modules""" - % (ue_descr, ue_id, ue_id), + "title": f"""{ue_descr} : + inscrire | désinscrire + à 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(), - """

    Modifications effectuées

    -

    - Retour à la fiche étudiant

    - """ - % url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid), + f"""

    Modifications effectuées

    +

    + Retour à la fiche étudiant +

    + """, 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("") - H.append("

    Total: %d étudiants concernés.

    " % len(etudlist)) H.append( - """

    Ces étudiants sont inscrits dans le semestre sélectionné et aussi dans d'autres semestres qui se déroulent en même temps !
    Sauf exception, cette situation est anormale:

    + f""" + +

    Total: {len(etudlist)} étudiants concernés.

    + +

    Ces étudiants sont inscrits dans le semestre sélectionné et aussi + dans d'autres semestres qui se déroulent en même temps ! +

    +

    + Sauf exception, cette situation est anormale: +

    """ ) diff --git a/app/scodoc/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py index a4a9fd2c8..3f5008e3e 100644 --- a/app/scodoc/sco_synchro_etuds.py +++ b/app/scodoc/sco_synchro_etuds.py @@ -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 "Modifier ce semestre") + f"""opération impossible: ce semestre n'a pas de code étape + (voir Modifier ce semestre) """ - % 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, diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 5d141fd58..295be36cf 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -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 { diff --git a/app/templates/pn/form_ues.j2 b/app/templates/pn/form_ues.j2 index 133ccb66a..437d4da2b 100644 --- a/app/templates/pn/form_ues.j2 +++ b/app/templates/pn/form_ues.j2 @@ -4,6 +4,7 @@
    Unités d'Enseignement semestre {{semestre_idx}}  -  {{ects_by_sem[semestre_idx] | safe}} ECTS
    + {{ html_ue_warning[semestre_idx] | safe }}
      {% for ue in ues_by_sem[semestre_idx] %} @@ -62,6 +63,8 @@
      {% for parc in ue.parcours %}
      {{ parc.code }}
      + {% else %} +
      Tronc Commun
      {% endfor %}
      {% endif %}