diff --git a/app/comp/res_common.py b/app/comp/res_common.py index d7240598..7245439f 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -18,7 +18,7 @@ from app.auth.models import User from app.comp.res_cache import ResultatsCache from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults -from app.models import FormSemestre, FormSemestreUECoef +from app.models import FormSemestre, FormSemestreUECoef, formsemestre from app.models import Identite from app.models import ModuleImpl, ModuleImplInscription from app.models.ues import UniteEns @@ -388,7 +388,9 @@ class ResultatsSemestre(ResultatsCache): # --- TABLEAU RECAP - def get_table_recap(self, convert_values=False, include_evaluations=False): + def get_table_recap( + self, convert_values=False, include_evaluations=False, modejury=False + ): """Result: tuple avec - rows: liste de dicts { column_id : value } - titles: { column_id : title } @@ -538,6 +540,9 @@ class ResultatsSemestre(ResultatsCache): titles_bot[ f"_{col_id}_target_attrs" ] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """ + if modejury: + # pas d'autre colonnes de résultats + continue # Bonus (sport) dans cette UE ? # Le bonus sport appliqué sur cette UE if (self.bonus_ues is not None) and (ue.id in self.bonus_ues): @@ -632,6 +637,18 @@ class ResultatsSemestre(ResultatsCache): elif nb_ues_validables < len(ues_sans_bonus): row["_ues_validables_class"] += " moy_inf" row["_ues_validables_order"] = nb_ues_validables # pour tri + if modejury: + idx = add_cell( + row, + "jury_link", + "", + f"""saisir décision""", + "col_jury_link", + 1000, + ) rows.append(row) self._recap_add_partitions(rows, titles) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 275605d8..50b11f6f 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -404,9 +404,6 @@ def formsemestre_status_menubar(sem): "args": { "formsemestre_id": formsemestre_id, "modejury": 1, - "hidemodules": 1, - "hidebac": 1, - "pref_override": 0, }, "enabled": sco_permissions_check.can_validate_sem(formsemestre_id), }, diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index ba517e6a..f4896a8d 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -31,6 +31,7 @@ import time import flask from flask import url_for, g, request +from app.models.etudiants import Identite import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -107,29 +108,57 @@ def formsemestre_validation_etud_form( if not Se.sem["etat"]: raise ScoValueError("validation: semestre verrouille") + url_tableau = url_for( + "notes.formsemestre_recapcomplet", + scodoc_dept=g.scodoc_dept, + modejury=1, + formsemestre_id=formsemestre_id, + selected_etudid=etudid, # va a la bonne ligne + ) + H = [ html_sco_header.sco_header( - page_title="Parcours %(nomprenom)s" % etud, + page_title=f"Parcours {etud['nomprenom']}", javascripts=["js/recap_parcours.js"], ) ] - Footer = ["

"] # Navigation suivant/precedent - if etud_index_prev != None: - etud_p = sco_etud.get_etud_info(etudid=T[etud_index_prev][-1], filled=True)[0] - Footer.append( - 'Etud. précédent (%s)' - % (formsemestre_id, etud_index_prev, etud_p["nomprenom"]) + if etud_index_prev is not None: + etud_prev = Identite.query.get(T[etud_index_prev][-1]) + url_prev = url_for( + "notes.formsemestre_validation_etud_form", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + etud_index=etud_index_prev, ) - if etud_index_next != None: - etud_n = sco_etud.get_etud_info(etudid=T[etud_index_next][-1], filled=True)[0] - Footer.append( - 'Etud. suivant (%s)' - % (formsemestre_id, etud_index_next, etud_n["nomprenom"]) + else: + url_prev = None + if etud_index_next is not None: + etud_next = Identite.query.get(T[etud_index_next][-1]) + url_next = url_for( + "notes.formsemestre_validation_etud_form", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + etud_index=etud_index_next, ) - Footer.append("

