Skip to content
Snippets Groups Projects
Select Git revision
  • 3b436fa0f3df47952976df60f9ba920239064997
  • master default protected
2 results

sco_bulletins_pdf.py

Blame
  • Forked from Jean-Marie Place / SCODOC_R6A06
    652 commits behind the upstream repository.
    sco_bulletins_pdf.py 11.95 KiB
    # -*- mode: python -*-
    # -*- coding: utf-8 -*-
    
    ##############################################################################
    #
    # Gestion scolarite IUT
    #
    # Copyright (c) 1999 - 2024 Emmanuel Viennet.  All rights reserved.
    #
    # This program is free software; you can redistribute it and/or modify
    # it under the terms of the GNU General Public License as published by
    # the Free Software Foundation; either version 2 of the License, or
    # (at your option) any later version.
    #
    # This program is distributed in the hope that it will be useful,
    # but WITHOUT ANY WARRANTY; without even the implied warranty of
    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    # GNU General Public License for more details.
    #
    # You should have received a copy of the GNU General Public License
    # along with this program; if not, write to the Free Software
    # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
    #
    #   Emmanuel Viennet      emmanuel.viennet@viennet.net
    #
    ##############################################################################
    
    """Génération des bulletins de notes en format PDF
    
    On peut installer plusieurs classes générant des bulletins de formats différents.
    La préférence (par semestre) 'bul_pdf_class_name' conserve le nom de la classe Python
    utilisée pour générer les bulletins en PDF. Elle doit être une sous-classe de PDFBulletinGenerator
    et définir les méthodes fabriquant les éléments PDF:
     gen_part_title
     gen_table
     gen_part_below
     gen_signatures
    
    Les éléments PDF sont des objets PLATYPUS de la bibliothèque Reportlab.
    Voir la documentation (Reportlab's User Guide), chapitre 5 et suivants.
    
    Pour définir un nouveau type de bulletin:
     - créer un fichier source sco_bulletins_pdf_xxxx.py où xxxx est le nom (court) de votre type;
     - dans ce fichier, sous-classer PDFBulletinGenerator ou PDFBulletinGeneratorDefault
        (s'inspirer de sco_bulletins_pdf_default);
     - en fin du fichier sco_bulletins_pdf.py, ajouter la ligne
        import sco_bulletins_pdf_xxxx
     - votre type sera alors (après redémarrage de ScoDoc) proposé dans le formulaire de paramètrage.
    
    Chaque semestre peut si nécessaire utiliser un type de bulletin différent.
    
    """
    import io
    import pprint
    import pydoc
    import re
    import time
    import traceback
    
    from flask import g, request
    
    from app import log, ScoValueError
    from app.comp.res_but import ResultatsSemestreBUT
    from app.models import FormSemestre, Identite
    from app.scodoc import (
        codes_cursus,
        sco_cache,
        sco_pdf,
        sco_preferences,
    )
    from app.scodoc.sco_logos import find_logo
    import app.scodoc.sco_utils as scu
    
    import sco_version
    
    
    def assemble_bulletins_pdf(
        formsemestre_id: int,
        story: list,
        bul_title: str,
        infos,
        pagesbookmarks=None,
        filigranne=None,
        server_name="",
    ):
        "Generate PDF document from a story (list of PLATYPUS objects)."
        if not story:
            return ""
        # Paramètres de mise en page
        margins = (
            sco_preferences.get_preference("left_margin", formsemestre_id),
            sco_preferences.get_preference("top_margin", formsemestre_id),
            sco_preferences.get_preference("right_margin", formsemestre_id),
            sco_preferences.get_preference("bottom_margin", formsemestre_id),
        )
        report = io.BytesIO()  # in-memory document, no disk file
        document = sco_pdf.BulletinDocTemplate(report)
        document.addPageTemplates(
            sco_pdf.ScoDocPageTemplate(
                document,
                author=f"{sco_version.SCONAME} {sco_version.SCOVERSION} (E. Viennet)",
                title=f"Bulletin {bul_title}",
                subject="Bulletin de note",
                server_name=server_name,
                margins=margins,
                pagesbookmarks=pagesbookmarks,
                filigranne=filigranne,
                preferences=sco_preferences.SemPreferences(formsemestre_id),
            )
        )
        document.multiBuild(story)
        data = report.getvalue()
        return data
    
    
    def replacement_function(match) -> str:
        "remplace logo par balise html img"
        balise = match.group(1)
        name = match.group(3)
        logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
        if logo is not None:
            return r'<img %s src="%s"%s/>' % (match.group(2), logo.filepath, match.group(4))
        raise ScoValueError(
            'balise "%s": logo "%s" introuvable'
            % (pydoc.html.escape(balise), pydoc.html.escape(name)),
            safe=True,
        )
    
    
    class WrapDict(object):
        """Wrap a dict so that getitem returns '' when values are None
        and non existent keys returns an error message as value.
        """
    
        def __init__(self, adict, none_value=""):
            self.dict = adict
            self.none_value = none_value
    
        def __getitem__(self, key):
            try:
                value = self.dict[key]
            except KeyError:
                return f"XXX {key} invalide XXX"
            if value is None:
                return self.none_value
            return value
    
    
    def process_field(
        field, cdict, style, suppress_empty_pars=False, fmt="pdf", field_name=None
    ):
        """Process a field given in preferences, returns
        - if fmt = 'pdf': a list of Platypus objects
        - if fmt = 'html' : a string
    
        Substitutes all %()s markup
        Remove potentialy harmful <img> tags
        Replaces <logo name="header" width="xxx" height="xxx">
        by <img src=".../logos/logo_header" width="xxx" height="xxx">
    
        If fmt = 'html', replaces <para> by <p>. HTML does not allow logos.
        """
        try:
            # None values are mapped to empty strings by WrapDict
            text = (field or "") % WrapDict(cdict)
        except KeyError as exc:
            missing_key = exc.args[0] if len(exc.args) > 0 else "?"
            log(
                f"""process_field: KeyError {missing_key} on field={field!r}
            values={pprint.pformat(cdict)}
            """
            )
            text = f"""<para><i>format invalide: champ</i> {missing_key} <i>inexistant !</i></para>"""
            scu.flash_once(
                f"Attention: format PDF invalide (champ {field}, clef {missing_key})"
            )
            raise
        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})")
            text = (
                "<para><i>format invalide ! (1)</i></para><para>"
                + traceback.format_exc()
                + "</para>"
            )
        # remove unhandled or dangerous tags:
        text = re.sub(r"<\s*img", "", text)
        if fmt == "html":
            # convert <para>
            text = re.sub(r"<\s*para(\s*)(.*?)>", r"<p>", text)
            return text
        # --- PDF format:
        # handle logos:
        text = re.sub(
            r"<(\s*)logo(.*?)src\s*=\s*(.*?)>", r"<\1logo\2\3>", text
        )  # remove forbidden src attribute
        text = re.sub(
            r'(<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>)',
            replacement_function,
            text,
        )
        # nota: le match sur \w*? donne le nom du logo et interdit les .. et autres
        # tentatives d'acceder à d'autres fichiers !
        # la protection contre des noms malveillants est aussi assurée par l'utilisation de
        #        secure_filename dans la classe Logo
    
        # log('field: %s' % (text))
        return sco_pdf.make_paras(
            text, style, suppress_empty=suppress_empty_pars, field_name=field_name
        )
    
    
    def get_formsemestre_bulletins_pdf(
        formsemestre_id,
        version="selectedevals",
        groups_infos=None,  # si indiqué, ne prend que ces groupes
    ):
        "Document pdf avec tous les bulletins du semestre, et filename"
        from app.but import bulletin_but_court
        from app.scodoc import sco_bulletins
    
        formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
        versions = (
            scu.BULLETINS_VERSIONS_BUT
            if formsemestre.formation.is_apc()
            else scu.BULLETINS_VERSIONS
        )
        if version not in versions:
            raise ScoValueError(
                "get_formsemestre_bulletins_pdf: version de bulletin demandée invalide !"
            )
    
        etuds = formsemestre.get_inscrits(include_demdef=True, order=True)
        if groups_infos is None:
            gr_key = ""
        else:
            etudids = {m["etudid"] for m in groups_infos.members}
            etuds = [etud for etud in etuds if etud.id in etudids]
            gr_key = groups_infos.get_groups_key()
    
        cache_key = str(formsemestre_id) + "_" + version + "_" + gr_key
        cached = sco_cache.SemBulletinsPDFCache.get(cache_key)
        if cached:
            return cached[1], cached[0]
        fragments = []
        # Make each bulletin
        for etud in etuds:
            if version == "butcourt":
                frag = bulletin_but_court.bulletin_but_court_pdf_frag(etud, formsemestre)
            else:
                frag, _ = sco_bulletins.do_formsemestre_bulletinetud(
                    formsemestre,
                    etud,
                    fmt="pdfpart",
                    version=version,
                )
            fragments += frag
        #
        infos = {"DeptName": sco_preferences.get_preference("DeptName", formsemestre_id)}
        if request:
            server_name = request.url_root
        else:
            server_name = ""
        try:
            sco_pdf.PDFLOCK.acquire()
            pdfdoc = assemble_bulletins_pdf(
                formsemestre_id,
                fragments,
                formsemestre.titre_mois(),
                infos,
                server_name=server_name,
            )
        finally:
            sco_pdf.PDFLOCK.release()
        #
        date_iso = time.strftime("%Y-%m-%d")
        filename = f"bul-{formsemestre.titre_num()}-{date_iso}.pdf"
        filename = scu.unescape_html(filename).replace(" ", "_").replace("&", "")
        # fill cache
        sco_cache.SemBulletinsPDFCache.set(
            str(formsemestre_id) + "_" + version, (filename, pdfdoc)
        )
        return pdfdoc, filename
    
    
    def get_etud_bulletins_pdf(etudid, version="selectedevals"):
        "Bulletins pdf de tous les semestres de l'étudiant, et filename"
        from app.scodoc import sco_bulletins
    
        etud = Identite.get_etud(etudid)
        fragments = []
        bookmarks = {}
        filigrannes = {}
        i = 1
        for formsemestre in etud.get_formsemestres():
            frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
                formsemestre,
                etud,
                fmt="pdfpart",
                version=version,
            )
            fragments += frag
            filigrannes[i] = filigranne
            bookmarks[i] = formsemestre.session_id()  # eg RT-DUT-FI-S1-2015
            i = i + 1
        infos = {"DeptName": sco_preferences.get_preference("DeptName")}
        if request:
            server_name = request.url_root
        else:
            server_name = ""
        try:
            sco_pdf.PDFLOCK.acquire()
            pdfdoc = assemble_bulletins_pdf(
                None,
                fragments,
                etud.nomprenom,
                infos,
                bookmarks,
                filigranne=filigrannes,
                server_name=server_name,
            )
        finally:
            sco_pdf.PDFLOCK.release()
        #
        filename = f"bul-{etud.nomprenom}"
        filename = (
            scu.unescape_html(filename).replace(" ", "_").replace("&", "").replace(".", "")
            + ".pdf"
        )
    
        return pdfdoc, filename
    
    
    def get_filigranne(
        etud_etat: str, prefs, decision_sem: str | None | bool = None
    ) -> str:
        """Texte à placer en "filigranne" sur le bulletin pdf.
        etud_etat : etat de l'inscription (I ou D)
        decision_sem = code jury ou vide
        """
        if etud_etat == scu.DEMISSION:
            return "Démission"
        if etud_etat == codes_cursus.DEF:
            return "Défaillant"
        if (prefs["bul_show_temporary"] and not decision_sem) or prefs[
            "bul_show_temporary_forced"
        ]:
            return prefs["bul_temporary_txt"]
        return ""
    
    
    def get_filigranne_apc(
        etud_etat: str, prefs, etudid: int, res: ResultatsSemestreBUT
    ) -> str:
        """Texte à placer en "filigranne" sur le bulletin pdf.
        Version optimisée pour BUT
        """
        if prefs["bul_show_temporary_forced"]:
            return get_filigranne(etud_etat, prefs)
        if prefs["bul_show_temporary"]:
            # requete les décisions de jury
            decision_sem = res.etud_has_decision(etudid)
            return get_filigranne(etud_etat, prefs, decision_sem=decision_sem)
        return get_filigranne(etud_etat, prefs)