From 81e7914620b82c05fe859ed0f803067e0e2acc60 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 7 Jul 2022 16:24:52 +0200 Subject: [PATCH 01/18] Refactoring: cursus Classic/ECTS/BUT --- app/but/cursus_but.py | 56 ++++++ app/models/validations.py | 15 ++ app/scodoc/notes_table.py | 10 +- app/scodoc/sco_apogee_csv.py | 4 +- app/scodoc/sco_cache.py | 4 +- app/scodoc/sco_cursus.py | 134 ++++++++++++++ ...{sco_parcours_dut.py => sco_cursus_dut.py} | 168 +++--------------- app/scodoc/sco_edit_ue.py | 4 +- app/scodoc/sco_formsemestre_edit.py | 6 +- app/scodoc/sco_formsemestre_exterieurs.py | 4 +- app/scodoc/sco_formsemestre_validation.py | 44 +++-- app/scodoc/sco_groups.py | 7 +- app/scodoc/sco_groups_view.py | 4 +- app/scodoc/sco_moduleimpl_status.py | 2 +- app/scodoc/sco_page_etud.py | 4 +- app/scodoc/sco_permissions_check.py | 4 +- app/scodoc/sco_prepajury.py | 15 +- app/scodoc/sco_pvjury.py | 28 ++- app/scodoc/sco_pvpdf.py | 4 +- app/scodoc/sco_report.py | 11 +- tests/unit/test_sco_basic.py | 12 +- 21 files changed, 312 insertions(+), 228 deletions(-) create mode 100644 app/but/cursus_but.py create mode 100644 app/scodoc/sco_cursus.py rename app/scodoc/{sco_parcours_dut.py => sco_cursus_dut.py} (86%) diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py new file mode 100644 index 00000000..6626d20e --- /dev/null +++ b/app/but/cursus_but.py @@ -0,0 +1,56 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Cursus en BUT + +Classe raccordant avec ScoDoc 7: + ScoDoc 7 utilisait sco_cursus_dut.SituationEtudCursus + + Ce module définit une classe SituationEtudCursusBUT + avec la même interface. + +""" + +from typing import Union + +from flask import g, url_for + +from app import db +from app import log +from app.comp.res_but import ResultatsSemestreBUT +from app.comp import res_sem +from app.models import formsemestre + +from app.models.but_refcomp import ( + ApcAnneeParcours, + ApcCompetence, + ApcNiveau, + ApcParcours, + ApcParcoursNiveauCompetence, +) +from app.models import Scolog, ScolarAutorisationInscription +from app.models.but_validations import ( + ApcValidationAnnee, + ApcValidationRCUE, + RegroupementCoherentUE, +) +from app.models.etudiants import Identite +from app.models.formations import Formation +from app.models.formsemestre import FormSemestre, FormSemestreInscription +from app.models.ues import UniteEns +from app.models.validations import ScolarFormSemestreValidation +from app.scodoc import sco_codes_parcours as sco_codes +from app.scodoc.sco_codes_parcours import RED, UE_STANDARD +from app.scodoc import sco_utils as scu +from app.scodoc.sco_exceptions import ScoException, ScoValueError + +from app.scodoc import sco_cursus_dut + + +class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursus): + def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT): + self.semestre_non_terminal = bool + self.formation diff --git a/app/models/validations.py b/app/models/validations.py index 42d7ba0d..b24685a8 100644 --- a/app/models/validations.py +++ b/app/models/validations.py @@ -57,6 +57,11 @@ class ScolarFormSemestreValidation(db.Model): def __repr__(self): return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})" + def to_dict(self) -> dict: + d = dict(self.__dict__) + d.pop("_sa_instance_state", None) + return d + class ScolarAutorisationInscription(db.Model): """Autorisation d'inscription dans un semestre""" @@ -78,6 +83,11 @@ class ScolarAutorisationInscription(db.Model): db.ForeignKey("notes_formsemestre.id"), ) + def to_dict(self) -> dict: + d = dict(self.__dict__) + d.pop("_sa_instance_state", None) + return d + @classmethod def autorise_etud( cls, @@ -146,3 +156,8 @@ class ScolarEvent(db.Model): db.Integer, db.ForeignKey("notes_formsemestre.id"), ) + + def to_dict(self) -> dict: + d = dict(self.__dict__) + d.pop("_sa_instance_state", None) + return d diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py index a8cd0eb7..56f2b93e 100644 --- a/app/scodoc/notes_table.py +++ b/app/scodoc/notes_table.py @@ -54,22 +54,22 @@ from app.scodoc.sco_codes_parcours import ( ue_is_fondamentale, ue_is_professionnelle, ) -from app.scodoc.sco_parcours_dut import formsemestre_get_etud_capitalisation +from app.scodoc import sco_cache from app.scodoc import sco_codes_parcours from app.scodoc import sco_compute_moy -from app.scodoc import sco_cache +from app.scodoc.sco_cursus import formsemestre_get_etud_capitalisation +from app.scodoc import sco_cursus_dut from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_module from app.scodoc import sco_edit_ue +from app.scodoc import sco_etud from app.scodoc import sco_evaluations from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl -from app.scodoc import sco_parcours_dut from app.scodoc import sco_preferences -from app.scodoc import sco_etud def comp_ranks(T): @@ -1175,7 +1175,7 @@ class NotesTable: ): if not cnx: cnx = ndb.GetDBConnexion() - sco_parcours_dut.do_formsemestre_validate_ue( + sco_cursus_dut.do_formsemestre_validate_ue( cnx, nt_cap, ue_cap["formsemestre_id"], diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index e309beda..da1aed8d 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -112,8 +112,8 @@ from app.scodoc.sco_codes_parcours import ( NAR, RAT, ) +from app.scodoc import sco_cursus from app.scodoc import sco_formsemestre -from app.scodoc import sco_parcours_dut from app.scodoc import sco_etud APO_PORTAL_ENCODING = ( @@ -413,7 +413,7 @@ class ApoEtud(dict): export_res_etape = self.export_res_etape if (not export_res_etape) and cur_sem: # exporte toujours le résultat de l'étape si l'étudiant est diplômé - Se = sco_parcours_dut.SituationEtudParcours( + Se = sco_cursus.get_situation_etud_cursus( self.etud, cur_sem["formsemestre_id"] ) export_res_etape = Se.all_other_validated() diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index 7975e3b2..0798a37d 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -231,7 +231,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa """expire cache pour un semestre (ou tous si formsemestre_id non spécifié). Si pdfonly, n'expire que les bulletins pdf cachés. """ - from app.scodoc import sco_parcours_dut + from app.scodoc import sco_cursus if getattr(g, "defer_cache_invalidation", False): g.sem_to_invalidate.add(formsemestre_id) @@ -252,7 +252,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa else: formsemestre_ids = [ formsemestre_id - ] + sco_parcours_dut.list_formsemestre_utilisateurs_uecap(formsemestre_id) + ] + sco_cursus.list_formsemestre_utilisateurs_uecap(formsemestre_id) log(f"----- invalidate_formsemestre: clearing {formsemestre_ids} -----") if not pdfonly: diff --git a/app/scodoc/sco_cursus.py b/app/scodoc/sco_cursus.py new file mode 100644 index 00000000..eec02b9f --- /dev/null +++ b/app/scodoc/sco_cursus.py @@ -0,0 +1,134 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2022 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 +# +############################################################################## + +"""Gestion des cursus (jurys suivant la formation) +""" + +from app.but import cursus_but +from app.scodoc import sco_cursus_dut + +from app.comp.res_compat import NotesTableCompat +from app.comp import res_sem +from app.models import FormSemestre +from app.scodoc import sco_formsemestre +from app.scodoc import sco_formations +import app.scodoc.notesdb as ndb + +# SituationEtudParcours -> get_situation_etud_cursus +def get_situation_etud_cursus( + etud: dict, formsemestre_id: int +) -> sco_cursus_dut.SituationEtudCursus: + """renvoie une instance de SituationEtudCursus (ou sous-classe spécialisée)""" + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + + if formsemestre.formation.is_apc(): + return cursus_but.SituationEtudCursusBUT(etud, formsemestre_id, nt) + + parcours = nt.parcours + if parcours.ECTS_ONLY: + return sco_cursus_dut.SituationEtudCursusECTS(etud, formsemestre_id, nt) + return sco_cursus_dut.SituationEtudCursusClassic(etud, formsemestre_id, nt) + + +def formsemestre_get_etud_capitalisation( + formation_id: int, semestre_idx: int, date_debut, etudid: int +) -> list[dict]: + """Liste des UE capitalisées (ADM) correspondant au semestre sem et à l'étudiant. + + Recherche dans les semestres de la même formation (code) avec le même + semestre_id et une date de début antérieure à celle du semestre mentionné. + Et aussi les UE externes validées. + + Resultat: [ { 'formsemestre_id' : + 'ue_id' : ue_id dans le semestre origine + 'ue_code' : + 'moy_ue' : + 'event_date' : + 'is_external' + } ] + """ + cnx = ndb.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) + cursor.execute( + """ + SELECT DISTINCT SFV.*, ue.ue_code + FROM notes_ue ue, notes_formations nf, + notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem + + WHERE ue.formation_id = nf.id + and nf.formation_code = nf2.formation_code + and nf2.id=%(formation_id)s + + and SFV.ue_id = ue.id + and SFV.code = 'ADM' + and SFV.etudid = %(etudid)s + + and ( (sem.id = SFV.formsemestre_id + and sem.date_debut < %(date_debut)s + and sem.semestre_id = %(semestre_id)s ) + or ( + ((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures" + AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s) + ) ) + """, + { + "etudid": etudid, + "formation_id": formation_id, + "semestre_id": semestre_idx, + "date_debut": date_debut, + }, + ) + + return cursor.dictfetchall() + + +def list_formsemestre_utilisateurs_uecap(formsemestre_id): + """Liste des formsemestres pouvant utiliser une UE capitalisee de ce semestre + (et qui doivent donc etre sortis du cache si l'on modifie ce + semestre): meme code formation, meme semestre_id, date posterieure""" + cnx = ndb.GetDBConnexion() + sem = sco_formsemestre.get_formsemestre(formsemestre_id) + F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] + cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) + cursor.execute( + """SELECT sem.id + FROM notes_formsemestre sem, notes_formations F + WHERE sem.formation_id = F.id + and F.formation_code = %(formation_code)s + and sem.semestre_id = %(semestre_id)s + and sem.date_debut >= %(date_debut)s + and sem.id != %(formsemestre_id)s; + """, + { + "formation_code": F["formation_code"], + "semestre_id": sem["semestre_id"], + "formsemestre_id": formsemestre_id, + "date_debut": ndb.DateDMYtoISO(sem["date_debut"]), + }, + ) + return [x[0] for x in cursor.fetchall()] diff --git a/app/scodoc/sco_parcours_dut.py b/app/scodoc/sco_cursus_dut.py similarity index 86% rename from app/scodoc/sco_parcours_dut.py rename to app/scodoc/sco_cursus_dut.py index f3441386..5d2f31bf 100644 --- a/app/scodoc/sco_parcours_dut.py +++ b/app/scodoc/sco_cursus_dut.py @@ -28,9 +28,10 @@ """Semestres: gestion parcours DUT (Arreté du 13 août 2005) """ +from app import db from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre, UniteEns +from app.models import FormSemestre, UniteEns, ScolarAutorisationInscription import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb @@ -105,27 +106,14 @@ class DecisionSem(object): ) ) ) - # xxx debug - # log('%s: %s %s %s %s %s' % (self.codechoice,code_etat,new_code_prev,formsemestre_id_utilise_pour_compenser,devenir,assiduite) ) -def SituationEtudParcours(etud: dict, formsemestre_id: int): - """renvoie une instance de SituationEtudParcours (ou sous-classe spécialisée)""" - formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - - # if formsemestre.formation.is_apc(): - # return SituationEtudParcoursBUT(etud, formsemestre_id, nt) - - parcours = nt.parcours - # - if parcours.ECTS_ONLY: - return SituationEtudParcoursECTS(etud, formsemestre_id, nt) - else: - return SituationEtudParcoursGeneric(etud, formsemestre_id, nt) +class SituationEtudCursus: + "Semestre dans un cursus" + pass -class SituationEtudParcoursGeneric: +class SituationEtudCursusClassic(SituationEtudCursus): "Semestre dans un parcours" def __init__(self, etud: dict, formsemestre_id: int, nt: NotesTableCompat): @@ -454,8 +442,7 @@ class SituationEtudParcoursGeneric: break if not cur or cur["formsemestre_id"] != self.formsemestre_id: log( - "*** SituationEtudParcours: search_prev: cur not found (formsemestre_id=%s, etudid=%s)" - % (self.formsemestre_id, self.etudid) + f"*** SituationEtudCursus: search_prev: cur not found (formsemestre_id={self.formsemestre_id}, etudid={self.etudid})" ) return None # pas de semestre courant !!! # Cherche semestre antérieur de même formation (code) et semestre_id precedent @@ -633,31 +620,27 @@ class SituationEtudParcoursGeneric: formsemestre_id=self.prev["formsemestre_id"] ) # > modif decisions jury (sem, UE) - # -- supprime autorisations venant de ce formsemestre - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) try: - cursor.execute( - """delete from scolar_autorisation_inscription - where etudid = %(etudid)s and origin_formsemestre_id=%(origin_formsemestre_id)s - """, - {"etudid": self.etudid, "origin_formsemestre_id": self.formsemestre_id}, + # -- Supprime autorisations venant de ce formsemestre + autorisations = ScolarAutorisationInscription.query.filter_by( + etudid=self.etudid, origin_formsemestre_id=self.formsemestre_id ) - - # -- enregistre autorisations inscription + for autorisation in autorisations: + db.session.delete(autorisation) + db.session.flush() + # -- Enregistre autorisations inscription next_semestre_ids = self.get_next_semestre_ids(decision.devenir) for next_semestre_id in next_semestre_ids: - _scolar_autorisation_inscription_editor.create( - cnx, - { - "etudid": self.etudid, - "formation_code": self.formation.formation_code, - "semestre_id": next_semestre_id, - "origin_formsemestre_id": self.formsemestre_id, - }, + autorisation = ScolarAutorisationInscription( + etudid=self.etudid, + formation_code=self.formation.formation_code, + semestre_id=next_semestre_id, + origin_formsemestre_id=self.formsemestre_id, ) - cnx.commit() + db.session.add(autorisation) + db.session.commit() except: - cnx.rollback() + cnx.session.rollback() raise sco_cache.invalidate_formsemestre( formsemestre_id=self.formsemestre_id @@ -673,11 +656,11 @@ class SituationEtudParcoursGeneric: ) # > modif decision jury -class SituationEtudParcoursECTS(SituationEtudParcoursGeneric): +class SituationEtudCursusECTS(SituationEtudCursusClassic): """Gestion parcours basés sur ECTS""" def __init__(self, etud, formsemestre_id, nt): - SituationEtudParcoursGeneric.__init__(self, etud, formsemestre_id, nt) + SituationEtudCursusClassic.__init__(self, etud, formsemestre_id, nt) def could_be_compensated(self): return False # jamais de compensations dans ce parcours @@ -1020,9 +1003,9 @@ def etud_est_inscrit_ue(cnx, etudid, formsemestre_id, ue_id): """ cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( - """SELECT mi.* + """SELECT mi.* FROM notes_moduleimpl mi, notes_modules mo, notes_ue ue, notes_moduleimpl_inscription i - WHERE i.etudid = %(etudid)s + WHERE i.etudid = %(etudid)s and i.moduleimpl_id=mi.id and mi.formsemestre_id = %(formsemestre_id)s and mi.module_id = mo.id @@ -1032,102 +1015,3 @@ def etud_est_inscrit_ue(cnx, etudid, formsemestre_id, ue_id): ) return len(cursor.fetchall()) - - -_scolar_autorisation_inscription_editor = ndb.EditableTable( - "scolar_autorisation_inscription", - "autorisation_inscription_id", - ("etudid", "formation_code", "semestre_id", "date", "origin_formsemestre_id"), - output_formators={"date": ndb.DateISOtoDMY}, - input_formators={"date": ndb.DateDMYtoISO}, -) -scolar_autorisation_inscription_list = _scolar_autorisation_inscription_editor.list - - -def formsemestre_get_autorisation_inscription(etudid, origin_formsemestre_id): - """Liste des autorisations d'inscription pour cet étudiant - émanant du semestre indiqué. - """ - cnx = ndb.GetDBConnexion() - return scolar_autorisation_inscription_list( - cnx, {"origin_formsemestre_id": origin_formsemestre_id, "etudid": etudid} - ) - - -def formsemestre_get_etud_capitalisation( - formation_id: int, semestre_idx: int, date_debut, etudid: int -) -> list[dict]: - """Liste des UE capitalisées (ADM) correspondant au semestre sem et à l'étudiant. - - Recherche dans les semestres de la même formation (code) avec le même - semestre_id et une date de début antérieure à celle du semestre mentionné. - Et aussi les UE externes validées. - - Resultat: [ { 'formsemestre_id' : - 'ue_id' : ue_id dans le semestre origine - 'ue_code' : - 'moy_ue' : - 'event_date' : - 'is_external' - } ] - """ - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - """ - SELECT DISTINCT SFV.*, ue.ue_code - FROM notes_ue ue, notes_formations nf, - notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem - - WHERE ue.formation_id = nf.id - and nf.formation_code = nf2.formation_code - and nf2.id=%(formation_id)s - - and SFV.ue_id = ue.id - and SFV.code = 'ADM' - and SFV.etudid = %(etudid)s - - and ( (sem.id = SFV.formsemestre_id - and sem.date_debut < %(date_debut)s - and sem.semestre_id = %(semestre_id)s ) - or ( - ((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures" - AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s) - ) ) - """, - { - "etudid": etudid, - "formation_id": formation_id, - "semestre_id": semestre_idx, - "date_debut": date_debut, - }, - ) - - return cursor.dictfetchall() - - -def list_formsemestre_utilisateurs_uecap(formsemestre_id): - """Liste des formsemestres pouvant utiliser une UE capitalisee de ce semestre - (et qui doivent donc etre sortis du cache si l'on modifie ce - semestre): meme code formation, meme semestre_id, date posterieure""" - cnx = ndb.GetDBConnexion() - sem = sco_formsemestre.get_formsemestre(formsemestre_id) - F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - """SELECT sem.id - FROM notes_formsemestre sem, notes_formations F - WHERE sem.formation_id = F.id - and F.formation_code = %(formation_code)s - and sem.semestre_id = %(semestre_id)s - and sem.date_debut >= %(date_debut)s - and sem.id != %(formsemestre_id)s; - """, - { - "formation_code": F["formation_code"], - "semestre_id": sem["semestre_id"], - "formsemestre_id": formsemestre_id, - "date_debut": ndb.DateDMYtoISO(sem["date_debut"]), - }, - ) - return [x[0] for x in cursor.fetchall()] diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index def072c3..0c052805 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -140,7 +140,7 @@ def do_ue_create(args): def do_ue_delete(ue_id, delete_validations=False, force=False): "delete UE and attached matieres (but not modules)" - from app.scodoc import sco_parcours_dut + from app.scodoc import sco_cursus_dut ue = UniteEns.query.get_or_404(ue_id) formation = ue.formation @@ -164,7 +164,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): # raise ScoLockedFormError() # Il y a-t-il des etudiants ayant validé cette UE ? # si oui, propose de supprimer les validations - validations = sco_parcours_dut.scolar_formsemestre_validation_list( + validations = sco_cursus_dut.scolar_formsemestre_validation_list( cnx, args={"ue_id": ue.id} ) if validations and not delete_validations and not force: diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index c4e32946..cf24c4b0 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -60,7 +60,7 @@ from app.scodoc import sco_formsemestre from app.scodoc import sco_groups_copy from app.scodoc import sco_modalites from app.scodoc import sco_moduleimpl -from app.scodoc import sco_parcours_dut +from app.scodoc import sco_cursus_dut from app.scodoc import sco_permissions_check from app.scodoc import sco_portal_apogee from app.scodoc import sco_preferences @@ -1362,14 +1362,14 @@ def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new) if e["ue_id"]: e["ue_id"] = ues_old2new[e["ue_id"]] sco_etud.scolar_events_edit(cnx, e) - validations = sco_parcours_dut.scolar_formsemestre_validation_list( + validations = sco_cursus_dut.scolar_formsemestre_validation_list( cnx, args={"formsemestre_id": formsemestre_id} ) for e in validations: if e["ue_id"]: e["ue_id"] = ues_old2new[e["ue_id"]] # log('e=%s' % e ) - sco_parcours_dut.scolar_formsemestre_validation_edit(cnx, e) + sco_cursus_dut.scolar_formsemestre_validation_edit(cnx, e) def formsemestre_delete(formsemestre_id): diff --git a/app/scodoc/sco_formsemestre_exterieurs.py b/app/scodoc/sco_formsemestre_exterieurs.py index 0da85f2c..62d49c6d 100644 --- a/app/scodoc/sco_formsemestre_exterieurs.py +++ b/app/scodoc/sco_formsemestre_exterieurs.py @@ -51,7 +51,7 @@ from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_validation -from app.scodoc import sco_parcours_dut +from app.scodoc import sco_cursus_dut from app.scodoc import sco_etud @@ -450,7 +450,7 @@ def _list_ue_with_coef_and_validations(sem, etudid): else: ue["uecoef"] = {} # add validation - validation = sco_parcours_dut.scolar_formsemestre_validation_list( + validation = sco_cursus_dut.scolar_formsemestre_validation_list( cnx, args={ "formsemestre_id": formsemestre_id, diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index a9d13c01..2effc138 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -59,8 +59,9 @@ from app.scodoc import sco_edit_ue from app.scodoc import sco_etud from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions -from app.scodoc import sco_parcours_dut -from app.scodoc.sco_parcours_dut import etud_est_inscrit_ue +from app.scodoc import sco_cursus +from app.scodoc import sco_cursus_dut +from app.scodoc.sco_cursus_dut import etud_est_inscrit_ue from app.scodoc import sco_photos from app.scodoc import sco_preferences from app.scodoc import sco_pvjury @@ -108,7 +109,7 @@ def formsemestre_validation_etud_form( check = True etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id) + Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id) if not Se.sem["etat"]: raise ScoValueError("validation: semestre verrouille") @@ -274,15 +275,12 @@ def formsemestre_validation_etud_form( ass = "non assidu" H.append("

