Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc into iziram-main96

This commit is contained in:
Emmanuel Viennet 2024-04-23 18:31:10 +02:00
commit 2f2e25171e
11 changed files with 172 additions and 117 deletions

View File

@ -3,14 +3,15 @@
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
"""ScoDoc 9 API : Assiduités """ScoDoc 9 API : Assiduités"""
"""
from datetime import datetime from datetime import datetime
from flask import g, request from flask import g, request
from flask_json import as_json from flask_json import as_json
from flask_login import current_user, login_required from flask_login import current_user, login_required
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
from sqlalchemy.orm.exc import ObjectDeletedError
from app import db, log, set_sco_dept from app import db, log, set_sco_dept
import app.scodoc.sco_assiduites as scass import app.scodoc.sco_assiduites as scass
@ -858,7 +859,10 @@ def assiduite_edit(assiduite_id: int):
msg=f"assiduite: modif {assiduite_unique}", msg=f"assiduite: modif {assiduite_unique}",
) )
db.session.commit() db.session.commit()
scass.simple_invalidate_cache(assiduite_unique.to_dict()) try:
scass.simple_invalidate_cache(assiduite_unique.to_dict())
except ObjectDeletedError:
return json_error(404, "Assiduité supprimée / inexistante")
return {"OK": True} return {"OK": True}

View File

@ -611,16 +611,17 @@ class BasePreferences:
"explanation": "toute saisie d'absence doit indiquer le module concerné", "explanation": "toute saisie d'absence doit indiquer le module concerné",
}, },
), ),
# ( (
# "forcer_present", "non_present",
# { {
# "initvalue": 0, "initvalue": 0,
# "title": "Forcer l'appel des présents", "title": "Désactiver la saisie des présences",
# "input_type": "boolcheckbox", "input_type": "boolcheckbox",
# "labels": ["non", "oui"], "labels": ["non", "oui"],
# "category": "assi", "category": "assi",
# }, "explanation": "Désactive la saisie et l'affichage des présences",
# ), },
),
( (
"periode_defaut", "periode_defaut",
{ {
@ -644,18 +645,18 @@ class BasePreferences:
"category": "assi", "category": "assi",
}, },
), ),
( # (
"assi_etat_defaut", # "assi_etat_defaut",
{ # {
"explanation": "⚠ non fonctionnel, travaux en cours !", # "explanation": "⚠ non fonctionnel, travaux en cours !",
"initvalue": "aucun", # "initvalue": "aucun",
"input_type": "menu", # "input_type": "menu",
"labels": ["aucun", "present", "retard", "absent"], # "labels": ["aucun", "present", "retard", "absent"],
"allowed_values": ["aucun", "present", "retard", "absent"], # "allowed_values": ["aucun", "present", "retard", "absent"],
"title": "Définir l'état par défaut", # "title": "Définir l'état par défaut",
"category": "assi", # "category": "assi",
}, # },
), # ),
( (
"non_travail", "non_travail",
{ {

View File

@ -730,31 +730,11 @@ tr.row-justificatif.non_valide td.assi-type {
background-color: var(--color-defaut) !important; background-color: var(--color-defaut) !important;
} }
.color.est_just.sans_etat::before { .color.invalide {
content: "";
position: absolute;
width: 25%;
height: 100%;
background-color: var(--color-justi) !important;
right: 0;
}
.color.invalide::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
right: 0;
background-color: var(--color-justi-invalide) !important; background-color: var(--color-justi-invalide) !important;
} }
.color.attente::before, .color.attente {
.color.modifie::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
right: 0;
background: repeating-linear-gradient(to bottom, background: repeating-linear-gradient(to bottom,
var(--color-justi-attente-stripe) 0px, var(--color-justi-attente-stripe) 0px,
var(--color-justi-attente-stripe) 4px, var(--color-justi-attente-stripe) 4px,
@ -762,6 +742,10 @@ tr.row-justificatif.non_valide td.assi-type {
var(--color-justi-attente) 7px) !important; var(--color-justi-attente) 7px) !important;
} }
.color.est_just {
background-color: var(--color-justi) !important;
}
#gtrcontent .pdp { #gtrcontent .pdp {
display: none; display: none;
} }

