# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@viennet.net # ############################################################################## """Edition des PV de jury Formulaires paramétrage PV et génération des tables """ import collections import time from reportlab.platypus import Paragraph from reportlab.lib import styles import flask from flask import flash, g, redirect, render_template, request, url_for from app.models import FormSemestre, Identite import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc import codes_cursus from app.scodoc import sco_pv_dict from app.scodoc import sco_etud from app.scodoc import sco_groups from app.scodoc import sco_groups_view from app.scodoc import sco_pdf from app.scodoc import sco_preferences from app.scodoc import sco_pv_pdf from app.scodoc import sco_pv_lettres_inviduelles from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.gen_tables import GenTable from app.scodoc.codes_cursus import NO_SEMESTRE_ID from app.scodoc.sco_pdf import PDFLOCK from app.scodoc.TrivialFormulator import TrivialFormulator def _descr_decision_sem_abbrev(etat, decision_sem): "résumé textuel tres court (code) de la décision de semestre" if etat == "D": decision = "Démission" else: if decision_sem: decision = decision_sem["code"] else: decision = "" return decision def pvjury_table( dpv, only_diplome=False, anonymous=False, with_parcours_decisions=False, with_paragraph_nom=False, # cellule paragraphe avec nom, date, code NIP ): """idem mais rend list de dicts Si only_diplome, n'extrait que les etudiants qui valident leur diplome. """ sem = dpv["formsemestre"] formsemestre_id = sem["formsemestre_id"] sem_id_txt_sp = sem["sem_id_txt"] if sem_id_txt_sp: sem_id_txt_sp = " " + sem_id_txt_sp titles = { "etudid": "etudid", "code_nip": "NIP", "nomprenom": "Nom", # si with_paragraph_nom, sera un Paragraph "parcours": "Parcours", "decision": "Décision" + sem_id_txt_sp, "mention": "Mention", "ue_cap": "UE" + sem_id_txt_sp + " capitalisées", "ects": "ECTS", "devenir": "Devenir", "validation_parcours_code": "Résultat au diplôme", "observations": "Observations", } if anonymous: titles["nomprenom"] = "Code" columns_ids = ["nomprenom", "parcours"] if with_parcours_decisions: all_idx = set() for e in dpv["decisions"]: all_idx |= set(e["parcours_decisions"].keys()) sem_ids = sorted(all_idx) for i in sem_ids: if i != NO_SEMESTRE_ID: titles[i] = "S%d" % i else: titles[i] = "S" # pas très parlant ? columns_ids += [i] if dpv["has_prev"]: id_prev = sem["semestre_id"] - 1 # numero du semestre precedent titles["prev_decision"] = f"Décision S{id_prev}" columns_ids += ["prev_decision"] if not dpv["is_apc"]: # Décision de jury sur le semestre, sauf en BUT columns_ids += ["decision"] if sco_preferences.get_preference("bul_show_mention", formsemestre_id): columns_ids += ["mention"] columns_ids += ["ue_cap"] if sco_preferences.get_preference("bul_show_ects", formsemestre_id): columns_ids += ["ects"] # XXX if not dpv["semestre_non_terminal"]: # La colonne doit être présente: redoublants validant leur diplome # en répétant un semestre ancien: exemple: S1 (ADM), S2 (ADM), S3 (AJ), S4 (ADM), S3 (ADM)=> diplôme columns_ids += ["validation_parcours_code"] columns_ids += ["devenir"] columns_ids += ["observations"] lines = [] for e in dpv["decisions"]: sco_etud.format_etud_ident(e["identite"]) l = { "etudid": e["identite"]["etudid"], "code_nip": e["identite"]["code_nip"], "nomprenom": e["identite"]["nomprenom"], "_nomprenom_target": url_for( "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=e["identite"]["etudid"], ), "_nomprenom_td_attrs": f"""id="{e['identite']['etudid']}" class="etudinfo" """, "parcours": e["parcours_in_cur_formation"], "decision": _descr_decision_sem_abbrev(e["etat"], e["decision_sem"]), "ue_cap": e["decisions_ue_descr"], "validation_parcours_code": "ADM" if e["validation_parcours"] else "", "devenir": e["autorisations_descr"], "observations": ndb.unquote(e["observation"]), "mention": e["mention"], "ects": str(e["sum_ects"]), } if with_paragraph_nom: cell_style = styles.ParagraphStyle({}) cell_style.fontSize = sco_preferences.get_preference( "SCOLAR_FONT_SIZE", formsemestre_id ) cell_style.fontName = sco_preferences.get_preference( "PV_FONTNAME", formsemestre_id ) cell_style.leading = 1.0 * sco_preferences.get_preference( "SCOLAR_FONT_SIZE", formsemestre_id ) # vertical space i = e["identite"] l["nomprenom"] = [ Paragraph(sco_pdf.SU(i["nomprenom"]), cell_style), Paragraph(sco_pdf.SU(i["code_nip"]), cell_style), Paragraph( sco_pdf.SU( "Né le %s" % i["date_naissance"] + (" à %s" % i["lieu_naissance"] if i["lieu_naissance"] else "") + (" (%s)" % i["dept_naissance"] if i["dept_naissance"] else "") ), cell_style, ), ] if anonymous: # Mode anonyme: affiche INE ou sinon NIP, ou id l["nomprenom"] = ( e["identite"]["code_ine"] or e["identite"]["code_nip"] or e["identite"]["etudid"] ) if with_parcours_decisions: for i in e[ "parcours_decisions" ]: # or equivalently: l.update(e['parcours_decisions']) l[i] = e["parcours_decisions"][i] if e["validation_parcours"]: l["devenir"] = "Diplôme obtenu" if dpv["has_prev"]: l["prev_decision"] = _descr_decision_sem_abbrev( None, e["prev_decision_sem"] ) if e["validation_parcours"] or not only_diplome: lines.append(l) return lines, titles, columns_ids def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True): """Page récapitulant les décisions de jury En classique: table spécifique avec les deux semestres pour le DUT En APC/BUT: renvoie vers table recap, en mode jury. """ formsemestre = FormSemestre.get_formsemestre(formsemestre_id) is_apc = formsemestre.formation.is_apc() if fmt == "html" and is_apc: return redirect( url_for( "notes.formsemestre_recapcomplet", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1, ) ) dpv = sco_pv_dict.dict_pvjury(formsemestre_id, with_prev=True) if not dpv: if fmt == "html": return render_template( "sco_page.j2", title="PV Jury", content="