") - Footer.append(html_sco_header.sco_footer()) + else: + url_next = None + footer = ["""") + + footer.append(html_sco_header.sco_footer()) H.append('{"".join([_gen_cell(key, row, elt) for key in keys])}' + tr_id = ( + f"""id="row_selected" """ if (row.get("etudid", "") == selected_etudid) else "" + ) + return f'{"".join([_gen_cell(key, row, elt) for key in keys])}' def gen_formsemestre_recapcomplet_html( formsemestre: FormSemestre, res: NotesTableCompat, include_evaluations=False, + modejury=False, filename="", + selected_etudid=None, ): """Construit table recap pour le BUT - Cache le résultat pour le semestre. + Cache le résultat pour le semestre (sauf en mode jury). + + Si modejury, cache colonnes modules et affiche un lien vers la saisie de la décision de jury + Return: data, filename data est une chaine, le
...
incluant le tableau. """ - if include_evaluations: - table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id) - else: - table_html = sco_cache.TableRecapCache.get(formsemestre.id) - if table_html is None: - table_html = _gen_formsemestre_recapcomplet_html( - formsemestre, res, include_evaluations, filename - ) + table_html = None + if not (modejury or selected_etudid): if include_evaluations: - sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html) + table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id) else: - sco_cache.TableRecapCache.set(formsemestre.id, table_html) + table_html = sco_cache.TableRecapCache.get(formsemestre.id) + if modejury or (table_html is None): + table_html = _gen_formsemestre_recapcomplet_html( + formsemestre, + res, + include_evaluations, + modejury, + filename, + selected_etudid=selected_etudid, + ) + if not modejury: + if include_evaluations: + sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html) + else: + sco_cache.TableRecapCache.set(formsemestre.id, table_html) return table_html @@ -425,11 +426,13 @@ def _gen_formsemestre_recapcomplet_html( formsemestre: FormSemestre, res: NotesTableCompat, include_evaluations=False, + modejury=False, filename: str = "", + selected_etudid=None, ) -> str: """Génère le html""" rows, footer_rows, titles, column_ids = res.get_table_recap( - convert_values=True, include_evaluations=include_evaluations + convert_values=True, include_evaluations=include_evaluations, modejury=modejury ) if not rows: return ( @@ -437,7 +440,8 @@ def _gen_formsemestre_recapcomplet_html( ) H = [ f"""
') if not check: @@ -171,7 +200,7 @@ def formsemestre_validation_etud_form( """ ) ) - return "\n".join(H + Footer) + return "\n".join(H + footer) H.append( formsemestre_recap_parcours_table( @@ -180,18 +209,10 @@ def formsemestre_validation_etud_form( ) if check: if not desturl: - desturl = url_for( - "notes.formsemestre_recapcomplet", - scodoc_dept=g.scodoc_dept, - modejury=1, - formsemestre_id=formsemestre_id, - sortcol=sortcol - or None, # pour refaire tri sorttable du tableau de notes - _anchor="etudid%s" % etudid, # va a la bonne ligne - ) + desturl = url_tableau H.append(f'') - return "\n".join(H + Footer) + return "\n".join(H + footer) decision_jury = Se.nt.get_etud_decision_sem(etudid) @@ -207,7 +228,7 @@ def formsemestre_validation_etud_form( """ ) ) - return "\n".join(H + Footer) + return "\n".join(H + footer) # Infos si pas de semestre précédent if not Se.prev: @@ -345,7 +366,7 @@ def formsemestre_validation_etud_form( else: H.append("sans semestres décalés

") - return "".join(H + Footer) + return "".join(H + footer) def formsemestre_validation_etud( @@ -937,19 +958,23 @@ def do_formsemestre_validation_auto(formsemestre_id): ) if conflicts: H.append( - """

