Assiduites : Pages Bilans

This commit is contained in:
iziram 2023-06-28 17:15:24 +02:00
parent 9b8e09cdba
commit 7a42c24fc4
15 changed files with 697 additions and 56 deletions

View File

@ -18,7 +18,7 @@ from app.api import api_bp as bp
from app.api import api_web_bp from app.api import api_web_bp
from app.api import get_model_api_object from app.api import get_model_api_object
from app.decorators import permission_required, scodoc from app.decorators import permission_required, scodoc
from app.models import Identite, Justificatif from app.models import Identite, Justificatif, Departement
from app.models.assiduites import compute_assiduites_justified from app.models.assiduites import compute_assiduites_justified
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
@ -105,6 +105,31 @@ def justificatifs(etudid: int = None, with_query: bool = False):
return data_set return data_set
@api_web_bp.route("/justificatifs/dept/<int:dept_id>", defaults={"with_query": False})
@api_web_bp.route(
"/justificatifs/dept/<int:dept_id>/query", defaults={"with_query": True}
)
@login_required
@scodoc
@as_json
@permission_required(Permission.ScoView)
def justificatifs_dept(dept_id: int = None, with_query : bool = False):
""" """
dept = Departement.query.get_or_404(dept_id)
etuds = [etud.id for etud in dept.etudiants]
justificatifs_query = Justificatif.query.filter(Justificatif.etudid.in_(etuds))
if with_query:
justificatifs_query = _filter_manager(request, justificatifs_query)
data_set: list[dict] = []
for just in justificatifs_query.all():
data = just.to_dict(format_api=True)
data_set.append(data)
return data_set
@bp.route("/justificatif/<int:etudid>/create", methods=["POST"]) @bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
@api_web_bp.route("/justificatif/<int:etudid>/create", methods=["POST"]) @api_web_bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
@scodoc @scodoc

View File

