Merge branch 'iziram-modif_assi'

This commit is contained in:
Emmanuel Viennet 2024-05-25 18:13:16 +02:00
commit 5c6935337e
19 changed files with 215 additions and 429 deletions

View File

@ -353,12 +353,12 @@ class Assiduite(ScoDocModel):
elif self.external_data is not None and "module" in self.external_data:
return (
"Tout module"
"Autre module (pas dans la liste)"
if self.external_data["module"] == "Autre"
else self.external_data["module"]
)
return "Non spécifié" if traduire else None
return "Module non spécifié" if traduire else None
def get_saisie(self) -> str:
"""

View File

@ -25,8 +25,7 @@
#
##############################################################################
"""HTML Header/Footer for ScoDoc pages
"""
"""HTML Header/Footer for ScoDoc pages"""
import html
@ -101,7 +100,7 @@ _HTML_BEGIN = f"""<!DOCTYPE html>
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
<script>
window.onload=function(){{enableTooltips("gtrcontent")}};
window.onload=function(){{enableTooltips("gtrcontent"); enableTooltips("sidebar");}};
</script>
<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
@ -218,7 +217,7 @@ def sco_header(
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
<script>
window.onload=function(){{enableTooltips("gtrcontent")}};
window.onload=function(){{enableTooltips("gtrcontent"); enableTooltips("sidebar");}};
const SCO_URL="{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}";
const SCO_TIMEZONE="{scu.TIME_ZONE}";

View File

@ -28,6 +28,7 @@
"""
Génération de la "sidebar" (marge gauche des pages HTML)
"""
from flask import render_template, url_for
from flask import g, request
from flask_login import current_user
@ -151,7 +152,7 @@ def sidebar(etudid: int = None):
H = [
f"""
<!-- sidebar py -->
<div class="sidebar">
<div class="sidebar" id="sidebar">
{ sidebar_common() }
<div class="box-chercheetud">Chercher étudiant:<br>
<form method="get" id="form-chercheetud"
@ -193,7 +194,7 @@ def sidebar(etudid: int = None):
formsemestre.date_debut.strftime(scu.DATE_FMT)
} au {
formsemestre.date_fin.strftime(scu.DATE_FMT)
}">({
}" data-tooltip>({
sco_preferences.get_preference("assi_metrique", None)})
<br>{nbabsjust:1g} J., {nbabsnj:1g} N.J.</span>"""
)
@ -227,12 +228,9 @@ def sidebar(etudid: int = None):
<li><a href="{ url_for('assiduites.calendrier_assi_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>
}" title="Les pages bilan et liste ont été fusionnées">Liste/Bilan</a></li>
</ul>
"""
)

View File

@ -822,6 +822,35 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
</div>
</div>
<div class="sem-groups-assi">
"""
)
if can_edit_abs:
H.append(
f"""
<div>
<a class="stdlink" href="{
url_for("assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept,
jour=datetime.date.today().isoformat(),
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
Saisir l'assiduité</a>
</div>
<div>
<a class="stdlink" href="{
url_for("assiduites.bilan_dept",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
Justificatifs en attente</a>
</div>
"""
)
H.append(
f"""
<div>
<a class="stdlink" href="{
url_for("assiduites.visu_assi_group",
@ -834,48 +863,20 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
</div>
"""
)
if can_edit_abs:
H.append(
f"""
<div>
<a class="stdlink" href="{
url_for("assiduites.visu_assiduites_group",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
jour = datetime.date.today().isoformat(),
group_ids=group.id,
)}">
Visualiser</a>
</div>
<div>
<a class="stdlink" href="{
url_for("assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept,
jour=datetime.date.today().isoformat(),
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
Saisie journalière</a>
</div>
<div>
<a class="stdlink" href="{
url_for("assiduites.signal_assiduites_diff",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
Saisie différée</a>
</div>
<div>
<a class="stdlink" href="{
url_for("assiduites.bilan_dept",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
Justificatifs en attente</a>
</div>
"""
)}" title="Page en cours de fusion et sera prochainement supprimée. Veuillez utiliser la page `Saisir l'assiduité`">
(Saisie différée)</a>
</div>
"""
)
H.append("</div>") # /sem-groups-assi

View File

@ -110,6 +110,7 @@ get_base_preferences(formsemestre_id)
Return base preferences for current scodoc_dept (instance BasePreferences)
"""
import flask
from flask import current_app, flash, g, request, url_for
@ -622,18 +623,6 @@ class BasePreferences:
"explanation": "Désactive la saisie et l'affichage des présences",
},
),
(
"periode_defaut",
{
"initvalue": 2.0,
"size": 10,
"title": "Durée par défaut d'un créneau",
"type": "float",
"category": "assi",
"only_global": True,
"explanation": "Durée d'un créneau en heure. Utilisé dans les pages de saisie",
},
),
(
"nb_heures_par_jour",
{
@ -2302,9 +2291,7 @@ class BasePreferences:
if "explanation" in descr:
del descr["explanation"]
if formsemestre_id:
descr[
"explanation"
] = f"""ou <span class="spanlink"
descr["explanation"] = f"""ou <span class="spanlink"
onclick="set_global_pref(this, '{pref_name}');"
>utiliser paramètre global</span>"""
if formsemestre_id and self.is_global(formsemestre_id, pref_name):

View File

@ -302,7 +302,6 @@
.rbtn {
-webkit-appearance: none;
appearance: none;
cursor: pointer;
}
@ -327,9 +326,12 @@
background-image: url(../icons/absent.svg);
}
.rbtn.aucun::before {
background-image: url(../icons/aucun.svg);
background-color: var(--color-defaut-dark);
.rbtn.aucun {
background-image: url("../icons/delete.svg");
background-size: calc(100% - 8px) calc(100% - 8px);
/* Adjust size to create "margin" */
background-position: center;
background-repeat: no-repeat;
}
.rbtn.retard::before {

View File

@ -0,0 +1 @@
<svg id="Layer_1" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m170.8 14.221a14.21 14.21 0 0 1 14.2-14.207l141.991-.008a14.233 14.233 0 0 1 14.2 14.223v35.117h-170.391zm233.461 477.443a21.75 21.75 0 0 1 -21.856 20.33h-254.451a21.968 21.968 0 0 1 -21.854-20.416l-21.774-318.518h343.174l-23.234 318.6zm56.568-347.452h-409.658v-33a33.035 33.035 0 0 1 33.005-33.012l343.644-.011a33.051 33.051 0 0 1 33 33.02v33zm-270.79 291.851a14.422 14.422 0 1 0 28.844 0v-202.247a14.42 14.42 0 0 0 -28.839-.01v202.257zm102.9 0a14.424 14.424 0 1 0 28.848 0v-202.247a14.422 14.422 0 0 0 -28.843-.01z" fill="#fc3333" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 689 B

View File

@ -53,7 +53,7 @@ async function async_get(path, success, errors) {
* @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/
async function async_post(path, data, success, errors) {
console.log("async_post " + path);
// console.log("async_post " + path);
let response;
try {
response = await fetch(path, {
@ -401,7 +401,7 @@ async function creerTousLesEtudiants(etuds) {
* @returns {String}
*/
async function getModuleImpl(assiduite) {
if (assiduite == null) return "Pas de module";
if (assiduite == null) return "Module non spécifié";
const id = assiduite.moduleimpl_id;
if (id == null || id == undefined) {
@ -414,7 +414,7 @@ async function getModuleImpl(assiduite) {
? "Autre module (pas dans la liste)"
: assiduite.external_data.module;
} else {
return "Pas de module";
return "Module non spécifié";
}
}
@ -643,6 +643,9 @@ function mettreToutLeMonde(etat, el = null) {
// Suppression des assiduités
if (etat == "vide") {
if (!confirm("Effacer tout les évènements correspondant à cette plage ?")) {
return; // annulation
}
const assiduites_id = lignesEtuds
.filter((e) => e.getAttribute("type") == "edition")
.map((e) => Number(e.getAttribute("assiduite_id")));
@ -759,6 +762,7 @@ function envoiToastEtudiant(etat, etud) {
pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5));
}
// TODO commenter toutes les fonctions js
function envoiToastTous(etat, count) {
const span = document.createElement("span");
let etatAffiche = etat;
@ -798,13 +802,16 @@ function estJourTravail(jour, nonWorkdays) {
return !nonWorkdays.includes(d);
}
function retourJourTravail(date) {
function retourJourTravail(date, anti = true) {
const jourMiliSecondes = 86400000; // 24 * 3600 * 1000 | H * s * ms
let jour = date;
let compte = 0;
while (!estJourTravail(jour, nonWorkDays) && compte++ < 7) {
jour = new Date(jour - jourMiliSecondes);
let temps = anti
? jour - jourMiliSecondes
: jour.valueOf() + jourMiliSecondes;
jour = new Date(temps);
}
return jour;
}
@ -814,9 +821,12 @@ function dateCouranteEstTravaillee() {
if (!estJourTravail(date, nonWorkDays)) {
const nouvelleDate = retourJourTravail(date);
$("#date").datepicker("setDate", nouvelleDate);
let msg = "Le jour sélectionné";
if ((new Date()).format("YYYY-MM-DD") == date.format("YYYY-MM-DD")) {
msg = "Aujourd'hui";
}
const att = document.createTextNode(
`Le jour sélectionné (${Date.toFRA(
`${msg} (${Date.toFRA(
date.format("YYYY-MM-DD")
)}) n'est pas un jour travaillé.`
);
@ -837,6 +847,17 @@ function dateCouranteEstTravaillee() {
return true;
}
function jourSuivant(anti = false) {
let date = $("#date").datepicker("getDate");
date = anti ? date.add(-1, "days") : date.add(1, "days");
const nouvelleDate = retourJourTravail(date, anti);
$("#date").datepicker("setDate", nouvelleDate);
creerTousLesEtudiants(etuds);
}
/**
* Ajout de la visualisation des assiduités de la mini timeline
* @param {HTMLElement} el l'élément survollé
@ -876,6 +897,11 @@ function setupAssiduiteBubble(el, assiduite) {
actionsDiv.appendChild(infos);
bubble.appendChild(actionsDiv);
const stateDiv = document.createElement("div");
stateDiv.className = "assiduite-state";
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
bubble.appendChild(stateDiv);
const idDiv = document.createElement("div");
idDiv.className = "assiduite-id";
getModuleImpl(assiduite).then((modImpl) => {
@ -883,26 +909,32 @@ function setupAssiduiteBubble(el, assiduite) {
});
bubble.appendChild(idDiv);
const periodDivDeb = document.createElement("div");
periodDivDeb.className = "assiduite-period";
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`;
bubble.appendChild(periodDivDeb);
const periodDivFin = document.createElement("div");
periodDivFin.className = "assiduite-period";
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`;
bubble.appendChild(periodDivFin);
// Affichage des dates
// si les jours sont les mêmes, on affiche "jour hh:mm - hh:mm"
// sinon on affiche "jour hh:mm - jour hh:mm"
const periodDiv = document.createElement("div");
periodDiv.className = "assiduite-period";
const dateDeb = new Date(Date.removeUTC(assiduite.date_debut));
const dateFin = new Date(Date.removeUTC(assiduite.date_fin));
if (dateDeb.isSame(dateFin, "day")) {
const jour = dateDeb.format("DD/MM/YYYY");
const deb = dateDeb.format("HH:mm");
const fin = dateFin.format("HH:mm");
periodDiv.textContent = `${jour} de ${deb} à ${fin}`;
} else {
const jourDeb = dateDeb.format("DD/MM/YYYY");
const jourFin = dateFin.format("DD/MM/YYYY");
periodDiv.textContent = `du ${jourDeb} au ${jourFin}`;
}
const stateDiv = document.createElement("div");
stateDiv.className = "assiduite-state";
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
bubble.appendChild(stateDiv);
bubble.appendChild(periodDiv);
const motifDiv = document.createElement("div");
stateDiv.className = "assiduite-why";
motifDiv.className = "assiduite-why";
const motif = ["", null, undefined].includes(assiduite.desc)
? "Pas de motif"
? "Non spécifié"
: assiduite.desc.capitalize();
stateDiv.textContent = `Motif: ${motif}`;
motifDiv.textContent = `Motif: ${motif}`;
bubble.appendChild(motifDiv);
const userIdDiv = document.createElement("div");

View File

@ -11,8 +11,8 @@ h.id="btc";
h.setAttribute("id","btc");
h.style.position="absolute";
document.getElementsByTagName("body")[0].appendChild(h);
if(id==null) links=document.getElementsByTagName("a");
else links=document.getElementById(id).getElementsByTagName("a");
if(id==null) links=document.querySelectorAll("a, [data-tooltip]"); // was document.getElementsByTagName("a")
else links=document.getElementById(id).querySelectorAll("a, [data-tooltip]");// was document.getElementById(id).getElementsByTagName("a")
for(i=0;i<links.length;i++){
Prepare(links[i]);
}

View File

@ -12,7 +12,7 @@
<h1>Traitement de l'assiduité</h1>
<p class="help">
Pour saisir l'assiduité ou consulter les états, il est recommandé de passer par
Pour saisir l'assiduité ou consulter les états, passer par
le semestre concerné (saisie par jour ou saisie différée).
</p>

View File

@ -86,9 +86,6 @@ Bilan assiduité de {{sco.etud.nomprenom}}
<div class="scobox">
<section class="nonvalide">
<div class="help">Le tableau n'affiche que les assiduités non justifiées
et les justificatifs soumis / modifiés
</div>
{{tableau | safe }}
</section>
</div>
@ -99,6 +96,9 @@ Bilan assiduité de {{sco.etud.nomprenom}}
département)</p>
<p>Les statistiques sont calculées entre les deux dates sélectionnées. Après modification des dates,
appuyer sur le bouton "Actualiser"</p>
{% include "assiduites/explication_etats_justifs.j2" %}
</div>
</div>

View File

@ -1,27 +0,0 @@
{% extends "sco_page.j2" %}
{% block title %}
Assiduité de {{etud.nomprenom}}
{% endblock title %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
{% endblock styles %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
{% endblock %}
{% block app_content %}
<div class="pageContent">
<h2>Liste de l'assiduité et des justificatifs de {{sco.etud.html_link_fiche()|safe}}</h2>
{{tableau | safe }}
</div>
{% include "assiduites/explication_etats_justifs.j2" %}
{% endblock app_content %}

View File

@ -570,6 +570,13 @@ window.addEventListener("load", main);
<h2>Signalement différé de l'assiduité {{gr |safe}}</h2>
<div class="ue_warning">
Attention, cette page est en cours de fusion avec la page de Saisie journalière.
<br>
Vous pouvez dès à présent cliquer sur <a href="{{url_for('assiduites.signal_assiduites_group', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, group_ids=group_ids)}}" target="_blank">ce lien</a> pour accéder à la nouvelle version.
</div>
<div id="fix">
<!-- Nouvelle Plage

View File

@ -105,6 +105,24 @@
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.css">
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
<style>
#retour-haut{
position: fixed;
bottom: 10px;
right: 10px;
font-size: 3em;
padding: 5px;
border-radius: 5px;
text-decoration: none;
}
html{
scroll-behavior: smooth !important;
}
</style>
{% endblock styles %}
@ -113,6 +131,10 @@
{{ minitimeline|safe }}
<section id="content">
<a id="retour-haut" href="#gtrcontent">
⬆️
</a>
<div class="no-display">
<span class="formsemestre_id">{{formsemestre_id}}</span>
<span id="formsemestre_date_debut">{{formsemestre_date_debut}}</span>
@ -131,12 +153,22 @@
<div class="infos">
<div class="infos-button">Groupes&nbsp;: {{grp|safe}}</div>
<div>
<button class="btn_date" onclick="jourSuivant(true)">
&LeftArrowBar;
</button>
<input type="text" name="date" id="date" class="datepicker" value="{{date}}">
</div>
<button class="btn_date" onclick="jourSuivant(false)">
&RightArrowBar;
</button>
</div>
</fieldset>
<div style="display: {{'none' if readonly == 'true' else 'block'}};">
{{timeline|safe}}
<div>
<button onclick="setPeriodValues(t_start, t_mid)">Matin</button>
<button onclick="setPeriodValues(t_mid, t_end)">Après-Midi</button>
</div>
</div>
{% if readonly == "false" %}
@ -162,14 +194,14 @@
<fieldset class="btns_field mass">
{% if not non_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="Indique l'état Présent pour tous les étudiants" data-tooltip>
{% endif %}
<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="Indique l'état Retard pour tous les étudiants" data-tooltip>
<input type="checkbox" value="absent" name="mass_btn_assiduites" id="mass_rbtn_absent"
class="rbtn absent" onclick="mettreToutLeMonde('absent', this)" title="Absent">
class="rbtn absent" onclick="mettreToutLeMonde('absent', this)" title="Indique l'état Absent pour tous les étudiants" data-tooltip>
<input type="checkbox" value="remove" name="mass_btn_assiduites" id="mass_rbtn_aucun"
class="rbtn aucun" onclick="mettreToutLeMonde('vide', this)" title="Supprimer">
class="rbtn aucun" onclick="mettreToutLeMonde('vide', this)" title="Retire l'état pour tous les étudiants" data-tooltip>
</fieldset>
<em>Les saisies ci-dessous sont enregistrées au fur et à mesure.</em>
</div>

View File

@ -1,8 +1,7 @@
<div class="assiduite-bubble {{etat}}">
<div class="assiduite-id">{{moduleimpl}}</div>
<div class="assiduite-period">{{date_debut}}</div>
<div class="assiduite-period">{{date_fin}}</div>
<div class="assiduite-state">État: {{etat}}</div>
<div class="assiduite-id">{{moduleimpl}}</div>
<div class="assiduite-period">{{date}}</div>
<div class="assiduite-why">Motif: {{motif}}</div>
<div class="assiduite-user_id">{{saisie}}</div>
</div>

View File

@ -17,12 +17,13 @@
const timelineContainer = document.querySelector(".timeline-container");
const periodTimeLine = document.querySelector(".period");
const t_start = {{ t_start }};
const t_mid = {{ t_mid }};
const t_end = {{ t_end }};
const tick_time = 60 / {{ tick_time }};
const tick_delay = 1 / tick_time;
const period_default = {{ periode_defaut }};
const period_default = 2;
let handleMoving = false;
@ -133,6 +134,7 @@
timelineContainer.removeEventListener("mousemove", onMouseMove);
handleMoving = false;
func_call();
savePeriodInLocalStorage();
}
timelineContainer.addEventListener("mousemove", onMouseMove);
@ -166,6 +168,7 @@
snapHandlesToQuarters();
timelineContainer.removeEventListener("mousemove", onMouseMove);
func_call();
savePeriodInLocalStorage();
}
timelineContainer.addEventListener("mousemove", onMouseMove);
timelineContainer.addEventListener("touchmove", onMouseMove);
@ -264,6 +267,7 @@
snapHandlesToQuarters();
updatePeriodTimeLabel()
func_call();
savePeriodInLocalStorage();
}
function snapHandlesToQuarters() {
@ -309,9 +313,23 @@
}
}
function savePeriodInLocalStorage(){
const dates = getPeriodValues();
localStorage.setItem("sco-timeline-values", JSON.stringify(dates));
}
function loadPeriodFromLocalStorage(){
const dates = JSON.parse(localStorage.getItem("sco-timeline-values"));
if(dates){
setPeriodValues(...dates);
}else{
setPeriodValues(t_start, t_start + period_default);
}
}
createTicks();
setPeriodValues(t_start, t_start + period_default);
loadPeriodFromLocalStorage();
{% if heures %}
let [heure_deb, heure_fin] = [{{ heures | safe }}]

View File

@ -49,7 +49,7 @@
<script src="{{scu.STATIC_DIR}}/js/scodoc.js"></script>
<script src="{{scu.STATIC_DIR}}/DataTables/datatables.min.js"></script>
<script>
window.onload = function () { enableTooltips("gtrcontent") };
window.onload = function () { enableTooltips("gtrcontent"); enableTooltips("sidebar"); };
const SCO_URL = "{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}";
</script>

View File

@ -1,7 +1,7 @@
{# Barre marge gauche ScoDoc #}
{# -*- mode: jinja-html -*- #}
<!-- sidebar -->
<div class="sidebar">
<div class="sidebar" id="sidebar">
{# sidebar_common #}
<a class="scodoc_title" href="{{
url_for('scodoc.index', scodoc_dept=g.scodoc_dept) }}">ScoDoc {{ sco.SCOVERSION }}</a>
@ -57,7 +57,7 @@
<b>Absences</b>
{% if sco.etud_cur_sem %}
<span title="absences du {{ sco.etud_cur_sem['date_debut'].strftime('%d/%m/%Y') }}
au {{ sco.etud_cur_sem['date_fin'].strftime('%d/%m/%Y') }}">({{sco.prefs["assi_metrique"]}})
au {{ sco.etud_cur_sem['date_fin'].strftime('%d/%m/%Y') }}" data-tooltip>({{sco.prefs["assi_metrique"]}})
<br />{{'%1g'|format(sco.nb_abs_just)}} J., {{'%1g'|format(sco.nb_abs_nj)}} N.J.</span>
{% endif %}
<ul>
@ -73,10 +73,8 @@
{% endif %}
<li><a href="{{ url_for('assiduites.calendrier_assi_etud', scodoc_dept=g.scodoc_dept,
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>
etudid=sco.etud.id) }}" title="Les pages bilan et liste ont été fusionnées">Liste/Bilan</a></li>
</ul>
{% endif %}
</div> {# /etud-insidebar #}

View File

@ -511,51 +511,6 @@ def _record_assiduite_etud(
return False
@bp.route("/liste_assiduites_etud")
@scodoc
@permission_required(Permission.ScoView)
def liste_assiduites_etud():
"""
liste_assiduites_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é
"""
# Récupération de l'étudiant concerné
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")
# Gestion d'une assiduité unique (redirigé depuis le calendrier) TODO-Assiduites
assiduite_id: int = request.args.get("assiduite_id", -1)
# Préparation de la page
tableau = _prepare_tableau(
liste_assi.AssiJustifData.from_etudiants(
etud,
),
filename=f"assiduites-justificatifs-{etud.id}",
afficher_etu=False,
filtre=liste_assi.AssiFiltre(type_obj=0),
options=liste_assi.AssiDisplayOptions(show_module=True),
cache_key=f"tableau-etud-{etud.id}",
)
if not tableau[0]:
return tableau[1]
# Page HTML:
return render_template(
"assiduites/pages/liste_assiduites.j2",
assi_id=assiduite_id,
etud=etud,
tableau=tableau[1],
sco=ScoData(etud),
)
@bp.route("/bilan_etud")
@scodoc
@permission_required(Permission.ScoView)
@ -583,28 +538,19 @@ def bilan_etud():
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id),
)
# Récupération des assiduités et justificatifs de l'étudiant
data = liste_assi.AssiJustifData(
etud.assiduites.filter(
Assiduite.etat != scu.EtatAssiduite.PRESENT, Assiduite.est_just == False
# Préparation de la page
tableau = _prepare_tableau(
liste_assi.AssiJustifData.from_etudiants(
etud,
),
etud.justificatifs.filter(
Justificatif.etat.in_(
[scu.EtatJustificatif.ATTENTE, scu.EtatJustificatif.MODIFIE]
)
),
)
table = _prepare_tableau(
data,
filename=f"assiduites-justificatifs-{etud.id}",
afficher_etu=False,
filename=f"Bilan assiduité {etud.nomprenom}",
titre="Bilan de l'assiduité de l'étudiant",
cache_key=f"tableau-etud-{etud.id}-bilan",
filtre=liste_assi.AssiFiltre(type_obj=0),
options=liste_assi.AssiDisplayOptions(show_module=True),
cache_key=f"tableau-etud-{etud.id}",
)
if not table[0]:
return table[1]
if not tableau[0]:
return tableau[1]
# Génération de la page HTML
return render_template(
@ -614,7 +560,7 @@ def bilan_etud():
date_debut=date_debut,
date_fin=date_fin,
sco=ScoData(etud),
tableau=table[1],
tableau=tableau[1],
)
@ -654,7 +600,7 @@ def edit_justificatif_etud(justif_id: int):
back_url = request.args.get("back_url", None)
redirect_url = back_url or url_for(
"assiduites.liste_assiduites_etud",
"assiduites.bilan_etud",
scodoc_dept=g.scodoc_dept,
etudid=justif.etudiant.id,
)
@ -958,8 +904,6 @@ def choix_date() -> str:
Route utilisée uniquement si la date courante n'est pas dans le semestre
concerné par la requête vers une des pages suivantes :
- saisie_assiduites_group
- visu_assiduites_group
"""
formsemestre_id = request.args.get("formsemestre_id")
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
@ -989,11 +933,7 @@ def choix_date() -> str:
if ok:
return redirect(
url_for(
(
"assiduites.signal_assiduites_group"
if request.args.get("readonly") is None
else "assiduites.visu_assiduites_group"
),
"assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
group_ids=group_ids,
@ -1065,7 +1005,7 @@ def signal_assiduites_group():
)
if not groups_infos.members:
return (
html_sco_header.sco_header(page_title="Saisie journalière de l'assiduité")
html_sco_header.sco_header(page_title="Saisie de l'assiduité")
+ "<h3>Aucun étudiant ! </h3>"
+ html_sco_header.sco_footer()
)
@ -1156,137 +1096,7 @@ def signal_assiduites_group():
sco=ScoData(formsemestre=formsemestre),
sem=sem["titre_num"],
timeline=_timeline(heures=",".join([f"'{s}'" for s in heures])),
title="Saisie journalière des assiduités",
)
@bp.route("/visu_assiduites_group")
@scodoc
@permission_required(Permission.ScoView)
def visu_assiduites_group():
"""
Visualisation des assiduités des groupes pour le jour donné
dans le formsemestre_id et le moduleimpl_id
Returns:
str: l'html généré
"""
# Récupération des paramètres de la requête
formsemestre_id: int = request.args.get("formsemestre_id", -1)
moduleimpl_id: int = request.args.get("moduleimpl_id")
date: str = request.args.get("jour", datetime.date.today().isoformat())
group_ids: list[int] = request.args.get("group_ids", None)
if group_ids is None:
group_ids = []
else:
group_ids = group_ids.split(",")
map(str, group_ids)
# Vérification du moduleimpl_id
if moduleimpl_id is not None:
try:
moduleimpl_id = int(moduleimpl_id)
except (TypeError, ValueError) as exc:
raise ScoValueError("identifiant de moduleimpl invalide") from exc
# Vérification du formsemestre_id
if formsemestre_id is not None:
try:
formsemestre_id = int(formsemestre_id)
except (TypeError, ValueError) as exc:
raise ScoValueError("identifiant de formsemestre invalide") from exc
# Récupérations des/du groupe(s)
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
)
if not groups_infos.members:
return (
html_sco_header.sco_header(page_title="Saisie journalière de l'assiduité")
+ "<h3>Aucun étudiant ! </h3>"
+ html_sco_header.sco_footer()
)
# --- Filtrage par formsemestre ---
formsemestre_id = groups_infos.formsemestre_id
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.dept_id != g.scodoc_dept_id:
abort(404, "groupes inexistants dans ce département")
# Récupération des étudiants du/des groupe(s)
etuds = [
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
for m in groups_infos.members
]
# --- Vérification de la date ---
real_date = scu.is_iso_formated(date, True).date()
if real_date < formsemestre.date_debut or real_date > formsemestre.date_fin:
# Si le jour est hors semestre, renvoyer vers choix date
return redirect(
url_for(
"assiduites.choix_date",
formsemestre_id=formsemestre_id,
group_ids=group_ids,
moduleimpl_id=moduleimpl_id,
scodoc_dept=g.scodoc_dept,
readonly="true",
)
)
# --- Restriction en fonction du moduleimpl_id ---
if moduleimpl_id:
mod_inscrits = {
x["etudid"]
for x in sco_moduleimpl.do_moduleimpl_inscription_list(
moduleimpl_id=moduleimpl_id
)
}
etuds_inscrits_module = [e for e in etuds if e["etudid"] in mod_inscrits]
if etuds_inscrits_module:
etuds = etuds_inscrits_module
else:
# Si aucun etudiant n'est inscrit au module choisi...
moduleimpl_id = None
# --- Génération du HTML ---
if groups_infos.tous_les_etuds_du_sem:
gr_tit = "en"
else:
if len(groups_infos.group_ids) > 1:
grp = "des groupes"
else:
grp = "du groupe"
gr_tit = (
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
)
# Récupération du semestre en dictionnaire
sem = formsemestre.to_dict()
return render_template(
"assiduites/pages/signal_assiduites_group.j2",
date=_dateiso_to_datefr(date),
defdem=_get_etuds_dem_def(formsemestre),
forcer_module=sco_preferences.get_preference(
"forcer_module",
formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_id,
),
formsemestre_date_debut=str(formsemestre.date_debut),
formsemestre_date_fin=str(formsemestre.date_fin),
formsemestre_id=formsemestre_id,
gr_tit=gr_tit,
grp=sco_groups_view.menu_groups_choice(groups_infos),
minitimeline=_mini_timeline(),
moduleimpl_select=_module_selector(formsemestre, moduleimpl_id),
nonworkdays=_non_work_days(),
sem=sem["titre_num"],
timeline=_timeline(),
readonly="true",
sco=ScoData(formsemestre=formsemestre),
title="Saisie journalière de l'assiduité",
title="Saisie de l'assiduité",
)
@ -2028,6 +1838,8 @@ def signal_assiduites_diff():
dept_id=g.scodoc_dept_id,
),
nouv_plage=nouv_plage,
formsemestre_id=formsemestre_id,
group_ids=group_ids,
)
@ -2298,89 +2110,6 @@ def _get_date_str(deb: datetime.datetime, fin: datetime.datetime) -> str:
return f'du {deb.strftime("%d/%m/%Y %H:%M")} au {fin.strftime("%d/%m/%Y %H:%M")}'
def _get_days_between_dates(deb: str, fin: str) -> str:
"""
_get_days_between_dates récupère tous les jours entre deux dates
Args:
deb (str): date de début
fin (str): date de fin
Returns:
str: une chaine json représentant une liste des jours
['date_iso','date_iso2', ...]
"""
if deb is None or fin is None:
return "null"
try:
if isinstance(deb, str) and isinstance(fin, str):
date_deb: datetime.date = datetime.date.fromisoformat(deb)
date_fin: datetime.date = datetime.date.fromisoformat(fin)
else:
date_deb, date_fin = deb.date(), fin.date()
except ValueError:
return "null"
dates: list[str] = []
while date_deb <= date_fin:
dates.append(f'"{date_deb.isoformat()}"')
date_deb = date_deb + datetime.timedelta(days=1)
return f"[{','.join(dates)}]"
def _differee(
etudiants: list[dict],
moduleimpl_select: str,
date: str = None,
periode: dict[str, str] = None,
formsemestre_id: int = None,
) -> str:
"""
_differee Génère un tableau de saisie différé
Args:
etudiants (list[dict]): la liste des étudiants (représentés par des dictionnaires)
moduleimpl_select (str): l'html représentant le selecteur de module
date (str, optional): la première date à afficher. Defaults to None.
periode (dict[str, str], optional):La période par défaut de la première colonne.
formsemestre_id (int, optional): l'id du semestre pour le selecteur de module.
Returns:
str: le widget (html/css/js)
"""
if date is None:
date = datetime.date.today().isoformat()
forcer_module = sco_preferences.get_preference(
"forcer_module",
formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_id,
)
assi_etat_defaut = sco_preferences.get_preference(
"assi_etat_defaut",
formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_id,
)
periode_defaut = sco_preferences.get_preference(
"periode_defaut",
formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_id,
)
return render_template(
"assiduites/widgets/differee.j2",
etudiants=etudiants,
assi_etat_defaut=assi_etat_defaut,
periode_defaut=periode_defaut,
forcer_module=forcer_module,
moduleimpl_select=moduleimpl_select,
date=date,
periode=periode,
)
def _module_selector(formsemestre: FormSemestre, moduleimpl_id: int = None) -> str:
"""
_module_selector Génère un HTMLSelectElement à partir des moduleimpl du formsemestre
@ -2450,7 +2179,7 @@ def _module_selector_multiple(
)
def _timeline(formsemestre_id: int = None, heures=None) -> str:
def _timeline(heures=None) -> str:
"""
_timeline retourne l'html de la timeline
@ -2465,11 +2194,9 @@ def _timeline(formsemestre_id: int = None, heures=None) -> str:
return render_template(
"assiduites/widgets/timeline.j2",
t_start=ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00"),
t_mid=ScoDocSiteConfig.assi_get_rounded_time("assi_lunch_time", "13:00:00"),
t_end=ScoDocSiteConfig.assi_get_rounded_time("assi_afternoon_time", "18:00:00"),
tick_time=ScoDocSiteConfig.get("assi_tick_time", 15),
periode_defaut=sco_preferences.get_preference(
"periode_defaut", formsemestre_id
),
heures=heures,
)
@ -2854,14 +2581,26 @@ def _generate_assiduite_bubble(assiduite: Assiduite) -> str:
# Récupérer informations saisie
saisie: str = assiduite.get_saisie()
motif: str = assiduite.description if assiduite.description else ""
motif: str = assiduite.description or "Non spécifié"
# Récupérer date
if assiduite.date_debut.date() == assiduite.date_fin.date():
jour = assiduite.date_debut.strftime("%d/%m/%Y")
heure_deb: str = assiduite.date_debut.strftime("%H:%M")
heure_fin: str = assiduite.date_fin.strftime("%H:%M")
date: str = f"{jour} de {heure_deb} à {heure_fin}"
else:
date: str = (
f"du {assiduite.date_debut.strftime('%d/%m/%Y')} "
+ f"au {assiduite.date_fin.strftime('%d/%m/%Y')}"
)
return render_template(
"assiduites/widgets/assiduite_bubble.j2",
moduleimpl=moduleimpl_infos,
etat=scu.EtatAssiduite(assiduite.etat).name.lower(),
date_debut=assiduite.date_debut.strftime("%d/%m/%Y %H:%M"),
date_fin=assiduite.date_fin.strftime("%d/%m/%Y %H:%M"),
date=date,
saisie=saisie,
motif=motif,
)