Attention: %d étudiants non modifiés car décisions différentes - déja saisies :

    """ - % len(conflicts) + f"""

    Attention: {len(conflicts)} étudiants non modifiés + car décisions différentes déja saisies : +

    ") H.append( - 'continuer' - % formsemestre_id + f"""continuer""" ) H.append(html_sco_header.sco_footer()) return "\n".join(H) diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 72d16012..0dc6b19c 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -32,7 +32,7 @@ import time from xml.etree import ElementTree from flask import g, request -from flask import make_response, url_for +from flask import url_for from app import log from app.but import bulletin_but @@ -40,39 +40,29 @@ from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.models.etudiants import Identite -from app.models.evaluations import Evaluation from app.scodoc.gen_tables import GenTable import app.scodoc.sco_utils as scu from app.scodoc import html_sco_header from app.scodoc import sco_bulletins_json from app.scodoc import sco_bulletins_xml -from app.scodoc import sco_bulletins, sco_excel from app.scodoc import sco_cache -from app.scodoc import sco_codes_parcours from app.scodoc import sco_evaluations -from app.scodoc import sco_evaluation_db from app.scodoc.sco_exceptions import ScoValueError -from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_status -from app.scodoc import sco_groups from app.scodoc import sco_permissions_check from app.scodoc import sco_preferences -from app.scodoc import sco_etud -from app.scodoc import sco_users -from app.scodoc import sco_xml -from app.scodoc.sco_codes_parcours import DEF, UE_SPORT def formsemestre_recapcomplet( formsemestre_id=None, - modejury=False, # affiche lien saisie decision jury + modejury=False, tabformat="html", sortcol=None, - xml_with_decisions=False, # XML avec decisions - rank_partition_id=None, # si None, calcul rang global - force_publishing=True, # publie les XML/JSON meme si bulletins non publiés + xml_with_decisions=False, + force_publishing=True, + selected_etudid=None, ): """Page récapitulant les notes d'un semestre. Grand tableau récapitulatif avec toutes les notes de modules @@ -89,7 +79,9 @@ def formsemestre_recapcomplet( pdf : NON SUPPORTE (car tableau trop grand pour générer un pdf utilisable) modejury: 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 + selected_etudid: etudid sélectionné (pour scroller au bon endroit) """ formsemestre = FormSemestre.query.get_or_404(formsemestre_id) @@ -98,98 +90,92 @@ def formsemestre_recapcomplet( xml_with_decisions = int(xml_with_decisions) force_publishing = int(force_publishing) is_file = tabformat in {"csv", "json", "xls", "xlsx", "xlsall", "xml"} - H = [] - if not is_file: - H += [ - html_sco_header.sco_header( - page_title="Récapitulatif", - no_side_bar=True, - init_qtip=True, - javascripts=["js/etud_info.js", "js/table_recap.js"], - ), - sco_formsemestre_status.formsemestre_status_head( - formsemestre_id=formsemestre_id - ), - ] - if len(formsemestre.inscriptions) > 0: - H.append( - f"""
    - - """ - ) - if modejury: - H.append( - f'' - ) - H.append( - '") - - H.append( - f""" (cliquer sur un nom pour afficher son bulletin ou ici avoir le classeur papier) - """ - ) - - data = do_formsemestre_recapcomplet( + data = _do_formsemestre_recapcomplet( formsemestre_id, format=tabformat, modejury=modejury, sortcol=sortcol, xml_with_decisions=xml_with_decisions, - rank_partition_id=rank_partition_id, force_publishing=force_publishing, + selected_etudid=selected_etudid, ) - if tabformat == "xml": - response = make_response(data) - response.headers["Content-Type"] = scu.XML_MIMETYPE - return response + if is_file: + return data + H = [ + html_sco_header.sco_header( + page_title="Récapitulatif", + no_side_bar=True, + init_qtip=True, + javascripts=["js/etud_info.js", "js/table_recap.js"], + ), + sco_formsemestre_status.formsemestre_status_head( + formsemestre_id=formsemestre_id + ), + ] + if len(formsemestre.inscriptions) > 0: + H.append( + f""" + + """ + ) + if modejury: + H.append( + f'' + ) + H.append( + '") + + H.append( + f""" (cliquer sur un nom pour afficher son bulletin ou ici avoir le classeur papier) + """ + ) H.append(data) - if not is_file: - if len(formsemestre.inscriptions) > 0: - H.append("
    ") - H.append( - f"""

    Voir les décisions du jury

    """ - ) - if sco_permissions_check.can_validate_sem(formsemestre_id): - H.append("

    ") - if modejury: - H.append( - f"""Calcul automatique des décisions du jury

    """ - ) - else: - H.append( - f"""Saisie des décisions du jury""" - ) - H.append("

    ") - if sco_preferences.get_preference("use_ue_coefs", formsemestre_id): + if len(formsemestre.inscriptions) > 0: + H.append("") + H.append( + f"""

    Voir les décisions du jury

    """ + ) + if sco_permissions_check.can_validate_sem(formsemestre_id): + H.append("

    ") + if modejury: H.append( - """ -

    utilise les coefficients d'UE pour calculer la moyenne générale.

    - """ + f"""Calcul automatique des décisions du jury

    """ ) - H.append(html_sco_header.sco_footer()) + else: + H.append( + f"""Saisie des décisions du jury""" + ) + H.append("

    ") + if sco_preferences.get_preference("use_ue_coefs", formsemestre_id): + H.append( + """ +

    utilise les coefficients d'UE pour calculer la moyenne générale.

    + """ + ) + H.append(html_sco_header.sco_footer()) # HTML or binary data ? if len(H) > 1: return "".join(H) @@ -199,18 +185,15 @@ def formsemestre_recapcomplet( return H -def do_formsemestre_recapcomplet( +def _do_formsemestre_recapcomplet( formsemestre_id=None, format="html", # html, xml, xls, xlsall, json - hidemodules=False, # ne pas montrer les modules (ignoré en XML) - hidebac=False, # pas de colonne Bac (ignoré en XML) xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML) modejury=False, # saisie décisions jury sortcol=None, # indice colonne a trier dans table T xml_with_decisions=False, - disable_etudlink=False, - rank_partition_id=None, # si None, calcul rang global force_publishing=True, + selected_etudid=None, ): """Calcule et renvoie le tableau récapitulatif.""" formsemestre = FormSemestre.query.get_or_404(formsemestre_id) @@ -219,13 +202,15 @@ def do_formsemestre_recapcomplet( f"""recap-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}""" ) - if (format == "html" or format == "evals") and not modejury: + if format == "html" or format == "evals": res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) data = gen_formsemestre_recapcomplet_html( formsemestre, res, include_evaluations=(format == "evals"), + modejury=modejury, filename=filename, + selected_etudid=selected_etudid, ) return data elif format.startswith("xls") or format == "csv": @@ -388,35 +373,51 @@ def _gen_cell(key: str, row: dict, elt="td"): return f"<{elt} {attrs}>{content}" -def _gen_row(keys: list[str], row, elt="td"): +def _gen_row(keys: list[str], row, elt="td", selected_etudid=None): klass = row.get("_tr_class") tr_class = f'class="{klass}"' if klass else "" - return f'
""" ] # header @@ -451,7 +455,7 @@ def _gen_formsemestre_recapcomplet_html( # body H.append("") for row in rows: - H.append(f"{_gen_row(column_ids, row)}\n") + H.append(f"{_gen_row(column_ids, row, selected_etudid=selected_etudid)}\n") H.append("\n") # footer H.append("") diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index e9712ba7..3625d5f0 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1146,6 +1146,18 @@ span.jurylink a { text-decoration: underline; } +div.jury_footer { + display: flex; + justify-content: space-evenly; +} + +div.jury_footer>span { + border: 2px solid rgb(90, 90, 90); + border-radius: 4px; + padding: 4px; + background-color: rgb(230, 242, 230); +} + .eval_description p { margin-left: 15px; margin-bottom: 2px; diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js index 174fb186..4dcd0c23 100644 --- a/app/static/js/table_recap.js +++ b/app/static/js/table_recap.js @@ -21,43 +21,54 @@ $(function () { dt.columns(".partition_aux").visible(!visible); dt.buttons('toggle_partitions:name').text(visible ? "Toutes les partitions" : "Cacher les partitions"); } - }, - $('table.table_recap').hasClass("apc") ? - { - name: "toggle_res", - text: "Cacher les ressources", - action: function (e, dt, node, config) { - let visible = dt.columns(".col_res").visible()[0]; - dt.columns(".col_res").visible(!visible); - dt.columns(".col_ue_bonus").visible(!visible); - dt.columns(".col_malus").visible(!visible); - dt.buttons('toggle_res:name').text(visible ? "Montrer les ressources" : "Cacher les ressources"); + }]; + if (!$('table.table_recap').hasClass("jury")) { + buttons.push( + $('table.table_recap').hasClass("apc") ? + { + name: "toggle_res", + text: "Cacher les ressources", + action: function (e, dt, node, config) { + let visible = dt.columns(".col_res").visible()[0]; + dt.columns(".col_res").visible(!visible); + dt.columns(".col_ue_bonus").visible(!visible); + dt.columns(".col_malus").visible(!visible); + dt.buttons('toggle_res:name').text(visible ? "Montrer les ressources" : "Cacher les ressources"); + } + } : { + name: "toggle_mod", + text: "Cacher les modules", + action: function (e, dt, node, config) { + let visible = dt.columns(".col_mod:not(.col_empty)").visible()[0]; + dt.columns(".col_mod:not(.col_empty)").visible(!visible); + dt.columns(".col_ue_bonus").visible(!visible); + dt.columns(".col_malus").visible(!visible); + dt.buttons('toggle_mod:name').text(visible ? "Montrer les modules" : "Cacher les modules"); + visible = dt.columns(".col_empty").visible()[0]; + dt.buttons('toggle_col_empty:name').text(visible ? "Cacher mod. vides" : "Montrer mod. vides"); + } } - } : { - name: "toggle_mod", - text: "Cacher les modules", + ); + if ($('table.table_recap').hasClass("apc")) { + buttons.push({ + name: "toggle_sae", + text: "Cacher les SAÉs", action: function (e, dt, node, config) { - let visible = dt.columns(".col_mod:not(.col_empty)").visible()[0]; - dt.columns(".col_mod:not(.col_empty)").visible(!visible); - dt.columns(".col_ue_bonus").visible(!visible); - dt.columns(".col_malus").visible(!visible); - dt.buttons('toggle_mod:name').text(visible ? "Montrer les modules" : "Cacher les modules"); - visible = dt.columns(".col_empty").visible()[0]; - dt.buttons('toggle_col_empty:name').text(visible ? "Cacher mod. vides" : "Montrer mod. vides"); + let visible = dt.columns(".col_sae").visible()[0]; + dt.columns(".col_sae").visible(!visible); + dt.buttons('toggle_sae:name').text(visible ? "Montrer les SAÉs" : "Cacher les SAÉs"); } - } - ]; - if ($('table.table_recap').hasClass("apc")) { + }) + } buttons.push({ - name: "toggle_sae", - text: "Cacher les SAÉs", + name: "toggle_col_empty", + text: "Montrer mod. vides", action: function (e, dt, node, config) { - let visible = dt.columns(".col_sae").visible()[0]; - dt.columns(".col_sae").visible(!visible); - dt.buttons('toggle_sae:name').text(visible ? "Montrer les SAÉs" : "Cacher les SAÉs"); + let visible = dt.columns(".col_empty").visible()[0]; + dt.columns(".col_empty").visible(!visible); + dt.buttons('toggle_col_empty:name').text(visible ? "Montrer mod. vides" : "Cacher mod. vides"); } }) - } buttons.push({ name: "toggle_admission", @@ -68,15 +79,6 @@ $(function () { dt.buttons('toggle_admission:name').text(visible ? "Montrer infos admission" : "Cacher infos admission"); } }) - buttons.push({ - name: "toggle_col_empty", - text: "Montrer mod. vides", - action: function (e, dt, node, config) { - let visible = dt.columns(".col_empty").visible()[0]; - dt.columns(".col_empty").visible(!visible); - dt.buttons('toggle_col_empty:name').text(visible ? "Montrer mod. vides" : "Cacher mod. vides"); - } - }) $('table.table_recap').DataTable( { paging: false, @@ -143,4 +145,10 @@ $(function () { $(this).addClass('selected'); } }); + // Pour montrer et highlihter l'étudiant sélectionné: + $(function () { + document.querySelector("#row_selected").scrollIntoView(); + window.scrollBy(0, -50); + document.querySelector("#row_selected").classList.add("selected"); + }); });