@ -54,7 +54,7 @@ def sidebar_common():
<h2 class="insidebar">Scolarité</h2> <h2 class="insidebar">Scolarité</h2>
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br> <a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br>
<a href="{scu.NotesURL()}" class="sidebar">Programmes</a> <br> <a href="{scu.NotesURL()}" class="sidebar">Programmes</a> <br>
<a href="{scu.AbsencesURL()}" class="sidebar">Absences</a> <br> <a href="{scu.AssiduitesURL()}" class="sidebar">Assiduités</a> <br>
""" """
] ]
if current_user.has_permission( if current_user.has_permission(
@ -138,6 +138,7 @@ def sidebar(etudid: int = None):
f""" f"""
<li><a href="{ url_for('assiduites.calendrier_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Calendrier</a></li> <li><a href="{ url_for('assiduites.calendrier_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Calendrier</a></li>
<li><a href="{ url_for('assiduites.liste_assiduites_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Liste</a></li> <li><a href="{ url_for('assiduites.liste_assiduites_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Liste</a></li>
<li><a href="{ url_for('assiduites.bilan_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Bilan</a></li>
</ul> </ul>
""" """
) )

View File

@ -213,13 +213,14 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
"enabled": True, "enabled": True,
"helpmsg": "", "helpmsg": "",
}, },
{ # TODO: Mettre à jour avec module Assiduités
"title": "Vérifier absences aux évaluations", # {
"endpoint": "notes.formsemestre_check_absences_html", # "title": "Vérifier absences aux évaluations",
"args": {"formsemestre_id": formsemestre_id}, # "endpoint": "notes.formsemestre_check_absences_html",
"enabled": True, # "args": {"formsemestre_id": formsemestre_id},
"helpmsg": "", # "enabled": True,
}, # "helpmsg": "",
# },
{ {
"title": "Lister tous les enseignants", "title": "Lister tous les enseignants",
"endpoint": "notes.formsemestre_enseignants_list", "endpoint": "notes.formsemestre_enseignants_list",

View File

@ -138,7 +138,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
}, },
{ {
"title": "Absences ce jour", "title": "Absences ce jour",
"endpoint": "absences.EtatAbsencesDate", "endpoint": "assiduites.get_etat_abs_date",
"args": { "args": {
"group_ids": group_id, "group_ids": group_id,
"desc": E["description"], "desc": E["description"],

View File

@ -661,7 +661,19 @@ class BasePreferences(object):
"labels": ["1/2 J.", "J.", "H."], "labels": ["1/2 J.", "J.", "H."],
"allowed_values": ["1/2 J.", "J.", "H."], "allowed_values": ["1/2 J.", "J.", "H."],
"title": "Métrique de l'assiduité", "title": "Métrique de l'assiduité",
"explanation": "Unité affichée dans la fiche étudiante et le bilan\n(J. = journée, H. = heure)", "explanation": "Unité utilisée dans la fiche étudiante, le bilan, et dans les calculs (J. = journée, H. = heure)",
"category": "assi",
"only_global": True,
},
),
(
"assi_seuil",
{
"initvalue": 3.0,
"size": 10,
"title": "Seuil d'alerte des absences",
"type": "float",
"explanation": "Nombres d'absences limite avant alerte dans le bilan (utilisation de l'unité métrique ↑ )",
"category": "assi", "category": "assi",
"only_global": True, "only_global": True,
}, },

View File

@ -619,6 +619,13 @@ def AbsencesURL():
] ]
def AssiduitesURL():
"""URL of Assiduités"""
return url_for("assiduites.index_html", scodoc_dept=g.scodoc_dept)[
: -len("/index_html")
]
def UsersURL(): def UsersURL():
"""URL of Users """URL of Users
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Users e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Users

View File

@ -0,0 +1,160 @@
{% include "assiduites/widgets/tableau_base.j2" %}
<section class="alerte invisible">
<p>Attention, cet étudiant a trop d'absences</p>
</section>
<section class="nonvalide">
<!-- Tableaux des justificatifs à valider (attente / modifié ) -->
<h4>Justificatifs en attente (ou modifiés)</h4>
{% include "assiduites/widgets/tableau_justi.j2" %}
</section>
<div class="annee">
<span>Année scolaire 2022-2023 Changer année: </span>
<select name="" id="annee" onchange="setterAnnee(this.value)">
</select>
</div>
<div class="legende">
</div>
<script>
function loadAll() {
generate(defAnnee)
}
function getDeptJustificatifsFromPeriod(action) {
const path = getUrl() + `/api/justificatifs/dept/${dept_id}/query?date_debut=${bornes.deb}&date_fin=${bornes.fin}&etat=attente,modifie`
async_get(
path,
(data, status) => {
console.log(data);
justificatifCallBack(data);
},
(data, status) => {
console.error(data, status)
}
)
}
function generate(annee) {
if (annee < 1999 || annee > 2999) {
openAlertModal("Année impossible", document.createTextNode("L'année demandé n'existe pas."));
return;
}
bornes = {
deb: `${annee}-09-01T00:00`,
fin: `${annee + 1}-06-30T23:59`
}
defAnnee = annee;
getDeptJustificatifsFromPeriod()
}
function setterAnnee(annee) {
annee = parseInt(annee);
document.querySelector('.annee span').textContent = `Année scolaire ${annee}-${annee + 1} Changer année: `
generate(annee)
}
let defAnnee = {{ annee }};
let bornes = {
deb: `${defAnnee}-09-01T00:00`,
fin: `${defAnnee + 1}-06-30T23:59`
}
const dept_id = {{ dept_id }};
window.addEventListener('load', () => {
filterJustificatifs = {
"columns": [
"etudid",
"entry_date",
"date_debut",
"date_fin",
"etat",
"raison",
"fichier"
],
"filters": {
"etat": [
"attente",
"modifie"
]
}
}
const select = document.querySelector('#annee');
for (let i = defAnnee + 1; i > defAnnee - 6; i--) {
const opt = document.createElement("option");
opt.value = i + "",
opt.textContent = i + "";
if (i === defAnnee) {
opt.selected = true;
}
select.appendChild(opt)
}
setterAnnee(defAnnee)
})
</script>
<style>
.stats-values-item {
display: flex;
justify-content: space-evenly;
align-items: center;
flex-direction: column;
}
.stats {
border: 1px solid #333;
padding: 5px 2px;
width: fit-content;
}
.stats-values {
display: flex;
justify-content: flex-start;
gap: 15px;
}
.stats-values-item h5 {
font-weight: bold;
text-decoration-line: underline;
}
.stats-values-part {
display: flex;
flex-direction: column;
}
.alerte {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
margin: 5px 0;
border-radius: 7px;
background-color: crimson;
}
.alerte.invisible {
display: none;
}
.alerte p {
font-size: larger;
color: whitesmoke;
}
.suppr {
margin: 5px 0;
}
</style>

View File

@ -0,0 +1,342 @@
{% block app_content %}
{% include "assiduites/widgets/tableau_base.j2" %}
<div class="pageContent">
<h2>Bilan de l'assiduité de <span class="rouge">{{sco.etud.nomprenom}}</span></h2>
<section class="alerte invisible">
<p>Attention, cet étudiant a trop d'absences</p>
</section>
<section class="stats">
<!-- Statistiques d'assiduité (nb pres, nb retard, nb absence) + nb justifié -->
<h4>Statistiques d'assiduité</h4>
<div class="stats-inputs">
<label class="stats-label"> Date de début<input type="date" name="stats_date_debut" id="stats_date_debut"
value="{{date_debut}}"></label>
<label class="stats-label"> Date de fin<input type="date" name="stats_date_fin" id="stats_date_fin"
value="{{date_fin}}"></label>
<button onclick="stats()">Actualiser</button>
</div>
<div class="stats-values">
</div>
</section>
<section class="nonvalide">
<!-- Tableaux des assiduités (retard/abs) non justifiées -->
<h4>Assiduités non justifiées (Uniquement les retards et les absences)</h4>
{% include "assiduites/widgets/tableau_assi.j2" %}
<!-- Tableaux des justificatifs à valider (attente / modifié ) -->
<h4>Justificatifs en attente (ou modifiés)</h4>
{% include "assiduites/widgets/tableau_justi.j2" %}
</section>
<section class="suppr">
<h4>Boutons de suppresions (toute suppression est définitive) </h4>
<button type="button" onclick="removeAllAssiduites()">Suppression des assiduités</button>
<button type="button" onclick="removeAllJustificatifs()">Suppression des justificatifs</button>
</section>
<div class="legende">
</div>
</div>
{% endblock app_content %}
<script>
function stats() {
const dd_val = document.getElementById('stats_date_debut').value;
const df_val = document.getElementById('stats_date_fin').value;
if (dd_val == "" || df_val == "") {
openAlertModal("Dates invalides", document.createTextNode('Les dates sélectionnées sont invalides'));
return;
}
const date_debut = new moment.tz(dd_val + "T00:00", TIMEZONE);
const date_fin = new moment.tz(df_val + "T23:59", TIMEZONE);
if (date_debut.valueOf() > date_fin.valueOf()) {
openAlertModal("Dates invalides", document.createTextNode('La date de début se situe après la date de fin.'));
return;
}
countAssiduites(date_debut.format(), date_fin.format())
}
function getAssiduitesCount(dateDeb, dateFin, query) {
const url_api = getUrl() + `/api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&${query}`;
return $.ajax({
async: true,
type: "GET",
url: url_api,
success: (data, status) => {
if (status === "success") {
}
},
error: () => { },
});
}
function countAssiduites(dateDeb, dateFin) {
$.when(
getAssiduitesCount(dateDeb, dateFin, `etat=present`),
getAssiduitesCount(dateDeb, dateFin, `etat=present&est_just=v`),
getAssiduitesCount(dateDeb, dateFin, `etat=retard`),
getAssiduitesCount(dateDeb, dateFin, `etat=retard&est_just=v`),
getAssiduitesCount(dateDeb, dateFin, `etat=absent`),
getAssiduitesCount(dateDeb, dateFin, `etat=absent&est_just=v`),
).then(
(pt, pj, rt, rj, at, aj) => {
const counter = {
"present": {
"total": pt[0],
"justi": pj[0],
},
"retard": {
"total": rt[0],
"justi": rj[0],
},
"absent": {
"total": at[0],
"justi": aj[0],
}
}
const values = document.querySelector('.stats-values');
values.innerHTML = "";
Object.keys(counter).forEach((key) => {
const item = document.createElement('div');
item.classList.add('stats-values-item');
const div = document.createElement('div');
div.classList.add('stats-values-part');
const heure = document.createElement('span');
heure.textContent = `${counter[key].total.heure} heure(s) dont ${counter[key].justi.heure} justifiées`;
const demi = document.createElement('span');
demi.textContent = `${counter[key].total.demi} demi-journée(s) dont ${counter[key].justi.demi} justifiées`;
const jour = document.createElement('span');
jour.textContent = `${counter[key].total.journee} journée(s) dont ${counter[key].justi.journee} justifiées`;
div.append(jour, demi, heure);
const title = document.createElement('h5');
title.textContent = key.capitalize();
item.append(title, div)
values.appendChild(item);
});
const nbAbs = counter.absent.total[assi_metric] - counter.absent.justi[assi_metric];
if (nbAbs > assi_seuil) {
document.querySelector('.alerte').classList.remove('invisible');
document.querySelector('.alerte p').textContent = `Attention, cet étudiant a trop d'absences ${nbAbs} / ${assi_seuil} (${metriques[assi_metric]})`
} else {
document.querySelector('.alerte').classList.add('invisible');
}
}
);
}
function removeAllAssiduites() {
openPromptModal(
"Suppression des assiduités",
document.createTextNode(
'Souhaitez vous réelement supprimer toutes les assiduités de cet étudiant ? Cette supression est irréversible.')
,
() => {
getAllAssiduitesFromEtud(etudid, (data) => {
const toRemove = data.map((a) => a.assiduite_id);
console.log(toRemove)
deleteAssiduites(toRemove);
})
})
}
function removeAllJustificatifs() {
openPromptModal(
"Suppression des justificatifs",
document.createTextNode(
'Souhaitez vous réelement supprimer tous les justificatifs de cet étudiant ? Cette supression est irréversible.')
,
() => {
getAllJustificatifsFromEtud(etudid, (data) => {
const toRemove = data.map((a) => a.justif_id);
deleteJustificatifs(toRemove);
})
})
}
/**
* Suppression des assiduties
*/
function deleteAssiduites(assi) {
const path = getUrl() + `/api/assiduite/delete`;
async_post(
path,
assi,
(data, status) => {
//success
if (data.success.length > 0) {
}
location.reload();
},
(data, status) => {
//error
console.error(data, status);
}
);
}
/**
* Suppression des justificatifs
*/
function deleteJustificatifs(justis) {
const path = getUrl() + `/api/justificatif/delete`;
async_post(
path,
justis,
(data, status) => {
//success
location.reload();
},
(data, status) => {
//error
console.error(data, status);
}
);
}
const metriques = {
"heure": "H.",
"demi": "1/2 J.",
"journee": "J."
}
const etudid = {{ sco.etud.id }};
const assi_metric = "{{ assi_metric | safe }}";
const assi_seuil = {{ assi_seuil }};
const assi_date_debut = "{{date_debut}}";
const assi_date_fin = "{{date_fin}}";
window.addEventListener('load', () => {
filterAssiduites = {
"columns": [
"entry_date",
"date_debut",
"date_fin",
"etat",
"moduleimpl_id",
"est_just"
],
"filters": {
"etat": [
"retard",
"absent"
],
"moduleimpl_id": "",
"est_just": "false"
}
};
filterJustificatifs = {
"columns": [
"entry_date",
"date_debut",
"date_fin",
"etat",
"raison",
"fichier"
],
"filters": {
"etat": [
"attente",
"modifie"
]
}
}
document.getElementById('stats_date_fin').value = assi_date_fin;
document.getElementById('stats_date_debut').value = assi_date_debut;
loadAll();
stats();
})
</script>
<style>
.stats-values-item {
display: flex;
justify-content: space-evenly;
align-items: center;
flex-direction: column;
}
.stats {
border: 1px solid #333;
padding: 5px 2px;
width: fit-content;
}
.stats-values {
display: flex;
justify-content: flex-start;
gap: 15px;
}
.stats-values-item h5 {
font-weight: bold;
text-decoration-line: underline;
}
.stats-values-part {
display: flex;
flex-direction: column;
}
.alerte {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
margin: 5px 0;
border-radius: 7px;
background-color: crimson;
}
.alerte.invisible {
display: none;
}
.alerte p {
font-size: larger;
color: whitesmoke;
}
.suppr {
margin: 5px 0;
}
</style>

View File

@ -59,6 +59,8 @@
assi = filterArray(assi, filterAssiduites.filters) assi = filterArray(assi, filterAssiduites.filters)
renderTableAssiduites(currentPageAssiduites, assi); renderTableAssiduites(currentPageAssiduites, assi);
renderPaginationButtons(assi); renderPaginationButtons(assi);
try { stats() } catch (_) { }
} }
const moduleimpls = {} const moduleimpls = {}
@ -109,6 +111,7 @@
row.appendChild(td) row.appendChild(td)
}) })
row.addEventListener("contextmenu", openContext); row.addEventListener("contextmenu", openContext);
tableBodyAssiduites.appendChild(row); tableBodyAssiduites.appendChild(row);

