Règle de calcul:
moyenne={mi_dict["computation_expr"]}
+ >moyenne={modimpl.computation_expr}
"""
)
H.append("""inutilisée dans cette version de ScoDoc""")
@@ -421,21 +417,20 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
"""
- % mi_dict
)
# -------- Tableau des evaluations
top_table_links = ""
if can_edit_evals:
top_table_links = f"""Créer nouvelle évaluation
"""
if nb_evaluations > 0:
top_table_links += f"""
Trier par date
"""
diff --git a/app/scodoc/sco_pv_forms.py b/app/scodoc/sco_pv_forms.py
index b1f9a404..5b7be89e 100644
--- a/app/scodoc/sco_pv_forms.py
+++ b/app/scodoc/sco_pv_forms.py
@@ -38,18 +38,13 @@ import flask
from flask import flash, redirect, url_for
from flask import g, request
-from app.models import (
- Formation,
- FormSemestre,
- ScolarAutorisationInscription,
-)
-from app.models.etudiants import Identite
+from app.models import FormSemestre, Identite
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus
-from app.scodoc import sco_dict_pv_jury
+from app.scodoc import sco_pv_dict
from app.scodoc import sco_etud
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
@@ -230,7 +225,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
footer = html_sco_header.sco_footer()
- dpv = sco_dict_pv_jury.dict_pvjury(formsemestre_id, with_prev=True)
+ dpv = sco_pv_dict.dict_pvjury(formsemestre_id, with_prev=True)
if not dpv:
if format == "html":
return (
@@ -411,7 +406,7 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
tf[2]["anonymous"] = bool(tf[2]["anonymous"])
try:
PDFLOCK.acquire()
- pdfdoc = sco_pvpdf.pvjury_pdf(
+ pdfdoc = sco_pv_pdf.pvjury_pdf(
formsemestre,
etudids,
numero_arrete=tf[2]["numero_arrete"],
diff --git a/app/scodoc/sco_pvpdf.py b/app/scodoc/sco_pvpdf.py
deleted file mode 100644
index 765d850f..00000000
--- a/app/scodoc/sco_pvpdf.py
+++ /dev/null
@@ -1,937 +0,0 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-#
-# Emmanuel Viennet emmanuel.viennet@viennet.net
-#
-##############################################################################
-
-"""Edition des PV de jury
-"""
-import io
-import re
-
-from PIL import Image as PILImage
-from PIL import UnidentifiedImageError
-
-import reportlab
-from reportlab.lib.units import cm, mm
-from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_JUSTIFY
-from reportlab.platypus import Paragraph, Spacer, Frame, PageBreak
-from reportlab.platypus import Table, TableStyle, Image
-from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
-from reportlab.lib.pagesizes import A4, landscape
-from reportlab.lib import styles
-from reportlab.lib.colors import Color
-
-from flask import g
-from app.models import FormSemestre, Identite
-
-import app.scodoc.sco_utils as scu
-from app.scodoc import sco_bulletins_pdf
-from app.scodoc import codes_cursus
-from app.scodoc import sco_dict_pv_jury
-from app.scodoc import sco_etud
-from app.scodoc import sco_pdf
-from app.scodoc import sco_preferences
-from app.scodoc.sco_exceptions import ScoValueError
-from app.scodoc.sco_logos import find_logo
-from app.scodoc.sco_cursus_dut import SituationEtudCursus
-from app.scodoc.sco_pdf import SU
-import sco_version
-
-LOGO_FOOTER_ASPECT = scu.CONFIG.LOGO_FOOTER_ASPECT # XXX A AUTOMATISER
-LOGO_FOOTER_HEIGHT = scu.CONFIG.LOGO_FOOTER_HEIGHT * mm
-LOGO_FOOTER_WIDTH = LOGO_FOOTER_HEIGHT * scu.CONFIG.LOGO_FOOTER_ASPECT
-
-LOGO_HEADER_ASPECT = scu.CONFIG.LOGO_HEADER_ASPECT # XXX logo IUTV (A AUTOMATISER)
-LOGO_HEADER_HEIGHT = scu.CONFIG.LOGO_HEADER_HEIGHT * mm
-LOGO_HEADER_WIDTH = LOGO_HEADER_HEIGHT * scu.CONFIG.LOGO_HEADER_ASPECT
-
-
-def page_footer(canvas, doc, logo, preferences, with_page_numbers=True):
- "Add footer on page"
- width = doc.pagesize[0] # - doc.pageTemplate.left_p - doc.pageTemplate.right_p
- foot = Frame(
- 0.1 * mm,
- 0.2 * cm,
- width - 1 * mm,
- 2 * cm,
- leftPadding=0,
- rightPadding=0,
- topPadding=0,
- bottomPadding=0,
- id="monfooter",
- showBoundary=0,
- )
-
- left_foot_style = reportlab.lib.styles.ParagraphStyle({})
- left_foot_style.fontName = preferences["SCOLAR_FONT"]
- left_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
- left_foot_style.leftIndent = 0
- left_foot_style.firstLineIndent = 0
- left_foot_style.alignment = TA_RIGHT
- right_foot_style = reportlab.lib.styles.ParagraphStyle({})
- right_foot_style.fontName = preferences["SCOLAR_FONT"]
- right_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
- right_foot_style.alignment = TA_RIGHT
-
- p = sco_pdf.make_paras(
- f"""{preferences["INSTITUTION_NAME"]}{
- preferences["INSTITUTION_ADDRESS"]}""",
- left_foot_style,
- )
-
- np = Paragraph(f'{doc.page}', right_foot_style)
- tabstyle = TableStyle(
- [
- ("LEFTPADDING", (0, 0), (-1, -1), 0),
- ("RIGHTPADDING", (0, 0), (-1, -1), 0),
- ("ALIGN", (0, 0), (-1, -1), "RIGHT"),
- # ('INNERGRID', (0,0), (-1,-1), 0.25, black),#debug
- # ('LINEABOVE', (0,0), (-1,0), 0.5, black),
- ("VALIGN", (1, 0), (1, 0), "MIDDLE"),
- ("RIGHTPADDING", (-1, 0), (-1, 0), 1 * cm),
- ]
- )
- elems = [p]
- if logo:
- elems.append(logo)
- colWidths = [None, LOGO_FOOTER_WIDTH + 2 * mm]
- if with_page_numbers:
- elems.append(np)
- colWidths.append(2 * cm)
- else:
- elems.append("")
- colWidths.append(8 * mm) # force marge droite
- tab = Table([elems], style=tabstyle, colWidths=colWidths)
- canvas.saveState() # is it necessary ?
- foot.addFromList([tab], canvas)
- canvas.restoreState()
-
-
-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]
- head = Frame(
- -22 * mm,
- height - 13 * mm - LOGO_HEADER_HEIGHT,
- 10 * cm,
- LOGO_HEADER_HEIGHT + 2 * mm,
- leftPadding=0,
- rightPadding=0,
- topPadding=0,
- bottomPadding=0,
- id="monheader",
- showBoundary=0,
- )
- if logo:
- canvas.saveState() # is it necessary ?
- head.addFromList([logo], canvas)
- canvas.restoreState()
-
-
-class CourrierIndividuelTemplate(PageTemplate):
- """Template pour courrier avisant des decisions de jury (1 page par étudiant)"""
-
- def __init__(
- self,
- document,
- pagesbookmarks=None,
- author=None,
- title=None,
- subject=None,
- margins=(0, 0, 0, 0), # additional margins in mm (left,top,right, bottom)
- preferences=None, # dictionnary with preferences, required
- force_header=False,
- force_footer=False, # always add a footer (whatever the preferences, use for PV)
- template_name="CourrierJuryTemplate",
- ):
- """Initialise our page template."""
- self.pagesbookmarks = pagesbookmarks or {}
- self.pdfmeta_author = author
- self.pdfmeta_title = title
- self.pdfmeta_subject = subject
- self.preferences = preferences
- self.force_header = force_header
- self.force_footer = force_footer
- self.with_footer = (
- self.force_footer or self.preferences["PV_LETTER_WITH_HEADER"]
- )
- self.with_header = (
- self.force_header or self.preferences["PV_LETTER_WITH_FOOTER"]
- )
- self.with_page_background = self.preferences["PV_LETTER_WITH_BACKGROUND"]
- self.with_page_numbers = False
- self.header_only_on_first_page = False
- # Our doc is made of a single frame
- left, top, right, bottom = margins # marge additionnelle en mm
- # marges du Frame principal
- self.bot_p = 2 * cm
- self.left_p = 2.5 * cm
- self.right_p = 2.5 * cm
- self.top_p = 0 * cm
- # log("margins=%s" % str(margins))
- content = Frame(
- self.left_p + left * mm,
- self.bot_p + bottom * mm,
- document.pagesize[0] - self.right_p - self.left_p - left * mm - right * mm,
- document.pagesize[1] - self.top_p - self.bot_p - top * mm - bottom * mm,
- )
-
- PageTemplate.__init__(self, template_name, [content])
-
- self.background_image_filename = None
- self.logo_footer = None
- self.logo_header = None
- # Search logos in dept specific dir, then in global scu.CONFIG dir
- if template_name == "PVJuryTemplate":
- background = find_logo(
- logoname="pvjury_background",
- dept_id=g.scodoc_dept_id,
- ) or find_logo(
- logoname="pvjury_background",
- dept_id=g.scodoc_dept_id,
- prefix="",
- )
- else:
- background = find_logo(
- logoname="letter_background",
- dept_id=g.scodoc_dept_id,
- ) or find_logo(
- logoname="letter_background",
- dept_id=g.scodoc_dept_id,
- prefix="",
- )
- if not self.background_image_filename and background is not None:
- self.background_image_filename = background.filepath
-
- footer = find_logo(logoname="footer", dept_id=g.scodoc_dept_id)
- if footer is not None:
- self.logo_footer = Image(
- footer.filepath,
- height=LOGO_FOOTER_HEIGHT,
- width=LOGO_FOOTER_WIDTH,
- )
-
- header = find_logo(logoname="header", dept_id=g.scodoc_dept_id)
- if header is not None:
- self.logo_header = Image(
- header.filepath,
- height=LOGO_HEADER_HEIGHT,
- width=LOGO_HEADER_WIDTH,
- )
-
- 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:
- canv.setAuthor(SU(self.pdfmeta_author))
- if self.pdfmeta_title:
- canv.setTitle(SU(self.pdfmeta_title))
- if self.pdfmeta_subject:
- canv.setSubject(SU(self.pdfmeta_subject))
- bm = self.pagesbookmarks.get(doc.page, None)
- if bm != None:
- key = bm
- txt = SU(bm)
- canv.bookmarkPage(key)
- canv.addOutlineEntry(txt, bm)
-
- # ---- Background image
- if self.background_image_filename and self.with_page_background:
- canv.drawImage(
- self.background_image_filename, 0, 0, doc.pagesize[0], doc.pagesize[1]
- )
-
- # ---- Header/Footer
- if self.with_header:
- page_header(
- canv,
- doc,
- self.logo_header,
- self.preferences,
- self.header_only_on_first_page,
- )
- if self.with_footer:
- page_footer(
- canv,
- doc,
- self.logo_footer,
- self.preferences,
- with_page_numbers=self.with_page_numbers,
- )
-
-
-class PVTemplate(CourrierIndividuelTemplate):
- """Template pour les pages des PV de jury"""
-
- def __init__(
- self,
- document,
- author=None,
- title=None,
- subject=None,
- margins=None, # additional margins in mm (left,top,right, bottom)
- preferences=None, # dictionnary with preferences, required
- ):
- if margins is None:
- margins = (
- preferences["pv_left_margin"],
- preferences["pv_top_margin"],
- preferences["pv_right_margin"],
- preferences["pv_bottom_margin"],
- )
- CourrierIndividuelTemplate.__init__(
- self,
- document,
- author=author,
- title=title,
- subject=subject,
- margins=margins,
- preferences=preferences,
- force_header=True,
- force_footer=True,
- template_name="PVJuryTemplate",
- )
- self.with_page_numbers = True
- self.header_only_on_first_page = True
- self.with_header = self.preferences["PV_WITH_HEADER"]
- self.with_footer = self.preferences["PV_WITH_FOOTER"]
- self.with_page_background = self.preferences["PV_WITH_BACKGROUND"]
-
- def afterDrawPage(self, canv, doc):
- """Called after all flowables have been drawn on a page"""
- pass
-
- def beforeDrawPage(self, canv, doc):
- """Called before any flowables are drawn on a page"""
- # If the page number is even, force a page break
- CourrierIndividuelTemplate.beforeDrawPage(self, canv, doc)
- # Note: on cherche un moyen de generer un saut de page double
- # (redémarrer sur page impaire, nouvelle feuille en recto/verso). Pas trouvé en Platypus.
- #
- # if self.__pageNum % 2 == 0:
- # canvas.showPage()
- # # Increment pageNum again since we've added a blank page
- # 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
- Also replaces by
- """
- return ("" + para).join(
- re.split(r"<.*?br.*?/>", paragraph_txt.replace(" ", " "))
- )
-
-
-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,
- date_jury="",
- date_commission="",
- signature=None,
-):
- """Document PDF avec les lettres d'avis pour les etudiants mentionnés
- (tous ceux du semestre, ou la liste indiquée par etudids)
- Renvoie pdf data ou chaine vide si aucun etudiant avec décision de jury.
- """
- dpv = sco_dict_pv_jury.dict_pvjury(formsemestre_id, etudids=etudids, with_prev=True)
- if not dpv:
- return ""
- # Ajoute infos sur etudiants
- etuds = [x["identite"] for x in dpv["decisions"]]
- sco_etud.fill_etuds_info(etuds)
- #
- formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
- prefs = sco_preferences.SemPreferences(formsemestre_id)
- params = {
- "date_jury": date_jury,
- "date_commission": date_commission,
- "titre_formation": dpv["formation"]["titre_officiel"],
- "htab1": "8cm", # lignes à droite (entete, signature)
- "htab2": "1cm",
- }
- # copie preferences
- for name in sco_preferences.get_base_preferences().prefs_name:
- params[name] = sco_preferences.get_preference(name, formsemestre_id)
-
- bookmarks = {}
- objects = [] # list of PLATYPUS objects
- npages = 0
- 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)
- try:
- objects += pdf_lettre_individuelle(
- dpv["formsemestre"], decision, etud, params, signature
- )
- except UnidentifiedImageError as exc:
- raise ScoValueError(
- "Fichier image (signature ou logo ?) invalide !"
- ) from exc
- objects.append(PageBreak())
- npages += 1
- if npages == 0:
- return ""
- # Paramètres de mise en page
- margins = (
- prefs["left_margin"],
- prefs["top_margin"],
- prefs["right_margin"],
- prefs["bottom_margin"],
- )
-
- # ----- Build PDF
- report = io.BytesIO() # in-memory document, no disk file
- document = BaseDocTemplate(report)
- document.addPageTemplates(
- CourrierIndividuelTemplate(
- document,
- author=f"{sco_version.SCONAME} {sco_version.SCOVERSION} (E. Viennet)",
- title=f"Lettres décision {formsemestre.titre_annee()}",
- subject="Décision jury",
- margins=margins,
- pagesbookmarks=bookmarks,
- preferences=prefs,
- )
- )
-
- document.build(objects)
- data = report.getvalue()
- return data
-
-
-def _descr_jury(formsemestre: FormSemestre, diplome):
-
- if not diplome:
- 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: Identite, params, signature=None):
- """
- Renvoie une liste d'objets PLATYPUS pour intégration
- dans un autre document.
- """
- #
- formsemestre_id = sem["formsemestre_id"]
- formsemestre = FormSemestre.query.get(formsemestre_id)
- Se: SituationEtudCursus = decision["Se"]
- t, s = _descr_jury(
- formsemestre, Se.parcours_validated() or not Se.semestre_non_terminal
- )
- objects = []
- style = reportlab.lib.styles.ParagraphStyle({})
- style.fontSize = 14
- style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre_id)
- style.leading = 18
- style.alignment = TA_LEFT
-
- 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é
- params["decisions_ue_descr"] = decision["decisions_ue_descr"]
- if decision["decisions_ue_nb"] > 1:
- params["decisions_ue_descr_plural"] = "s"
- else:
- params["decisions_ue_descr_plural"] = ""
-
- 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_decision_sem_txt"] = ""
- params["decision_orig"] = ""
-
- params.update(decision["identite"])
- # fix domicile
- if params["domicile"]:
- params["domicile"] = params["domicile"].replace("\\n", " ")
-
- # UE capitalisées:
- if decision["decisions_ue"] and decision["decisions_ue_descr"]:
- params["decision_ue_txt"] = (
- """Unité%(decisions_ue_descr_plural)s d'Enseignement %(decision_orig)s capitalisée%(decisions_ue_descr_plural)s : %(decisions_ue_descr)s"""
- % params
- )
- else:
- params["decision_ue_txt"] = ""
- # Mention
- params["mention"] = decision["mention"]
- # Informations sur compensations
- if decision["observation"]:
- params["observation_txt"] = (
- """Observation : %(observation)s.""" % decision
- )
- else:
- params["observation_txt"] = ""
- # Autorisations de passage
- if decision["autorisations"] and not Se.parcours_validated():
- if len(decision["autorisations"]) > 1:
- s = "s"
- else:
- s = ""
- params[
- "autorisations_txt"
- ] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : %s""" % (
- etud.e,
- s,
- s,
- decision["autorisations_descr"],
- )
- else:
- params["autorisations_txt"] = ""
-
- if decision["decision_sem"] and Se.parcours_validated():
- params["diplome_txt"] = (
- """Vous avez donc obtenu le diplôme : %(titre_formation)s""" % params
- )
- 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"]),
- params,
- style,
- suppress_empty_pars=True,
- )
-
- # Signature:
- # nota: si semestre terminal, signature par directeur IUT, sinon, signature par
- # chef de département.
- if Se.semestre_non_terminal:
- sig = (
- sco_preferences.get_preference(
- "PV_LETTER_PASSAGE_SIGNATURE", formsemestre_id
- )
- or ""
- ) % params
- sig = _simulate_br(sig, '')
- objects += sco_pdf.make_paras(
- (
- """"""
- + sig
- + """"""
- )
- % params,
- style,
- )
- else:
- sig = (
- sco_preferences.get_preference(
- "PV_LETTER_DIPLOMA_SIGNATURE", formsemestre_id
- )
- or ""
- ) % params
- sig = _simulate_br(sig, '')
- objects += sco_pdf.make_paras(
- (
- """"""
- + sig
- + """"""
- )
- % params,
- style,
- )
-
- if signature:
- try:
- objects.append(
- _make_signature_image(signature, params["htab1"], formsemestre_id)
- )
- except UnidentifiedImageError as exc:
- raise ScoValueError("Image signature invalide !") from exc
-
- return objects
-
-
-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 add_apc_infos(formsemestre: FormSemestre, params: dict, decision: dict):
- """Ajoute les champs pour les formations APC (BUT), donc avec codes RCUE et année"""
- annee_but = (formsemestre.semestre_id + 1) // 2
- params["decision_orig"] = f"année BUT{annee_but}"
- if decision is None:
- params["decision_sem_descr"] = ""
- params["decision_ue_txt"] = ""
- else:
- decision_annee = decision.get("decision_annee") or {}
- params["decision_sem_descr"] = decision_annee.get("code") or ""
- params[
- "decision_ue_txt"
- ] = f"""{params["decision_ue_txt"]}
- Niveaux de compétences: {decision.get("descr_decisions_rcue") or ""}
- """
-
-
-# ----------------------------------------------
-def pvjury_pdf(
- formsemestre: FormSemestre,
- etudids: list[int],
- date_commission=None,
- date_jury=None,
- numero_arrete=None,
- code_vdi=None,
- show_title=False,
- pv_title=None,
- with_paragraph_nom=False,
- anonymous=False,
-) -> bytes:
- """Doc PDF récapitulant les décisions de jury
- (tableau en format paysage)
- """
- objects, a_diplome = _pvjury_pdf_type(
- formsemestre,
- etudids,
- only_diplome=False,
- date_commission=date_commission,
- numero_arrete=numero_arrete,
- code_vdi=code_vdi,
- date_jury=date_jury,
- show_title=show_title,
- pv_title=pv_title,
- with_paragraph_nom=with_paragraph_nom,
- anonymous=anonymous,
- )
- if not objects:
- return b""
-
- jury_de_diplome = formsemestre.est_terminal()
-
- # Si Jury de passage et qu'un étudiant valide le parcours (car il a validé antérieurement le dernier semestre)
- # alors on génère aussi un PV de diplome (à la suite dans le même doc PDF)
- if not jury_de_diplome and a_diplome:
- # au moins un etudiant a validé son diplome:
- objects.append(PageBreak())
- objects += _pvjury_pdf_type(
- formsemestre,
- etudids,
- only_diplome=True,
- date_commission=date_commission,
- date_jury=date_jury,
- numero_arrete=numero_arrete,
- code_vdi=code_vdi,
- show_title=show_title,
- pv_title=pv_title,
- with_paragraph_nom=with_paragraph_nom,
- anonymous=anonymous,
- )[0]
-
- # ----- Build PDF
- report = io.BytesIO() # in-memory document, no disk file
- document = BaseDocTemplate(report)
- document.pagesize = landscape(A4)
- document.addPageTemplates(
- PVTemplate(
- document,
- author=f"{sco_version.SCONAME} {sco_version.SCOVERSION} (E. Viennet)",
- title=SU(f"PV du jury de {formsemestre.titre_num()}"),
- subject="PV jury",
- preferences=sco_preferences.SemPreferences(formsemestre.id),
- )
- )
-
- document.build(objects)
- data = report.getvalue()
- return data
-
-
-def _pvjury_pdf_type(
- formsemestre: FormSemestre,
- etudids: list[int],
- only_diplome=False,
- date_commission=None,
- date_jury=None,
- numeroArrete=None,
- VDICode=None,
- showTitle=False,
- pv_title=None,
- anonymous=False,
- with_paragraph_nom=False,
-) -> tuple[list, bool]:
- """Objets platypus PDF récapitulant les décisions de jury
- pour un type de jury (passage ou delivrance).
- Ramene: liste d'onj platypus, et un boolen indiquant si au moins un étudiant est diplômé.
- """
- from app.scodoc import sco_pvjury
-
- a_diplome = False
- # Jury de diplome si sem. terminal OU que l'on demande seulement les diplomés
- diplome = formsemestre.est_terminal() or only_diplome
- titre_jury, _ = _descr_jury(formsemestre, diplome)
- titre_diplome = pv_title or formsemestre.formation.titre_officiel
- objects = []
-
- style = reportlab.lib.styles.ParagraphStyle({})
- style.fontSize = 12
- style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre_id)
- style.leading = 18
- style.alignment = TA_JUSTIFY
-
- indent = 1 * cm
- bulletStyle = reportlab.lib.styles.ParagraphStyle({})
- bulletStyle.fontSize = 12
- bulletStyle.fontName = sco_preferences.get_preference(
- "PV_FONTNAME", formsemestre_id
- )
- bulletStyle.leading = 12
- bulletStyle.alignment = TA_JUSTIFY
- bulletStyle.firstLineIndent = 0
- bulletStyle.leftIndent = indent
- bulletStyle.bulletIndent = indent
- bulletStyle.bulletFontName = "Times-Roman"
- bulletStyle.bulletFontSize = 11
- bulletStyle.spaceBefore = 5 * mm
- bulletStyle.spaceAfter = 5 * mm
-
- objects += [Spacer(0, 5 * mm)]
- objects += sco_pdf.make_paras(
- f"""
- Procès-verbal de {titre_jury} du département {
- sco_preferences.get_preference("DeptName", formsemestre.id) or "(sans nom)"
- } - Session unique {formsemestre.annee_scolaire()}
- """,
- style,
- )
-
- objects += sco_pdf.make_paras(
- """
- %s
- """
- % titre_diplome,
- style,
- )
-
- if showTitle:
- objects += sco_pdf.make_paras(
- """Semestre: %s""" % sem["titre"], style
- )
- if sco_preferences.get_preference("PV_TITLE_WITH_VDI", formsemestre.id):
- objects += sco_pdf.make_paras(
- """VDI et Code: %s""" % (VDICode or ""), style
- )
-
- if date_jury:
- objects += sco_pdf.make_paras(
- """Jury tenu le %s""" % date_jury, style
- )
-
- objects += sco_pdf.make_paras(
- ""
- + (sco_preferences.get_preference("PV_INTRO", formsemestre.id) or "")
- % {
- "Decnum": numero_arrete,
- "VDICode": code_vdi,
- "UnivName": sco_preferences.get_preference("UnivName", formsemestre.id),
- "Type": titre_jury,
- "Date": date_commission, # deprecated
- "date_commission": date_commission,
- }
- + "",
- bulletStyle,
- )
-
- objects += sco_pdf.make_paras(
- """Le jury propose les décisions suivantes :""", style
- )
- objects += [Spacer(0, 4 * mm)]
-
- if formsemestre.formation.is_apc():
- rows, titles = jury_but_pv.pvjury_table_but(
- formsemestre, etudids=etudids, line_sep=" "
- )
- columns_ids = list(titles.keys())
- a_diplome = codes_cursus.ADM in [row.get("diplome") for row in rows]
- else:
- dpv = sco_dict_pv_jury.dict_pvjury(
- formsemestre.id, etudids=etudids, with_prev=True
- )
- if not dpv:
- return [], False
- rows, titles, columns_ids = sco_pvjury.pvjury_table(
- dpv,
- only_diplome=only_diplome,
- anonymous=anonymous,
- with_paragraph_nom=with_paragraph_nom,
- )
- a_diplome = True in (x["validation_parcours"] for x in dpv["decisions"])
- # convert to lists of tuples:
- columns_ids = ["etudid"] + columns_ids
- lines = [[line.get(x, "") for x in columns_ids] for line in lines]
- titles = [titles.get(x, "") for x in columns_ids]
- # Make a new cell style and put all cells in paragraphs
- cell_style = styles.ParagraphStyle({})
- cell_style.fontSize = sco_preferences.get_preference(
- "SCOLAR_FONT_SIZE", formsemestre.id
- )
- cell_style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre.id)
- cell_style.leading = 1.0 * sco_preferences.get_preference(
- "SCOLAR_FONT_SIZE", formsemestre.id
- ) # vertical space
- LINEWIDTH = 0.5
- table_style = [
- (
- "FONTNAME",
- (0, 0),
- (-1, 0),
- sco_preferences.get_preference("PV_FONTNAME", formsemestre.id),
- ),
- ("LINEBELOW", (0, 0), (-1, 0), LINEWIDTH, Color(0, 0, 0)),
- ("GRID", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
- ("VALIGN", (0, 0), (-1, -1), "TOP"),
- ]
- titles = ["%s" % x for x in titles]
-
- def _format_pv_cell(x):
- """convert string to paragraph"""
- if isinstance(x, str):
- return Paragraph(SU(x), cell_style)
- else:
- return x
-
- widths_by_id = {
- "nom": 5 * cm,
- "cursus": 2.8 * cm,
- "ects": 1.4 * cm,
- "devenir": 1.8 * cm,
- "decision_but": 1.8 * cm,
- }
-
- table_cells = [[_format_pv_cell(x) for x in line[1:]] for line in ([titles] + rows)]
- widths = [widths_by_id.get(col_id) for col_id in columns_ids[1:]]
-
- objects.append(
- Table(table_cells, repeatRows=1, colWidths=widths, style=table_style)
- )
-
- # Signature du directeur
- objects += sco_pdf.make_paras(
- f"""{
- sco_preferences.get_preference("DirectorName", formsemestre.id) or ""
- }, {
- sco_preferences.get_preference("DirectorTitle", formsemestre.id) or ""
- }""",
- style,
- )
-
- # Légende des codes
- codes = list(codes_cursus.CODES_EXPL.keys())
- codes.sort()
- objects += sco_pdf.make_paras(
- """
- Codes utilisés :""",
- style,
- )
- L = []
- for code in codes:
- L.append((code, codes_cursus.CODES_EXPL[code]))
- TableStyle2 = [
- (
- "FONTNAME",
- (0, 0),
- (-1, 0),
- sco_preferences.get_preference("PV_FONTNAME", formsemestre.id),
- ),
- ("LINEBELOW", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
- ("LINEABOVE", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
- ("LINEBEFORE", (0, 0), (0, -1), LINEWIDTH, Color(0, 0, 0)),
- ("LINEAFTER", (-1, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
- ]
- objects.append(
- Table(
- [[Paragraph(SU(x), cell_style) for x in line] for line in L],
- colWidths=(2 * cm, None),
- style=TableStyle2,
- )
- )
-
- return objects, a_diplome
diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py
index b70ad701..af91313f 100644
--- a/app/scodoc/sco_recapcomplet.py
+++ b/app/scodoc/sco_recapcomplet.py
@@ -169,13 +169,14 @@ def formsemestre_recapcomplet(
if len(formsemestre.inscriptions) > 0:
H.append("""")
if sco_preferences.get_preference("use_ue_coefs", formsemestre_id):
@@ -431,13 +448,15 @@ def gen_formsemestre_recapcomplet_html_table(
"""
table = None
table_html = None
- if not (mode_jury or selected_etudid):
- if include_evaluations:
- 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):
+ cache_class = {
+ (True, True): sco_cache.TableJuryWithEvalsCache,
+ (True, False): sco_cache.TableJuryCache,
+ (False, True): sco_cache.TableRecapWithEvalsCache,
+ (False, False): sco_cache.TableRecapCache,
+ }[(bool(mode_jury), bool(include_evaluations))]
+ if not selected_etudid:
+ table_html = cache_class.get(formsemestre.id)
+ if table_html is None:
table = _gen_formsemestre_recapcomplet_table(
res,
include_evaluations,
@@ -446,11 +465,7 @@ def gen_formsemestre_recapcomplet_html_table(
selected_etudid=selected_etudid,
)
table_html = table.html()
- if not mode_jury:
- if include_evaluations:
- sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html)
- else:
- sco_cache.TableRecapCache.set(formsemestre.id, table_html)
+ cache_class.set(formsemestre.id, table_html)
return table_html, table
@@ -472,6 +487,7 @@ def _gen_formsemestre_recapcomplet_table(
mode_jury=mode_jury,
read_only=not res.formsemestre.can_edit_jury(),
)
+
table.data["filename"] = filename
table.select_row(selected_etudid)
return table
diff --git a/app/scodoc/sco_report.py b/app/scodoc/sco_report.py
index 178cf686..70bc7afe 100644
--- a/app/scodoc/sco_report.py
+++ b/app/scodoc/sco_report.py
@@ -1574,7 +1574,8 @@ def formsemestre_graph_cursus(
allkeys=False, # unused
):
"""Graphe suivi cohortes"""
- annee_bac = str(annee_bac)
+ annee_bac = str(annee_bac or "")
+ annee_admission = str(annee_admission or "")
# log("formsemestre_graph_cursus")
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if format == "pdf":
diff --git a/app/scodoc/table_builder.py b/app/scodoc/table_builder.py
deleted file mode 100644
index 8ddd70c8..00000000
--- a/app/scodoc/table_builder.py
+++ /dev/null
@@ -1,401 +0,0 @@
-##############################################################################
-# ScoDoc
-# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
-# See LICENSE
-##############################################################################
-
-"""Classes pour aider à construire des tables de résultats
-"""
-from collections import defaultdict
-
-
-class Element:
- def __init__(
- self,
- elt: str,
- content=None,
- classes: list[str] = None,
- attrs: dict[str, str] = None,
- data: dict = None,
- ):
- self.elt = elt
- self.attrs = attrs or {}
- self.classes = classes or []
- "list of classes for the element"
- self.content = content
- self.data = data or {}
- "data-xxx"
-
- def html(self, extra_classes: list[str] = None) -> str:
- "html for element"
- classes = [cls for cls in (self.classes + (extra_classes or [])) if cls]
- attrs_str = f"""class="{' '.join(classes)}" """ if classes else ""
- # Autres attributs:
- attrs_str += " " + " ".join([f'{k}="{v}"' for (k, v) in self.attrs.items()])
- # et data-x
- attrs_str += " " + " ".join([f'data-{k}="{v}"' for k, v in self.data.items()])
- return f"""<{self.elt} {attrs_str}>{self.html_content()}{self.elt}>"""
-
- def html_content(self) -> str:
- "Le contenu de l'élément, en html."
- return str(self.content or "")
-
-
-class Table(Element):
- """Construction d'une table de résultats
-
- table = Table()
- row = table.new_row(id="xxx", category="yyy")
- row.new_cell( col_id, title, content [,classes] [, idx], [group], [keys:dict={}] )
-
- rows = table.get_rows([category="yyy"])
- table.sort_rows(key [, reverse])
- table.set_titles(titles)
- table.update_titles(titles)
-
- table.set_column_groups(groups: list[str])
- table.insert_group(group:str, [after=str], [before=str])
-
- Ordre des colonnes: groupées par groupes, et dans chaque groupe par ordre d'insertion
- On fixe l'ordre des groupes par ordre d'insertion
- ou par insert_group ou par set_column_groups.
-
- """
-
- def __init__(
- self,
- selected_row_id: str = None,
- classes: list[str] = None,
- attrs: dict[str, str] = None,
- data: dict = None,
- ):
- super().__init__("table", classes=classes, attrs=attrs, data=data)
- self.rows: list["Row"] = []
- "ordered list of Rows"
- self.row_by_id: dict[str, "Row"] = {}
- self.column_ids = []
- "ordered list of columns ids"
- self.groups = []
- "ordered list of column groups names"
- self.head = []
- self.foot = []
- self.column_group = {}
- "the group of the column: { col_id : group }"
- self.column_classes: defaultdict[str, list[str]] = defaultdict(lambda: [])
- "classe ajoutée à toutes les cellules de la colonne: { col_id : class }"
- self.selected_row_id = selected_row_id
- "l'id de la ligne sélectionnée"
- self.titles = {}
- "Column title: { col_id : titre }"
- self.head_title_row: "Row" = Row(self, "title_head", cell_elt="th")
- self.foot_title_row: "Row" = Row(self, "title_foot", cell_elt="th")
- self.empty_cell = Cell.empty()
-
- def _prepare(self):
- """Prepare the table before generation:
- Sort table columns, add header/footer titles rows
- """
- self.sort_columns()
- # Titres
- self.add_head_row(self.head_title_row)
- self.add_foot_row(self.foot_title_row)
-
- def get_row_by_id(self, row_id) -> "Row":
- "return the row, or None"
- return self.row_by_id.get(row_id)
-
- def is_empty(self) -> bool:
- "true if table has no rows"
- return len(self.rows) == 0
-
- def select_row(self, row_id):
- "mark rows as 'selected'"
- self.selected_row_id = row_id
-
- def to_list(self) -> list[dict]:
- """as a list, each row is a dict"""
- self._prepare()
- return [row.to_dict() for row in self.rows]
-
- def html(self, extra_classes: list[str] = None) -> str:
- """HTML version of the table"""
- self._prepare()
- return super().html(extra_classes=extra_classes)
-
- def html_content(self) -> str:
- """Le contenu de la table en html."""
- newline = "\n"
- header = (
- f"""
-
- { newline.join(row.html() for row in self.head) }
-
- """
- if self.head
- else ""
- )
- footer = (
- f"""
-
- { newline.join(row.html() for row in self.foot) }
-
- """
- if self.foot
- else ""
- )
- return f"""
- {header}
-
- {
- newline.join(row.html() for row in self.rows)
- }
-
- {footer}
- """
-
- def add_row(self, row: "Row") -> "Row":
- """Append a new row"""
- self.rows.append(row)
- self.row_by_id[row.id] = row
- return row
-
- def add_head_row(self, row: "Row") -> "Row":
- "Add a row to table head"
- # row = Row(self, cell_elt="th", category="head")
- self.head.append(row)
- self.row_by_id[row.id] = row
- return row
-
- def add_foot_row(self, row: "Row") -> "Row":
- "Add a row to table foot"
- self.foot.append(row)
- self.row_by_id[row.id] = row
- return row
-
- def sort_rows(self, key: callable, reverse: bool = False):
- """Sort table rows"""
- self.rows.sort(key=key, reverse=reverse)
-
- def sort_columns(self):
- """Sort columns ids"""
- groups_order = {group: i for i, group in enumerate(self.groups)}
- cols_order = {col_id: i for i, col_id in enumerate(self.column_ids)}
- self.column_ids.sort(
- key=lambda col_id: (
- groups_order.get(self.column_group.get(col_id), col_id),
- cols_order[col_id],
- )
- )
-
- def insert_group(self, group: str, after: str = None, before: str = None):
- """Déclare un groupe de colonnes et le place avant ou après un autre groupe.
- Si pas d'autre groupe indiqué, le place après, à droite du dernier.
- Si le group existe déjà, ne fait rien (ne le déplace pas).
- """
- if group in self.groups:
- return
- other = after or before
- if other is None:
- self.groups.append(group)
- else:
- if not other in self.groups:
- raise ValueError(f"invalid column group '{other}'")
- index = self.groups.index(other)
- if after:
- index += 1
- self.groups.insert(index, group)
-
- def set_groups(self, groups: list[str]):
- """Define column groups and set order"""
- self.groups = groups
-
- def set_titles(self, titles: dict[str, str]):
- """Set columns titles"""
- self.titles = titles
-
- def update_titles(self, titles: dict[str, str]):
- """Set columns titles"""
- self.titles.update(titles)
-
- def add_title(
- self, col_id, title: str = None, classes: list[str] = None
- ) -> tuple["Cell", "Cell"]:
- """Record this title,
- and create cells for footer and header if they don't already exist.
- """
- title = title or ""
- if col_id not in self.titles:
- self.titles[col_id] = title
- self.head_title_row.cells[col_id] = self.head_title_row.add_cell(
- col_id, None, title, classes=classes
- )
- self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell(
- col_id, None, title, classes=classes
- )
-
- return self.head_title_row.cells.get(col_id), self.foot_title_row.cells[col_id]
-
-
-class Row(Element):
- """A row."""
-
- def __init__(
- self,
- table: Table,
- row_id=None,
- category=None,
- cell_elt: str = None,
- classes: list[str] = None,
- attrs: dict[str, str] = None,
- data: dict = None,
- ):
- super().__init__("tr", classes=classes, attrs=attrs, data=data)
- self.category = category
- self.cells = {}
- self.cell_elt = cell_elt
- self.classes: list[str] = classes or []
- "classes sur le "
- self.id = row_id
- self.table = table
-
- def add_cell(
- self,
- col_id: str,
- title: str,
- content: str,
- group: str = None,
- attrs: list[str] = None,
- classes: list[str] = None,
- data: dict[str, str] = None,
- elt: str = None,
- raw_content=None,
- target_attrs: dict = None,
- target: str = None,
- ) -> "Cell":
- """Create cell and add it to the row.
- group: groupe de colonnes
- classes is a list of css class names
- """
- cell = Cell(
- content,
- (classes or []) + [group or ""], # ajoute le nom de groupe aux classes
- elt=elt or self.cell_elt,
- attrs=attrs,
- data=data,
- raw_content=raw_content,
- target=target,
- target_attrs=target_attrs,
- )
- return self.add_cell_instance(col_id, cell, column_group=group, title=title)
-
- def add_cell_instance(
- self, col_id: str, cell: "Cell", column_group: str = None, title: str = None
- ) -> "Cell":
- """Add a cell to the row.
- Si title est None, il doit avoir été ajouté avec table.add_title().
- """
- cell.data["group"] = column_group
- self.cells[col_id] = cell
- if col_id not in self.table.column_ids:
- self.table.column_ids.append(col_id)
- self.table.insert_group(column_group)
- if column_group is not None:
- self.table.column_group[col_id] = column_group
-
- if title is not None:
- self.table.add_title(col_id, title, classes=cell.classes)
-
- return cell
-
- def html(self, extra_classes: list[str] = None) -> str:
- """html for row, with cells"""
- if (self.id is not None) and self.id == getattr(self.table, "selected_row_id"):
- self.classes.append("row_selected")
- return super().html(extra_classes=extra_classes)
-
- def html_content(self) -> str:
- "Le contenu du row en html."
- return "".join(
- [
- self.cells.get(col_id, self.table.empty_cell).html(
- extra_classes=self.table.column_classes.get(col_id)
- )
- for col_id in self.table.column_ids
- ]
- )
-
- def to_dict(self) -> dict:
- """row as a dict, with only cell contents"""
- return {
- col_id: self.cells.get(col_id, self.table.empty_cell).raw_content
- for col_id in self.table.column_ids
- }
-
-
-class BottomRow(Row):
- """Une ligne spéciale pour le pied de table
- avec un titre à gauche
- (répété sur les colonnes indiquées par left_title_col_ids),
- et automatiquement ajouté au footer.
- """
-
- def __init__(
- self, *args, left_title_col_ids: list[str] = None, left_title=None, **kwargs
- ):
- super().__init__(*args, **kwargs)
- self.left_title_col_ids = left_title_col_ids
- if left_title is not None:
- self.set_left_title(left_title)
- self.table.add_foot_row(self)
-
- def set_left_title(self, title: str = ""):
- "Fill left title cells"
- for col_id in self.left_title_col_ids:
- self.add_cell(col_id, None, title)
-
-
-class Cell(Element):
- """Une cellule de table"""
-
- def __init__(
- self,
- content,
- classes: list[str] = None,
- elt="td",
- attrs: dict[str, str] = None,
- data: dict = None,
- raw_content=None,
- target: str = None,
- target_attrs: dict = None,
- ):
- """if specified, raw_content will be used for raw exports like xlsx"""
- super().__init__(
- elt if elt is not None else "td", content, classes, attrs, data
- )
- if self.elt == "th":
- self.attrs["scope"] = "row"
-
- self.data = data or {}
- self.raw_content = raw_content or content
- self.target = target
- self.target_attrs = target_attrs or {}
-
- @classmethod
- def empty(cls):
- "create a new empty cell"
- return cls("")
-
- def __str__(self):
- return str(self.content)
-
- def html_content(self) -> str:
- "content of the table cell, as html"
- # entoure le contenu par un lien ?
- if (self.target is not None) or self.target_attrs:
- href = f'href="{self.target}"' if self.target else ""
- target_attrs_str = " ".join(
- [f'{k}="{v}"' for (k, v) in self.target_attrs.items()]
- )
- return f"{super().html_content()}"
-
- return super().html_content()
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index eaecffe8..d4674f3c 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -4171,10 +4171,29 @@ table.table_recap .cursus {
white-space: nowrap;
}
-table.table_recap .col_ue,
-table.table_recap .col_ue_code,
-table.table_recap .col_moy_gen,
-table.table_recap .group {
+table.table_recap td.col_rcue,
+table.table_recap th.col_rcue,
+table.table_recap td.cursus_but.first,
+table.table_recap td.cursus_but.first {
+ border-left: 1px solid rgb(221, 221, 221);
+}
+
+table.table_recap td.cursus_BUT1 {
+ color: #007bff;
+}
+
+table.table_recap td.cursus_BUT2 {
+ color: #d39f00;
+}
+
+table.table_recap td.cursus_BUT3 {
+ color: #7f00ff;
+}
+
+table.table_recap td.col_ue,
+table.table_recap td.col_ue_code,
+table.table_recap td.col_moy_gen,
+table.table_recap td.group {
border-left: 1px solid blue;
}
diff --git a/app/static/js/jury_but.js b/app/static/js/jury_but.js
index df6d5b9e..c53b223a 100644
--- a/app/static/js/jury_but.js
+++ b/app/static/js/jury_but.js
@@ -1,80 +1,94 @@
+
+
// active les menus des codes "manuels" (année, RCUEs)
function enable_manual_codes(elt) {
- $(".jury_but select.manual").prop("disabled", !elt.checked);
+ $(".jury_but select.manual").prop("disabled", !elt.checked);
}
// changement d'un menu code:
function change_menu_code(elt) {
- // Ajuste styles pour visualiser codes enregistrés/modifiés
- if (elt.value != elt.dataset.orig_code) {
- elt.parentElement.parentElement.classList.add("modified");
- } else {
- elt.parentElement.parentElement.classList.remove("modified");
- }
- if (elt.value == elt.dataset.orig_recorded) {
- elt.parentElement.parentElement.classList.add("recorded");
- } else {
- elt.parentElement.parentElement.classList.remove("recorded");
- }
- // Si RCUE passant en ADJ, change les menus des UEs associées ADJR
- if (
- elt.classList.contains("code_rcue") &&
- elt.dataset.niveau_id &&
- elt.value == "ADJ" &&
- elt.value != elt.dataset.orig_recorded
- ) {
- let ue_selects =
- elt.parentElement.parentElement.parentElement.querySelectorAll(
- "select.ue_rcue_" + elt.dataset.niveau_id
- );
- ue_selects.forEach((select) => {
- if (select.value != "ADM") {
- select.value = "ADJR";
- change_menu_code(select); // pour changer les styles
- }
- });
- }
+ // Ajuste styles pour visualiser codes enregistrés/modifiés
+ if (elt.value != elt.dataset.orig_code) {
+ elt.parentElement.parentElement.classList.add("modified");
+ } else {
+ elt.parentElement.parentElement.classList.remove("modified");
+ }
+ if (elt.value == elt.dataset.orig_recorded) {
+ elt.parentElement.parentElement.classList.add("recorded");
+ } else {
+ elt.parentElement.parentElement.classList.remove("recorded");
+ }
+ // Si RCUE passant en ADJ, change les menus des UEs associées ADJR
+ if (elt.classList.contains("code_rcue")
+ && elt.dataset.niveau_id
+ && elt.value == "ADJ"
+ && elt.value != elt.dataset.orig_recorded) {
+ let ue_selects = elt.parentElement.parentElement.parentElement.querySelectorAll(
+ "select.ue_rcue_" + elt.dataset.niveau_id);
+ ue_selects.forEach(select => {
+ if (select.value != "ADM") {
+ select.value = "ADJR";
+ change_menu_code(select); // pour changer les styles
+ }
+ });
+ }
}
$(function () {
- // Recupère la liste ordonnées des etudids
- // pour avoir le "suivant" etr le "précédent"
- // (liens de navigation)
- const url = new URL(document.URL);
- const frags = url.pathname.split("/"); // .../formsemestre_validation_but/formsemestre_id/etudid
- const etudid = frags[frags.length - 1];
- const formsemestre_id = frags[frags.length - 2];
- const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]);
- const etudids_str = localStorage.getItem(etudids_key);
- const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]);
- const noms_str = localStorage.getItem(noms_key);
- if (etudids_str && noms_str) {
- const etudids = JSON.parse(etudids_str);
- const noms = JSON.parse(noms_str);
- const cur_idx = etudids.indexOf(etudid);
- let prev_idx = -1;
- let next_idx = -1;
- if (cur_idx != -1) {
- if (cur_idx > 0) {
- prev_idx = cur_idx - 1;
- }
- if (cur_idx < etudids.length - 1) {
- next_idx = cur_idx + 1;
- }
- }
- if (prev_idx != -1) {
- let elem = document.querySelector("div.prev a");
- if (elem) {
- elem.href = elem.href.replace("PREV", etudids[prev_idx]);
- elem.innerHTML = noms[prev_idx];
- }
+ // Recupère la liste ordonnées des etudids
+ // pour avoir le "suivant" et le "précédent"
+ // (liens de navigation)
+ const url = new URL(document.URL);
+ const frags = url.pathname.split("/"); // .../formsemestre_validation_but/formsemestre_id/etudid
+ const etudid = frags[frags.length - 1];
+ const formsemestre_id = frags[frags.length - 2];
+ const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]);
+ const etudids_str = localStorage.getItem(etudids_key);
+ const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]);
+ const noms_str = localStorage.getItem(noms_key);
+ if (etudids_str && noms_str) {
+ const etudids = JSON.parse(etudids_str);
+ const noms = JSON.parse(noms_str);
+ const cur_idx = etudids.indexOf(etudid);
+ let prev_idx = -1;
+ let next_idx = -1
+ if (cur_idx != -1) {
+ if (cur_idx > 0) {
+ prev_idx = cur_idx - 1;
+ }
+ if (cur_idx < etudids.length - 1) {
+ next_idx = cur_idx + 1;
+ }
+ }
+ if (prev_idx != -1) {
+ let elem = document.querySelector("div.prev a");
+ if (elem) {
+ elem.href = elem.href.replace("PREV", etudids[prev_idx]);
+ elem.innerHTML = noms[prev_idx];
+ }
+ } else {
+ document.querySelector("div.prev").innerHTML = "";
+ }
+ if (next_idx != -1) {
+ let elem = document.querySelector("div.next a");
+ if (elem) {
+ elem.href = elem.href.replace("NEXT", etudids[next_idx]);
+ elem.innerHTML = noms[next_idx];
+ }
+ } else {
+ document.querySelector("div.next").innerHTML = "";
+ }
} else {
- document.querySelector("div.prev").innerHTML = "";
+ // Supprime les liens de navigation
+ document.querySelector("div.prev").innerHTML = "";
+ document.querySelector("div.next").innerHTML = "";
}
});
// ----- Etat du formulaire jury pour éviter sortie sans enregistrer
let FORM_STATE = "";
+let IS_SUBMITTING = false;
+
// Une chaine décrivant l'état du form
function get_form_state() {
let codes = [];
@@ -85,13 +99,19 @@ function get_form_state() {
$('document').ready(function () {
FORM_STATE = get_form_state();
+ document.querySelector("form#jury_but").addEventListener('submit', jury_form_submit);
});
function is_modified() {
return FORM_STATE != get_form_state();
}
+
+function jury_form_submit(event) {
+ IS_SUBMITTING = true;
+}
+
window.addEventListener("beforeunload", function (e) {
- if (is_modified()) {
+ if ((!IS_SUBMITTING) && is_modified()) {
var confirmationMessage = 'Changements non enregistrés !';
(e || window.event).returnValue = confirmationMessage;
return confirmationMessage;
diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js
index 1c87ffd7..56d43e3d 100644
--- a/app/static/js/table_recap.js
+++ b/app/static/js/table_recap.js
@@ -47,26 +47,30 @@ $(function () {
}
});
}
- }
- }
- // Les colonnes visibles sont mémorisées, il faut initialiser l'état des boutons
- function update_buttons_labels(dt) {
- // chaque bouton controle une classe stockée dans le data-group du span
- document.querySelectorAll("button.dt-button").forEach((but) => {
- let g_span = but.querySelector("span > span");
- if (g_span) {
- let group = g_span.dataset["group"];
- if (group) {
- // si le group (= la 1ere col.) est visible, but_on
- if (dt.columns("." + group).visible()[0]) {
- but.classList.add("but_on");
- but.classList.remove("but_off");
+ // Changement visibilité groupes colonnes (boutons)
+ function toggle_col_but_visibility(e, dt, node, config) {
+ let group = node.children()[0].firstChild.dataset.group;
+ toggle_col_group_visibility(dt, group, node.hasClass("but_on"));
+ }
+ function toggle_col_ident_visibility(e, dt, node, config) {
+ let onoff = node.hasClass("but_on");
+ toggle_col_group_visibility(dt, "identite_detail", onoff);
+ toggle_col_group_visibility(dt, "identite_court", !onoff);
+ }
+ function toggle_col_ressources_visibility(e, dt, node, config) {
+ let onoff = node.hasClass("but_on");
+ toggle_col_group_visibility(dt, "col_res", onoff);
+ toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
+ toggle_col_group_visibility(dt, "col_malus", onoff);
+ }
+ function toggle_col_group_visibility(dt, group, onoff) {
+ if (onoff) {
+ dt.columns('.' + group).visible(false);
} else {
- but.classList.add("but_off");
- but.classList.remove("but_on");
+ dt.columns('.' + group).visible(true);
}
- }
+ update_buttons_labels(dt);
}
// Definition des boutons au dessus de la table:
let buttons = [
@@ -102,11 +106,11 @@ $(function () {
action: toggle_col_ident_visibility,
},
{
- text: 'Groupes',
+ text: 'Groupes',
action: toggle_col_but_visibility,
},
{
- text: 'Rg',
+ text: 'Rg',
action: toggle_col_but_visibility,
},
]; // fin des boutons communs à toutes les tables recap
@@ -156,19 +160,21 @@ $(function () {
action: toggle_col_but_visibility,
});
}
- : {
- name: "toggle_mod",
- text: "Cacher les modules",
- action: function (e, dt, node, config) {
- let onoff = node.hasClass("but_on");
- toggle_col_group_visibility(
- dt,
- "col_mod:not(.col_empty)",
- onoff
+ // S'il y a des colonnes vides:
+ if ($('table.table_recap td.col_empty').length > 0) {
+ buttons.push({ // modules vides
+ text: 'Vides',
+ action: toggle_col_but_visibility,
+ });
+ }
+ // Boutons admission (pas en jury)
+ if (!$('table.table_recap').hasClass("jury")) {
+ buttons.push(
+ {
+ text: 'Admission',
+ action: toggle_col_but_visibility,
+ }
);
- toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
- toggle_col_group_visibility(dt, "col_malus", onoff);
- },
}
}
// Boutons évaluations (si présentes)
@@ -230,7 +236,8 @@ $(function () {
buttons: buttons,
"drawCallback": function (settings) {
// permet de conserver l'ordre de tri des colonnes
- let order_info = JSON.stringify($('table.table_recap').DataTable().order());
+ let table = $('table.table_recap').DataTable();
+ let order_info = JSON.stringify(table.order());
if (formsemestre_id) {
localStorage.setItem(order_info_key, order_info);
}
@@ -270,114 +277,9 @@ $(function () {
$(function () {
let row_selected = document.querySelector(".row_selected");
if (row_selected) {
- /*row_selected.scrollIntoView();
- window.scrollBy(0, -50);*/
+ row_selected.scrollIntoView();
+ window.scrollBy(0, -125);
row_selected.classList.add("selected");
}
-
- // ------------- LA TABLE ---------
- try {
- let table = $("table.table_recap").DataTable({
- paging: false,
- searching: true,
- info: false,
- autoWidth: false,
- fixedHeader: {
- header: true,
- footer: false,
- },
- orderCellsTop: true, // cellules ligne 1 pour tri
- aaSorting: [], // Prevent initial sorting
- colReorder: true,
- stateSave: true, // enregistre état de la table (tris, ...)
- columnDefs: [
- {
- // cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
- targets: hidden_colums,
- visible: false,
- },
- {
- // Elimine les 0 à gauche pour les exports excel et les "copy"
- targets: [
- "col_mod",
- "col_moy_gen",
- "col_moy_ue",
- "col_res",
- "col_sae",
- "evaluation",
- "col_rcue",
- ],
- render: function (data, type, row) {
- return type === "export" ? data.replace(/0(\d\..*)/, "$1") : data;
- },
- },
- {
- // Elimine les "+"" pour les exports
- targets: ["col_ue_bonus", "col_malus"],
- render: function (data, type, row) {
- return type === "export"
- ? data
- .replace(/.*\+(\d?\d?\.\d\d).*/m, "$1")
- .replace(/0(\d\..*)/, "$1")
- : data;
- },
- },
- {
- // Elimine emoji warning sur UEs
- targets: ["col_ues_validables"],
- render: function (data, type, row) {
- return type === "export"
- ? data.replace(/(\d+\/\d+).*/, "$1")
- : data;
- },
- },
- ],
- dom: "Bfrtip",
- buttons: buttons,
- drawCallback: function (settings) {
- // permet de conserver l'ordre de tri des colonnes
- let table = $("table.table_recap").DataTable();
- let order_info = JSON.stringify(table.order());
- if (formsemestre_id) {
- localStorage.setItem(order_info_key, order_info);
- }
- let etudids = [];
- document.querySelectorAll("td.identite_court").forEach((e) => {
- etudids.push(e.dataset.etudid);
- });
- let noms = [];
- document.querySelectorAll("td.identite_court").forEach((e) => {
- noms.push(e.dataset.nomprenom);
- });
- localStorage.setItem(etudids_key, JSON.stringify(etudids));
- localStorage.setItem(noms_key, JSON.stringify(noms));
- },
- order: order_info,
- });
- update_buttons_labels(table);
- } catch (error) {
- // l'erreur peut etre causee par un ancien storage:
- localStorage.removeItem(etudids_key);
- localStorage.removeItem(noms_key);
- localStorage.removeItem(order_info_key);
- location.reload();
- }
- });
- $("table.table_recap tbody").on("click", "tr", function () {
- if ($(this).hasClass("selected")) {
- $(this).removeClass("selected");
- } else {
- $("table.table_recap tr.selected").removeClass("selected");
- $(this).addClass("selected");
- }
- });
- // Pour montrer et surligner l'étudiant sélectionné:
- $(function () {
- let row_selected = document.querySelector(".row_selected");
- if (row_selected) {
- row_selected.scrollIntoView();
- window.scrollBy(0, -125);
- row_selected.classList.add("selected");
- }
- });
+ });
});
diff --git a/app/tables/jury_recap.py b/app/tables/jury_recap.py
index 0e91ddbe..2be036a6 100644
--- a/app/tables/jury_recap.py
+++ b/app/tables/jury_recap.py
@@ -23,7 +23,6 @@ from app.comp.res_compat import NotesTableCompat
from app.models import ApcNiveau, UniteEns
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
-from app.scodoc import html_sco_header
from app.scodoc.codes_cursus import (
BUT_BARRE_RCUE,
BUT_RCUE_SUFFISANT,
diff --git a/app/tables/recap.py b/app/tables/recap.py
index 3449aaa0..989629ae 100644
--- a/app/tables/recap.py
+++ b/app/tables/recap.py
@@ -12,8 +12,7 @@ import numpy as np
from app.auth.models import User
from app.comp.res_common import ResultatsSemestre
-from app.models import Identite
-from app.models.ues import UniteEns
+from app.models import Identite, FormSemestre, UniteEns
from app.scodoc.codes_cursus import UE_SPORT, DEF
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_groups
diff --git a/app/templates/pn/form_mods.j2 b/app/templates/pn/form_mods.j2
index f88140f1..21677f40 100644
--- a/app/templates/pn/form_mods.j2
+++ b/app/templates/pn/form_mods.j2
@@ -49,7 +49,7 @@
({{mod.ue.acronyme}}),
{% endif %}
- - parcours {{ mod.get_cursus()|map(attribute="code")|join(", ")|default('tronc commun',
+ - parcours {{ mod.get_parcours()|map(attribute="code")|join(", ")|default('tronc commun',
true)|safe
}}
{% if mod.heures_cours or mod.heures_td or mod.heures_tp %}
diff --git a/app/views/notes.py b/app/views/notes.py
index 4116cb7a..046e078f 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -1590,10 +1590,24 @@ def etud_desinscrit_ue(etudid, formsemestre_id, ue_id):
ue = UniteEns.query.get_or_404(ue_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if ue.formation.is_apc():
- if DispenseUE.query.filter_by(etudid=etudid, ue_id=ue_id).count() == 0:
- disp = DispenseUE(ue_id=ue_id, etudid=etudid)
+ if (
+ DispenseUE.query.filter_by(
+ formsemestre_id=formsemestre_id, etudid=etudid, ue_id=ue_id
+ ).count()
+ == 0
+ ):
+ disp = DispenseUE(
+ formsemestre_id=formsemestre_id, ue_id=ue_id, etudid=etudid
+ )
db.session.add(disp)
db.session.commit()
+ log(f"etud_desinscrit_ue {etud} {ue}")
+ Scolog.logdb(
+ method="etud_desinscrit_ue",
+ etudid=etud.id,
+ msg=f"Désinscription de l'UE {ue.acronyme} de {formsemestre.titre_annee()}",
+ commit=True,
+ )
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
else:
sco_moduleimpl_inscriptions.do_etud_desinscrit_ue_classic(
diff --git a/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py b/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py
index 9463f6b5..b74fa27d 100644
--- a/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py
+++ b/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py
@@ -11,7 +11,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "dbcf2175e87f"
-down_revision = "d8288b7f0a3e"
+down_revision = "6520faf67508"
branch_labels = None
depends_on = None
diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py
index b7714368..48712483 100644
--- a/tests/unit/sco_fake_gen.py
+++ b/tests/unit/sco_fake_gen.py
@@ -19,7 +19,6 @@ from app.auth.models import User
from app.models import Departement, Formation, FormationModalite, Matiere
from app.scodoc import notesdb as ndb
from app.scodoc import codes_cursus
-from app.scodoc import sco_edit_formation
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
@@ -154,7 +153,7 @@ class ScoFake(object):
acronyme="test",
titre="Formation test",
titre_officiel="Le titre officiel de la formation test",
- type_parcours=codes_cursus.CursusDUT.TYPE_CURSUS,
+ type_parcours: int = codes_cursus.CursusDUT.TYPE_CURSUS,
formation_code=None,
code_specialite=None,
) -> int:
diff --git a/tests/unit/test_but_ues.py b/tests/unit/test_but_ues.py
index 693c2201..ae850b53 100644
--- a/tests/unit/test_but_ues.py
+++ b/tests/unit/test_but_ues.py
@@ -123,7 +123,13 @@ def test_ue_moy(test_client):
modimpl.module.ue.type != UE_SPORT for modimpl in formsemestre.modimpls_sorted
]
etud_moy_ue = moy_ue.compute_ue_moys_apc(
- sem_cube, etuds, modimpls, modimpl_inscr_df, modimpl_coefs_df, modimpl_mask
+ sem_cube,
+ etuds,
+ modimpls,
+ modimpl_inscr_df,
+ modimpl_coefs_df,
+ modimpl_mask,
+ set(),
)
assert etud_moy_ue[ue1.id][etudid] == n1
assert etud_moy_ue[ue2.id][etudid] == n1
|