ScoDoc-Lille/app/scodoc/sco_pv_forms.py

659 lines
22 KiB
Python

# -*- 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, redirect, url_for
from flask import g, request
from app.models import FormSemestre, 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_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.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,
)
)
footer = html_sco_header.sco_footer()
dpv = sco_pv_dict.dict_pvjury(formsemestre_id, with_prev=True)
if not dpv:
if fmt == "html":
return (
html_sco_header.sco_header()
+ "<h2>Aucune information disponible !</h2>"
+ footer
)
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 = [
html_sco_header.html_sem_header(
"Décisions du jury pour le semestre",
javascripts=["js/etud_info.js"],
),
"""<p>(dernière modif le %s)</p>""" % dpv["date"],
]
H.append(
'<ul><li><a class="stdlink" href="formsemestre_lettres_individuelles?formsemestre_id=%s">Courriers individuels (classeur pdf)</a></li>'
% formsemestre_id
)
H.append(
'<li><a class="stdlink" href="formsemestre_pvjury_pdf?formsemestre_id=%s">PV officiel (pdf)</a></li></ul>'
% 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("""<div class="codes"><h3>Explication des codes</h3>""")
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(
"""<style>
div.codes {
margin-bottom: 12px;
}
table.codes-jury th, table.codes-jury td {
padding: 4px 8px 4px 8px;
}
table.codes-jury td {
background-color: #CCCCCC;
}
table.codes-jury td.count {
text-align: right;
}
</style>
"""
)
H.append("</div>") # /codes
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.get_etud(etudid)
etuddescr = f"""<a class="discretelink" href="{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">{etud.nomprenom}</a>"""
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,
),
f"""<div class="help">Utiliser cette page pour éditer des versions provisoires des PV.
<span class="fontred">Il est recommandé d'archiver les versions définitives:
<a class="stdlink" href="{url_for(
'notes.formsemestre_archive',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">voir cette page</a></span>
</div>""",
]
F = [
"""<p><em>Voir aussi si besoin les réglages sur la page "Paramétrage"
(accessible à l'administrateur du département).</em>
</p>""",
html_sco_header.sco_footer(),
]
descr = descrform_pvjury(formsemestre)
if etudid:
descr.append(("etudid", {"input_type": "hidden"}))
if groups_infos:
menu_choix_groupe = (
"""<div class="group_ids_sel_menu">Groupes d'étudiants à lister sur le PV: """
+ sco_groups_view.menu_groups_choice(groups_infos)
+ """</div>"""
)
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_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_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, 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,
),
f"""<p class="help">Utiliser cette page pour éditer des versions provisoires des PV.
<span class="fontred">Il est recommandé d'archiver les versions définitives: <a
href="{url_for(
"notes.formsemestre_archive",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)}"
>voir cette page</a></span></p>
""",
]
F = html_sco_header.sco_footer()
descr = descrform_lettres_individuelles()
menu_choix_groupe = (
"""<div class="group_ids_sel_menu">Groupes d'étudiants à lister: """
+ sco_groups_view.menu_groups_choice(groups_infos)
+ """</div>"""
)
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
descr,
cancelbutton="Annuler",
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_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"}),
]