View File

@ -132,6 +132,11 @@
function renderPaginationButtons(array, assi = true) { function renderPaginationButtons(array, assi = true) {
const totalPages = Math.ceil(array.length / itemsPerPage); const totalPages = Math.ceil(array.length / itemsPerPage);
if (totalPages <= 1) { if (totalPages <= 1) {
if (assi) {
paginationContainerAssiduites.innerHTML = ""
} else {
paginationContainerJustificatifs.innerHTML = ""
}
return; return;
} }
@ -139,14 +144,15 @@
paginationContainerAssiduites.innerHTML = "<span class='liste_pagination'><button class='pagination_moins'>&lt;</button><select id='paginationAssi'></select><button class='pagination_plus'>&gt;</button></span>" paginationContainerAssiduites.innerHTML = "<span class='liste_pagination'><button class='pagination_moins'>&lt;</button><select id='paginationAssi'></select><button class='pagination_plus'>&gt;</button></span>"
paginationContainerAssiduites.querySelector('#paginationAssi')?.addEventListener('change', (e) => { paginationContainerAssiduites.querySelector('#paginationAssi')?.addEventListener('change', (e) => {
currentPageAssiduites = e.target.value; currentPageAssiduites = e.target.value;
renderTableAssiduites(currentPageAssiduites, array); assiduiteCallBack(array);
}) })
paginationContainerAssiduites.querySelector('.pagination_moins').addEventListener('click', () => { paginationContainerAssiduites.querySelector('.pagination_moins').addEventListener('click', () => {
if (currentPageAssiduites > 1) { if (currentPageAssiduites > 1) {
currentPageAssiduites--; currentPageAssiduites--;
paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites
renderTableAssiduites(currentPageAssiduites, array); assiduiteCallBack(array);
} }
}) })
@ -154,21 +160,21 @@
if (currentPageAssiduites < totalPages) { if (currentPageAssiduites < totalPages) {
currentPageAssiduites++; currentPageAssiduites++;
paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites
renderTableAssiduites(currentPageAssiduites, array); assiduiteCallBack(array);
} }
}) })
} else { } else {
paginationContainerJustificatifs.innerHTML = "<span class='liste_pagination'><button class='pagination_moins'>&lt;</button><select id='paginationJusti'></select><button class='pagination_plus'>&gt;</button></span>" paginationContainerJustificatifs.innerHTML = "<span class='liste_pagination'><button class='pagination_moins'>&lt;</button><select id='paginationJusti'></select><button class='pagination_plus'>&gt;</button></span>"
paginationContainerJustificatifs.querySelector('#paginationJusti')?.addEventListener('change', (e) => { paginationContainerJustificatifs.querySelector('#paginationJusti')?.addEventListener('change', (e) => {
currentPageJustificatifs = e.target.value; currentPageJustificatifs = e.target.value;
renderTableJustificatifs(currentPageJustificatifs, array); justificatifCallBack(array);
}) })
paginationContainerJustificatifs.querySelector('.pagination_moins').addEventListener('click', () => { paginationContainerJustificatifs.querySelector('.pagination_moins').addEventListener('click', () => {
if (currentPageJustificatifs > 1) { if (currentPageJustificatifs > 1) {
currentPageJustificatifs--; currentPageJustificatifs--;
paginationContainerJustificatifs.querySelector('#paginationJusti').value = currentPageAssiduites paginationContainerJustificatifs.querySelector('#paginationJusti').value = currentPageAssiduites
renderTableJustificatifs(currentPageJustificatifs, array); justificatifCallBack(array);
} }
}) })
@ -176,7 +182,7 @@
if (currentPageJustificatifs < totalPages) { if (currentPageJustificatifs < totalPages) {
currentPageJustificatifs++; currentPageJustificatifs++;
paginationContainerJustificatifs.querySelector('#paginationJusti').value = currentPageAssiduites paginationContainerJustificatifs.querySelector('#paginationJusti').value = currentPageAssiduites
renderTableJustificatifs(currentPageJustificatifs, array); justificatifCallBack(array);
} }
}) })
} }
@ -624,6 +630,8 @@
return "Raison"; return "Raison";
case "fichier": case "fichier":
return "Fichier"; return "Fichier";
case "etudid":
return "Etudiant";
} }
} }
@ -776,7 +784,7 @@
margin-left: 2px !important; margin-left: 2px !important;
} }
label { .filter-body label {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;

View File

@ -57,6 +57,17 @@
renderPaginationButtons(justi, false); renderPaginationButtons(justi, false);
} }
function getEtudiant(id) {
if (id in etuds) {
return etuds[id];
}
getSingleEtud(id);
return etuds[id];
}
function renderTableJustificatifs(page, justificatifs) { function renderTableJustificatifs(page, justificatifs) {
generateTableHead(filterJustificatifs.columns, false) generateTableHead(filterJustificatifs.columns, false)
@ -85,9 +96,13 @@
td.textContent = moment.tz(justificatif[k], TIMEZONE).format(`DD/MM/Y HH:mm`) td.textContent = moment.tz(justificatif[k], TIMEZONE).format(`DD/MM/Y HH:mm`)
} else if (k.indexOf('fichier') != -1) { } else if (k.indexOf('fichier') != -1) {
td.textContent = justificatif.fichier ? "Oui" : "Non"; td.textContent = justificatif.fichier ? "Oui" : "Non";
} else if (k.indexOf('etudid') != -1) {
const e = getEtudiant(justificatif.etudid);
td.textContent = `${e.prenom.capitalize()} ${e.nom.toUpperCase()}`;
} }
else { else {
td.textContent = justificatif[k].capitalize() td.textContent = `${justificatif[k]}`.capitalize()
} }
row.appendChild(td) row.appendChild(td)

