Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
15 changed files with 697 additions and 56 deletions
Showing only changes of commit 7a42c24fc4 - Show all commits

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 get_model_api_object
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.scodoc.sco_archives_justificatifs import JustificatifArchiver
from app.scodoc.sco_exceptions import ScoValueError
@ -105,6 +105,31 @@ def justificatifs(etudid: int = None, with_query: bool = False):
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"])
@api_web_bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
@scodoc

View File

@ -54,7 +54,7 @@ def sidebar_common():
<h2 class="insidebar">Scolarité</h2>
<a href="{scu.ScoURL()}" class="sidebar">Semestres</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(
@ -138,6 +138,7 @@ def sidebar(etudid: int = None):
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.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>
"""
)

View File

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

View File

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

View File

@ -661,7 +661,19 @@ class BasePreferences(object):
"labels": ["1/2 J.", "J.", "H."],
"allowed_values": ["1/2 J.", "J.", "H."],
"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",
"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():
"""URL of 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)
renderTableAssiduites(currentPageAssiduites, assi);
renderPaginationButtons(assi);
try { stats() } catch (_) { }
}
const moduleimpls = {}
@ -109,6 +111,7 @@
row.appendChild(td)
})
row.addEventListener("contextmenu", openContext);
tableBodyAssiduites.appendChild(row);

View File

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

View File

@ -57,6 +57,17 @@
renderPaginationButtons(justi, false);
}
function getEtudiant(id) {
if (id in etuds) {
return etuds[id];
}
getSingleEtud(id);
return etuds[id];
}
function renderTableJustificatifs(page, justificatifs) {
generateTableHead(filterJustificatifs.columns, false)
@ -85,9 +96,13 @@
td.textContent = moment.tz(justificatif[k], TIMEZONE).format(`DD/MM/Y HH:mm`)
} else if (k.indexOf('fichier') != -1) {
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 {
td.textContent = justificatif[k].capitalize()
td.textContent = `${justificatif[k]}`.capitalize()
}
row.appendChild(td)

View File

@ -24,7 +24,7 @@
<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('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)
or current_user.has_permission(sco.Permission.ScoUsersView)
@ -73,6 +73,8 @@
etudid=sco.etud.id) }}">Calendrier</a></li>
<li><a href="{{ url_for('assiduites.liste_assiduites_etud', scodoc_dept=g.scodoc_dept,
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>
{% endif %}
</div> {# /etud-insidebar #}

View File

@ -10,7 +10,7 @@ from app.decorators import (
scodoc,
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 ScoData
@ -125,35 +125,48 @@ class HTMLBuilder:
@permission_required(Permission.ScoView)
def index_html():
"""Gestionnaire assiduités, page principale"""
H = [
html_sco_header.sco_header(
page_title="Saisie des assiduités",
cssstyles=["css/calabs.css"],
javascripts=["js/calabs.js"],
javascripts=[
"js/assiduites.js",
"libjs/moment.new.min.js",
"libjs/moment-timezone.js",
],
cssstyles=[
"css/assiduites.css",
],
),
"""<h2>Traitement des assiduités</h2>
<p class="help">
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>
""",
]
H.append(
"""<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())
if current_user.has_permission(
Permission.ScoAbsChange
) and sco_preferences.get_preference("handle_billets_abs"):
# if current_user.has_permission(
# Permission.ScoAbsChange
# ) and sco_preferences.get_preference("handle_billets_abs"):
# H.append(
# f"""
# <h2 style="margin-top: 30px;">Billets d'absence</h2>
# <ul><li><a href="{url_for("absences.list_billets", scodoc_dept=g.scodoc_dept)
# }">Traitement des billets d'absence en attente</a>
# </li></ul>
# """
# )
H.append(
f"""
<h2 style="margin-top: 30px;">Billets d'absence</h2>
<ul><li><a href="{url_for("absences.list_billets", scodoc_dept=g.scodoc_dept)
}">Traitement des billets d'absence en attente</a>
</li></ul>
"""
render_template(
"assiduites/pages/bilan_dept.j2",
dept_id=g.scodoc_dept_id,
annee=scu.annee_scolaire(),
),
)
H.append(html_sco_header.sco_footer())
return "\n".join(H)
@ -269,6 +282,60 @@ def liste_assiduites_etud():
).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")
@scodoc
@permission_required(Permission.ScoAbsChange)
@ -549,7 +616,7 @@ def get_etat_abs_date():
etat = scu.EtatAssiduite.inverse().get(assi.etat).name
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,
}
@ -777,3 +844,7 @@ def _str_to_num(string: str):
def get_time(label: str, default: str):
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",
"--morning",
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(
"-n",
"--noon",
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(
"-e",
"--evening",
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
def migrate_abs_to_assiduites(

View File

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