Aucune information disponible !

", ) else: return None sem = dpv["formsemestre"] formsemestre_id = sem["formsemestre_id"] rows, titles, columns_ids = pvjury_table(dpv) if fmt != "html" and fmt != "pdf": columns_ids = ["etudid", "code_nip"] + columns_ids tab = GenTable( rows=rows, titles=titles, columns_ids=columns_ids, filename=scu.make_filename("decisions " + sem["titreannee"]), origin=f"Généré par {scu.sco_version.SCONAME} le {scu.timedate_human_repr()}", caption="Décisions jury pour " + sem["titreannee"], html_class="table_leftalign", html_sortable=True, preferences=sco_preferences.SemPreferences(formsemestre_id), table_id="formsemestre_pvjury", ) if fmt != "html": return tab.make_page( fmt=fmt, with_html_headers=False, publish=publish, ) tab.base_url = "%s?formsemestre_id=%s" % (request.base_url, formsemestre_id) H = [ f"""

Décisions du jury pour le semestre

(dernière modif le {dpv["date"]})

""", ] H.append( '' % formsemestre_id ) H.append(tab.html()) # Count number of cases for each decision counts = collections.defaultdict(int) for row in rows: counts[row["decision"]] += 1 # add codes for previous (for explanation, without count) if "prev_decision" in row and row["prev_decision"]: counts[row["prev_decision"]] += 0 # Légende des codes codes = list(counts.keys()) codes.sort() H.append("""

Explication des codes

