# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2021 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 """ import time from reportlab.platypus import Paragraph from reportlab.lib import styles import sco_utils as scu import notesdb as ndb from notes_log import log import scolars import sco_formsemestre import sco_groups import sco_groups_view import sco_parcours_dut import sco_codes_parcours from sco_codes_parcours import NO_SEMESTRE_ID import sco_excel from TrivialFormulator import TrivialFormulator from gen_tables import GenTable import sco_pvpdf import sco_pdf from sco_pdf import PDFLOCK """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 """ def _descr_decisions_ues(context, nt, etudid, decisions_ue, decision_sem): """Liste des UE validées dans ce semestre""" if not decisions_ue: return [] uelist = [] # Les UE validées dans ce semestre: for ue_id in decisions_ue.keys(): try: if decisions_ue[ue_id] and ( decisions_ue[ue_id]["code"] == sco_codes_parcours.ADM or ( scu.CONFIG.CAPITALIZE_ALL_UES and sco_codes_parcours.code_semestre_validant(decision_sem["code"]) ) ): ue = context.do_ue_list(args={"ue_id": ue_id})[0] uelist.append(ue) except: log("descr_decisions_ues: ue_id=%s decisions_ue=%s" % (ue_id, decisions_ue)) pass # Les UE capitalisées dans d'autres semestres: for ue in nt.ue_capitalisees[etudid]: try: uelist.append(nt.get_etud_ue_status(etudid, ue["ue_id"])["ue"]) except KeyError: pass uelist.sort(lambda x, y: cmp(x["numero"], y["numero"])) return uelist def _descr_decision_sem(context, etat, decision_sem): "résumé textuel de la décision de semestre" if etat == "D": decision = "Démission" else: if decision_sem: cod = decision_sem["code"] decision = sco_codes_parcours.CODES_EXPL.get(cod, "") # + ' (%s)' % cod else: decision = "" return decision def _descr_decision_sem_abbrev(context, 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 descr_autorisations(context, autorisations): "résumé textuel des autorisations d'inscription (-> 'S1, S3' )" alist = [] for aut in autorisations: alist.append("S" + str(aut["semestre_id"])) return ", ".join(alist) def _comp_ects_by_ue_code_and_type(nt, decision_ues): """Calcul somme des ECTS validés dans ce semestre (sans les UE capitalisées) decision_ues est le resultat de nt.get_etud_decision_ues Chaque resultat est un dict: { ue_code : ects } """ if not decision_ues: return {}, {} ects_by_ue_code = {} ects_by_ue_type = scu.DictDefault(defaultvalue=0) # { ue_type : ects validés } for ue_id in decision_ues: d = decision_ues[ue_id] ue = nt.uedict[ue_id] ects_by_ue_code[ue["ue_code"]] = d["ects"] ects_by_ue_type[ue["type"]] += d["ects"] return ects_by_ue_code, ects_by_ue_type def _comp_ects_capitalises_by_ue_code(nt, etudid): """Calcul somme des ECTS des UE capitalisees""" ues = nt.get_ues() ects_by_ue_code = {} for ue in ues: ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) if ue_status["is_capitalized"]: try: ects_val = float(ue_status["ue"]["ects"]) except (ValueError, TypeError): ects_val = 0.0 ects_by_ue_code[ue["ue_code"]] = ects_val return ects_by_ue_code def _sum_ects_dicts(s, t): """Somme deux dictionnaires { ue_code : ects }, quand une UE de même code apparait deux fois, prend celle avec le plus d'ECTS. """ sum_ects = sum(s.values()) + sum(t.values()) for ue_code in set(s).intersection(set(t)): sum_ects -= min(s[ue_code], t[ue_code]) return sum_ects def dict_pvjury( context, formsemestre_id, etudids=None, with_prev=False, with_parcours_decisions=False, ): """Données pour édition jury etudids == None => tous les inscrits, sinon donne la liste des ids Si with_prev: ajoute infos sur code jury semestre precedent Si with_parcours_decisions: ajoute infos sur code decision jury de tous les semestre du parcours Résultat: { 'date' : date de la decision la plus recente, 'formsemestre' : sem, 'formation' : { 'acronyme' :, 'titre': ... } 'decisions' : { [ { 'identite' : {'nom' :, 'prenom':, ...,}, 'etat' : I ou D ou DEF 'decision_sem' : {'code':, 'code_prev': }, 'decisions_ue' : { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' :, 'acronyme', 'numero': } }, 'autorisations' : [ { 'semestre_id' : { ... } } ], 'validation_parcours' : True si parcours validé (diplome obtenu) 'prev_code' : code (calculé slt si with_prev), 'mention' : mention (en fct moy gen), 'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées) 'sum_ects_capitalises' : somme des ECTS des UE capitalisees } ] }, 'decisions_dict' : { etudid : decision (comme ci-dessus) }, } """ nt = context._getNotesCache().get_NotesTable( context, formsemestre_id ) # > get_etudids, get_etud_etat, get_etud_decision_sem, get_etud_decision_ues if etudids is None: etudids = nt.get_etudids() if not etudids: return {} cnx = context.GetDBConnexion() sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) max_date = "0000-01-01" has_prev = False # vrai si au moins un etudiant a un code prev semestre_non_terminal = False # True si au moins un etudiant a un devenir L = [] D = {} # même chose que L, mais { etudid : dec } for etudid in etudids: etud = context.getEtudInfo(etudid=etudid, filled=True)[0] Se = sco_parcours_dut.SituationEtudParcours(context, etud, formsemestre_id) semestre_non_terminal = semestre_non_terminal or Se.semestre_non_terminal d = {} d["identite"] = nt.identdict[etudid] d["etat"] = nt.get_etud_etat( etudid ) # I|D|DEF (inscription ou démission ou défaillant) d["decision_sem"] = nt.get_etud_decision_sem(etudid) d["decisions_ue"] = nt.get_etud_decision_ues(etudid) d["last_formsemestre_id"] = Se.get_semestres()[ -1 ] # id du dernier semestre (chronologiquement) dans lequel il a été inscrit ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid) d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values()) ects_by_ue_code, ects_by_ue_type = _comp_ects_by_ue_code_and_type( nt, d["decisions_ue"] ) d["sum_ects"] = _sum_ects_dicts(ects_capitalises_by_ue_code, ects_by_ue_code) d["sum_ects_by_type"] = ects_by_ue_type if d["decision_sem"] and sco_codes_parcours.code_semestre_validant( d["decision_sem"]["code"] ): d["mention"] = scu.get_mention(nt.get_etud_moy_gen(etudid)) else: d["mention"] = "" # Versions "en français": (avec les UE capitalisées d'ailleurs) dec_ue_list = _descr_decisions_ues( context, nt, etudid, d["decisions_ue"], d["decision_sem"] ) d["decisions_ue_nb"] = len( dec_ue_list ) # avec les UE capitalisées, donc des éventuels doublons # Mais sur la description (eg sur les bulletins), on ne veut pas # afficher ces doublons: on uniquifie sur ue_code _codes = set() ue_uniq = [] for ue in dec_ue_list: if ue["ue_code"] not in _codes: ue_uniq.append(ue) _codes.add(ue["ue_code"]) d["decisions_ue_descr"] = ", ".join([ue["acronyme"] for ue in ue_uniq]) d["decision_sem_descr"] = _descr_decision_sem( context, d["etat"], d["decision_sem"] ) d["autorisations"] = sco_parcours_dut.formsemestre_get_autorisation_inscription( context, etudid, formsemestre_id ) d["autorisations_descr"] = descr_autorisations(context, d["autorisations"]) d["validation_parcours"] = Se.parcours_validated() d["parcours"] = Se.get_parcours_descr(filter_futur=True) if with_parcours_decisions: d["parcours_decisions"] = Se.get_parcours_decisions() # Observations sur les compensations: compensators = sco_parcours_dut.scolar_formsemestre_validation_list( cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid} ) obs = [] for compensator in compensators: # nb: il ne devrait y en avoir qu'un ! csem = sco_formsemestre.get_formsemestre( context, compensator["formsemestre_id"] ) obs.append( "%s compensé par %s (%s)" % (sem["sem_id_txt"], csem["sem_id_txt"], csem["anneescolaire"]) ) if d["decision_sem"] and d["decision_sem"]["compense_formsemestre_id"]: compensed = sco_formsemestre.get_formsemestre( context, d["decision_sem"]["compense_formsemestre_id"] ) obs.append( "%s compense %s (%s)" % ( sem["sem_id_txt"], compensed["sem_id_txt"], compensed["anneescolaire"], ) ) d["observation"] = ", ".join(obs) # Cherche la date de decision (sem ou UE) la plus récente: if d["decision_sem"]: date = ndb.DateDMYtoISO(d["decision_sem"]["event_date"]) if date > max_date: # decision plus recente max_date = date if d["decisions_ue"]: for dec_ue in d["decisions_ue"].values(): if dec_ue: date = ndb.DateDMYtoISO(dec_ue["event_date"]) if date > max_date: # decision plus recente max_date = date # Code semestre precedent if with_prev: # optionnel car un peu long... info = context.getEtudInfo(etudid=etudid, filled=True) if not info: continue # should not occur etud = info[0] if Se.prev and Se.prev_decision: d["prev_decision_sem"] = Se.prev_decision d["prev_code"] = Se.prev_decision["code"] d["prev_code_descr"] = _descr_decision_sem( context, "I", Se.prev_decision ) d["prev"] = Se.prev has_prev = True else: d["prev_decision_sem"] = None d["prev_code"] = "" d["prev_code_descr"] = "" d["Se"] = Se L.append(d) D[etudid] = d return { "date": ndb.DateISOtoDMY(max_date), "formsemestre": sem, "has_prev": has_prev, "semestre_non_terminal": semestre_non_terminal, "formation": context.formation_list(args={"formation_id": sem["formation_id"]})[ 0 ], "decisions": L, "decisions_dict": D, } def pvjury_table( context, 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"] = "Décision S%s" % id_prev columns_ids += ["prev_decision"] columns_ids += ["decision"] if sco_preferences.get_preference(context, "bul_show_mention", formsemestre_id): columns_ids += ["mention"] columns_ids += ["ue_cap"] if sco_preferences.get_preference(context, "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"]: scolars.format_etud_ident(e["identite"]) l = { "etudid": e["identite"]["etudid"], "code_nip": e["identite"]["code_nip"], "nomprenom": e["identite"]["nomprenom"], "_nomprenom_target": "%s/ficheEtud?etudid=%s" % (context.ScoURL(), e["identite"]["etudid"]), "_nomprenom_td_attrs": 'id="%s" class="etudinfo"' % e["identite"]["etudid"], "parcours": e["parcours"], "decision": _descr_decision_sem_abbrev( context, 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(context, "SCOLAR_FONT_SIZE", formsemestre_id ) cell_style.fontName = sco_preferences.get_preference(context, "PV_FONTNAME", formsemestre_id) cell_style.leading = 1.0 * sco_preferences.get_preference(context, "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( context, None, e["prev_decision_sem"] ) if e["validation_parcours"] or not only_diplome: lines.append(l) return lines, titles, columns_ids def formsemestre_pvjury( context, formsemestre_id, format="html", publish=True, REQUEST=None ): """Page récapitulant les décisions de jury dpv: result of dict_pvjury """ footer = html_sco_header.sco_footer(context, REQUEST) dpv = dict_pvjury(context, formsemestre_id, with_prev=True) if not dpv: if format == "html": return ( html_sco_header.sco_header(context, REQUEST) + "
(dernière modif le %s)
""" % dpv["date"], ] H.append( 'Utiliser cette page pour éditer des versions provisoires des PV. Il est recommandé d'archiver les versions définitives: voir cette page
""" % formsemestre_id, ] F = [ """Voir aussi si besoin les réglages sur la page "Paramétrage" (accessible à l'administrateur du département).
""", html_sco_header.sco_footer(context, REQUEST), ] descr = descrform_pvjury(context, sem) 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.URL0, REQUEST.form, 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 REQUEST.RESPONSE.redirect( "formsemestre_pvjury?formsemestre_id=%s" % (formsemestre_id) ) else: # submit dpv = dict_pvjury(context, formsemestre_id, etudids=etudids, with_prev=True) if tf[2]["showTitle"]: tf[2]["showTitle"] = True else: tf[2]["showTitle"] = False if tf[2]["anonymous"]: tf[2]["anonymous"] = True else: tf[2]["anonymous"] = False try: PDFLOCK.acquire() pdfdoc = sco_pvpdf.pvjury_pdf( context, dpv, REQUEST, numeroArrete=tf[2]["numeroArrete"], VDICode=tf[2]["VDICode"], date_commission=tf[2]["date_commission"], date_jury=tf[2]["date_jury"], showTitle=tf[2]["showTitle"], pv_title=tf[2]["pv_title"], with_paragraph_nom=tf[2]["with_paragraph_nom"], anonymous=tf[2]["anonymous"], ) finally: PDFLOCK.release() sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) dt = time.strftime("%Y-%m-%d") if groups_infos: groups_filename = "-" + groups_infos.groups_filename else: groups_filename = "" filename = "PV-%s%s-%s.pdf" % (sem["titre_num"], groups_filename, dt) return scu.sendPDFFile(REQUEST, pdfdoc, filename) def descrform_pvjury(context, sem): """Définition de formulaire pour PV jury PDF""" F = context.Notes.formation_list(formation_id=sem["formation_id"])[0] 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)", }, ), ( "numeroArrete", { "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", }, ), ( "VDICode", { "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["titre_officiel"], }, ), ( "showTitle", { "input_type": "checkbox", "title": "Indiquer en plus le titre du semestre sur le PV", "explanation": '(le titre est "%s")' % sem["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( context, formsemestre_id, group_ids=[], REQUEST=None ): "Lettres avis jury en PDF" sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) if not group_ids: # tous les inscrits du semestre group_ids = [sco_groups.get_default_group(context, formsemestre_id)] groups_infos = sco_groups_view.DisplayedGroupsInfos( context, group_ids, formsemestre_id=formsemestre_id, REQUEST=REQUEST ) etudids = [m["etudid"] for m in groups_infos.members] H = [ html_sco_header.html_sem_header( context, REQUEST, "Edition des lettres individuelles", sem=sem, javascripts=sco_groups_view.JAVASCRIPTS, cssstyles=sco_groups_view.CSSSTYLES, init_qtip=True, ), """Utiliser cette page pour éditer des versions provisoires des PV. Il est recommandé d'archiver les versions définitives: voir cette page
""" % formsemestre_id, ] F = html_sco_header.sco_footer(context, REQUEST) descr = descrform_lettres_individuelles() menu_choix_groupe = ( """ """ ) tf = TrivialFormulator( REQUEST.URL0, REQUEST.form, 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 REQUEST.RESPONSE.redirect( "formsemestre_pvjury?formsemestre_id=%s" % (formsemestre_id) ) else: # submit sf = tf[2]["signature"] signature = sf.read() # image of signature try: PDFLOCK.acquire() pdfdoc = sco_pvpdf.pdf_lettres_individuelles( context, formsemestre_id, etudids=etudids, date_jury=tf[2]["date_jury"], date_commission=tf[2]["date_commission"], signature=signature, ) finally: PDFLOCK.release() if not pdfdoc: return REQUEST.RESPONSE.redirect( "formsemestre_status?formsemestre_id={}&head_message=Aucun%20%C3%A9tudiant%20n%27a%20de%20d%C3%A9cision%20de%20jury".format( formsemestre_id ) ) sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) dt = time.strftime("%Y-%m-%d") groups_filename = "-" + groups_infos.groups_filename filename = "lettres-%s%s-%s.pdf" % (sem["titre_num"], groups_filename, dt) return scu.sendPDFFile(REQUEST, 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"}), ]