# -*- 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"""
(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 = (
""""""
)
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""""""
+ "\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=f"""{tf[2]["code_vdi"]} {tf[2]["code_diplome"]}""",
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"],
pv_subtitle=tf[2]["pv_subtitle"],
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": "Version et code étape",
"explanation": "VDI et étape Apogée (format libre, n'est pas vérifié par ScoDoc)",
},
),
(
"code_diplome",
{
"input_type": "text",
"size": 15,
"title": "Version et code diplôme",
"explanation": "format libre, sera écrit à la suite du code étape ci-dessus",
},
),
(
"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"],
},
),
(
"pv_subtitle",
{
"input_type": "text",
"size": 90,
"title": "Sous-titre",
"explanation": "optionnel, placé sous le titre du PV",
},
),
(
"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.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"""
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 = (
""""""
)
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"}),
]