""") lines = [] for code in codes: lines.append( { "code": code, "count": counts[code], "expl": codes_cursus.CODES_EXPL.get(code, ""), } ) H.append( GenTable( rows=lines, titles={"code": "Code", "count": "Nombre", "expl": ""}, columns_ids=("code", "count", "expl"), html_class="table_leftalign codes-jury", html_class_ignore_default=True, # pas une DataTable html_sortable=True, html_with_td_classes=True, preferences=sco_preferences.SemPreferences(formsemestre_id), table_id="formsemestre_pvjury_counts", ).html() ) H.append( """ """ ) H.append("
") # /codes return render_template( "sco_page.j2", title="Décisions du jury pour le semestre", content="\n".join(H) ) # --------------------------------------------------------------------------- def formsemestre_pvjury_pdf(formsemestre_id, etudid=None): """Génération PV jury en PDF: saisie des paramètres Si etudid, PV pour un seul etudiant. Sinon, tout les inscrits au(x) groupe(s) indiqué(s). """ if request.method == "POST": group_ids = request.form.getlist("group_ids") else: group_ids = request.args.getlist("group_ids") try: group_ids = [int(gid) for gid in group_ids] except ValueError as exc: raise ScoValueError("group_ids invalide") from exc formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id) # Mise à jour des groupes d'étapes: sco_groups.create_etapes_partition(formsemestre_id) groups_infos = None if etudid: # PV pour ce seul étudiant: etud = Identite.get_etud(etudid) etudids = [etudid] else: etud = None if not group_ids: # tous les inscrits du semestre group_ids = [sco_groups.get_default_group(formsemestre_id)] groups_infos = sco_groups_view.DisplayedGroupsInfos( group_ids, formsemestre_id=formsemestre_id ) etudids = [m["etudid"] for m in groups_infos.members] H = [ f"""
Utiliser cette page pour éditer des versions provisoires des PV. Il est recommandé d'archiver les versions définitives: voir cette page
""", ] F = [ """

