# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2023 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 PV Jury IUTV 2006: on détaillait 8 cas: Jury de semestre n On a 8 types de décisions: Passages: 1. passage de ceux qui ont validés Sn-1 2. passage avec compensation Sn-1, Sn 3. passage sans validation de Sn avec validation d'UE 4. passage sans validation de Sn sans validation d'UE Redoublements: 5. redoublement de Sn-1 et Sn sans validation d'UE pour Sn 6. redoublement de Sn-1 et Sn avec validation d'UE pour Sn Reports 7. report sans validation d'UE 8. non validation de Sn-1 et Sn et non redoublement """ import time from reportlab.platypus import Paragraph from reportlab.lib import styles import flask from flask import flash, redirect, url_for from flask import g, request from app.models import ( Formation, FormSemestre, ScolarAutorisationInscription, ) from app.models.etudiants import Identite import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc import html_sco_header from app.scodoc import codes_cursus from app.scodoc import sco_dict_pv_jury from app.scodoc import sco_etud from app.scodoc import sco_formsemestre 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_pvpdf 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.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=e["identite"]["etudid"], ), "_nomprenom_td_attrs": f"""id="{e['identite']['etudid']}" class="etudinfo" """, "parcours": e["parcours"], "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 # XXX TODO cette page a vocation a disparaitre, # remplacée par formsemestre_recapcomplet en mode jury (déjà le cas pour les BUT) def formsemestre_pvjury(formsemestre_id, format="html", publish=True): # XXX """Page récapitulant les décisions de jury""" formsemestre = FormSemestre.query.get_or_404(formsemestre_id) is_apc = formsemestre.formation.is_apc() if format == "html" and is_apc: return redirect( url_for( "notes.formsemestre_recapcomplet", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1, ) ) footer = html_sco_header.sco_footer() dpv = sco_dict_pv_jury.dict_pvjury(formsemestre_id, with_prev=True) if not dpv: if format == "html": return ( html_sco_header.sco_header() + "

Aucune information disponible !

" + footer ) else: return None sem = dpv["formsemestre"] formsemestre_id = sem["formsemestre_id"] rows, titles, columns_ids = pvjury_table(dpv) if format != "html" and format != "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="Généré par %s le " % scu.sco_version.SCONAME + scu.timedate_human_repr() + "", caption="Décisions jury pour " + sem["titreannee"], html_class="table_leftalign", html_sortable=True, preferences=sco_preferences.SemPreferences(formsemestre_id), ) if format != "html": return tab.make_page( format=format, with_html_headers=False, publish=publish, ) tab.base_url = "%s?formsemestre_id=%s" % (request.base_url, formsemestre_id) H = [ html_sco_header.html_sem_header( "Décisions du jury pour le semestre", init_qtip=True, javascripts=["js/etud_info.js"], ), """

(dernière modif le %s)

""" % dpv["date"], ] H.append( '' % formsemestre_id ) H.append(tab.html()) # Count number of cases for each decision counts = scu.DictDefault() 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", html_sortable=True, preferences=sco_preferences.SemPreferences(formsemestre_id), ).html() ) H.append("

") # force space at bottom return "\n".join(H) + footer # --------------------------------------------------------------------------- def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid=None): """Generation PV jury en PDF: saisie des paramètres Si etudid, PV pour un seul etudiant. Sinon, tout les inscrits au groupe indiqué. """ group_ids = group_ids or [] formsemestre: FormSemestre = FormSemestre.query.get_or_404(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.query.get_or_404(etudid) etuddescr = f"""{etud.nomprenom}""" etudids = [etudid] else: etuddescr = "" 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 = [ html_sco_header.html_sem_header( f"Édition du PV de jury {etuddescr}", javascripts=sco_groups_view.JAVASCRIPTS, cssstyles=sco_groups_view.CSSSTYLES, init_qtip=True, ), 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).

""", html_sco_header.sco_footer(), ] 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) + """
""" ) else: menu_choix_groupe = "" # un seul etudiant à editer tf = TrivialFormulator( request.base_url, scu.get_request_args(), descr, cancelbutton="Annuler", method="get", submitlabel="Générer document", name="tf", formid="group_selector", html_foot_markup=menu_choix_groupe, ) if tf[0] == 0: return "\n".join(H) + "\n" + tf[1] + "\n".join(F) elif tf[0] == -1: return flask.redirect( url_for( "notes.formsemestre_pvjury", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) else: # submit tf[2]["show_title"] = bool(tf[2]["show_title"]) tf[2]["anonymous"] = bool(tf[2]["anonymous"]) try: PDFLOCK.acquire() pdfdoc = sco_pvpdf.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=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", { "input_type": "text", "size": 64, "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, group_ids=[]): "Lettres avis jury en PDF" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) 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 = [ html_sco_header.html_sem_header( "Édition des lettres individuelles", javascripts=sco_groups_view.JAVASCRIPTS, cssstyles=sco_groups_view.CSSSTYLES, init_qtip=True, ), f"""

Utiliser cette page pour éditer des versions provisoires des PV. Il est recommandé d'archiver les versions définitives: voir cette page

""", ] F = html_sco_header.sco_footer() descr = descrform_lettres_individuelles() menu_choix_groupe = ( """
Groupes d'étudiants à lister: """ + sco_groups_view.menu_groups_choice(groups_infos) + """
""" ) tf = TrivialFormulator( request.base_url, scu.get_request_args(), descr, cancelbutton="Annuler", method="POST", submitlabel="Générer document", name="tf", formid="group_selector", html_foot_markup=menu_choix_groupe, ) if tf[0] == 0: return "\n".join(H) + "\n" + tf[1] + F 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_pvpdf.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"}), ]