Tableau saisie jury basé sur recap_complet

This commit is contained in:
Emmanuel Viennet 2022-04-07 23:31:08 +02:00
parent ec57ba4ef7
commit 7a8c77add4
6 changed files with 260 additions and 197 deletions

View File

@ -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"""<a href="{url_for('notes.formsemestre_validation_etud_form',
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid
)
}">saisir décision</a>""",
"col_jury_link",
1000,
)
rows.append(row)
self._recap_add_partitions(rows, titles)

View File

@ -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),
},

View File

@ -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 = ["<p>"]
# 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(
'<span><a href="formsemestre_validation_etud_form?formsemestre_id=%s&etud_index=%s">Etud. précédent (%s)</a></span>'
% (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(
'<span style="padding-left: 50px;"><a href="formsemestre_validation_etud_form?formsemestre_id=%s&etud_index=%s">Etud. suivant (%s)</a></span>'
% (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("</p>")
Footer.append(html_sco_header.sco_footer())
else:
url_next = None
footer = ["""<div class="jury_footer"><span>"""]
if url_prev:
footer.append(
f'< <a class="stdlink" href="{url_prev}">{etud_prev.nomprenom}</a>'
)
footer.append(
f"""</span><span><a class="stdlink" href="{url_tableau}">retour à la liste</a></span><span>"""
)
if url_next:
footer.append(
f'<a class="stdlink" href="{url_next}">{etud_next.nomprenom}</a> >'
)
footer.append("</span></div>")
footer.append(html_sco_header.sco_footer())
H.append('<table style="width: 100%"><tr><td>')
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'<ul><li><a href="{desturl}">Continuer</a></li></ul>')
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</p>")
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(
"""<p><b>Attention:</b> %d étudiants non modifiés car décisions différentes
déja saisies :<ul>"""
% len(conflicts)
f"""<p><b>Attention:</b> {len(conflicts)} étudiants non modifiés
car décisions différentes déja saisies :
<ul>"""
)
for etud in conflicts:
H.append(
'<li><a href="formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&check=1">%s</li>'
% (formsemestre_id, etud["etudid"], etud["nomprenom"])
f"""<li><a href="{
url_for('notes.formsemestre_validation_etud_form',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id,
etudid=etud["etudid"], check=1)
}">{etud["nomprenom"]}</li>"""
)
H.append("</ul>")
H.append(
'<a href="formsemestre_recapcomplet?formsemestre_id=%s&modejury=1&hidemodules=1&hidebac=1&pref_override=0">continuer</a>'
% formsemestre_id
f"""<a href="{url_for('notes.formsemestre_recapcomplet',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, modejury=1)
}">continuer</a>"""
)
H.append(html_sco_header.sco_footer())
return "\n".join(H)

View File

@ -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,9 +90,18 @@ 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 += [
data = _do_formsemestre_recapcomplet(
formsemestre_id,
format=tabformat,
modejury=modejury,
sortcol=sortcol,
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
selected_etudid=selected_etudid,
)
if is_file:
return data
H = [
html_sco_header.sco_header(
page_title="Récapitulatif",
no_side_bar=True,
@ -144,23 +145,8 @@ def formsemestre_recapcomplet(
}">ici avoir le classeur papier</a>)
"""
)
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,
)
if tabformat == "xml":
response = make_response(data)
response.headers["Content-Type"] = scu.XML_MIMETYPE
return response
H.append(data)
if not is_file:
if len(formsemestre.inscriptions) > 0:
H.append("</form>")
H.append(
@ -172,8 +158,8 @@ def formsemestre_recapcomplet(
H.append("<p>")
if modejury:
H.append(
f"""<a class="stdlink" href="{url_for('formsemestre_validation_auto',
formsemestre_id=formsemestre_id)
f"""<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></p>"""
)
else:
@ -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,31 +373,47 @@ def _gen_cell(key: str, row: dict, elt="td"):
return f"<{elt} {attrs}>{content}</{elt}>"
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'<tr {tr_class}>{"".join([_gen_cell(key, row, elt) for key in keys])}</tr>'
tr_id = (
f"""id="row_selected" """ if (row.get("etudid", "") == selected_etudid) else ""
)
return f'<tr {tr_id} {tr_class}>{"".join([_gen_cell(key, row, elt) for key in keys])}</tr>'
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 <div>...</div> incluant le tableau.
"""
table_html = None
if not (modejury or selected_etudid):
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:
if modejury or (table_html is None):
table_html = _gen_formsemestre_recapcomplet_html(
formsemestre, res, include_evaluations, filename
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:
@ -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"""<div class="table_recap"><table class="table_recap {
'apc' if formsemestre.formation.is_apc() else 'classic'}"
'apc' if formsemestre.formation.is_apc() else 'classic'
} {'jury' if modejury else ''}"
data-filename="{filename}">"""
]
# header
@ -451,7 +455,7 @@ def _gen_formsemestre_recapcomplet_html(
# body
H.append("<tbody>")
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("</tbody>\n")
# footer
H.append("<tfoot>")

View File

@ -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;

View File

@ -21,7 +21,9 @@ $(function () {
dt.columns(".partition_aux").visible(!visible);
dt.buttons('toggle_partitions:name').text(visible ? "Toutes les partitions" : "Cacher les partitions");
}
},
}];
if (!$('table.table_recap').hasClass("jury")) {
buttons.push(
$('table.table_recap').hasClass("apc") ?
{
name: "toggle_res",
@ -46,7 +48,7 @@ $(function () {
dt.buttons('toggle_col_empty:name').text(visible ? "Cacher mod. vides" : "Montrer mod. vides");
}
}
];
);
if ($('table.table_recap').hasClass("apc")) {
buttons.push({
name: "toggle_sae",
@ -57,7 +59,16 @@ $(function () {
dt.buttons('toggle_sae:name').text(visible ? "Montrer les SAÉs" : "Cacher les SAÉs");
}
})
}
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");
}
})
}
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");
});
});