Voir aussi si besoin les réglages sur la page "Paramétrage" (accessible à l'administrateur du département).

""", ] descr = descrform_pvjury(formsemestre) if etudid: descr.append(("etudid", {"input_type": "hidden"})) if groups_infos: menu_choix_groupe = ( """
Groupes d'étudiants à lister sur le PV: """ + sco_groups_view.menu_groups_choice(groups_infos, submit_on_change=True) + """
""" ) else: menu_choix_groupe = "" # un seul etudiant à editer tf = TrivialFormulator( request.base_url, scu.get_request_args(), descr, cancelbutton="Annuler", submitlabel="Générer document", name="tf", formid="group_selector", html_head_markup=menu_choix_groupe, ) if tf[0] == 0: info_etud = ( f"""de {etud.nomprenom}""" if etud else "" ) return render_template( "sco_page.j2", title=f"Édition du PV de jury {('de ' + etud.nom_prenom()) if etud else ''}", content=f"""

Édition du PV de jury {info_etud}

""" + "\n".join(H) + "\n" + tf[1] + "\n".join(F), javascripts=["js/groups_view.js"], ) if tf[0] == -1: return flask.redirect( url_for( "notes.formsemestre_pvjury", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) # submit tf[2]["show_title"] = bool(tf[2]["show_title"]) tf[2]["anonymous"] = bool(tf[2]["anonymous"]) try: PDFLOCK.acquire() pdfdoc = sco_pv_pdf.pvjury_pdf( formsemestre, etudids, numero_arrete=tf[2]["numero_arrete"], code_vdi=tf[2]["code_vdi"], date_commission=tf[2]["date_commission"], date_jury=tf[2]["date_jury"], show_title=tf[2]["show_title"], pv_title_session=tf[2]["pv_title_session"], pv_title=tf[2]["pv_title"], with_paragraph_nom=tf[2]["with_paragraph_nom"], anonymous=tf[2]["anonymous"], ) finally: PDFLOCK.release() date_iso = time.strftime("%Y-%m-%d") if groups_infos: groups_filename = "-" + groups_infos.groups_filename else: groups_filename = "" filename = f"""PV-{formsemestre.titre_num()}{groups_filename}-{date_iso}.pdf""" return scu.sendPDFFile(pdfdoc, filename) def descrform_pvjury(formsemestre: FormSemestre): """Définition de formulaire pour PV jury PDF""" f_dict = formsemestre.formation.to_dict() return [ ( "date_commission", { "input_type": "text", "size": 50, "title": "Date de la commission", "explanation": "(format libre)", }, ), ( "date_jury", { "input_type": "text", "size": 50, "title": "Date du Jury", "explanation": "(si le jury a eu lieu)", }, ), ( "numero_arrete", { "input_type": "text", "size": 50, "title": "Numéro de l'arrêté du président", "explanation": "le président de l'Université prend chaque année un arrêté formant les jurys", }, ), ( "code_vdi", { "input_type": "text", "size": 15, "title": "VDI et Code", "explanation": "VDI et code du diplôme Apogée (format libre, n'est pas vérifié par ScoDoc)", }, ), ( "pv_title_session", { "input_type": "text", "size": 48, "title": "Nom de la session", "explanation": "utilisé dans le titre du PV", "default": "Session unique", }, ), ( "pv_title", { "input_type": "text", "size": 96, "title": "Titre du PV", "explanation": "par défaut, titre officiel de la formation", "default": f_dict["titre_officiel"], }, ), ( "show_title", { "input_type": "checkbox", "title": "Indiquer en plus le titre du semestre sur le PV", "explanation": f'(le titre est "{formsemestre.titre}")', "labels": [""], "allowed_values": ("1",), }, ), ( "with_paragraph_nom", { "input_type": "boolcheckbox", "title": "Avec date naissance et code", "explanation": "ajoute informations sous le nom", "default": True, }, ), ( "anonymous", { "input_type": "checkbox", "title": "PV anonyme", "explanation": "remplace nom par code étudiant (INE ou NIP)", "labels": [""], "allowed_values": ("1",), }, ), ("formsemestre_id", {"input_type": "hidden"}), ] def formsemestre_lettres_individuelles(formsemestre_id): "Lettres avis jury en PDF" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if request.method == "POST": group_ids = request.form.getlist("group_ids") else: group_ids = request.args.getlist("group_ids") try: group_ids = [int(gid) for gid in group_ids] except ValueError as exc: raise ScoValueError("group_ids invalide") from exc if not group_ids: # tous les inscrits du semestre group_ids = [sco_groups.get_default_group(formsemestre_id)] groups_infos = sco_groups_view.DisplayedGroupsInfos( group_ids, formsemestre_id=formsemestre_id ) etudids = [m["etudid"] for m in groups_infos.members] H = [ f"""

Édition des lettres individuelles

Utiliser cette page pour éditer des versions provisoires des PV. Il est recommandé d'archiver les versions définitives: voir cette page
""", ] descr = descrform_lettres_individuelles() menu_choix_groupe = ( """
Groupes d'étudiants à lister: """ + sco_groups_view.menu_groups_choice(groups_infos, submit_on_change=True) + """
""" ) tf = TrivialFormulator( request.base_url, scu.get_request_args(), descr, cancelbutton="Annuler", submitlabel="Générer document", name="tf", formid="group_selector", html_head_markup=menu_choix_groupe, ) if tf[0] == 0: return render_template( "sco_page.j2", title="Édition des lettres individuelles", content="\n".join(H) + "\n" + tf[1], javascripts=["js/groups_view.js"], ) elif tf[0] == -1: return flask.redirect( url_for( "notes.formsemestre_pvjury", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) else: # submit sf = tf[2]["signature"] signature = sf.read() # image of signature try: PDFLOCK.acquire() pdfdoc = sco_pv_lettres_inviduelles.pdf_lettres_individuelles( formsemestre_id, etudids=etudids, date_jury=tf[2]["date_jury"], date_commission=tf[2]["date_commission"], signature=signature, ) finally: PDFLOCK.release() if not pdfdoc: flash("Aucun étudiant n'a de décision de jury !") return flask.redirect( url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) groups_filename = "-" + groups_infos.groups_filename filename = f"""lettres-{formsemestre.titre_num()}{groups_filename}-{time.strftime("%Y-%m-%d")}.pdf""" return scu.sendPDFFile(pdfdoc, filename) def descrform_lettres_individuelles(): return [ ( "date_commission", { "input_type": "text", "size": 50, "title": "Date de la commission", "explanation": "(format libre)", }, ), ( "date_jury", { "input_type": "text", "size": 50, "title": "Date du Jury", "explanation": "(si le jury a eu lieu)", }, ), ( "signature", { "input_type": "file", "size": 30, "explanation": "optionnel: image scannée de la signature", }, ), ("formsemestre_id", {"input_type": "hidden"}), ]