View File

@ -24,7 +24,7 @@
<h2 class="insidebar">Scolarité</h2> <h2 class="insidebar">Scolarité</h2>
<a href="{{url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Semestres</a> <br> <a href="{{url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Semestres</a> <br>
<a href="{{url_for('notes.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Programmes</a> <br> <a href="{{url_for('notes.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Programmes</a> <br>
<a href="{{url_for('absences.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Absences</a> <br> <a href="{{url_for('assiduites.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Assiduités</a> <br>
{% if current_user.has_permission(sco.Permission.ScoUsersAdmin) {% if current_user.has_permission(sco.Permission.ScoUsersAdmin)
or current_user.has_permission(sco.Permission.ScoUsersView) or current_user.has_permission(sco.Permission.ScoUsersView)
@ -73,6 +73,8 @@
etudid=sco.etud.id) }}">Calendrier</a></li> etudid=sco.etud.id) }}">Calendrier</a></li>
<li><a href="{{ url_for('assiduites.liste_assiduites_etud', scodoc_dept=g.scodoc_dept, <li><a href="{{ url_for('assiduites.liste_assiduites_etud', scodoc_dept=g.scodoc_dept,
etudid=sco.etud.id) }}">Liste</a></li> etudid=sco.etud.id) }}">Liste</a></li>
<li><a href="{{ url_for('assiduites.bilan_etud', scodoc_dept=g.scodoc_dept,
etudid=sco.etud.id) }}">Bilan</a></li>
</ul> </ul>
{% endif %} {% endif %}
</div> {# /etud-insidebar #} </div> {# /etud-insidebar #}

View File

@ -10,7 +10,7 @@ from app.decorators import (
scodoc, scodoc,
permission_required, permission_required,
) )
from app.models import FormSemestre, Identite, ScoDocSiteConfig, Assiduite from app.models import FormSemestre, Identite, ScoDocSiteConfig, Assiduite, Departement
from app.views import assiduites_bp as bp from app.views import assiduites_bp as bp
from app.views import ScoData from app.views import ScoData
@ -125,36 +125,49 @@ class HTMLBuilder:
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def index_html(): def index_html():
"""Gestionnaire assiduités, page principale""" """Gestionnaire assiduités, page principale"""
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Saisie des assiduités", page_title="Saisie des assiduités",
cssstyles=["css/calabs.css"], javascripts=[
javascripts=["js/calabs.js"], "js/assiduites.js",
"libjs/moment.new.min.js",
"libjs/moment-timezone.js",
],
cssstyles=[
"css/assiduites.css",
],
), ),
"""<h2>Traitement des assiduités</h2> """<h2>Traitement des assiduités</h2>
<p class="help"> <p class="help">
Pour saisir des assiduités ou consulter les états, il est recommandé par passer par Pour saisir des assiduités ou consulter les états, il est recommandé par passer par
le semestre concerné (saisie par jours nommés ou par semaines). le semestre concerné (saisie par jour ou saisie différée).
</p> </p>
""", """,
] ]
H.append( H.append(
"""<p class="help">Pour signaler, annuler ou justifier une assiduité pour un seul étudiant, """<p class="help">Pour signaler, annuler ou justifier une assiduité pour un seul étudiant,
choisissez d'abord concerné:</p>""" choisissez d'abord le concerné:</p>"""
) )
H.append(sco_find_etud.form_search_etud()) H.append(sco_find_etud.form_search_etud())
if current_user.has_permission( # if current_user.has_permission(
Permission.ScoAbsChange # Permission.ScoAbsChange
) and sco_preferences.get_preference("handle_billets_abs"): # ) and sco_preferences.get_preference("handle_billets_abs"):
H.append( # H.append(
f""" # f"""
<h2 style="margin-top: 30px;">Billets d'absence</h2> # <h2 style="margin-top: 30px;">Billets d'absence</h2>
<ul><li><a href="{url_for("absences.list_billets", scodoc_dept=g.scodoc_dept) # <ul><li><a href="{url_for("absences.list_billets", scodoc_dept=g.scodoc_dept)
}">Traitement des billets d'absence en attente</a> # }">Traitement des billets d'absence en attente</a>
</li></ul> # </li></ul>
""" # """
) # )
H.append(
render_template(
"assiduites/pages/bilan_dept.j2",
dept_id=g.scodoc_dept_id,
annee=scu.annee_scolaire(),
),
)
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
return "\n".join(H) return "\n".join(H)
@ -269,6 +282,60 @@ def liste_assiduites_etud():
).build() ).build()
@bp.route("/BilanEtud")
@scodoc
@permission_required(Permission.ScoAbsChange)
def bilan_etud():
"""
bilan_etud Affichage de toutes les assiduites et justificatifs d'un etudiant
Args:
etudid (int): l'identifiant de l'étudiant
Returns:
str: l'html généré
"""
etudid = request.args.get("etudid", -1)
etud: Identite = Identite.query.get_or_404(etudid)
if etud.dept_id != g.scodoc_dept_id:
abort(404, "étudiant inexistant dans ce département")
header: str = html_sco_header.sco_header(
page_title="Bilan de l'assiduité étudiante",
init_qtip=True,
javascripts=[
"js/assiduites.js",
"libjs/moment.new.min.js",
"libjs/moment-timezone.js",
],
cssstyles=CSSSTYLES
+ [
"css/assiduites.css",
],
)
date_debut: str = f"{scu.annee_scolaire()}-09-01"
date_fin: str = f"{scu.annee_scolaire()+1}-06-30"
assi_metric = {
"H.": "heure",
"J.": "journee",
"1/2 J.": "demi",
}.get(sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id))
return HTMLBuilder(
header,
render_template(
"assiduites/pages/bilan_etud.j2",
sco=ScoData(etud),
date_debut=date_debut,
date_fin=date_fin,
assi_metric=assi_metric,
assi_seuil=_get_seuil(),
),
).build()
@bp.route("/AjoutJustificatifEtud") @bp.route("/AjoutJustificatifEtud")
@scodoc @scodoc
@permission_required(Permission.ScoAbsChange) @permission_required(Permission.ScoAbsChange)
@ -549,7 +616,7 @@ def get_etat_abs_date():
etat = scu.EtatAssiduite.inverse().get(assi.etat).name etat = scu.EtatAssiduite.inverse().get(assi.etat).name
etudiant = { etudiant = {
"nom": f'<a href="{url_for("absences.CalAbs", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"])}"><font color="#A00000">{etud["nomprenom"]}</font></a>', "nom": f'<a href="{url_for("assiduites.calendrier_etud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"])}"><font color="#A00000">{etud["nomprenom"]}</font></a>',
"etat": etat, "etat": etat,
} }
@ -777,3 +844,7 @@ def _str_to_num(string: str):
def get_time(label: str, default: str): def get_time(label: str, default: str):
return _str_to_num(ScoDocSiteConfig.get(label, default)) return _str_to_num(ScoDocSiteConfig.get(label, default))
def _get_seuil():
return sco_preferences.get_preference("assi_seuil", dept_id=g.scodoc_dept_id)