View File

@ -296,7 +296,13 @@ function creerLigneEtudiant(etud, index) {
// Création des boutons d'assiduités // Création des boutons d'assiduités
if (readOnly) { if (readOnly) {
} else if (currentAssiduite.type != "conflit") { } else if (currentAssiduite.type != "conflit") {
["present", "retard", "absent"].forEach((abs) => { const etats = ["retard", "absent"];
if (!window.nonPresent) {
etats.splice(0, 0, "present");
}
etats.forEach((abs) => {
const btn = document.createElement("input"); const btn = document.createElement("input");
btn.type = "checkbox"; btn.type = "checkbox";
btn.value = abs; btn.value = abs;
@ -425,7 +431,7 @@ async function getModuleImpl(assiduite) {
return res.json(); return res.json();
}) })
.then((data) => { .then((data) => {
moduleimpls[id] = `${data.module.code} ${data.module.abbrev || ''}`; moduleimpls[id] = `${data.module.code} ${data.module.abbrev || ""}`;
return moduleimpls[id]; return moduleimpls[id];
}) })
.catch((_) => { .catch((_) => {
@ -531,12 +537,7 @@ async function MiseAJourLigneEtud(etud) {
async function actionAssiduite(etud, etat, type, assiduite = null) { async function actionAssiduite(etud, etat, type, assiduite = null) {
const modimpl_id = $("#moduleimpl_select").val(); const modimpl_id = $("#moduleimpl_select").val();
if ( if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression";
assiduite &&
assiduite.etat.toLowerCase() === etat &&
assiduite.moduleimpl_id == modimpl_id
)
type = "suppression";
const { deb, fin } = getPeriodAsDate(); const { deb, fin } = getPeriodAsDate();

View File

@ -4,8 +4,8 @@
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
"""Liste simple d'étudiants """Liste simple d'étudiants"""
"""
import datetime import datetime
from flask import g, url_for from flask import g, url_for
from app import log from app import log
@ -140,6 +140,13 @@ class RowAssi(tb.Row):
) )
stats = self._get_etud_stats(etud) stats = self._get_etud_stats(etud)
for key, value in stats.items(): for key, value in stats.items():
if key == "present" and sco_preferences.get_preference(
"non_present",
dept_id=g.scodoc_dept_id,
formsemestre_id=self.table.formsemestre.id,
):
continue
self.add_cell(key, value[0], fmt_num(value[1] - value[2]), "assi_stats") self.add_cell(key, value[0], fmt_num(value[1] - value[2]), "assi_stats")
if key != "present": if key != "present":
self.add_cell( self.add_cell(

View File

@ -75,36 +75,7 @@ Calendrier de l'assiduité
<div class="help"> <div class="help">
<h3>Calendrier</h3> <h3>Calendrier</h3>
<p>Code couleur</p> {% include "assiduites/widgets/legende_couleur.j2" %}
<ul class="couleurs">
<li><span title="Vert" class="present demo"></span> &rightarrow; présence de l'étudiant lors de la
période
</li>
<li><span title="Bleu clair" class="nonwork demo"></span> &rightarrow; la période n'est pas travaillée
</li>
<li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la
période
</li>
<li><span title="Rose" class="demo color absent est_just"></span> &rightarrow; absence justifiée
</li>
<li><span title="Orange" class="retard demo"></span> &rightarrow; retard de l'étudiant lors de la
période
</li>
<li><span title="Jaune clair" class="demo color retard est_just"></span> &rightarrow; retard justifié
</li>
<li><span title="Quart Bleu" class="est_just demo"></span> &rightarrow; la période est couverte par un
justificatif valide</li>
<li><span title="Justif. non valide" class="invalide demo"></span> &rightarrow; la période est
couverte par un justificatif non valide
</li>
<li><span title="Justif. en attente" class="attente demo"></span> &rightarrow; la période
a un justificatif en attente de validation
</li>
</ul>
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>
</div> </div>
<ul class="couleurs print"> <ul class="couleurs print">
<li><span title="Vert" class="present demo"></span> présence <li><span title="Vert" class="present demo"></span> présence
@ -180,21 +151,8 @@ Calendrier de l'assiduité
justify-content: start; justify-content: start;
} }
.demo.invalide {
background-color: var(--color-justi-invalide) !important;
}
.demo.attente {
background: repeating-linear-gradient(to bottom,
var(--color-justi-attente-stripe) 0px,
var(--color-justi-attente-stripe) 4px,
var(--color-justi-attente) 4px,
var(--color-justi-attente) 7px) !important;
}
.demo.est_just {
background-color: var(--color-justi) !important;
}
.demi .day.nonwork>span { .demi .day.nonwork>span {

View File

@ -310,8 +310,13 @@ async function nouvellePeriode(period = null) {
const assi_btns = document.createElement('div'); const assi_btns = document.createElement('div');
assi_btns.classList.add('assi-btns'); assi_btns.classList.add('assi-btns');
const etats = ["retard", "absent"];
["present", "retard", "absent"].forEach((value) => { if(!window.nonPresent){
etats.splice(0,0,"present");
}
etats.forEach((value) => {
const cbox = document.createElement("input"); const cbox = document.createElement("input");
cbox.type = "checkbox"; cbox.type = "checkbox";
cbox.value = value; cbox.value = value;
@ -499,6 +504,8 @@ const moduleimpls = new Map();
const inscriptionsModules = new Map(); const inscriptionsModules = new Map();
const nonWorkDays = [{{ nonworkdays| safe }}]; const nonWorkDays = [{{ nonworkdays| safe }}];
window.nonPresent = {{ 'true' if non_present else 'false' }};
// Vérification du forçage de module // Vérification du forçage de module
window.forceModule = "{{ forcer_module }}" == "True"; window.forceModule = "{{ forcer_module }}" == "True";
if (window.forceModule) { if (window.forceModule) {
@ -518,12 +525,29 @@ if (window.forceModule) {
} }
}); });
} }
const defaultPlage = {{ nouv_plage | safe}} || [];
/** /**
* Fonction exécutée au lancement de la page * Fonction exécutée au lancement de la page
* - On affiche ou non les photos des étudiants * - On affiche ou non les photos des étudiants
* - On vérifie si la date est un jour travaillé * - On vérifie si la date est un jour travaillé
*/ */
async function main() { async function main() {
// On initialise les sélecteurs avec les valeurs par défaut (si elles existent)
if (defaultPlage.every((e) => e)) {
$("#date").datepicker("setDate", defaultPlage[0]);
$("#debut").val(defaultPlage[1]);
$("#fin").val(defaultPlage[2]);
// On ajoute la période si la date est un jour travaillé
if(dateCouranteEstTravaillee()){
await nouvellePeriode();
}
}
const checked = localStorage.getItem("scodoc-etud-pdp") == "true"; const checked = localStorage.getItem("scodoc-etud-pdp") == "true";
afficherPDP(checked); afficherPDP(checked);
$("#date").on("change", async function (d) { $("#date").on("change", async function (d) {
@ -532,7 +556,7 @@ async function main() {
}); });
} }
main(); window.addEventListener("load", main);
</script> </script>
@ -600,7 +624,9 @@ main();
Intialiser les étudiants comme : Intialiser les étudiants comme :
<select name="etatDef" id="etatDef"> <select name="etatDef" id="etatDef">
<option value="">-</option> <option value="">-</option>
{% if not non_present %}
<option value="present">présents</option> <option value="present">présents</option>
{% endif %}
<option value="retard">en retard</option> <option value="retard">en retard</option>
<option value="absent">absents</option> <option value="absent">absents</option>
</select> </select>

View File

@ -31,6 +31,7 @@
const readOnly = {{ readonly }}; const readOnly = {{ readonly }};
window.forceModule = "{{ forcer_module }}" == "True" window.forceModule = "{{ forcer_module }}" == "True"
window.nonPresent = {{ 'true' if non_present else 'false' }};
const etudsDefDem = {{ defdem | safe }} const etudsDefDem = {{ defdem | safe }}
@ -159,8 +160,10 @@
<div class="mass-selection"> <div class="mass-selection">
<span>Mettre tout le monde :</span> <span>Mettre tout le monde :</span>
<fieldset class="btns_field mass"> <fieldset class="btns_field mass">
{% if not non_present %}
<input type="checkbox" value="present" name="mass_btn_assiduites" id="mass_rbtn_present" <input type="checkbox" value="present" name="mass_btn_assiduites" id="mass_rbtn_present"
class="rbtn present" onclick="mettreToutLeMonde('present', this)" title="Present"> class="rbtn present" onclick="mettreToutLeMonde('present', this)" title="Present">
{% endif %}
<input type="checkbox" value="retard" name="mass_btn_assiduites" id="mass_rbtn_retard" <input type="checkbox" value="retard" name="mass_btn_assiduites" id="mass_rbtn_retard"
class="rbtn retard" onclick="mettreToutLeMonde('retard', this)" title="Retard"> class="rbtn retard" onclick="mettreToutLeMonde('retard', this)" title="Retard">
<input type="checkbox" value="absent" name="mass_btn_assiduites" id="mass_rbtn_absent" <input type="checkbox" value="absent" name="mass_btn_assiduites" id="mass_rbtn_absent"
@ -178,6 +181,11 @@
</p> </p>
</div> </div>
<div class="help">
<h3>Calendrier</h3>
{% include "assiduites/widgets/legende_couleur.j2" %}
</div>
{% include "assiduites/widgets/toast.j2" %} {% include "assiduites/widgets/toast.j2" %}
{% include "assiduites/widgets/alert.j2" %} {% include "assiduites/widgets/alert.j2" %}
{% include "assiduites/widgets/prompt.j2" %} {% include "assiduites/widgets/prompt.j2" %}

View File

@ -1,12 +1,28 @@
<li><span title="Vert" class="present demo"></span> &rightarrow; présence de l'étudiant lors de la période <p>Code couleur</p>
</li> <ul class="couleurs">
<li><span title="Orange" class="retard demo"></span> &rightarrow; retard de l'étudiant lors de la période <li><span title="Vert" class="present demo"></span> &rightarrow; présence de l'étudiant lors de la
</li> période
<li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la période </li>
</li> <li><span title="Bleu clair" class="nonwork demo"></span> &rightarrow; la période n'est pas travaillée
</li>
<li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la
période
</li>
<li><span title="Rose" class="demo color absent est_just"></span> &rightarrow; absence justifiée
</li>
<li><span title="Orange" class="retard demo"></span> &rightarrow; retard de l'étudiant lors de la
période
</li>
<li><span title="Jaune clair" class="demo color retard est_just"></span> &rightarrow; retard justifié
</li>
<li><span title="Hachure Bleue" class="justified demo"></span> &rightarrow; l'assiduité est justifiée par un <li><span title="Quart Bleu" class="est_just demo color"></span> &rightarrow; la période est couverte par un
justificatif valide</li> justificatif valide</li>
<li><span title="Hachure Rouge" class="invalid_justified demo"></span> &rightarrow; l'assiduité est <li><span title="Justif. non valide" class="invalide demo color "></span> &rightarrow; la période est
justifiée par un justificatif non valide / en attente de validation couverte par un justificatif non valide
</li> </li>
<li><span title="Justif. en attente" class="attente demo color"></span> &rightarrow; la période
a un justificatif en attente de validation
</li>
</ul>
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>

View File

@ -74,7 +74,13 @@
setupAssiduiteBubble(block, assiduité); setupAssiduiteBubble(block, assiduité);
} }
// TODO: ajout couleur justificatif // ajout couleur justificatif
const justificatifs = assiduité.justificatifs || [];
const justified = justificatifs.some(
(justificatif) => justificatif.etat === "VALIDE"
)
if(justified) block.classList.add("est_just");
block.classList.add(assiduité.etat.toLowerCase()); block.classList.add(assiduité.etat.toLowerCase());
if(assiduité.etat != "CRENEAU") block.classList.add("color"); if(assiduité.etat != "CRENEAU") block.classList.add("color");

View File

@ -1132,6 +1132,11 @@ def signal_assiduites_group():
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_id, dept_id=g.scodoc_dept_id,
), ),
non_present=sco_preferences.get_preference(
"non_present",
formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_id,
),
formsemestre_date_debut=str(formsemestre.date_debut), formsemestre_date_debut=str(formsemestre.date_debut),
formsemestre_date_fin=str(formsemestre.date_fin), formsemestre_date_fin=str(formsemestre.date_fin),
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
@ -1914,8 +1919,29 @@ def _preparer_objet(
@scodoc @scodoc
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
def signal_assiduites_diff(): def signal_assiduites_diff():
"""TODO documenter """
Utilisé notamment par "Saisie différée" sur tableau de bord semetstre" Utilisé notamment par "Saisie différée" sur tableau de bord semetstre"
Arguments de la requête:
- group_ids : liste des groupes
example : group_ids=1,2,3
- formsemestre_id : id du formsemestre
example : formsemestre_id=1
- moduleimpl_id : id du moduleimpl
example : moduleimpl_id=1
(Permet de pré-générer une plage. Si non renseigné, la plage sera vide)
(Les trois valeurs suivantes doivent être renseignées ensemble)
- date
example : date=01/01/2021
- heure_debut
example : heure_debut=08:00
- heure_fin
example : heure_fin=10:00
Exemple de requête :
signal_assiduites_diff?formsemestre_id=67&group_ids=400&moduleimpl_id=1229&date=15/04/2024&heure_debut=12:34&heure_fin=12:55
""" """
# Récupération des paramètres de la requête # Récupération des paramètres de la requête
group_ids: list[int] = request.args.get("group_ids", None) group_ids: list[int] = request.args.get("group_ids", None)
@ -1957,11 +1983,23 @@ def signal_assiduites_diff():
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>" grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
) )
# Pré-remplissage des sélecteurs
moduleimpl_id = request.args.get("moduleimpl_id", -1) moduleimpl_id = request.args.get("moduleimpl_id", -1)
try: try:
moduleimpl_id = int(moduleimpl_id) moduleimpl_id = int(moduleimpl_id)
except ValueError: except ValueError:
moduleimpl_id = -1 moduleimpl_id = -1
# date fra (dd/mm/yyyy)
date = request.args.get("date", "")
# heures (hh:mm)
heure_deb = request.args.get("heure_debut", "")
heure_fin = request.args.get("heure_fin", "")
# vérifications des sélecteurs
date = date if re.match(r"^\d{2}\/\d{2}\/\d{4}$", date) else ""
heure_deb = heure_deb if re.match(r"^[0-2]\d:[0-5]\d$", heure_deb) else ""
heure_fin = heure_fin if re.match(r"^[0-2]\d:[0-5]\d$", heure_fin) else ""
nouv_plage: list[str] = [date, heure_deb, heure_fin]
return render_template( return render_template(
"assiduites/pages/signal_assiduites_diff.j2", "assiduites/pages/signal_assiduites_diff.j2",
@ -1977,6 +2015,12 @@ def signal_assiduites_diff():
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_id, dept_id=g.scodoc_dept_id,
), ),
non_present=sco_preferences.get_preference(
"non_present",
formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_id,
),
nouv_plage=nouv_plage,
) )