Décision existante du %(event_date)s: %(code)s" % decision_jury) H.append(" (%s)" % ass) - auts = sco_parcours_dut.formsemestre_get_autorisation_inscription( - etudid, formsemestre_id - ) - if auts: + autorisations = ScolarAutorisationInscription.query.filter_by( + etudid=etudid, origin_formsemestre_id=formsemestre_id + ).all() + if autorisations: H.append(". Autorisé%s à s'inscrire en " % etud["ne"]) - alist = [] - for aut in auts: - alist.append(str(aut["semestre_id"])) - H.append(", ".join(["S%s" % x for x in alist]) + ".") + H.append(", ".join([f"S{aut.semestre_id}" for aut in autorisations]) + ".") H.append("

") # Cas particulier pour ATJ: corriger precedent avant de continuer @@ -382,7 +380,7 @@ def formsemestre_validation_etud( ): """Enregistre validation""" etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id) + Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id) # retrouve la decision correspondant au code: choices = Se.get_possible_choices(assiduite=True) choices += Se.get_possible_choices(assiduite=False) @@ -415,7 +413,7 @@ def formsemestre_validation_etud_manu( if assidu: assidu = True etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id) + Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id) if code_etat in Se.parcours.UNUSED_CODES: raise ScoValueError("code decision invalide dans ce parcours") # Si code ADC, extrait le semestre utilisé: @@ -430,7 +428,7 @@ def formsemestre_validation_etud_manu( formsemestre_id_utilise_pour_compenser = None # Construit le choix correspondant: - choice = sco_parcours_dut.DecisionSem( + choice = sco_cursus_dut.DecisionSem( code_etat=code_etat, new_code_prev=new_code_prev, devenir=devenir, @@ -910,7 +908,7 @@ def do_formsemestre_validation_auto(formsemestre_id): conflicts = [] # liste des etudiants avec decision differente déjà saisie for etudid in etudids: etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id) + Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id) ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( {"etudid": etudid, "formsemestre_id": formsemestre_id} )[0] @@ -932,15 +930,13 @@ def do_formsemestre_validation_auto(formsemestre_id): if decision_sem and decision_sem["code"] != ADM: ok = False conflicts.append(etud) - autorisations = sco_parcours_dut.formsemestre_get_autorisation_inscription( - etudid, formsemestre_id - ) - if ( - len(autorisations) != 0 - ): # accepte le cas ou il n'y a pas d'autorisation : BUG 23/6/7, A RETIRER ENSUITE + autorisations = ScolarAutorisationInscription.query.filter_by( + etudid=etudid, origin_formsemestre_id=formsemestre_id + ).all() + if len(autorisations) != 0: if ( - len(autorisations) != 1 - or autorisations[0]["semestre_id"] != next_semestre_id + len(autorisations) > 1 + or autorisations[0].semestre_id != next_semestre_id ): if ok: conflicts.append(etud) @@ -1176,7 +1172,7 @@ def do_formsemestre_validate_previous_ue( ) else: sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre_id, ue_id) - sco_parcours_dut.do_formsemestre_validate_ue( + sco_cursus_dut.do_formsemestre_validate_ue( cnx, nt, formsemestre_id, # "importe" cette UE dans le semestre (new 3/2015) diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index f17e017e..d2cd636c 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -56,8 +56,9 @@ import app.scodoc.notesdb as ndb from app import log, cache from app.scodoc.scolog import logdb from app.scodoc import html_sco_header -from app.scodoc import sco_codes_parcours from app.scodoc import sco_cache +from app.scodoc import sco_codes_parcours +from app.scodoc import sco_cursus from app.scodoc import sco_etud from app.scodoc import sco_permissions_check from app.scodoc import sco_xml @@ -1489,13 +1490,13 @@ def _get_prev_moy(etudid, formsemestre_id): """Donne la derniere moyenne generale calculee pour cette étudiant, ou 0 si on n'en trouve pas (nouvel inscrit,...). """ - from app.scodoc import sco_parcours_dut + from app.scodoc import sco_cursus_dut info = sco_etud.get_etud_info(etudid=etudid, filled=True) if not info: raise ScoValueError("etudiant invalide: etudid=%s" % etudid) etud = info[0] - Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id) + Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id) if Se.prev: prev_sem = FormSemestre.query.get(Se.prev["formsemestre_id"]) nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_sem) diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index 259256bd..9588abd4 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -49,7 +49,7 @@ from app.scodoc import sco_excel from app.scodoc import sco_formsemestre from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl -from app.scodoc import sco_parcours_dut +from app.scodoc import sco_cursus from app.scodoc import sco_portal_apogee from app.scodoc import sco_preferences from app.scodoc import sco_etud @@ -776,7 +776,7 @@ def groups_table( m.update(etud) sco_etud.etud_add_lycee_infos(etud) # et ajoute le parcours - Se = sco_parcours_dut.SituationEtudParcours( + Se = sco_cursus.get_situation_etud_cursus( etud, groups_infos.formsemestre_id ) m["parcours"] = Se.get_parcours_descr() diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index c49ef080..66387efa 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -39,7 +39,7 @@ from app.models import ModuleImpl from app.models.evaluations import Evaluation import app.scodoc.sco_utils as scu from app.scodoc.sco_exceptions import ScoInvalidIdType -from app.scodoc.sco_parcours_dut import formsemestre_has_decisions +from app.scodoc.sco_cursus_dut import formsemestre_has_decisions from app.scodoc.sco_permissions import Permission from app.scodoc import html_sco_header diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index b0690ea4..298181bf 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -46,7 +46,7 @@ from app.scodoc import sco_codes_parcours from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_status from app.scodoc import sco_groups -from app.scodoc import sco_parcours_dut +from app.scodoc import sco_cursus from app.scodoc import sco_permissions_check from app.scodoc import sco_photos from app.scodoc import sco_users @@ -269,7 +269,7 @@ def ficheEtud(etudid=None): sem_info[sem["formsemestre_id"]] = grlink if info["sems"]: - Se = sco_parcours_dut.SituationEtudParcours(etud, info["last_formsemestre_id"]) + Se = sco_cursus.get_situation_etud_cursus(etud, info["last_formsemestre_id"]) info["liste_inscriptions"] = formsemestre_recap_parcours_table( Se, etudid, diff --git a/app/scodoc/sco_permissions_check.py b/app/scodoc/sco_permissions_check.py index fe21b167..9ad457d9 100644 --- a/app/scodoc/sco_permissions_check.py +++ b/app/scodoc/sco_permissions_check.py @@ -24,14 +24,14 @@ def can_edit_notes(authuser, moduleimpl_id, allow_ens=True): seul le directeur des études peut saisir des notes (et il ne devrait pas). """ from app.scodoc import sco_formsemestre - from app.scodoc import sco_parcours_dut + from app.scodoc import sco_cursus_dut M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) if not sem["etat"]: return False # semestre verrouillé - if sco_parcours_dut.formsemestre_has_decisions(sem["formsemestre_id"]): + if sco_cursus_dut.formsemestre_has_decisions(sem["formsemestre_id"]): # il y a des décisions de jury dans ce semestre ! return ( authuser.has_permission(Permission.ScoEditAllNotes) diff --git a/app/scodoc/sco_prepajury.py b/app/scodoc/sco_prepajury.py index 678b81ab..81093af7 100644 --- a/app/scodoc/sco_prepajury.py +++ b/app/scodoc/sco_prepajury.py @@ -37,14 +37,14 @@ from flask_login import current_user from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre, Identite +from app.models import FormSemestre, Identite, ScolarAutorisationInscription from app.scodoc import sco_abs from app.scodoc import sco_codes_parcours from app.scodoc import sco_groups from app.scodoc import sco_etud from app.scodoc import sco_excel from app.scodoc import sco_formsemestre -from app.scodoc import sco_parcours_dut +from app.scodoc import sco_cursus from app.scodoc import sco_preferences import app.scodoc.sco_utils as scu import sco_version @@ -78,7 +78,7 @@ def feuille_preparation_jury(formsemestre_id): nbabs = {} nbabsjust = {} for etud in etuds: - Se = sco_parcours_dut.SituationEtudParcours( + Se = sco_cursus.get_situation_etud_cursus( etud.to_dict_scodoc7(), formsemestre_id ) if Se.prev: @@ -119,10 +119,11 @@ def feuille_preparation_jury(formsemestre_id): if decision["compense_formsemestre_id"]: code[etud.id] += "+" # indique qu'il a servi a compenser assidu[etud.id] = {False: "Non", True: "Oui"}.get(decision["assidu"], "") - aut_list = sco_parcours_dut.formsemestre_get_autorisation_inscription( - etud.id, formsemestre_id - ) - autorisations[etud.id] = ", ".join(["S%s" % x["semestre_id"] for x in aut_list]) + + autorisations = ScolarAutorisationInscription.query.filter_by( + etudid=etud.id, origin_formsemestre_id=formsemestre_id + ).all() + autorisations[etud.id] = ", ".join(["S{x.semestre_id}" for x in autorisations]) # parcours: parcours[etud.id] = Se.get_parcours_descr() # groupe principal (td) diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py index 9b374b8e..9975e7c6 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pvjury.py @@ -57,24 +57,24 @@ from flask import g, request from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre, UniteEns +from app.models import FormSemestre, UniteEns, ScolarAutorisationInscription import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log from app.scodoc import html_sco_header from app.scodoc import sco_codes_parcours -from app.scodoc import sco_cache +from app.scodoc import sco_cursus +from app.scodoc import sco_cursus_dut from app.scodoc import sco_edit_ue +from app.scodoc import sco_etud from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_groups from app.scodoc import sco_groups_view -from app.scodoc import sco_parcours_dut from app.scodoc import sco_pdf from app.scodoc import sco_preferences from app.scodoc import sco_pvpdf -from app.scodoc import sco_etud from app.scodoc.gen_tables import GenTable from app.scodoc.sco_codes_parcours import NO_SEMESTRE_ID from app.scodoc.sco_pdf import PDFLOCK @@ -138,12 +138,9 @@ def _descr_decision_sem_abbrev(etat, decision_sem): return decision -def descr_autorisations(autorisations): +def descr_autorisations(autorisations: list[ScolarAutorisationInscription]) -> str: "résumé textuel des autorisations d'inscription (-> 'S1, S3' )" - alist = [] - for aut in autorisations: - alist.append("S" + str(aut["semestre_id"])) - return ", ".join(alist) + return ", ".join([f"S{a.semestre_id}" for a in autorisations]) def _comp_ects_by_ue_code(nt, decision_ues): @@ -234,7 +231,7 @@ def dict_pvjury( D = {} # même chose que L, mais { etudid : dec } for etudid in etudids: etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id) + Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id) semestre_non_terminal = semestre_non_terminal or Se.semestre_non_terminal d = {} d["identite"] = nt.identdict[etudid] @@ -280,17 +277,18 @@ def dict_pvjury( else: d["decision_sem_descr"] = _descr_decision_sem(d["etat"], d["decision_sem"]) - d["autorisations"] = sco_parcours_dut.formsemestre_get_autorisation_inscription( - etudid, formsemestre_id - ) - d["autorisations_descr"] = descr_autorisations(d["autorisations"]) + autorisations = ScolarAutorisationInscription.query.filter_by( + etudid=etudid, origin_formsemestre_id=formsemestre_id + ).all() + d["autorisations"] = [a.to_dict() for a in autorisations] + d["autorisations_descr"] = descr_autorisations(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( + compensators = sco_cursus_dut.scolar_formsemestre_validation_list( cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid} ) obs = [] diff --git a/app/scodoc/sco_pvpdf.py b/app/scodoc/sco_pvpdf.py index 8ef1c12c..d889277e 100644 --- a/app/scodoc/sco_pvpdf.py +++ b/app/scodoc/sco_pvpdf.py @@ -50,7 +50,7 @@ from app.scodoc import sco_formsemestre from app.scodoc import sco_pdf from app.scodoc import sco_preferences from app.scodoc.sco_logos import find_logo -from app.scodoc.sco_parcours_dut import SituationEtudParcours +from app.scodoc.sco_cursus_dut import SituationEtudCursus from app.scodoc.sco_pdf import SU import sco_version @@ -428,7 +428,7 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None): """ # formsemestre_id = sem["formsemestre_id"] - Se: SituationEtudParcours = decision["Se"] + Se: SituationEtudCursus = decision["Se"] t, s = _descr_jury(sem, Se.parcours_validated() or not Se.semestre_non_terminal) objects = [] style = reportlab.lib.styles.ParagraphStyle({}) diff --git a/app/scodoc/sco_report.py b/app/scodoc/sco_report.py index 214532a5..97f01f75 100644 --- a/app/scodoc/sco_report.py +++ b/app/scodoc/sco_report.py @@ -41,7 +41,7 @@ import pydot from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre +from app.models import FormSemestre, ScolarAutorisationInscription import app.scodoc.sco_utils as scu from app.models import FormationModalite @@ -51,7 +51,6 @@ from app.scodoc import sco_codes_parcours from app.scodoc import sco_etud from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions -from app.scodoc import sco_parcours_dut from app.scodoc import sco_preferences import sco_version from app.scodoc.gen_tables import GenTable @@ -81,10 +80,10 @@ def formsemestre_etuds_stats(sem, only_primo=False): if "codedecision" not in etud: etud["codedecision"] = "(nd)" # pas de decision jury # Ajout devenir (autorisations inscriptions), utile pour stats passage - aut_list = sco_parcours_dut.formsemestre_get_autorisation_inscription( - etudid, sem["formsemestre_id"] - ) - autorisations = ["S%s" % x["semestre_id"] for x in aut_list] + aut_list = ScolarAutorisationInscription.query.filter_by( + etudid=etudid, origin_formsemestre_id=sem["formsemestre_id"] + ).all() + autorisations = [f"S{a.semestre_id}" for a in aut_list] autorisations.sort() autorisations_str = ", ".join(autorisations) etud["devenir"] = autorisations_str diff --git a/tests/unit/test_sco_basic.py b/tests/unit/test_sco_basic.py index a00ab66e..52ed79f0 100644 --- a/tests/unit/test_sco_basic.py +++ b/tests/unit/test_sco_basic.py @@ -20,6 +20,7 @@ from config import TestConfig from tests.unit import sco_fake_gen import app +from app import db from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre @@ -31,8 +32,7 @@ from app.scodoc import sco_codes_parcours from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db from app.scodoc import sco_formsemestre_validation -from app.scodoc import sco_parcours_dut -from app.scodoc import sco_cache +from app.scodoc import sco_cursus_dut from app.scodoc import sco_saisie_notes from app.scodoc import sco_utils as scu @@ -194,20 +194,20 @@ def run_sco_basic(verbose=False): # --- Permission saisie notes et décisions de jury, avec ou sans démission ou défaillance # on n'a pas encore saisi de décisions - assert not sco_parcours_dut.formsemestre_has_decisions(formsemestre_id) + assert not sco_cursus_dut.formsemestre_has_decisions(formsemestre_id) # Saisie d'un décision AJ, non assidu etudid = etuds[-1]["etudid"] - sco_parcours_dut.formsemestre_validate_ues( + sco_cursus_dut.formsemestre_validate_ues( formsemestre_id, etudid, sco_codes_parcours.AJ, False ) - assert sco_parcours_dut.formsemestre_has_decisions( + assert sco_cursus_dut.formsemestre_has_decisions( formsemestre_id ), "décisions manquantes" # Suppression de la décision sco_formsemestre_validation.formsemestre_validation_suppress_etud( formsemestre_id, etudid ) - assert not sco_parcours_dut.formsemestre_has_decisions( + assert not sco_cursus_dut.formsemestre_has_decisions( formsemestre_id ), "décisions non effacées" From 543c3759d9c8acac1670304ed89d5b3531c57bb9 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 7 Jul 2022 23:34:14 +0200 Subject: [PATCH 02/18] =?UTF-8?q?refactoring=20et=20pr=C3=A9paratifs=20pou?= =?UTF-8?q?r=20lettres=20individuelles=20BUT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/cursus_but.py | 17 +++- app/scodoc/sco_cursus_dut.py | 85 ++++++++++--------- app/scodoc/sco_formsemestre.py | 2 +- app/scodoc/sco_prepajury.py | 102 ++++++++++++----------- app/scodoc/sco_pvpdf.py | 144 ++++++++++++++++++--------------- app/views/notes.py | 8 +- 6 files changed, 195 insertions(+), 163 deletions(-) diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index 6626d20e..e3ae8bb6 100644 --- a/app/but/cursus_but.py +++ b/app/but/cursus_but.py @@ -21,6 +21,8 @@ from flask import g, url_for from app import db from app import log from app.comp.res_but import ResultatsSemestreBUT +from app.comp.res_compat import NotesTableCompat + from app.comp import res_sem from app.models import formsemestre @@ -50,7 +52,16 @@ from app.scodoc.sco_exceptions import ScoException, ScoValueError from app.scodoc import sco_cursus_dut -class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursus): +class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic): def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT): - self.semestre_non_terminal = bool - self.formation + super().__init__(etud, formsemestre_id, res) + # Ajustements pour le BUT + self.can_compensate_with_prev = False # jamais de compensation à la mode DUT + + def check_compensation_dut(self, semc: dict, ntc: NotesTableCompat): + "Jamais de compensation façon DUT" + return False + + def parcours_validated(self): + "True si le parcours est validé" + return False # XXX TODO diff --git a/app/scodoc/sco_cursus_dut.py b/app/scodoc/sco_cursus_dut.py index 5d2f31bf..f4354c6f 100644 --- a/app/scodoc/sco_cursus_dut.py +++ b/app/scodoc/sco_cursus_dut.py @@ -341,9 +341,7 @@ class SituationEtudCursusClassic(SituationEtudCursus): )[0]["formation_code"] # si sem peut servir à compenser le semestre courant, positionne # can_compensate - sem["can_compensate"] = check_compensation( - self.etudid, self.sem, self.nt, sem, nt - ) + sem["can_compensate"] = self.check_compensation_dut(sem, nt) self.ue_acros = list(ue_acros.keys()) self.ue_acros.sort() @@ -655,6 +653,46 @@ class SituationEtudCursusClassic(SituationEtudCursus): formsemestre_id=formsemestre_id ) # > modif decision jury + def check_compensation_dut(self, semc: dict, ntc: NotesTableCompat): + """Compensations DUT + Vérifie si le semestre sem peut se compenser en utilisant semc + - semc non utilisé par un autre semestre + - decision du jury prise ADM ou ADJ ou ATT ou ADC + - barres UE (moy ue > 8) dans sem et semc + - moyenne des moy_gen > 10 + Return boolean + """ + # -- deja utilise ? + decc = ntc.get_etud_decision_sem(self.etudid) + if ( + decc + and decc["compense_formsemestre_id"] + and decc["compense_formsemestre_id"] != self.sem["formsemestre_id"] + ): + return False + # -- semestres consecutifs ? + if abs(self.sem["semestre_id"] - semc["semestre_id"]) != 1: + return False + # -- decision jury: + if decc and not decc["code"] in (ADM, ADJ, ATT, ADC): + return False + # -- barres UE et moyenne des moyennes: + moy_gen = self.nt.get_etud_moy_gen(self.etudid) + moy_genc = ntc.get_etud_moy_gen(self.etudid) + try: + moy_moy = (moy_gen + moy_genc) / 2 + except: # un des semestres sans aucune note ! + return False + + if ( + self.nt.etud_check_conditions_ues(self.etudid)[0] + and ntc.etud_check_conditions_ues(self.etudid)[0] + and moy_moy >= NOTES_BARRE_GEN_COMPENSATION + ): + return True + else: + return False + class SituationEtudCursusECTS(SituationEtudCursusClassic): """Gestion parcours basés sur ECTS""" @@ -701,47 +739,6 @@ class SituationEtudCursusECTS(SituationEtudCursusClassic): return choices -# -def check_compensation(etudid, sem, nt, semc, ntc): - """Verifie si le semestre sem peut se compenser en utilisant semc - - semc non utilisé par un autre semestre - - decision du jury prise ADM ou ADJ ou ATT ou ADC - - barres UE (moy ue > 8) dans sem et semc - - moyenne des moy_gen > 10 - Return boolean - """ - # -- deja utilise ? - decc = ntc.get_etud_decision_sem(etudid) - if ( - decc - and decc["compense_formsemestre_id"] - and decc["compense_formsemestre_id"] != sem["formsemestre_id"] - ): - return False - # -- semestres consecutifs ? - if abs(sem["semestre_id"] - semc["semestre_id"]) != 1: - return False - # -- decision jury: - if decc and not decc["code"] in (ADM, ADJ, ATT, ADC): - return False - # -- barres UE et moyenne des moyennes: - moy_gen = nt.get_etud_moy_gen(etudid) - moy_genc = ntc.get_etud_moy_gen(etudid) - try: - moy_moy = (moy_gen + moy_genc) / 2 - except: # un des semestres sans aucune note ! - return False - - if ( - nt.etud_check_conditions_ues(etudid)[0] - and ntc.etud_check_conditions_ues(etudid)[0] - and moy_moy >= NOTES_BARRE_GEN_COMPENSATION - ): - return True - else: - return False - - # ------------------------------------------------------------------------------------------- diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py index 32c2d9e3..ce7f2c10 100644 --- a/app/scodoc/sco_formsemestre.py +++ b/app/scodoc/sco_formsemestre.py @@ -101,7 +101,7 @@ def get_formsemestre(formsemestre_id, raise_soft_exc=False): return g.stored_get_formsemestre[formsemestre_id] if not isinstance(formsemestre_id, int): log(f"get_formsemestre: invalid id '{formsemestre_id}'") - raise ScoInvalidIdType("formsemestre_id must be an integer !") + raise ScoInvalidIdType("get_formsemestre: formsemestre_id must be an integer !") sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id}) if not sems: log("get_formsemestre: invalid formsemestre_id (%s)" % formsemestre_id) diff --git a/app/scodoc/sco_prepajury.py b/app/scodoc/sco_prepajury.py index 81093af7..0afb1c41 100644 --- a/app/scodoc/sco_prepajury.py +++ b/app/scodoc/sco_prepajury.py @@ -103,14 +103,14 @@ def feuille_preparation_jury(formsemestre_id): moy[etud.id] = nt.get_etud_moy_gen(etud.id) for ue in nt.get_ues_stat_dict(filter_sport=True): ue_status = nt.get_etud_ue_status(etud.id, ue["ue_id"]) - ue_code_s = ue["ue_code"] + "_%s" % nt.sem["semestre_id"] + ue_code_s = f'{ue["ue_code"]}_{nt.sem["semestre_id"]}' moy_ue[ue_code_s][etud.id] = ue_status["moy"] if ue_status else "" ue_acro[ue_code_s] = (ue["numero"], ue["acronyme"], ue["titre"]) if Se.prev: try: moy_inter[etud.id] = (moy[etud.id] + prev_moy[etud.id]) / 2.0 - except: + except (KeyError, TypeError): pass decision = nt.get_etud_decision_sem(etud.id) @@ -120,10 +120,12 @@ def feuille_preparation_jury(formsemestre_id): code[etud.id] += "+" # indique qu'il a servi a compenser assidu[etud.id] = {False: "Non", True: "Oui"}.get(decision["assidu"], "") - autorisations = ScolarAutorisationInscription.query.filter_by( + autorisations_etud = ScolarAutorisationInscription.query.filter_by( etudid=etud.id, origin_formsemestre_id=formsemestre_id ).all() - autorisations[etud.id] = ", ".join(["S{x.semestre_id}" for x in autorisations]) + autorisations[etud.id] = ", ".join( + [f"S{x.semestre_id}" for x in autorisations_etud] + ) # parcours: parcours[etud.id] = Se.get_parcours_descr() # groupe principal (td) @@ -154,11 +156,11 @@ def feuille_preparation_jury(formsemestre_id): sid = sem["semestre_id"] sn = sp = "" if sid >= 0: - sn = "S%s" % sid + sn = f"S{sid}" if prev_moy: # si qq chose dans precedent - sp = "S%s" % (sid - 1) + sp = f"S{sid - 1}" - ws = sco_excel.ScoExcelSheet(sheet_name="Prepa Jury %s" % sn) + sheet = sco_excel.ScoExcelSheet(sheet_name=f"Prepa Jury {sn}") # génération des styles style_bold = sco_excel.excel_make_style(size=10, bold=True) style_center = sco_excel.excel_make_style(halign="center") @@ -174,10 +176,10 @@ def feuille_preparation_jury(formsemestre_id): ) # Première ligne - ws.append_single_cell_row( + sheet.append_single_cell_row( "Feuille préparation Jury %s" % scu.unescape_html(sem["titreannee"]), style_bold ) - ws.append_blank_row() + sheet.append_blank_row() # Ligne de titre titles = ["Rang"] @@ -199,25 +201,25 @@ def feuille_preparation_jury(formsemestre_id): ] if prev_moy: # si qq chose dans precedent titles += [prev_ue_acro[x][1] for x in ue_prev_codes] + [ - "Moy %s" % sp, - "Décision %s" % sp, + f"Moy {sp}", + f"Décision {sp}", ] - titles += [ue_acro[x][1] for x in ue_codes] + ["Moy %s" % sn] + titles += [ue_acro[x][1] for x in ue_codes] + [f"Moy {sn}"] if moy_inter: - titles += ["Moy %s-%s" % (sp, sn)] + titles += [f"Moy {sp}-{sn}"] titles += ["Abs", "Abs Injust."] if code: - titles.append("Proposit. %s" % sn) + titles.append("Proposit. {sn}") if autorisations: titles.append("Autorisations") # titles.append('Assidu') - ws.append_row(ws.make_row(titles, style_boldcenter)) - if prev_moy: - tit_prev_moy = "Moy " + sp - col_prev_moy = titles.index(tit_prev_moy) - tit_moy = "Moy " + sn - col_moy = titles.index(tit_moy) - col_abs = titles.index("Abs") + sheet.append_row(sheet.make_row(titles, style_boldcenter)) + # if prev_moy: + # tit_prev_moy = "Moy " + sp + # # col_prev_moy = titles.index(tit_prev_moy) + # tit_moy = "Moy " + sn + # col_moy = titles.index(tit_moy) + # col_abs = titles.index("Abs") def fmt(x): "reduit les notes a deux chiffres" @@ -230,13 +232,13 @@ def feuille_preparation_jury(formsemestre_id): i = 1 # numero etudiant for etud in etuds: cells = [] - cells.append(ws.make_cell(str(i))) + cells.append(sheet.make_cell(str(i))) if sco_preferences.get_preference("prepa_jury_nip"): - cells.append(ws.make_cell(etud.code_nip)) + cells.append(sheet.make_cell(etud.code_nip)) if sco_preferences.get_preference("prepa_jury_ine"): - cells.append(ws.make_cell(etud.code_ine)) + cells.append(sheet.make_cell(etud.code_ine)) admission = etud.admission.first() - cells += ws.make_row( + cells += sheet.make_row( [ etud.id, etud.civilite_str, @@ -254,50 +256,52 @@ def feuille_preparation_jury(formsemestre_id): if prev_moy: for ue_acro in ue_prev_codes: cells.append( - ws.make_cell( + sheet.make_cell( fmt(prev_moy_ue.get(ue_acro, {}).get(etud.id, "")), style_note ) ) co += 1 cells.append( - ws.make_cell(fmt(prev_moy.get(etud.id, "")), style_bold) + sheet.make_cell(fmt(prev_moy.get(etud.id, "")), style_bold) ) # moy gen prev cells.append( - ws.make_cell(fmt(prev_code.get(etud.id, "")), style_moy) + sheet.make_cell(fmt(prev_code.get(etud.id, "")), style_moy) ) # decision prev co += 2 for ue_acro in ue_codes: cells.append( - ws.make_cell(fmt(moy_ue.get(ue_acro, {}).get(etud.id, "")), style_note) + sheet.make_cell( + fmt(moy_ue.get(ue_acro, {}).get(etud.id, "")), style_note + ) ) co += 1 cells.append( - ws.make_cell(fmt(moy.get(etud.id, "")), style_note_bold) + sheet.make_cell(fmt(moy.get(etud.id, "")), style_note_bold) ) # moy gen co += 1 if moy_inter: - cells.append(ws.make_cell(fmt(moy_inter.get(etud.id, "")), style_note)) - cells.append(ws.make_cell(str(nbabs.get(etud.id, "")), style_center)) - cells.append(ws.make_cell(str(nbabsjust.get(etud.id, "")), style_center)) + cells.append(sheet.make_cell(fmt(moy_inter.get(etud.id, "")), style_note)) + cells.append(sheet.make_cell(str(nbabs.get(etud.id, "")), style_center)) + cells.append(sheet.make_cell(str(nbabsjust.get(etud.id, "")), style_center)) if code: - cells.append(ws.make_cell(code.get(etud.id, ""), style_moy)) - cells.append(ws.make_cell(autorisations.get(etud.id, ""), style_moy)) + cells.append(sheet.make_cell(code.get(etud.id, ""), style_moy)) + cells.append(sheet.make_cell(autorisations.get(etud.id, ""), style_moy)) # l.append(assidu.get(etud.id, '')) - ws.append_row(cells) + sheet.append_row(cells) i += 1 # - ws.append_blank_row() + sheet.append_blank_row() # Explications des codes codes = list(sco_codes_parcours.CODES_EXPL.keys()) codes.sort() - ws.append_single_cell_row("Explication des codes") + sheet.append_single_cell_row("Explication des codes") for code in codes: - ws.append_row( - ws.make_row(["", "", "", code, sco_codes_parcours.CODES_EXPL[code]]) + sheet.append_row( + sheet.make_row(["", "", "", code, sco_codes_parcours.CODES_EXPL[code]]) ) - ws.append_row( - ws.make_row( + sheet.append_row( + sheet.make_row( [ "", "", @@ -308,16 +312,16 @@ def feuille_preparation_jury(formsemestre_id): ) ) # UE : Correspondances acronyme et titre complet - ws.append_blank_row() - ws.append_single_cell_row("Titre des UE") + sheet.append_blank_row() + sheet.append_single_cell_row("Titre des UE") if prev_moy: for ue in ntp.get_ues_stat_dict(filter_sport=True): - ws.append_row(ws.make_row(["", "", "", ue["acronyme"], ue["titre"]])) + sheet.append_row(sheet.make_row(["", "", "", ue["acronyme"], ue["titre"]])) for ue in nt.get_ues_stat_dict(filter_sport=True): - ws.append_row(ws.make_row(["", "", "", ue["acronyme"], ue["titre"]])) + sheet.append_row(sheet.make_row(["", "", "", ue["acronyme"], ue["titre"]])) # - ws.append_blank_row() - ws.append_single_cell_row( + sheet.append_blank_row() + sheet.append_single_cell_row( "Préparé par %s le %s sur %s pour %s" % ( sco_version.SCONAME, @@ -326,7 +330,7 @@ def feuille_preparation_jury(formsemestre_id): current_user, ) ) - xls = ws.generate() + xls = sheet.generate() flash("Feuille préparation jury générée") return scu.send_file( xls, diff --git a/app/scodoc/sco_pvpdf.py b/app/scodoc/sco_pvpdf.py index d889277e..dba7801b 100644 --- a/app/scodoc/sco_pvpdf.py +++ b/app/scodoc/sco_pvpdf.py @@ -30,6 +30,8 @@ import io import re +from PIL import Image as PILImage + import reportlab from reportlab.lib.units import cm, mm from reportlab.lib.enums import TA_RIGHT, TA_JUSTIFY @@ -41,6 +43,7 @@ from reportlab.lib import styles from reportlab.lib.colors import Color from flask import g +from app.models.formsemestre import FormSemestre import app.scodoc.sco_utils as scu from app.scodoc import sco_bulletins_pdf @@ -125,6 +128,7 @@ def page_footer(canvas, doc, logo, preferences, with_page_numbers=True): def page_header(canvas, doc, logo, preferences, only_on_first_page=False): + "Ajoute au canvas le frame avec le logo" if only_on_first_page and int(doc.page) > 1: return height = doc.pagesize[1] @@ -147,12 +151,12 @@ def page_header(canvas, doc, logo, preferences, only_on_first_page=False): class CourrierIndividuelTemplate(PageTemplate): - """Template pour courrier avisant des decisions de jury (1 page /etudiant)""" + """Template pour courrier avisant des decisions de jury (1 page par étudiant)""" def __init__( self, document, - pagesbookmarks={}, + pagesbookmarks=None, author=None, title=None, subject=None, @@ -163,7 +167,7 @@ class CourrierIndividuelTemplate(PageTemplate): template_name="CourrierJuryTemplate", ): """Initialise our page template.""" - self.pagesbookmarks = pagesbookmarks + self.pagesbookmarks = pagesbookmarks or {} self.pdfmeta_author = author self.pdfmeta_title = title self.pdfmeta_subject = subject @@ -237,32 +241,32 @@ class CourrierIndividuelTemplate(PageTemplate): width=LOGO_HEADER_WIDTH, ) - def beforeDrawPage(self, canvas, doc): + def beforeDrawPage(self, canv, doc): """Draws a logo and an contribution message on each page.""" # ---- Add some meta data and bookmarks if self.pdfmeta_author: - canvas.setAuthor(SU(self.pdfmeta_author)) + canv.setAuthor(SU(self.pdfmeta_author)) if self.pdfmeta_title: - canvas.setTitle(SU(self.pdfmeta_title)) + canv.setTitle(SU(self.pdfmeta_title)) if self.pdfmeta_subject: - canvas.setSubject(SU(self.pdfmeta_subject)) + canv.setSubject(SU(self.pdfmeta_subject)) bm = self.pagesbookmarks.get(doc.page, None) if bm != None: key = bm txt = SU(bm) - canvas.bookmarkPage(key) - canvas.addOutlineEntry(txt, bm) + canv.bookmarkPage(key) + canv.addOutlineEntry(txt, bm) # ---- Background image if self.background_image_filename and self.with_page_background: - canvas.drawImage( + canv.drawImage( self.background_image_filename, 0, 0, doc.pagesize[0], doc.pagesize[1] ) # ---- Header/Footer if self.with_header: page_header( - canvas, + canv, doc, self.logo_header, self.preferences, @@ -270,7 +274,7 @@ class CourrierIndividuelTemplate(PageTemplate): ) if self.with_footer: page_footer( - canvas, + canv, doc, self.logo_footer, self.preferences, @@ -332,6 +336,39 @@ class PVTemplate(CourrierIndividuelTemplate): # self.__pageNum += 1 +def _simulate_br(paragraph_txt: str, para="") -> str: + """Reportlab bug turnaround (could be removed in a future version). + p is a string with Reportlab intra-paragraph XML tags. + Replaces
(currently ignored by Reportlab) by
+ """ + return ("" + para).join(re.split(r"<.*?br.*?/>", paragraph_txt)) + + +def _make_signature_image(signature, leftindent, formsemestre_id) -> Table: + "crée un paragraphe avec l'image signature" + # cree une image PIL pour avoir la taille (W,H) + + f = io.BytesIO(signature) + img = PILImage.open(f) + width, height = img.size + pdfheight = ( + 1.0 + * sco_preferences.get_preference("pv_sig_image_height", formsemestre_id) + * mm + ) + f.seek(0, 0) + + style = styles.ParagraphStyle({}) + style.leading = 1.0 * sco_preferences.get_preference( + "SCOLAR_FONT_SIZE", formsemestre_id + ) # vertical space + style.leftIndent = leftindent + return Table( + [("", Image(f, width=width * pdfheight / float(height), height=pdfheight))], + colWidths=(9 * cm, 7 * cm), + ) + + def pdf_lettres_individuelles( formsemestre_id, etudids=None, @@ -394,8 +431,8 @@ def pdf_lettres_individuelles( document.addPageTemplates( CourrierIndividuelTemplate( document, - author="%s %s (E. Viennet)" % (sco_version.SCONAME, sco_version.SCOVERSION), - title="Lettres décision %s" % sem["titreannee"], + author=f"{sco_version.SCONAME} {sco_version.SCOVERSION} (E. Viennet)", + title=f"Lettres décision {sem['titreannee']}", subject="Décision jury", margins=margins, pagesbookmarks=bookmarks, @@ -410,10 +447,7 @@ def pdf_lettres_individuelles( def _descr_jury(sem, diplome): if not diplome: - t = "passage de Semestre %d en Semestre %d" % ( - sem["semestre_id"], - sem["semestre_id"] + 1, - ) + t = f"""passage de Semestre {sem["semestre_id"]} en Semestre {sem["semestre_id"] + 1}""" s = "passage de semestre" else: t = "délivrance du diplôme" @@ -428,6 +462,7 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None): """ # formsemestre_id = sem["formsemestre_id"] + formsemestre = FormSemestre.query.get(formsemestre_id) Se: SituationEtudCursus = decision["Se"] t, s = _descr_jury(sem, Se.parcours_validated() or not Se.semestre_non_terminal) objects = [] @@ -437,7 +472,7 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None): style.leading = 18 style.alignment = TA_JUSTIFY - params["semestre_id"] = sem["semestre_id"] + params["semestre_id"] = formsemestre.semestre_id params["decision_sem_descr"] = decision["decision_sem_descr"] params["type_jury"] = t # type de jury (passage ou delivrance) params["type_jury_abbrv"] = s # idem, abbrégé @@ -450,28 +485,25 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None): params["INSTITUTION_CITY"] = ( sco_preferences.get_preference("INSTITUTION_CITY", formsemestre_id) or "" ) + if decision["prev_decision_sem"]: params["prev_semestre_id"] = decision["prev"]["semestre_id"] - params["prev_code_descr"] = decision["prev_code_descr"] + + params["prev_decision_sem_txt"] = "" + params["decision_orig"] = "" + + if formsemestre.formation.is_apc(): + # ajout champs spécifiques PV BUT + add_apc_infos(formsemestre, params, decision) + else: + # ajout champs spécifiques PV DUT + add_classic_infos(formsemestre, params, decision) params.update(decision["identite"]) # fix domicile if params["domicile"]: params["domicile"] = params["domicile"].replace("\\n", "
") - # Décision semestre courant: - if sem["semestre_id"] >= 0: - params["decision_orig"] = "du semestre S%s" % sem["semestre_id"] - else: - params["decision_orig"] = "" - - if decision["prev_decision_sem"]: - params["prev_decision_sem_txt"] = ( - """Décision du semestre antérieur S%(prev_semestre_id)s : %(prev_code_descr)s""" - % params - ) - else: - params["prev_decision_sem_txt"] = "" # UE capitalisées: if decision["decisions_ue"] and decision["decisions_ue_descr"]: params["decision_ue_txt"] = ( @@ -567,39 +599,23 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None): return objects -def _simulate_br(p, para=""): - """Reportlab bug turnaround (could be removed in a future version). - p is a string with Reportlab intra-paragraph XML tags. - Replaces
(currently ignored by Reportlab) by
- """ - l = re.split(r"<.*?br.*?/>", p) - return ("" + para).join(l) +def add_classic_infos(formsemestre: FormSemestre, params: dict, decision: dict): + """Ajoute les champs pour les formations classiques, donc avec codes semestres""" + if decision["prev_decision_sem"]: + params["prev_code_descr"] = decision["prev_code_descr"] + params[ + "prev_decision_sem_txt" + ] = f"""Décision du semestre antérieur S{params['prev_semestre_id']} : {params['prev_code_descr']}""" + # Décision semestre courant: + if formsemestre.semestre_id >= 0: + params["decision_orig"] = f"du semestre S{formsemestre.semestre_id}" + else: + params["decision_orig"] = "" -def _make_signature_image(signature, leftindent, formsemestre_id): - "cree un paragraphe avec l'image signature" - # cree une image PIL pour avoir la taille (W,H) - from PIL import Image as PILImage - - f = io.BytesIO(signature) - im = PILImage.open(f) - width, height = im.size - pdfheight = ( - 1.0 - * sco_preferences.get_preference("pv_sig_image_height", formsemestre_id) - * mm - ) - f.seek(0, 0) - - style = styles.ParagraphStyle({}) - style.leading = 1.0 * sco_preferences.get_preference( - "SCOLAR_FONT_SIZE", formsemestre_id - ) # vertical space - style.leftIndent = leftindent - return Table( - [("", Image(f, width=width * pdfheight / float(height), height=pdfheight))], - colWidths=(9 * cm, 7 * cm), - ) +def add_apc_infos(formsemestre: FormSemestre, params: dict, decision: dict): + """Ajoute les champs pour les formations APC (BUT), donc avec codes RCUE et année""" + pass # TODO XXX # ---------------------------------------------- diff --git a/app/views/notes.py b/app/views/notes.py index c1715c92..4485c1d5 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -296,7 +296,9 @@ def formsemestre_bulletinetud( format = format or "html" if not isinstance(formsemestre_id, int): - raise ScoInvalidIdType("formsemestre_id must be an integer !") + raise ScoInvalidIdType( + "formsemestre_bulletinetud: formsemestre_id must be an integer !" + ) formsemestre = FormSemestre.query.get_or_404(formsemestre_id) if etudid: etud = models.Identite.query.get_or_404(etudid) @@ -826,7 +828,9 @@ def XMLgetFormsemestres(etape_apo=None, formsemestre_id=None): if not formsemestre_id: return flask.abort(404, "argument manquant: formsemestre_id") if not isinstance(formsemestre_id, int): - return flask.abort(404, "formsemestre_id must be an integer !") + return flask.abort( + 404, "XMLgetFormsemestres: formsemestre_id must be an integer !" + ) args = {} if etape_apo: args["etape_apo"] = etape_apo From 8e463e5971228c3235f89eefdcb3505dac716010 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 8 Jul 2022 06:32:43 +0200 Subject: [PATCH 03/18] Missing import in sco_excel --- app/scodoc/sco_excel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 2c95c7ef..1ea4cee8 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -45,6 +45,7 @@ from openpyxl.worksheet.worksheet import Worksheet import app.scodoc.sco_utils as scu from app import log from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc import notesdb, sco_preferences class COLORS(Enum): @@ -793,7 +794,7 @@ def excel_feuille_listeappel( # ligne 3 cell_2 = ws.make_cell("Enseignant :", style2) - cell_6 = ws.make_cell(("Groupe %s" % groupname), style3) + cell_6 = ws.make_cell(f"Groupe {groupname}", style3) ws.append_row([None, cell_2, None, None, None, None, cell_6]) # ligne 4: Avertissement pour ne pas confondre avec listes notes From bf27fcbdc64c36743512637b9d7b02ce2bec2fbb Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 8 Jul 2022 18:09:45 +0200 Subject: [PATCH 04/18] =?UTF-8?q?Bulletins=20et=20PV:=20champs=20d=C3=A9ci?= =?UTF-8?q?sions=20jury=20BUT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/bulletin_but.py | 5 +--- app/models/but_validations.py | 6 ++-- app/scodoc/sco_bulletins.py | 48 +++++++++++++++++++++----------- app/scodoc/sco_bulletins_json.py | 28 +++++++++++-------- app/scodoc/sco_bulletins_pdf.py | 5 +++- app/scodoc/sco_pdf.py | 10 ++++--- app/scodoc/sco_pvjury.py | 35 ++++++++++++++--------- app/scodoc/sco_utils.py | 9 ++++++ app/views/notes.py | 3 +- sco_version.py | 2 +- 10 files changed, 97 insertions(+), 54 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 3b90187b..dcbcd417 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -326,10 +326,7 @@ class BulletinBUT: semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot} if sco_preferences.get_preference("bul_show_decision", formsemestre.id): semestre_infos.update( - sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id) - ) - semestre_infos.update( - but_validations.dict_decision_jury(etud, formsemestre) + sco_bulletins_json.dict_decision_jury(etud, formsemestre) ) if etat_inscription == scu.INSCRIT: # moyenne des moyennes générales du semestre diff --git a/app/models/but_validations.py b/app/models/but_validations.py index 9d42c14d..71b5f883 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -67,7 +67,7 @@ class ApcValidationRCUE(db.Model): return self.ue2.niveau_competence def to_dict_bul(self) -> dict: - "Export dict pour bulletins" + "Export dict pour bulletins: le code et le niveau de compétence" return {"code": self.code, "niveau": self.niveau().to_dict_bul()} @@ -309,7 +309,9 @@ class ApcValidationAnnee(db.Model): def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict: """ - Un dict avec les décisions de jury BUT enregistrées. + Un dict avec les décisions de jury BUT enregistrées: + - decision_rcue : list[dict] + - decision_annee : dict Ne reprend pas les décisions d'UE, non spécifiques au BUT. """ decisions = {} diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 4a82674e..f737c4ee 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -434,7 +434,7 @@ def _get_etud_etat_html(etat: str) -> str: elif etat == scu.DEF: # "DEF" return ' (DEFAILLANT) ' else: - return ' (%s) ' % etat + return f' ({etat}) ' def _sort_mod_by_matiere(modlist, nt, etudid): @@ -707,6 +707,7 @@ def etud_descr_situation_semestre( descr_decisions_ue : ' UE acquises: UE1, UE2', ou vide si pas de dec. ou si pas show_uevalid descr_mention : 'Mention Bien', ou vide si pas de mention ou si pas show_mention """ + # Fonction utilisée par tous les bulletins (APC ou classiques) cnx = ndb.GetDBConnexion() infos = scu.DictDefault(defaultvalue="") @@ -728,8 +729,7 @@ def etud_descr_situation_semestre( # il y a eu une erreur qui a laissé un event 'inscription' # on l'efface: log( - "etud_descr_situation_semestre: removing duplicate INSCRIPTION event for etudid=%s !" - % etudid + f"etud_descr_situation_semestre: removing duplicate INSCRIPTION event for etudid={etudid} !" ) sco_etud.scolar_events_delete(cnx, event["event_id"]) else: @@ -738,8 +738,7 @@ def etud_descr_situation_semestre( # assert date_dem == None, 'plusieurs démissions !' if date_dem: # cela ne peut pas arriver sauf bug (signale a Evry 2013?) log( - "etud_descr_situation_semestre: removing duplicate DEMISSION event for etudid=%s !" - % etudid + f"etud_descr_situation_semestre: removing duplicate DEMISSION event for etudid={etudid} !" ) sco_etud.scolar_events_delete(cnx, event["event_id"]) else: @@ -747,8 +746,7 @@ def etud_descr_situation_semestre( elif event_type == "DEFAILLANCE": if date_def: log( - "etud_descr_situation_semestre: removing duplicate DEFAILLANCE event for etudid=%s !" - % etudid + f"etud_descr_situation_semestre: removing duplicate DEFAILLANCE event for etudid={etudid} !" ) sco_etud.scolar_events_delete(cnx, event["event_id"]) else: @@ -756,10 +754,10 @@ def etud_descr_situation_semestre( if show_date_inscr: if not date_inscr: infos["date_inscription"] = "" - infos["descr_inscription"] = "Pas inscrit%s." % ne + infos["descr_inscription"] = f"Pas inscrit{ne}." else: infos["date_inscription"] = date_inscr - infos["descr_inscription"] = "Inscrit%s le %s." % (ne, date_inscr) + infos["descr_inscription"] = f"Inscrit{ne} le {date_inscr}." else: infos["date_inscription"] = "" infos["descr_inscription"] = "" @@ -767,15 +765,15 @@ def etud_descr_situation_semestre( infos["situation"] = infos["descr_inscription"] if date_dem: - infos["descr_demission"] = "Démission le %s." % date_dem + infos["descr_demission"] = f"Démission le {date_dem}." infos["date_demission"] = date_dem infos["descr_decision_jury"] = "Démission" infos["situation"] += " " + infos["descr_demission"] return infos, None # ne donne pas les dec. de jury pour les demissionnaires if date_def: - infos["descr_defaillance"] = "Défaillant%s" % ne + infos["descr_defaillance"] = f"Défaillant{ne}" infos["date_defaillance"] = date_def - infos["descr_decision_jury"] = "Défaillant%s" % ne + infos["descr_decision_jury"] = f"Défaillant{ne}" infos["situation"] += " " + infos["descr_defaillance"] dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=[etudid]) @@ -798,6 +796,7 @@ def etud_descr_situation_semestre( dec = infos["descr_decision_jury"] else: infos["descr_decision_jury"] = "" + infos["decision_jury"] = "" if pv["decisions_ue_descr"] and show_uevalid: infos["decisions_ue"] = pv["decisions_ue_descr"] @@ -809,14 +808,31 @@ def etud_descr_situation_semestre( infos["mention"] = pv["mention"] if pv["mention"] and show_mention: - dec += "Mention " + pv["mention"] + ". " + dec += f"Mention {pv['mention']}." + + # Décisions APC / BUT + if pv.get("decision_annee", {}): + infos["descr_decision_annee"] = "Décision année: " + pv.get( + "decision_annee", {} + ).get("code", "") + else: + infos["descr_decision_annee"] = "" + if pv.get("decision_rcue", []): + infos["descr_decisions_rcue"] = "Niveaux de compétences: " + ", ".join( + [ + f"""{dec_rcue["niveau"]["competence"]["titre"]} {dec_rcue["niveau"]["ordre"]}: {dec_rcue["code"]}""" + for dec_rcue in pv.get("decision_rcue", []) + ] + ) + else: + infos["descr_decisions_rcue"] = "" infos["situation"] += " " + dec if not pv["validation_parcours"]: # parcours non terminé if pv["autorisations_descr"]: - infos["situation"] += ( - " Autorisé à s'inscrire en %s." % pv["autorisations_descr"] - ) + infos[ + "situation" + ] += f" Autorisé à s'inscrire en {pv['autorisations_descr']}." else: infos["situation"] += " Diplôme obtenu." return infos, dpv diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index c48c0d43..3d9a6592 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -25,7 +25,7 @@ # ############################################################################## -"""Génération du bulletin en format JSON (beta, non completement testé) +"""Génération du bulletin en format JSON """ import datetime @@ -33,8 +33,9 @@ import json from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models.formsemestre import FormSemestre +from app.models import but_validations from app.models.etudiants import Identite +from app.models.formsemestre import FormSemestre import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb @@ -139,7 +140,7 @@ def formsemestre_bulletinetud_published_dict( etat_inscription = etud.inscription_etat(formsemestre.id) if etat_inscription != scu.INSCRIT: - d.update(dict_decision_jury(etudid, formsemestre_id, with_decisions=True)) + d.update(dict_decision_jury(etud, formsemestre, with_decisions=True)) return d # Groupes: @@ -343,9 +344,7 @@ def formsemestre_bulletinetud_published_dict( d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust) # --- Decision Jury - d.update( - dict_decision_jury(etudid, formsemestre_id, with_decisions=xml_with_decisions) - ) + d.update(dict_decision_jury(etud, formsemestre, with_decisions=xml_with_decisions)) # --- Appreciations cnx = ndb.GetDBConnexion() apprecs = sco_etud.appreciations_list( @@ -364,7 +363,9 @@ def formsemestre_bulletinetud_published_dict( return d -def dict_decision_jury(etudid, formsemestre_id, with_decisions=False) -> dict: +def dict_decision_jury( + etud: Identite, formsemestre: FormSemestre, with_decisions: bool = False +) -> dict: """dict avec decision pour bulletins json - decision : décision semestre - decision_ue : list des décisions UE @@ -372,6 +373,8 @@ def dict_decision_jury(etudid, formsemestre_id, with_decisions=False) -> dict: with_decision donne les décision même si bul_show_decision est faux. + Si formation APC, indique aussi validations année et RCUEs + Exemple: { 'autorisation_inscription': [{'semestre_id': 4}], @@ -397,14 +400,14 @@ def dict_decision_jury(etudid, formsemestre_id, with_decisions=False) -> dict: d = {} if ( - sco_preferences.get_preference("bul_show_decision", formsemestre_id) + sco_preferences.get_preference("bul_show_decision", formsemestre.id) or with_decisions ): infos, dpv = sco_bulletins.etud_descr_situation_semestre( - etudid, - formsemestre_id, + etud.id, + formsemestre.id, show_uevalid=sco_preferences.get_preference( - "bul_show_uevalid", formsemestre_id + "bul_show_uevalid", formsemestre.id ), ) d["situation"] = infos["situation"] @@ -456,4 +459,7 @@ def dict_decision_jury(etudid, formsemestre_id, with_decisions=False) -> dict: ) else: d["decision"] = dict(code="", etat="DEM") + # Ajout jury BUT: + if formsemestre.formation.is_apc(): + d.update(but_validations.dict_decision_jury(etud, formsemestre)) return d diff --git a/app/scodoc/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py index 501bd98c..3d91beb7 100644 --- a/app/scodoc/sco_bulletins_pdf.py +++ b/app/scodoc/sco_bulletins_pdf.py @@ -140,12 +140,15 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"): text = (field or "") % scu.WrapDict( cdict ) # note that None values are mapped to empty strings - except: + except: # pylint: disable=bare-except log( f"""process_field: invalid format. field={field!r} values={pprint.pformat(cdict)} """ ) + # ne sera pas visible si lien vers pdf: + scu.flash_once(f"Attention: format PDF invalide (champs {field}") + raise ValueError text = ( "format invalide !" + traceback.format_exc() diff --git a/app/scodoc/sco_pdf.py b/app/scodoc/sco_pdf.py index 2f468ccf..3a7d7a55 100755 --- a/app/scodoc/sco_pdf.py +++ b/app/scodoc/sco_pdf.py @@ -55,6 +55,7 @@ from reportlab.lib import styles from flask import g +from app.scodoc import sco_utils as scu from app.scodoc.sco_utils import CONFIG from app import log from app.scodoc.sco_exceptions import ScoGenError, ScoValueError @@ -67,7 +68,7 @@ PAGE_WIDTH = defaultPageSize[0] DEFAULT_PDF_FOOTER_TEMPLATE = CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE -def SU(s): +def SU(s: str) -> str: "convert s from string to string suitable for ReportLab" if not s: return "" @@ -145,9 +146,9 @@ def makeParas(txt, style, suppress_empty=False): ) from e else: raise e - except Exception as e: + except Exception as exc: log(traceback.format_exc()) - log("Invalid pdf para format: %s" % txt) + log(f"Invalid pdf para format: {txt}") try: result = [ Paragraph( @@ -155,13 +156,14 @@ def makeParas(txt, style, suppress_empty=False): style, ) ] - except ValueError as e: # probleme font ? essaye sans style + except ValueError as exc2: # probleme font ? essaye sans style # recupere font en cause ? m = re.match(r".*family/bold/italic for (.*)", e.args[0], re.DOTALL) if m: message = f"police non disponible: {m[1]}" else: message = "format invalide" + scu.flash_once(f"problème génération PDF: {message}") return [ Paragraph( SU(f'Erreur: {message}'), diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py index 9975e7c6..019dbfbf 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pvjury.py @@ -57,7 +57,13 @@ from flask import g, request from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre, UniteEns, ScolarAutorisationInscription +from app.models import ( + FormSemestre, + UniteEns, + ScolarAutorisationInscription, + but_validations, +) +from app.models.etudiants import Identite import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb @@ -81,8 +87,8 @@ from app.scodoc.sco_pdf import PDFLOCK from app.scodoc.TrivialFormulator import TrivialFormulator -def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem): - """Liste des UE validées dans ce semestre""" +def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem) -> list[dict]: + """Liste des UE validées dans ce semestre (incluant les UE capitalisées)""" if not decisions_ue: return [] uelist = [] @@ -93,14 +99,17 @@ def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem): decisions_ue[ue_id]["code"] == sco_codes_parcours.ADM or ( # XXX ceci devrait dépendre du parcours et non pas être une option ! #sco8 - scu.CONFIG.CAPITALIZE_ALL_UES + decision_sem + and scu.CONFIG.CAPITALIZE_ALL_UES and sco_codes_parcours.code_semestre_validant(decision_sem["code"]) ) ): ue = sco_edit_ue.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)) + log( + f"Exception in descr_decisions_ues: ue_id={ue_id} decisions_ue={decisions_ue}" + ) # Les UE capitalisées dans d'autres semestres: if etudid in nt.validations.ue_capitalisees.index: for ue_id in nt.validations.ue_capitalisees.loc[[etudid]]["ue_id"]: @@ -230,8 +239,11 @@ def dict_pvjury( L = [] D = {} # même chose que L, mais { etudid : dec } for etudid in etudids: - etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id) + # etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] + etud: Identite = Identite.query.get(etudid) + Se = sco_cursus.get_situation_etud_cursus( + etud.to_dict_scodoc7(), formsemestre_id + ) semestre_non_terminal = semestre_non_terminal or Se.semestre_non_terminal d = {} d["identite"] = nt.identdict[etudid] @@ -240,6 +252,8 @@ def dict_pvjury( ) # 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) + if formsemestre.formation.is_apc(): + d.update(but_validations.dict_decision_jury(etud, formsemestre)) d["last_formsemestre_id"] = Se.get_semestres()[ -1 ] # id du dernier semestre (chronologiquement) dans lequel il a été inscrit @@ -305,12 +319,7 @@ def dict_pvjury( d["decision_sem"]["compense_formsemestre_id"] ) obs.append( - "%s compense %s (%s)" - % ( - sem["sem_id_txt"], - compensed["sem_id_txt"], - compensed["anneescolaire"], - ) + f"""{sem["sem_id_txt"]} compense {compensed["sem_id_txt"]} ({compensed["anneescolaire"]})""" ) d["observation"] = ", ".join(obs) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 31de4bb2..2087a141 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -664,6 +664,15 @@ def flash_errors(form): # see https://getbootstrap.com/docs/4.0/components/alerts/ +def flash_once(message: str): + """Flash the message, but only once per request""" + if not hasattr(g, "sco_flashed_once"): + g.sco_flashed_once = set() + if not message in g.sco_flashed_once: + flash(message) + g.sco_flashed_once.add(message) + + def sendCSVFile(data, filename): # DEPRECATED utiliser send_file """publication fichier CSV.""" return send_file(data, filename=filename, mime=CSV_MIMETYPE, attached=True) diff --git a/app/views/notes.py b/app/views/notes.py index 4485c1d5..7bb9b027 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2552,9 +2552,8 @@ def formsemestre_validation_suppress_etud( ) if not dialog_confirmed: d = sco_bulletins_json.dict_decision_jury( - etudid, formsemestre_id, with_decisions=True + etud, formsemestre, with_decisions=True ) - d.update(but_validations.dict_decision_jury(etud, formsemestre)) descr_ues = [f"{u['acronyme']}: {u['code']}" for u in d.get("decision_ue", [])] dec_annee = d.get("decision_annee") diff --git a/sco_version.py b/sco_version.py index 256794b7..e635062a 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.3.16" +SCOVERSION = "9.3.17" SCONAME = "ScoDoc" From 5bc4b47e1e5f6743fe53321f3f5cdfb9b06f1aa6 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 8 Jul 2022 23:58:27 +0200 Subject: [PATCH 05/18] =?UTF-8?q?Lettres=20de=20d=C3=A9cisions=20jury=20BU?= =?UTF-8?q?T?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but_recap.py | 11 ++++- app/models/but_validations.py | 11 +++++ app/scodoc/notesdb.py | 4 +- app/scodoc/sco_bulletins.py | 12 ++---- app/scodoc/sco_edit_formation.py | 24 ++++++----- app/scodoc/sco_pvpdf.py | 71 ++++++++++++++++++++------------ 6 files changed, 84 insertions(+), 49 deletions(-) diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index 7d47cbe5..427c2d0f 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -101,10 +101,17 @@ def formsemestre_saisie_jury_but( f"""

Décisions de jury enregistrées pour les étudiants de ce semestre

""" diff --git a/app/models/but_validations.py b/app/models/but_validations.py index 71b5f883..7213af3c 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -322,8 +322,19 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict: etudid=etud.id, formsemestre_id=formsemestre.id ) decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues] + decisions["descr_decisions_rcue"] = ", ".join( + [ + f"""{dec_rcue["niveau"]["competence"]["titre"]} {dec_rcue["niveau"]["ordre"]}: {dec_rcue["code"]}""" + for dec_rcue in decisions["decision_rcue"] + ] + ) + decisions["descr_decisions_niveaux"] = ( + "Niveaux de compétences: " + decisions["descr_decisions_rcue"] + ) else: decisions["decision_rcue"] = [] + decisions["descr_decisions_rcue"] = "" + decisions["descr_decisions_niveaux"] = "" # --- Année: prend la validation pour l'année scolaire de ce semestre validation = ( ApcValidationAnnee.query.filter_by( diff --git a/app/scodoc/notesdb.py b/app/scodoc/notesdb.py index 3de5c281..7ad7896a 100644 --- a/app/scodoc/notesdb.py +++ b/app/scodoc/notesdb.py @@ -265,9 +265,9 @@ def DBUpdateArgs(cnx, table, vals, where=None, commit=False, convert_empty_to_nu cursor.execute(req, vals) # log('req=%s\n'%req) # log('vals=%s\n'%vals) - except psycopg2.errors.StringDataRightTruncation: + except psycopg2.errors.StringDataRightTruncation as exc: cnx.rollback() - raise ScoValueError("champs de texte trop long !") + raise ScoValueError("champs de texte trop long !") from exc except: cnx.rollback() # get rid of this transaction log('Exception in DBUpdateArgs:\n\treq="%s"\n\tvals="%s"\n' % (req, vals)) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index f737c4ee..83a51293 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -817,15 +817,9 @@ def etud_descr_situation_semestre( ).get("code", "") else: infos["descr_decision_annee"] = "" - if pv.get("decision_rcue", []): - infos["descr_decisions_rcue"] = "Niveaux de compétences: " + ", ".join( - [ - f"""{dec_rcue["niveau"]["competence"]["titre"]} {dec_rcue["niveau"]["ordre"]}: {dec_rcue["code"]}""" - for dec_rcue in pv.get("decision_rcue", []) - ] - ) - else: - infos["descr_decisions_rcue"] = "" + + infos["descr_decisions_rcue"] = pv.get("descr_decisions_rcue", "") + infos["descr_decisions_niveaux"] = pv.get("descr_decisions_niveaux", "") infos["situation"] += " " + dec if not pv["validation_parcours"]: # parcours non terminé diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py index 62fc2bbf..d7ea5e97 100644 --- a/app/scodoc/sco_edit_formation.py +++ b/app/scodoc/sco_edit_formation.py @@ -292,21 +292,25 @@ def do_formation_create(args): def do_formation_edit(args): "edit a formation" - # log('do_formation_edit( args=%s )'%args) - # On autorise la modif de la formation meme si elle est verrouillee - # car cela ne change que du cosmetique, (sauf eventuellement le code formation ?) - # mais si verrouillée on ne peut changer le type de parcours - if sco_formations.formation_has_locked_sems(args["formation_id"]): - if "type_parcours" in args: - del args["type_parcours"] # On ne peut jamais supprimer le code formation: if "formation_code" in args and not args["formation_code"]: del args["formation_code"] - cnx = ndb.GetDBConnexion() - sco_formations._formationEditor.edit(cnx, args) - formation: Formation = Formation.query.get(args["formation_id"]) + formation: Formation = Formation.query.get_or_404(args["formation_id"]) + # On autorise la modif de la formation meme si elle est verrouillee + # car cela ne change que du cosmetique, (sauf eventuellement le code formation ?) + # mais si verrouillée on ne peut changer le type de parcours + if formation.has_locked_sems(): + if "type_parcours" in args: + del args["type_parcours"] + + for field in formation.__dict__: + if field and field[0] != "_" and field in args: + setattr(formation, field, args[field]) + + db.session.add(formation) + db.session.commit() formation.invalidate_cached_sems() diff --git a/app/scodoc/sco_pvpdf.py b/app/scodoc/sco_pvpdf.py index dba7801b..ebfaefcf 100644 --- a/app/scodoc/sco_pvpdf.py +++ b/app/scodoc/sco_pvpdf.py @@ -43,13 +43,12 @@ from reportlab.lib import styles from reportlab.lib.colors import Color from flask import g -from app.models.formsemestre import FormSemestre +from app.models import FormSemestre, Identite import app.scodoc.sco_utils as scu from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_codes_parcours from app.scodoc import sco_etud -from app.scodoc import sco_formsemestre from app.scodoc import sco_pdf from app.scodoc import sco_preferences from app.scodoc.sco_logos import find_logo @@ -389,7 +388,7 @@ def pdf_lettres_individuelles( etuds = [x["identite"] for x in dpv["decisions"]] sco_etud.fill_etuds_info(etuds) # - sem = sco_formsemestre.get_formsemestre(formsemestre_id) + formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) prefs = sco_preferences.SemPreferences(formsemestre_id) params = { "date_jury": date_jury, @@ -400,18 +399,22 @@ def pdf_lettres_individuelles( } # copie preferences for name in sco_preferences.get_base_preferences().prefs_name: - params[name] = sco_preferences.get_preference(name, sem["formsemestre_id"]) + params[name] = sco_preferences.get_preference(name, formsemestre_id) bookmarks = {} objects = [] # list of PLATYPUS objects npages = 0 - for e in dpv["decisions"]: - if e["decision_sem"]: # decision prise - etud = sco_etud.get_etud_info(e["identite"]["etudid"], filled=True)[0] - params["nomEtud"] = etud["nomprenom"] - bookmarks[npages + 1] = scu.suppress_accents(etud["nomprenom"]) + for decision in dpv["decisions"]: + if ( + decision["decision_sem"] + or decision.get("decision_annee") + or decision.get("decision_rcue") + ): # decision prise + etud: Identite = Identite.query.get(decision["identite"]["etudid"]) + params["nomEtud"] = etud.nomprenom + bookmarks[npages + 1] = scu.suppress_accents(etud.nomprenom) objects += pdf_lettre_individuelle( - dpv["formsemestre"], e, etud, params, signature + dpv["formsemestre"], decision, etud, params, signature ) objects.append(PageBreak()) npages += 1 @@ -432,7 +435,7 @@ def pdf_lettres_individuelles( CourrierIndividuelTemplate( document, author=f"{sco_version.SCONAME} {sco_version.SCOVERSION} (E. Viennet)", - title=f"Lettres décision {sem['titreannee']}", + title=f"Lettres décision {formsemestre.titre_annee()}", subject="Décision jury", margins=margins, pagesbookmarks=bookmarks, @@ -445,17 +448,22 @@ def pdf_lettres_individuelles( return data -def _descr_jury(sem, diplome): +def _descr_jury(formsemestre: FormSemestre, diplome): + if not diplome: - t = f"""passage de Semestre {sem["semestre_id"]} en Semestre {sem["semestre_id"] + 1}""" - s = "passage de semestre" + if formsemestre.formation.is_apc(): + t = f"""BUT{(formsemestre.semestre_id+1)//2}""" + s = t + else: + t = f"""passage de Semestre {formsemestre.semestre_id} en Semestre {formsemestre.semestre_id + 1}""" + s = "passage de semestre" else: t = "délivrance du diplôme" s = t return t, s # titre long, titre court -def pdf_lettre_individuelle(sem, decision, etud, params, signature=None): +def pdf_lettre_individuelle(sem, decision, etud: Identite, params, signature=None): """ Renvoie une liste d'objets PLATYPUS pour intégration dans un autre document. @@ -464,7 +472,9 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None): formsemestre_id = sem["formsemestre_id"] formsemestre = FormSemestre.query.get(formsemestre_id) Se: SituationEtudCursus = decision["Se"] - t, s = _descr_jury(sem, Se.parcours_validated() or not Se.semestre_non_terminal) + t, s = _descr_jury( + formsemestre, Se.parcours_validated() or not Se.semestre_non_terminal + ) objects = [] style = reportlab.lib.styles.ParagraphStyle({}) style.fontSize = 14 @@ -492,13 +502,6 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None): params["prev_decision_sem_txt"] = "" params["decision_orig"] = "" - if formsemestre.formation.is_apc(): - # ajout champs spécifiques PV BUT - add_apc_infos(formsemestre, params, decision) - else: - # ajout champs spécifiques PV DUT - add_classic_infos(formsemestre, params, decision) - params.update(decision["identite"]) # fix domicile if params["domicile"]: @@ -530,7 +533,7 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None): params[ "autorisations_txt" ] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : %s""" % ( - etud["ne"], + etud.e, s, s, decision["autorisations_descr"], @@ -545,6 +548,14 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None): else: params["diplome_txt"] = "" + # Les fonctions ci-dessous ajoutent ou modifient des champs: + if formsemestre.formation.is_apc(): + # ajout champs spécifiques PV BUT + add_apc_infos(formsemestre, params, decision) + else: + # ajout champs spécifiques PV DUT + add_classic_infos(formsemestre, params, decision) + # Corps de la lettre: objects += sco_bulletins_pdf.process_field( sco_preferences.get_preference("PV_LETTER_TEMPLATE", sem["formsemestre_id"]), @@ -615,7 +626,14 @@ def add_classic_infos(formsemestre: FormSemestre, params: dict, decision: dict): def add_apc_infos(formsemestre: FormSemestre, params: dict, decision: dict): """Ajoute les champs pour les formations APC (BUT), donc avec codes RCUE et année""" - pass # TODO XXX + annee_but = (formsemestre.semestre_id + 1) // 2 + params["decision_orig"] = f"année BUT{annee_but}" + params["decision_sem_descr"] = decision.get("decision_annee", {}).get("code", "") + params[ + "decision_ue_txt" + ] = f"""{params["decision_ue_txt"]}
+ Niveaux de compétences:
{decision.get("descr_decisions_rcue", "")} + """ # ---------------------------------------------- @@ -715,7 +733,8 @@ def _pvjury_pdf_type( sem = dpv["formsemestre"] formsemestre_id = sem["formsemestre_id"] - titre_jury, _ = _descr_jury(sem, diplome) + formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) + titre_jury, _ = _descr_jury(formsemestre, diplome) titre_diplome = pv_title or dpv["formation"]["titre_officiel"] objects = [] From 1148bf59eb40781c82af6406dda1668d45909aa8 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 9 Jul 2022 12:55:23 +0200 Subject: [PATCH 06/18] =?UTF-8?q?Am=C3=A9liore=20=C3=A9dition=20programme?= =?UTF-8?q?=20BUT:=20choix=20niveaux=20comp=C3=A9tence=20/=20UE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/apc_edit_ue.py | 8 ++++++-- app/models/but_refcomp.py | 2 +- app/scodoc/sco_edit_apc.py | 2 ++ app/scodoc/sco_edit_ue.py | 3 ++- app/static/css/scodoc.css | 14 ++++++++++++++ app/static/js/edit_ue.js | 22 ++++++++++++---------- app/templates/pn/form_ues.html | 11 ++++++----- sco_version.py | 2 +- 8 files changed, 44 insertions(+), 20 deletions(-) diff --git a/app/but/apc_edit_ue.py b/app/but/apc_edit_ue.py index dd8e60a8..3e8be20e 100644 --- a/app/but/apc_edit_ue.py +++ b/app/but/apc_edit_ue.py @@ -70,7 +70,9 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
Niveau de compétence associé: - +

Le calcul prend quelques minutes, soyez patients !

- """ - % formsemestre_id, + """, html_sco_header.sco_footer(), ] return "\n".join(H) @@ -906,53 +904,56 @@ def do_formsemestre_validation_auto(formsemestre_id): etudids = nt.get_etudids() nb_valid = 0 conflicts = [] # liste des etudiants avec decision differente déjà saisie - for etudid in etudids: - etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id) - ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( - {"etudid": etudid, "formsemestre_id": formsemestre_id} - )[0] + with sco_cache.DeferredSemCacheManager(): + for etudid in etudids: + etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] + Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id) + ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( + {"etudid": etudid, "formsemestre_id": formsemestre_id} + )[0] - # Conditions pour validation automatique: - if ins["etat"] == "I" and ( - ( - (not Se.prev) - or (Se.prev_decision and Se.prev_decision["code"] in (ADM, ADC, ADJ)) - ) - and Se.barre_moy_ok - and Se.barres_ue_ok - and not etud_has_notes_attente(etudid, formsemestre_id) - ): - # check: s'il existe une decision ou autorisation et qu'elles sont differentes, - # warning (et ne fait rien) - decision_sem = nt.get_etud_decision_sem(etudid) - ok = True - if decision_sem and decision_sem["code"] != ADM: - ok = False - conflicts.append(etud) - autorisations = ScolarAutorisationInscription.query.filter_by( - etudid=etudid, origin_formsemestre_id=formsemestre_id - ).all() - if len(autorisations) != 0: - if ( - len(autorisations) > 1 - or autorisations[0].semestre_id != next_semestre_id - ): - if ok: - conflicts.append(etud) - ok = False - - # ok, valide ! - if ok: - formsemestre_validation_etud_manu( - formsemestre_id, - etudid, - code_etat=ADM, - devenir="NEXT", - assidu=True, - redirect=False, + # Conditions pour validation automatique: + if ins["etat"] == "I" and ( + ( + (not Se.prev) + or ( + Se.prev_decision and Se.prev_decision["code"] in (ADM, ADC, ADJ) + ) ) - nb_valid += 1 + and Se.barre_moy_ok + and Se.barres_ue_ok + and not etud_has_notes_attente(etudid, formsemestre_id) + ): + # check: s'il existe une decision ou autorisation et qu'elles sont differentes, + # warning (et ne fait rien) + decision_sem = nt.get_etud_decision_sem(etudid) + ok = True + if decision_sem and decision_sem["code"] != ADM: + ok = False + conflicts.append(etud) + autorisations = ScolarAutorisationInscription.query.filter_by( + etudid=etudid, origin_formsemestre_id=formsemestre_id + ).all() + if len(autorisations) != 0: + if ( + len(autorisations) > 1 + or autorisations[0].semestre_id != next_semestre_id + ): + if ok: + conflicts.append(etud) + ok = False + + # ok, valide ! + if ok: + formsemestre_validation_etud_manu( + formsemestre_id, + etudid, + code_etat=ADM, + devenir="NEXT", + assidu=True, + redirect=False, + ) + nb_valid += 1 log( "do_formsemestre_validation_auto: %d validations, %d conflicts" % (nb_valid, len(conflicts)) From 45d310627212e851980d6c0bee392b6693f96270 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 11 Jul 2022 18:31:58 +0200 Subject: [PATCH 17/18] comments --- app/comp/res_sem.py | 2 +- app/scodoc/sco_recapcomplet.py | 1 + app/views/notes.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/comp/res_sem.py b/app/comp/res_sem.py index e27a157c..0897da01 100644 --- a/app/comp/res_sem.py +++ b/app/comp/res_sem.py @@ -47,7 +47,7 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre: def load_formsemestre_validations(formsemestre: FormSemestre) -> ValidationsSemestre: """Charge les résultats de jury de ce semestre. Search in local cache (g.formsemestre_result_cache) - If not in cache, build it and cache it. + If not in cache, build it and cache it (in g). """ if not hasattr(g, "formsemestre_validation_cache"): g.formsemestre_validations_cache = {} # pylint: disable=C0237 diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index aad0b96b..96378587 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -386,6 +386,7 @@ def gen_formsemestre_recapcomplet_html( table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id) else: table_html = sco_cache.TableRecapCache.get(formsemestre.id) + # en mode jury ne cache pas la table html if mode_jury or (table_html is None): table_html = _gen_formsemestre_recapcomplet_html( formsemestre, diff --git a/app/views/notes.py b/app/views/notes.py index 7bb9b027..fe1774a7 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2664,6 +2664,7 @@ def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None): deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) deca.erase() db.session.commit() + log(f"formsemestre_jury_but_erase({formsemestre_id}, {etudid})") flash("décisions de jury effacées") return redirect(dest_url) From b34484784a90325ea7bb166ab213fb58180cf99e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 11 Jul 2022 18:33:48 +0200 Subject: [PATCH 18/18] Corrections diverses du code bulletins --- app/scodoc/sco_bulletins.py | 99 ++++++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 40 deletions(-) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 83a51293..989c7bd5 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -347,23 +347,23 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): u[ "modules_capitalized" ] = [] # modules de l'UE capitalisée (liste vide si pas capitalisée) - if ue_status["is_capitalized"]: - sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"]) - u["ue_descr_txt"] = "capitalisée le %s" % ndb.DateISOtoDMY( - ue_status["event_date"] - ) + if ue_status["is_capitalized"] and ue_status["formsemestre_id"] is not None: + sem_origin = FormSemestre.query.get(ue_status["formsemestre_id"]) u[ - "ue_descr_html" - ] = f"""{u["ue_descr_txt"]} pouet + "ue_descr_txt" + ] = f'capitalisée le {ndb.DateISOtoDMY(ue_status["event_date"])}' + u["ue_descr_html"] = ( + f"""{u["ue_descr_txt"]} """ - if ue_status["moy"] != "NA" and ue_status["formsemestre_id"]: + if sem_origin + else "" + ) + if ue_status["moy"] != "NA": # détail des modules de l'UE capitalisée - formsemestre_cap = FormSemestre.query.get_or_404( - ue_status["formsemestre_id"] - ) + formsemestre_cap = FormSemestre.query.get(ue_status["formsemestre_id"]) nt_cap: NotesTableCompat = res_sem.load_formsemestre_results( formsemestre_cap ) @@ -712,7 +712,6 @@ def etud_descr_situation_semestre( infos = scu.DictDefault(defaultvalue="") # --- Situation et décisions jury - # démission/inscription ? events = sco_etud.scolar_events_list( cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id} @@ -763,11 +762,17 @@ def etud_descr_situation_semestre( infos["descr_inscription"] = "" infos["situation"] = infos["descr_inscription"] + # Décision: valeurs par defaut vides: + infos["decision_jury"] = infos["descr_decision_jury"] = "" + infos["decision_sem"] = "" + infos["decisions_ue"] = infos["descr_decisions_ue"] = "" + infos["descr_decisions_niveaux"] = infos["descr_decisions_rcue"] = "" + infos["descr_decision_annee"] = "" if date_dem: infos["descr_demission"] = f"Démission le {date_dem}." infos["date_demission"] = date_dem - infos["descr_decision_jury"] = "Démission" + infos["decision_jury"] = infos["descr_decision_jury"] = "Démission" infos["situation"] += " " + infos["descr_demission"] return infos, None # ne donne pas les dec. de jury pour les demissionnaires if date_def: @@ -779,8 +784,6 @@ def etud_descr_situation_semestre( dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=[etudid]) if dpv: infos["decision_sem"] = dpv["decisions"][0]["decision_sem"] - else: - infos["decision_sem"] = "" if not show_decisions: return infos, dpv @@ -803,7 +806,7 @@ def etud_descr_situation_semestre( infos["descr_decisions_ue"] = " UE acquises: " + pv["decisions_ue_descr"] + ". " dec += infos["descr_decisions_ue"] else: - # infos['decisions_ue'] = None + infos["decisions_ue"] = "" infos["descr_decisions_ue"] = "" infos["mention"] = pv["mention"] @@ -842,9 +845,20 @@ def formsemestre_bulletinetud( force_publishing=False, # force publication meme si semestre non publie sur "portail" prefer_mail_perso=False, ): - """Page bulletin de notes - pour les formations classiques hors BUT (page HTML) - ou le format "oldjson". + """Page bulletin de notes pour + - HTML des formations classiques (non BUT) + - le format "oldjson" (les "json" sont générés à part, voir get_formsemestre_bulletin_etud_json) + - les formats PDF, XML et mail pdf (toutes formations) + + Note: le format XML n'est plus maintenu et pour les BUT ne contient pas + toutes les informations. Privilégier le format JSON. + + Paramètres: + - version: pour les formations classqiues, versions short/selectedevals/long + - xml_with_decisions: inclue ou non les + - force_publishing: renvoie le bulletin même si semestre non publie sur "portail" + - prefer_mail_perso: pour pdfmail, utilise adresse mail perso en priorité. + """ format = format or "html" formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) @@ -903,7 +917,12 @@ def do_formsemestre_bulletinetud( prefer_mail_perso=False, # mails envoyés sur adresse perso si non vide ): """Génère le bulletin au format demandé. - Retourne: (bul, filigranne) + Utilisé pour: + - HTML des formations classiques (non BUT) + - le format "oldjson" (les json sont générés à part, voir get_formsemestre_bulletin_etud_json) + - les formats PDF, XML et mail pdf (toutes formations) + + Résultat: (bul, filigranne) où bul est str ou bytes au format demandé (html, pdf, pdfmail, pdfpart, xml, json) et filigranne est un message à placer en "filigranne" (eg "Provisoire"). """ @@ -919,7 +938,7 @@ def do_formsemestre_bulletinetud( return bul, "" - elif format == "json": + elif format == "json": # utilisé pour classic et "oldjson" bul = sco_bulletins_json.make_json_formsemestre_bulletinetud( formsemestre.id, etudid, @@ -932,20 +951,20 @@ def do_formsemestre_bulletinetud( if formsemestre.formation.is_apc(): etudiant = Identite.query.get(etudid) r = bulletin_but.BulletinBUT(formsemestre) - I = r.bulletin_etud_complet(etudiant, version=version) + infos = r.bulletin_etud_complet(etudiant, version=version) else: - I = formsemestre_bulletinetud_dict(formsemestre.id, etudid) - etud = I["etud"] + infos = formsemestre_bulletinetud_dict(formsemestre.id, etudid) + etud = infos["etud"] if format == "html": htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud( - I, version=version, format="html" + infos, version=version, format="html" ) - return htm, I["filigranne"] + return htm, infos["filigranne"] elif format == "pdf" or format == "pdfpart": bul, filename = sco_bulletins_generator.make_formsemestre_bulletinetud( - I, + infos, version=version, format="pdf", stand_alone=(format != "pdfpart"), @@ -953,10 +972,10 @@ def do_formsemestre_bulletinetud( if format == "pdf": return ( scu.sendPDFFile(bul, filename), - I["filigranne"], + infos["filigranne"], ) # unused ret. value else: - return bul, I["filigranne"] + return bul, infos["filigranne"] elif format == "pdfmail": # format pdfmail: envoie le pdf par mail a l'etud, et affiche le html @@ -965,7 +984,7 @@ def do_formsemestre_bulletinetud( raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") pdfdata, filename = sco_bulletins_generator.make_formsemestre_bulletinetud( - I, version=version, format="pdf" + infos, version=version, format="pdf" ) if prefer_mail_perso: @@ -975,21 +994,21 @@ def do_formsemestre_bulletinetud( if not recipient_addr: flash(f"{etud['nomprenom']} n'a pas d'adresse e-mail !") - return False, I["filigranne"] + return False, infos["filigranne"] else: - mail_bulletin(formsemestre.id, I, pdfdata, filename, recipient_addr) + mail_bulletin(formsemestre.id, infos, pdfdata, filename, recipient_addr) flash(f"mail envoyé à {recipient_addr}") - return True, I["filigranne"] + return True, infos["filigranne"] raise ValueError("do_formsemestre_bulletinetud: invalid format (%s)" % format) -def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr): +def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr): """Send bulletin by email to etud If bul_mail_list_abs pref is true, put list of absences in mail body (text). """ - etud = I["etud"] + etud = infos["etud"] webmaster = sco_preferences.get_preference("bul_mail_contact_addr", formsemestre_id) dept = scu.unescape_html( sco_preferences.get_preference("DeptName", formsemestre_id) @@ -1016,7 +1035,7 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr): etud["etudid"], with_evals=False, format="text" ) - subject = "Relevé de notes de %s" % etud["nomprenom"] + subject = f"""Relevé de notes de {etud["nomprenom"]}""" recipients = [recipient_addr] sender = sco_preferences.get_preference("email_from_addr", formsemestre_id) if copy_addr: @@ -1025,7 +1044,7 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr): bcc = "" # Attach pdf - log("mail bulletin a %s" % recipient_addr) + log(f"""mail bulletin a {recipient_addr}""") email.send_email( subject, sender,