View File

@ -655,22 +655,16 @@ def profile(host, port, length, profile_dir):
"-m", "-m",
"--morning", "--morning",
help="Spécifie l'heure de début des cours format `hh:mm`", help="Spécifie l'heure de début des cours format `hh:mm`",
default="Heure configurée dans la configuration générale / 08:00 sinon",
show_default=True,
) )
@click.option( @click.option(
"-n", "-n",
"--noon", "--noon",
help="Spécifie l'heure de fin du matin (et donc début de l'après-midi) format `hh:mm`", help="Spécifie l'heure de fin du matin (et donc début de l'après-midi) format `hh:mm`",
default="Heure configurée dans la configuration générale / 13:00 sinon",
show_default=True,
) )
@click.option( @click.option(
"-e", "-e",
"--evening", "--evening",
help="Spécifie l'heure de fin des cours format `hh:mm`", help="Spécifie l'heure de fin des cours format `hh:mm`",
default="Heure configurée dans la configuration générale / 18:00 sinon",
show_default=True,
) )
@with_appcontext @with_appcontext
def migrate_abs_to_assiduites( def migrate_abs_to_assiduites(

View File

@ -228,22 +228,22 @@ def migrate_abs_to_assiduites(
_glob.DEBUG = debug _glob.DEBUG = debug
if morning is None: if morning is None:
_glob.MORNING = ScoDocSiteConfig.get("assi_morning_time", time(8, 0)) morning = ScoDocSiteConfig.get("assi_morning_time", time(8, 0))
else:
morning: list[str] = morning.split(":") morning: list[str] = morning.split(":")
_glob.MORNING = time(int(morning[0]), int(morning[1])) _glob.MORNING = time(int(morning[0]), int(morning[1]))
if noon is None: if noon is None:
_glob.NOON = ScoDocSiteConfig.get("assi_lunch_time", time(13, 0)) noon = ScoDocSiteConfig.get("assi_lunch_time", time(13, 0))
else:
noon: list[str] = noon.split(":") noon: list[str] = noon.split(":")
_glob.NOON = time(int(noon[0]), int(noon[1])) _glob.NOON = time(int(noon[0]), int(noon[1]))
if evening is None: if evening is None:
_glob.EVENING = ScoDocSiteConfig.get("assi_afternoon_time", time(18, 0)) evening = ScoDocSiteConfig.get("assi_afternoon_time", time(18, 0))
else:
evening: list[str] = evening.split(":") evening: list[str] = evening.split(":")
_glob.EVENING = time(int(evening[0]), int(evening[1])) _glob.EVENING = time(int(evening[0]), int(evening[1]))
if dept is None: if dept is None:
prof_total = Profiler("MigrationTotal") prof_total = Profiler("MigrationTotal")