Skip to content
Snippets Groups Projects
Select Git revision
  • 7f592df32ed45a534af3d256c0c84fa623ec8152
  • master default protected
2 results

callbacks.py

Blame
  • sco_recapcomplet.py 20.89 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
    #
    ##############################################################################
    
    """Tableau récapitulatif des notes d'un semestre
    """
    import collections
    import datetime
    import time
    from xml.etree import ElementTree
    
    from flask import abort, g, render_template, request, url_for
    from flask_login import current_user
    
    from app import log
    from app.auth.models import Permission
    from app.but import bulletin_but
    from app.comp import res_sem
    from app.comp.res_common import ResultatsSemestre
    from app.comp.res_compat import NotesTableCompat
    from app.models import FormSemestre
    from app.models.etudiants import Identite
    
    import app.scodoc.sco_utils as scu
    from app.scodoc import sco_bulletins_json
    from app.scodoc import sco_bulletins_xml
    from app.scodoc import sco_cache
    from app.scodoc import sco_evaluations
    from app.scodoc.sco_exceptions import ScoValueError
    from app.scodoc import sco_formsemestre
    from app.scodoc import sco_preferences
    from app.tables.recap import TableRecap
    from app.tables.jury_recap import TableJury
    
    
    def formsemestre_recapcomplet(
        formsemestre_id=None,
        mode_jury=False,
        tabformat="html",
        xml_with_decisions=False,
        force_publishing=True,
        selected_etudid=None,
        visible_col_ids=None,
    ):
        """Page récapitulant les notes d'un semestre.
        Grand tableau récapitulatif avec toutes les notes de modules
        pour tous les étudiants, les moyennes par UE et générale,
        trié par moyenne générale décroissante.
    
        tabformat:
            html : page web
            evals : page web, avec toutes les évaluations dans le tableau
            xls, xlsx: export excel simple
            xlsall : export excel simple, avec toutes les évaluations dans le tableau
            csv : export CSV, avec toutes les évaluations
            xml, json : concaténation de tous les bulletins, au format demandé
            pdf : NON SUPPORTE (car tableau trop grand pour générer un pdf utilisable)
    
        mode_jury: cache modules, affiche lien saisie decision jury
        xml_with_decisions: publie décisions de jury dans xml et json
        force_publishing: publie les xml et json même si bulletins non publiés (sur la passerelle)
        selected_etudid: etudid sélectionné (pour scroller au bon endroit)
        """
        if not isinstance(formsemestre_id, int):
            abort(404)
        formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
        file_formats = {"csv", "json", "xls", "xlsx", "xlsall", "xlsvisible", "xml"}
        supported_formats = file_formats | {"html", "evals"}
        if tabformat not in supported_formats:
            raise ScoValueError(f"Format non supporté: {tabformat}")
        is_file = tabformat in file_formats
        mode_jury = int(mode_jury)
        xml_with_decisions = int(xml_with_decisions)
        force_publishing = int(force_publishing)
        visible_col_ids = visible_col_ids.split(",") if visible_col_ids else None
        filename = scu.sanitize_filename(
            f"""{'jury' if mode_jury else 'recap'
                }{'-evals' if tabformat == 'xlsall' else ''
                }-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
        )
        if is_file:
            return _formsemestre_recapcomplet_to_file(
                formsemestre,
                mode_jury=mode_jury,
                tabformat=tabformat,
                filename=filename,
                xml_with_decisions=xml_with_decisions,
                force_publishing=force_publishing,
                visible_col_ids=visible_col_ids,
            )
    
        table_html, _, freq_codes_annuels = _formsemestre_recapcomplet_to_html(
            formsemestre,
            filename=filename,
            mode_jury=mode_jury,
            tabformat=tabformat,
            selected_etudid=selected_etudid,
        )
    
        H = [
            # sco_formsemestre_status.formsemestre_status_head(
            #     formsemestre_id=formsemestre_id
            # ),
        ]
        if len(formsemestre.inscriptions) > 0:
            H.append(
                f"""<form id="export_menu" name="f" method="get" action="{request.base_url}">
                <input type="hidden" name="formsemestre_id" value="{formsemestre_id}"></input>
                <input type="hidden" id="visible_col_ids" name="visible_col_ids" value=""></input>
                """
            )
            if mode_jury:
                H.append(
                    f'<input type="hidden" name="mode_jury" value="{mode_jury}"></input>'
                )
            H.append(
                '<select name="tabformat" id="tabformat" onchange="submit_from_export_menu();" class="noprint">'
            )
            for fmt, label in (
                ("html", "Tableau"),
                ("evals", "Avec toutes les évaluations"),
                ("xlsx", "Excel (non formaté)"),
                ("xlsall", "Excel avec évaluations"),
                ("xlsvisible", "Excel avec colonnes telles affichées"),
                ("json", "Bulletins JSON"),
            ):
                if fmt == tabformat:
                    selected = " selected"
                else:
                    selected = ""
                H.append(f'<option value="{fmt}"{selected}>{label}</option>')
            H.append(
                f"""
                </select>&nbsp;<span class="help">cliquer sur un nom pour afficher son bulletin ou
                <a class="stdlink"
                href="{url_for('notes.formsemestre_bulletins_pdf',
                    scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
                }">ici avoir le classeur pdf</a>
                """
            )
            if formsemestre.formation.is_apc():
                H.append(
                    f"""&nbsp;ou en <a class="stdlink"
                href="{url_for('notes.formsemestre_bulletins_pdf',
                    scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, version="butcourt")
                }">version courte BUT</a>
                """
                )
    
            H.append(
                """</span>
                </form>
                """
            )
    
        H.append(table_html)  # La table
    
        if len(formsemestre.inscriptions) > 0:
            H.append("""<div class="links_under_recap"><ul>""")
            if not mode_jury:
                H.append(
                    f"""<li><a class="stdlink" href="{url_for('notes.formsemestre_recapcomplet',
                            scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
                    }">Décisions du jury</a>
                    </li>
                    """
                )
            if formsemestre.can_edit_jury():
                if mode_jury:
                    H.append(
                        f"""<li><a class="stdlink" href="{url_for('notes.formsemestre_validation_auto',
                        scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
                        }">Calcul automatique des décisions du jury</a>
                        </li>
                        <li><a class="stdlink" href="{url_for('notes.formsemestre_jury_erase',
                        scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, only_one_sem=1)
                        }">Effacer <em>toutes</em> les décisions de jury issues de ce semestre</a>
                        </li>
                        """
                    )
            if mode_jury:
                H.append(
                    f"""<li><a class="stdlink" href="{
                        url_for('notes.formsemestre_lettres_individuelles',
                            scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
                    }">Courriers individuels (classeur pdf)</a>
                    </li>
                    """
                )
                H.append(
                    f"""<li><a class="stdlink" href="{url_for('notes.formsemestre_pvjury_pdf',
                            scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
                    }">PV officiel (pdf)</a>
                    </li>
                    """
                )
            H.append("</ul></div>")
    
            if sco_preferences.get_preference("use_ue_coefs", formsemestre_id):
                H.append(
                    """
                <p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>
                """
                )
    
            if mode_jury and freq_codes_annuels and sum(freq_codes_annuels.values()) > 0:
                nb_etud_avec_decision_annuelle = (
                    sum(freq_codes_annuels.values()) - freq_codes_annuels["total"]
                )
                H.append(
                    f"""
                    <div class="jury_stats">
                        <div><b>Nb d'étudiants avec décision annuelle:</b>
                            {nb_etud_avec_decision_annuelle} / {freq_codes_annuels["total"]}
                        </div>
                    """
                )
                if nb_etud_avec_decision_annuelle > 0:
                    H.append(
                        """<div><b>Codes annuels octroyés:</b></div>
                        <table class="jury_stats_codes">
                        """
                    )
                    for code in sorted(freq_codes_annuels.keys()):
                        if code != "total":
                            H.append(
                                f"""<tr>
                                <td>{code}</td>
                                <td style="text-align:right">{freq_codes_annuels[code]}</td>
                                <td style="text-align:right">{
                                    (100*freq_codes_annuels[code] / freq_codes_annuels["total"]):2.1f}%
                                </td>
                                </tr>"""
                            )
                    H.append("""</table>""")
                H.append("""</div>""")
        # Légende
        H.append(
            """
        <div class="table_recap_caption">
            <div class="title">Codes utilisés dans cette table:</div>
            <div class="captions">
                <div><tt>~</tt></div><div>valeur manquante</div>
                <div><tt>=</tt></div><div>UE dispensée</div>
                <div><tt>nan</tt></div><div>valeur non disponible</div>
                <div>📍</div><div>code jury non enregistré</div>
                <div><span class="ue_hors_parcours">12.34</span></div><div>UE hors parcours</div>
            </div>
        </div>
        """
        )
        # HTML or binary data ?
        if len(H) > 1:
            return render_template(
                "sco_page.j2",
                content="".join(H),
                title=f"{formsemestre.sem_modalite()}: "
                + ("jury" if mode_jury else "moyennes"),
                javascripts=["js/table_recap.js"],
                formsemestre_id=formsemestre_id,
                no_sidebar=True,
            )
        elif len(H) == 1:
            return H[0]
        else:
            return H
    
    
    def _formsemestre_recapcomplet_to_html(
        formsemestre: FormSemestre,
        tabformat="html",  # "html" or "evals"
        filename: str = "",
        mode_jury=False,  # saisie décisions jury
        selected_etudid=None,
    ) -> tuple[str, TableRecap, collections.Counter]:
        """Le tableau recap en html"""
        if tabformat not in ("html", "evals"):
            raise ScoValueError("invalid table format")
        res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
        table_html, table, freq_codes_annuels = gen_formsemestre_recapcomplet_html_table(
            formsemestre,
            res,
            include_evaluations=(tabformat == "evals"),
            mode_jury=mode_jury,
            filename=filename,
            selected_etudid=selected_etudid,
        )
        return table_html, table, freq_codes_annuels
    
    
    def _formsemestre_recapcomplet_to_file(
        formsemestre: FormSemestre,
        tabformat: str = "json",  # xml, xls, xlsall, json
        mode_jury: bool = False,
        filename: str = "",
        xml_nodate=False,  # format XML sans dates (sert pour debug cache: comparaison de XML)
        xml_with_decisions=False,
        force_publishing=True,
        visible_col_ids=None,
    ):
        """Calcule et renvoie le tableau récapitulatif."""
        if tabformat.startswith("xls"):
            include_evaluations = tabformat == "xlsall"
            visible_col_ids = visible_col_ids if tabformat == "xlsvisible" else None
            res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
            data, filename = gen_formsemestre_recapcomplet_excel(
                res,
                mode_jury=mode_jury,
                include_evaluations=include_evaluations,
                filename=filename,
                visible_col_ids=visible_col_ids,
            )
            mime, suffix = scu.get_mime_suffix("xlsx")
            return scu.send_file(data, filename=filename, mime=mime, suffix=suffix)
        elif tabformat == "xml":
            data = gen_formsemestre_recapcomplet_xml(
                formsemestre.id,
                xml_nodate,
                xml_with_decisions=xml_with_decisions,
                force_publishing=force_publishing,
            )
            return scu.send_file(data, filename=filename, suffix=scu.XML_SUFFIX)
        elif tabformat == "json":
            data = gen_formsemestre_recapcomplet_json(
                formsemestre.id,
                xml_nodate=xml_nodate,
                xml_with_decisions=xml_with_decisions,
                force_publishing=force_publishing,
            )
            return scu.sendJSON(data, filename=filename)
    
        raise ScoValueError(f"Format demandé invalide: {tabformat}")
    
    
    def gen_formsemestre_recapcomplet_xml(
        formsemestre_id,
        xml_nodate,
        xml_with_decisions=False,
        force_publishing=True,
    ) -> str:
        "XML export: liste tous les bulletins XML."
        formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
        nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
        T = nt.get_table_moyennes_triees()
        if not T:
            return "", "", "xml"
    
        if xml_nodate:
            docdate = ""
        else:
            docdate = datetime.datetime.now().isoformat()
        doc = ElementTree.Element(
            "recapsemestre", formsemestre_id=str(formsemestre_id), date=docdate
        )
        evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre)
        doc.append(
            ElementTree.Element(
                "evals_info",
                nb_evals_completes=str(evals["nb_evals_completes"]),
                nb_evals_en_cours=str(evals["nb_evals_en_cours"]),
                nb_evals_vides=str(evals["nb_evals_vides"]),
                date_derniere_note=str(evals["last_modif"]),
            )
        )
        for t in T:
            etudid = t[-1]
            sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
                formsemestre_id,
                etudid,
                doc=doc,
                force_publishing=force_publishing,
                xml_nodate=xml_nodate,
                xml_with_decisions=xml_with_decisions,
            )
        return ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
    
    
    def gen_formsemestre_recapcomplet_json(
        formsemestre_id,
        xml_nodate=False,
        xml_with_decisions=False,
        force_publishing=True,
    ) -> dict:
        """JSON export: liste tous les bulletins JSON
        :param xml_nodate(bool): indique la date courante (attribut docdate)
        :param force_publishing: donne les bulletins même si non "publiés sur la passerelle"
        :returns: dict
        """
        formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
        is_apc = formsemestre.formation.is_apc()
    
        if xml_nodate:
            docdate = ""
        else:
            docdate = datetime.datetime.now().isoformat()
        evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre)
        js_data = {
            "docdate": docdate,
            "formsemestre_id": formsemestre_id,
            "evals_info": {
                "nb_evals_completes": evals["nb_evals_completes"],
                "nb_evals_en_cours": evals["nb_evals_en_cours"],
                "nb_evals_vides": evals["nb_evals_vides"],
                "date_derniere_note": evals["last_modif"],
            },
            "bulletins": [],
        }
        bulletins = js_data["bulletins"]
        formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
        nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
        T = nt.get_table_moyennes_triees()
        for t in T:
            etudid = t[-1]
            if is_apc:
                etud = Identite.get_etud(etudid)
                bulletins_sem = bulletin_but.BulletinBUT(formsemestre)
                bul = bulletins_sem.bulletin_etud(etud)
            else:
                bul = sco_bulletins_json.formsemestre_bulletinetud_published_dict(
                    formsemestre_id,
                    etudid,
                    force_publishing=force_publishing,
                    xml_with_decisions=xml_with_decisions,
                )
            bulletins.append(bul)
        return js_data
    
    
    def formsemestres_bulletins(annee_scolaire):
        """Tous les bulletins des semestres de l'année indiquée.
        :param annee_scolaire(int): année de début de l'année scolaire
        :returns: JSON
        """
        js_list = []
        sems = sco_formsemestre.list_formsemestre_by_etape(annee_scolaire=annee_scolaire)
        log(f"formsemestres_bulletins({annee_scolaire}): {len(sems)} sems")
        for sem in sems:
            js_data = gen_formsemestre_recapcomplet_json(
                sem["formsemestre_id"], force_publishing=False
            )
            js_list.append(js_data)
    
        return scu.sendJSON(js_list)
    
    
    def gen_formsemestre_recapcomplet_html_table(
        formsemestre: FormSemestre,
        res: NotesTableCompat,
        include_evaluations=False,
        mode_jury=False,
        filename="",
        selected_etudid=None,
    ) -> tuple[str, TableRecap, collections.Counter]:
        """Construit table recap pour le BUT
        Cache le résultat pour le semestre.
        Note: on cache le HTML et non l'objet Table.
    
        Si mode_jury, occultera colonnes modules (en js)
        et affiche un lien vers la saisie de la décision de jury
    
        Return: html (str), table (None sauf en mode jury ou si pas cachée)
    
        html est une chaine, le <div>...</div> incluant le tableau.
        """
        table = None
        table_html = None
        table_html_cached = 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_cached = cache_class.get(formsemestre.id)
        if table_html_cached is None:
            table = _gen_formsemestre_recapcomplet_table(
                res,
                include_evaluations=include_evaluations,
                mode_jury=mode_jury,
                filename=filename,
                selected_etudid=selected_etudid,
            )
            table_html = table.html()
            freq_codes_annuels = (
                table.freq_codes_annuels if hasattr(table, "freq_codes_annuels") else None
            )
            cache_class.set(formsemestre.id, (table_html, freq_codes_annuels))
        else:
            table_html, freq_codes_annuels = table_html_cached
    
        return table_html, table, freq_codes_annuels
    
    
    def _gen_formsemestre_recapcomplet_table(
        res: ResultatsSemestre,
        include_email_addresses=False,
        include_evaluations=False,
        mode_jury=False,
        convert_values: bool = True,
        filename: str = "",
        selected_etudid=None,
    ) -> TableRecap:
        """Construit la table récap."""
        table_class = TableJury if mode_jury else TableRecap
        table = table_class(
            res,
            convert_values=convert_values,
            include_email_addresses=include_email_addresses,
            include_evaluations=include_evaluations,
            mode_jury=mode_jury,
            read_only=not res.formsemestre.can_edit_jury(),
        )
    
        table.data["filename"] = filename
        table.select_row(selected_etudid)
        return table
    
    
    def gen_formsemestre_recapcomplet_excel(
        res: NotesTableCompat,
        mode_jury: bool = False,
        include_evaluations=False,
        filename: str = "",
        visible_col_ids: list[str] | None = None,
    ) -> tuple:
        """Génère le tableau recap ou jury en excel (xlsx).
        Utilisé pour menu (export excel), archives et autres besoins particuliers (API).
        Attention: le tableau exporté depuis la page html est celui généré en js par DataTables,
         et non celui-ci.
        Si visible_col_ids est non None, ne génère que les colonnes indiquées (+ les codes étudiants, toujours présents)
        """
        # En excel, ajoute les adresses mail, si on a le droit de les voir.
        table = _gen_formsemestre_recapcomplet_table(
            res,
            include_email_addresses=current_user.has_permission(Permission.ViewEtudData),
            include_evaluations=include_evaluations,
            mode_jury=mode_jury,
            convert_values=False,
            filename=filename,
        )
        if visible_col_ids is not None:
            # Ajoute colonnes qui doivent toujours être présentes en excel:
            for cod in ("code_nip", "etudid"):
                if cod not in visible_col_ids:
                    visible_col_ids = [cod] + visible_col_ids
        return table.excel(col_ids=visible_col_ids), filename