forked from ScoDoc/ScoDoc
Assiduites : WIP todos
This commit is contained in:
parent
44de81857a
commit
7659bcb488
@ -34,52 +34,11 @@ import re
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import DecimalField, SubmitField, ValidationError
|
from wtforms import DecimalField, SubmitField, ValidationError
|
||||||
from wtforms.fields.simple import StringField
|
from wtforms.fields.simple import StringField
|
||||||
from wtforms.validators import Optional
|
from wtforms.validators import Optional, Length
|
||||||
|
|
||||||
from wtforms.widgets import TimeInput
|
from wtforms.widgets import TimeInput
|
||||||
|
|
||||||
|
|
||||||
class TimeField(StringField):
|
|
||||||
"""HTML5 time input.
|
|
||||||
tiré de : https://gist.github.com/tachyondecay/6016d32f65a996d0d94f
|
|
||||||
"""
|
|
||||||
|
|
||||||
widget = TimeInput()
|
|
||||||
|
|
||||||
def __init__(self, label=None, validators=None, fmt="%H:%M:%S", **kwargs):
|
|
||||||
super(TimeField, self).__init__(label, validators, **kwargs)
|
|
||||||
self.fmt = fmt
|
|
||||||
self.data = None
|
|
||||||
|
|
||||||
def _value(self):
|
|
||||||
if self.raw_data:
|
|
||||||
return " ".join(self.raw_data)
|
|
||||||
if self.data and isinstance(self.data, str):
|
|
||||||
self.data = datetime.time(*map(int, self.data.split(":")))
|
|
||||||
return self.data and self.data.strftime(self.fmt) or ""
|
|
||||||
|
|
||||||
def process_formdata(self, valuelist):
|
|
||||||
if valuelist:
|
|
||||||
time_str = " ".join(valuelist)
|
|
||||||
try:
|
|
||||||
components = time_str.split(":")
|
|
||||||
hour = 0
|
|
||||||
minutes = 0
|
|
||||||
seconds = 0
|
|
||||||
if len(components) in range(2, 4):
|
|
||||||
hour = int(components[0])
|
|
||||||
minutes = int(components[1])
|
|
||||||
|
|
||||||
if len(components) == 3:
|
|
||||||
seconds = int(components[2])
|
|
||||||
else:
|
|
||||||
raise ValueError
|
|
||||||
self.data = datetime.time(hour, minutes, seconds)
|
|
||||||
except ValueError as exc:
|
|
||||||
self.data = None
|
|
||||||
raise ValueError(self.gettext("Not a valid time string")) from exc
|
|
||||||
|
|
||||||
|
|
||||||
def check_tick_time(form, field):
|
def check_tick_time(form, field):
|
||||||
"""Le tick_time doit être entre 0 et 60 minutes"""
|
"""Le tick_time doit être entre 0 et 60 minutes"""
|
||||||
if field.data < 1 or field.data > 59:
|
if field.data < 1 or field.data > 59:
|
||||||
@ -118,14 +77,36 @@ def check_ics_regexp(form, field):
|
|||||||
|
|
||||||
class ConfigAssiduitesForm(FlaskForm):
|
class ConfigAssiduitesForm(FlaskForm):
|
||||||
"Formulaire paramétrage Module Assiduité"
|
"Formulaire paramétrage Module Assiduité"
|
||||||
|
assi_morning_time = StringField(
|
||||||
assi_morning_time = TimeField(
|
"Début de la journée",
|
||||||
"Début de la journée"
|
default="",
|
||||||
) # TODO utiliser TextField + timepicker voir AjoutAssiOrJustForm
|
validators=[Length(max=5)],
|
||||||
assi_lunch_time = TimeField(
|
render_kw={
|
||||||
"Heure de midi (date pivot entre matin et après-midi)"
|
"class": "timepicker",
|
||||||
) # TODO
|
"size": 5,
|
||||||
assi_afternoon_time = TimeField("Fin de la journée") # TODO
|
"id": "assi_morning_time",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assi_lunch_time = StringField(
|
||||||
|
"Heure de midi (date pivot entre matin et après-midi)",
|
||||||
|
default="",
|
||||||
|
validators=[Length(max=5)],
|
||||||
|
render_kw={
|
||||||
|
"class": "timepicker",
|
||||||
|
"size": 5,
|
||||||
|
"id": "assi_lunch_time",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assi_afternoon_time = StringField(
|
||||||
|
"Fin de la journée",
|
||||||
|
validators=[Length(max=5)],
|
||||||
|
default="",
|
||||||
|
render_kw={
|
||||||
|
"class": "timepicker",
|
||||||
|
"size": 5,
|
||||||
|
"id": "assi_afternoon_time",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
assi_tick_time = DecimalField(
|
assi_tick_time = DecimalField(
|
||||||
"Granularité de la timeline (temps en minutes)",
|
"Granularité de la timeline (temps en minutes)",
|
||||||
|
@ -17,7 +17,15 @@ from app import log
|
|||||||
|
|
||||||
class Trace:
|
class Trace:
|
||||||
"""gestionnaire de la trace des fichiers justificatifs
|
"""gestionnaire de la trace des fichiers justificatifs
|
||||||
XXX TODO à documenter: rôle et format des fichier strace
|
|
||||||
|
Role des fichiers traces :
|
||||||
|
- Sauvegarder la date de dépot du fichier
|
||||||
|
- Sauvegarder la date de suppression du fichier (dans le cas de plusieurs fichiers pour un même justif)
|
||||||
|
- Sauvegarder l'user_id de l'utilisateur ayant déposé le fichier (=> permet de montrer les fichiers qu'aux personnes qui l'on déposé / qui ont le rôle AssiJustifView)
|
||||||
|
|
||||||
|
_trace.csv :
|
||||||
|
nom_fichier_srv,datetime_depot,datetime_suppr,user_id
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, path: str) -> None:
|
def __init__(self, path: str) -> None:
|
||||||
@ -39,7 +47,7 @@ class Trace:
|
|||||||
continue
|
continue
|
||||||
entry_date: datetime = is_iso_formated(csv[1], True)
|
entry_date: datetime = is_iso_formated(csv[1], True)
|
||||||
delete_date: datetime = is_iso_formated(csv[2], True)
|
delete_date: datetime = is_iso_formated(csv[2], True)
|
||||||
user_id = csv[3]
|
user_id = csv[3].strip()
|
||||||
self.content[fname] = [entry_date, delete_date, user_id]
|
self.content[fname] = [entry_date, delete_date, user_id]
|
||||||
|
|
||||||
if os.path.isfile(self.path):
|
if os.path.isfile(self.path):
|
||||||
@ -84,7 +92,14 @@ class Trace:
|
|||||||
self, fnames: list[str] = None
|
self, fnames: list[str] = None
|
||||||
) -> dict[str, list[datetime, datetime, str]]:
|
) -> dict[str, list[datetime, datetime, str]]:
|
||||||
"""Récupère la trace pour les noms de fichiers.
|
"""Récupère la trace pour les noms de fichiers.
|
||||||
si aucun nom n'est donné, récupère tous les fichiers"""
|
si aucun nom n'est donné, récupère tous les fichiers
|
||||||
|
|
||||||
|
retour :
|
||||||
|
{
|
||||||
|
"nom_fichier_srv": [datetime_depot, datetime_suppr/None, user_id],
|
||||||
|
...
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
if fnames is None:
|
if fnames is None:
|
||||||
return self.content
|
return self.content
|
||||||
@ -215,8 +230,7 @@ class JustificatifArchiver(BaseArchiver):
|
|||||||
filenames = self.list_archive(archive_id, dept_id=etud.dept_id)
|
filenames = self.list_archive(archive_id, dept_id=etud.dept_id)
|
||||||
trace: Trace = Trace(archive_id)
|
trace: Trace = Trace(archive_id)
|
||||||
traced = trace.get_trace(filenames)
|
traced = trace.get_trace(filenames)
|
||||||
|
return [(key, value[2]) for key, value in traced.items() if value is not None]
|
||||||
return [(key, value[2]) for key, value in traced.items()]
|
|
||||||
|
|
||||||
def get_justificatif_file(self, archive_name: str, etud: Identite, filename: str):
|
def get_justificatif_file(self, archive_name: str, etud: Identite, filename: str):
|
||||||
"""
|
"""
|
||||||
|
@ -450,8 +450,6 @@ def filter_by_date(
|
|||||||
if date_fin is None:
|
if date_fin is None:
|
||||||
date_fin = datetime.max
|
date_fin = datetime.max
|
||||||
|
|
||||||
date_deb = scu.localize_datetime(date_deb) # TODO A modifier (timezone ?)
|
|
||||||
date_fin = scu.localize_datetime(date_fin)
|
|
||||||
if not strict:
|
if not strict:
|
||||||
return collection.filter(
|
return collection.filter(
|
||||||
collection_cls.date_debut <= date_fin, collection_cls.date_fin >= date_deb
|
collection_cls.date_debut <= date_fin, collection_cls.date_fin >= date_deb
|
||||||
@ -558,15 +556,19 @@ def get_all_justified(
|
|||||||
return after
|
return after
|
||||||
|
|
||||||
|
|
||||||
def create_absence(
|
def create_absence_billet(
|
||||||
date_debut: datetime,
|
date_debut: datetime,
|
||||||
date_fin: datetime,
|
date_fin: datetime,
|
||||||
etudid: int,
|
etudid: int,
|
||||||
description: str = None,
|
description: str = None,
|
||||||
est_just: bool = False,
|
est_just: bool = False,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""TODO: doc, dire quand l'utiliser"""
|
"""
|
||||||
# TODO
|
Permet de rapidement créer une absence.
|
||||||
|
**UTILISÉ UNIQUEMENT POUR LES BILLETS**
|
||||||
|
Ne pas utiliser autre par.
|
||||||
|
TALK: Vérifier si nécessaire
|
||||||
|
"""
|
||||||
etud: Identite = Identite.query.filter_by(etudid=etudid).first_or_404()
|
etud: Identite = Identite.query.filter_by(etudid=etudid).first_or_404()
|
||||||
assiduite_unique: Assiduite = Assiduite.create_assiduite(
|
assiduite_unique: Assiduite = Assiduite.create_assiduite(
|
||||||
etud=etud,
|
etud=etud,
|
||||||
@ -648,8 +650,7 @@ def get_assiduites_count_in_interval(
|
|||||||
"""
|
"""
|
||||||
date_debut_iso = date_debut_iso or date_debut.isoformat()
|
date_debut_iso = date_debut_iso or date_debut.isoformat()
|
||||||
date_fin_iso = date_fin_iso or date_fin.isoformat()
|
date_fin_iso = date_fin_iso or date_fin.isoformat()
|
||||||
# TODO Question: pourquoi ne pas cacher toutes les métriques, si l'API les veut toutes ?
|
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites"
|
||||||
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}{metrique}_assiduites"
|
|
||||||
|
|
||||||
r = sco_cache.AbsSemEtudCache.get(key)
|
r = sco_cache.AbsSemEtudCache.get(key)
|
||||||
if not r or moduleimpl_id is not None:
|
if not r or moduleimpl_id is not None:
|
||||||
@ -666,24 +667,24 @@ def get_assiduites_count_in_interval(
|
|||||||
calculator: CountCalculator = CountCalculator()
|
calculator: CountCalculator = CountCalculator()
|
||||||
calculator.compute_assiduites(assiduites)
|
calculator.compute_assiduites(assiduites)
|
||||||
calcul: dict = calculator.to_dict(only_total=False)
|
calcul: dict = calculator.to_dict(only_total=False)
|
||||||
nb_abs: dict = calcul["absent"][metrique]
|
|
||||||
nb_abs_just: dict = calcul["absent_just"][metrique]
|
|
||||||
|
|
||||||
r = (nb_abs, nb_abs_just)
|
r = calcul
|
||||||
if moduleimpl_id is None:
|
if moduleimpl_id is None:
|
||||||
ans = sco_cache.AbsSemEtudCache.set(key, r)
|
ans = sco_cache.AbsSemEtudCache.set(key, r)
|
||||||
if not ans:
|
if not ans:
|
||||||
log("warning: get_assiduites_count failed to cache")
|
log("warning: get_assiduites_count failed to cache")
|
||||||
return r
|
|
||||||
|
nb_abs: dict = r["absent"][metrique]
|
||||||
|
nb_abs_just: dict = r["absent_just"][metrique]
|
||||||
|
return (nb_abs, nb_abs_just)
|
||||||
|
|
||||||
|
|
||||||
def invalidate_assiduites_count(etudid: int, sem: dict):
|
def invalidate_assiduites_count(etudid: int, sem: dict):
|
||||||
"""Invalidate (clear) cached counts"""
|
"""Invalidate (clear) cached counts"""
|
||||||
date_debut = sem["date_debut_iso"]
|
date_debut = sem["date_debut_iso"]
|
||||||
date_fin = sem["date_fin_iso"]
|
date_fin = sem["date_fin_iso"]
|
||||||
for met in scu.AssiduitesMetrics.TAG:
|
key = str(etudid) + "_" + date_debut + "_" + date_fin + "_assiduites"
|
||||||
key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites"
|
sco_cache.AbsSemEtudCache.delete(key)
|
||||||
sco_cache.AbsSemEtudCache.delete(key)
|
|
||||||
|
|
||||||
|
|
||||||
# Non utilisé
|
# Non utilisé
|
||||||
|
@ -299,8 +299,11 @@ def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or No
|
|||||||
|
|
||||||
|
|
||||||
def localize_datetime(date: datetime.datetime or str) -> datetime.datetime:
|
def localize_datetime(date: datetime.datetime or str) -> datetime.datetime:
|
||||||
"""Ajoute un timecode UTC à la date donnée.
|
"""Transforme une date sans offset en une date avec offset
|
||||||
XXX semble faire autre chose... TODO fix this comment
|
Tente de mettre l'offset de la timezone du serveur (ex : UTC+1)
|
||||||
|
Si erreur, mettra l'offset UTC
|
||||||
|
|
||||||
|
TODO : vérifier puis supprimer l'auto conversion str-> datetime
|
||||||
"""
|
"""
|
||||||
if isinstance(date, str):
|
if isinstance(date, str):
|
||||||
date = is_iso_formated(date, convert=True)
|
date = is_iso_formated(date, convert=True)
|
||||||
|
@ -1282,19 +1282,14 @@ function getAllAssiduitesFromEtud(
|
|||||||
.replace("°", courant ? "&courant" : "")
|
.replace("°", courant ? "&courant" : "")
|
||||||
: ""
|
: ""
|
||||||
}`;
|
}`;
|
||||||
//TODO Utiliser async_get au lieu de jquery
|
async_get(
|
||||||
$.ajax({
|
url_api,
|
||||||
async: true,
|
(data) => {
|
||||||
type: "GET",
|
assiduites[etudid] = data;
|
||||||
url: url_api,
|
action(data);
|
||||||
success: (data, status) => {
|
|
||||||
if (status === "success") {
|
|
||||||
assiduites[etudid] = data;
|
|
||||||
action(data);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
error: () => {},
|
(_) => {}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1864,18 +1859,13 @@ function getAllJustificatifsFromEtud(
|
|||||||
order ? "/query?order°".replace("°", courant ? "&courant" : "") : ""
|
order ? "/query?order°".replace("°", courant ? "&courant" : "") : ""
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
//TODO Utiliser async_get au lieu de jquery
|
async_get(
|
||||||
$.ajax({
|
url_api,
|
||||||
async: true,
|
(data) => {
|
||||||
type: "GET",
|
action(data);
|
||||||
url: url_api,
|
|
||||||
success: (data, status) => {
|
|
||||||
if (status === "success") {
|
|
||||||
action(data);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
error: () => {},
|
() => {}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteJustificatif(justif_id) {
|
function deleteJustificatif(justif_id) {
|
||||||
|
@ -129,41 +129,44 @@ class RowAssi(tb.Row):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _get_etud_stats(self, etud: Identite) -> dict[str, list[str, float, float]]:
|
def _get_etud_stats(self, etud: Identite) -> dict[str, list[str, float, float]]:
|
||||||
# XXX TODO @iziram commentaire sur la fonction et la var. retour
|
"""
|
||||||
|
Renvoie le comptage (dans la métrique du département) des différents états d'assiduité d'un étudiant
|
||||||
|
|
||||||
|
Returns :
|
||||||
|
{
|
||||||
|
"<etat>" : [<Etat version lisible>, <nb total etat>, <nb just etat>]
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Préparation du retour
|
||||||
retour: dict[str, tuple[str, float, float]] = {
|
retour: dict[str, tuple[str, float, float]] = {
|
||||||
"absent": ["Absences", 0.0, 0.0],
|
"absent": ["Absences", 0.0, 0.0],
|
||||||
"retard": ["Retards", 0.0, 0.0],
|
"retard": ["Retards", 0.0, 0.0],
|
||||||
"present": ["Présences", 0.0, 0.0],
|
"present": ["Présences", 0.0, 0.0],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Récupération de la métrique du département
|
||||||
assi_metric = scu.translate_assiduites_metric(
|
assi_metric = scu.translate_assiduites_metric(
|
||||||
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id),
|
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
compte_etat: dict[str, dict] = scass.get_assiduites_stats(
|
||||||
|
assiduites=etud.assiduites,
|
||||||
|
metric=assi_metric,
|
||||||
|
filtered={
|
||||||
|
"date_debut": self.dates[0],
|
||||||
|
"date_fin": self.dates[1],
|
||||||
|
"etat": "absent,present,retard", # pour tout compter d'un coup
|
||||||
|
"split": 1, # afin d'avoir la division des stats en état, etatjust, etatnonjust
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pour chaque état on mets à jour les valeurs de retour
|
||||||
for etat, valeur in retour.items():
|
for etat, valeur in retour.items():
|
||||||
compte_etat = scass.get_assiduites_stats(
|
valeur[1] = compte_etat[etat][assi_metric]
|
||||||
assiduites=etud.assiduites,
|
if etat != "present":
|
||||||
metric=assi_metric,
|
valeur[2] = compte_etat[etat]["justifie"][assi_metric]
|
||||||
filtered={
|
|
||||||
"date_debut": self.dates[0],
|
|
||||||
"date_fin": self.dates[1],
|
|
||||||
"etat": etat,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
compte_etat_just = scass.get_assiduites_stats(
|
|
||||||
assiduites=etud.assiduites,
|
|
||||||
metric=assi_metric,
|
|
||||||
filtered={
|
|
||||||
"date_debut": self.dates[0],
|
|
||||||
"date_fin": self.dates[1],
|
|
||||||
"etat": etat,
|
|
||||||
"est_just": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
valeur[1] = compte_etat[assi_metric]
|
|
||||||
valeur[2] = compte_etat_just[assi_metric]
|
|
||||||
return retour
|
return retour
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
<!-- Tableaux des assiduités (retard/abs) non justifiées -->
|
<!-- Tableaux des assiduités (retard/abs) non justifiées -->
|
||||||
<h4>Absences et retards non justifiés</h4>
|
<h4>Absences et retards non justifiés</h4>
|
||||||
|
|
||||||
{# XXX XXX XXX #}
|
{# TODO Utiliser python tableau plutot que js tableau #}
|
||||||
<div class="ue_warning">Attention, cette page utilise des couleurs et conventions différentes
|
<div class="ue_warning">Attention, cette page utilise des couleurs et conventions différentes
|
||||||
de celles des autres pages ScoDoc: elle sera prochainement modifée, merci de votre patience.
|
de celles des autres pages ScoDoc: elle sera prochainement modifée, merci de votre patience.
|
||||||
</div>
|
</div>
|
||||||
@ -111,89 +111,76 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAssiduitesCount(dateDeb, dateFin, query) {
|
function getAssiduitesCount(dateDeb, dateFin, action) {
|
||||||
const url_api = getUrl() + `/api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&${query}`;
|
const url_api = getUrl() + `/api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&etat=absent,retard,present&split`;
|
||||||
//Utiliser async_get au lieu de Jquery
|
//Utiliser async_get au lieu de Jquery
|
||||||
return $.ajax({
|
async_get(
|
||||||
async: true,
|
url_api,
|
||||||
type: "GET",
|
action,
|
||||||
url: url_api,
|
()=>{},
|
||||||
success: (data, status) => {
|
);
|
||||||
if (status === "success") {
|
}
|
||||||
}
|
|
||||||
|
function showStats(data){
|
||||||
|
const counter = {
|
||||||
|
"present": {
|
||||||
|
"total": data["present"],
|
||||||
},
|
},
|
||||||
error: () => { },
|
"retard": {
|
||||||
|
"total": data["retard"],
|
||||||
|
"justi": data["retard"]["justifie"],
|
||||||
|
},
|
||||||
|
"absent": {
|
||||||
|
"total": data["absent"],
|
||||||
|
"justi": data["absent"]["justifie"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 withJusti = (key, metric) => {
|
||||||
|
if (key == "present") return "";
|
||||||
|
return ` dont ${counter[key].justi[metric]} justifiées`
|
||||||
|
}
|
||||||
|
|
||||||
|
const heure = document.createElement('span');
|
||||||
|
heure.textContent = `${counter[key].total.heure} heure(s)${withJusti(key, "heure")}`;
|
||||||
|
|
||||||
|
const demi = document.createElement('span');
|
||||||
|
demi.textContent = `${counter[key].total.demi} demi-journée(s)${withJusti(key, "demi")}`;
|
||||||
|
|
||||||
|
const jour = document.createElement('span');
|
||||||
|
jour.textContent = `${counter[key].total.journee} journée(s)${withJusti(key, "journee")}`;
|
||||||
|
|
||||||
|
div.append(jour, demi, heure);
|
||||||
|
|
||||||
|
const title = document.createElement('h5');
|
||||||
|
title.textContent = key.capitalize();
|
||||||
|
|
||||||
|
item.append(title, div)
|
||||||
|
|
||||||
|
values.appendChild(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nbAbs = data["absent"]["non_justifie"][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 countAssiduites(dateDeb, dateFin) {
|
function countAssiduites(dateDeb, dateFin) {
|
||||||
//TODO Utiliser Fetch when plutot que jquery
|
getAssiduitesCount(dateDeb, dateFin, showStats);
|
||||||
$.when(
|
|
||||||
getAssiduitesCount(dateDeb, dateFin, `etat=present`),
|
|
||||||
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, rt, rj, at, aj) => {
|
|
||||||
const counter = {
|
|
||||||
"present": {
|
|
||||||
"total": pt[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 withJusti = (key, metric) => {
|
|
||||||
if (key == "present") return "";
|
|
||||||
return ` dont ${counter[key].justi[metric]} justifiées`
|
|
||||||
}
|
|
||||||
|
|
||||||
const heure = document.createElement('span');
|
|
||||||
heure.textContent = `${counter[key].total.heure} heure(s)${withJusti(key, "heure")}`;
|
|
||||||
|
|
||||||
const demi = document.createElement('span');
|
|
||||||
demi.textContent = `${counter[key].total.demi} demi-journée(s)${withJusti(key, "demi")}`;
|
|
||||||
|
|
||||||
const jour = document.createElement('span');
|
|
||||||
jour.textContent = `${counter[key].total.journee} journée(s)${withJusti(key, "journee")}`;
|
|
||||||
|
|
||||||
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() {
|
function removeAllAssiduites() {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
{% block styles %}
|
{% block styles %}
|
||||||
{{super()}}
|
{{super()}}
|
||||||
|
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.css"/>
|
||||||
<style>
|
<style>
|
||||||
div.config-section {
|
div.config-section {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -31,8 +32,18 @@ div.config-section {
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
<script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
$('.timepicker').timepicker({
|
||||||
|
timeFormat: 'HH:mm',
|
||||||
|
interval: {{ scu.get_assiduites_time_config("assi_tick_time") }},
|
||||||
|
minTime: "00:00",
|
||||||
|
maxTime: "23:59",
|
||||||
|
dynamic: false,
|
||||||
|
dropdown: true,
|
||||||
|
scrollbar: false
|
||||||
|
});
|
||||||
function update_test_button_state() {
|
function update_test_button_state() {
|
||||||
var inputValue = document.getElementById('test_edt_id').value;
|
var inputValue = document.getElementById('test_edt_id').value;
|
||||||
document.getElementById('test_load_ics').disabled = inputValue.length === 0;
|
document.getElementById('test_load_ics').disabled = inputValue.length === 0;
|
||||||
@ -78,10 +89,9 @@ c'est à dire à la montre des étudiants.
|
|||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
{{ wtf.form_errors(form, hiddens="only") }}
|
{{ wtf.form_errors(form, hiddens="only") }}
|
||||||
|
{{ wtf.form_field(form.assi_morning_time, class="timepicker") }}
|
||||||
{{ wtf.form_field(form.assi_morning_time) }}
|
{{ wtf.form_field(form.assi_lunch_time, class="timepicker") }}
|
||||||
{{ wtf.form_field(form.assi_lunch_time) }}
|
{{ wtf.form_field(form.assi_afternoon_time, class="timepicker") }}
|
||||||
{{ wtf.form_field(form.assi_afternoon_time) }}
|
|
||||||
{{ wtf.form_field(form.assi_tick_time) }}
|
{{ wtf.form_field(form.assi_tick_time) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
*/
|
*/
|
||||||
function getLeftPosition(start) {
|
function getLeftPosition(start) {
|
||||||
const startTime = new Date(start);
|
const startTime = new Date(start);
|
||||||
const startMins = (startTime.getHours() - 8) * 60 + startTime.getMinutes();
|
const startMins = (startTime.getHours() - t_start) * 60 + startTime.getMinutes();
|
||||||
return (startMins / (18 * 60 - 8 * 60)) * 100 + "%";
|
return (startMins / (t_end * 60 - t_start * 60)) * 100 + "%";
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Ajustement de l'espacement vertical entre les assiduités superposées
|
* Ajustement de l'espacement vertical entre les assiduités superposées
|
||||||
@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
const duration = (endTime - startTime) / 1000 / 60;
|
const duration = (endTime - startTime) / 1000 / 60;
|
||||||
|
|
||||||
const percent = (duration / (18 * 60 - 8 * 60)) * 100
|
const percent = (duration / (t_end * 60 - t_start * 60)) * 100
|
||||||
|
|
||||||
if (percent > 100) {
|
if (percent > 100) {
|
||||||
console.log(start, end);
|
console.log(start, end);
|
||||||
@ -162,6 +162,13 @@
|
|||||||
|
|
||||||
document.querySelector('#myModal .close').addEventListener('click', () => { this.close() })
|
document.querySelector('#myModal .close').addEventListener('click', () => { this.close() })
|
||||||
|
|
||||||
|
// fermeture du modal en appuyant sur echap
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
}, { once: true })
|
||||||
|
|
||||||
this.render()
|
this.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,6 +253,7 @@
|
|||||||
*/
|
*/
|
||||||
splitAssiduiteModal() {
|
splitAssiduiteModal() {
|
||||||
//Préparation du prompt
|
//Préparation du prompt
|
||||||
|
// TODO utiliser timepicker jquery + utiliser les bornes (t_start et t_end)
|
||||||
const htmlPrompt = `<legend>Entrez l'heure de séparation (HH:mm) :</legend>
|
const htmlPrompt = `<legend>Entrez l'heure de séparation (HH:mm) :</legend>
|
||||||
<input type="time" id="promptTime" name="appt"
|
<input type="time" id="promptTime" name="appt"
|
||||||
min="08:00" max="18:00" required>`;
|
min="08:00" max="18:00" required>`;
|
||||||
@ -371,8 +379,7 @@
|
|||||||
assiduitesContainer.innerHTML = '<div class="assiduite-special"></div>';
|
assiduitesContainer.innerHTML = '<div class="assiduite-special"></div>';
|
||||||
|
|
||||||
// Ajout des labels d'heure sur la frise chronologique
|
// Ajout des labels d'heure sur la frise chronologique
|
||||||
// TODO permettre la modification des bornes (8 et 18)
|
for (let i = t_start; i <= t_end; i++) {
|
||||||
for (let i = 8; i <= 18; i++) {
|
|
||||||
const timeLabel = document.createElement("div");
|
const timeLabel = document.createElement("div");
|
||||||
timeLabel.className = "time-label";
|
timeLabel.className = "time-label";
|
||||||
timeLabel.textContent = i < 10 ? `0${i}:00` : `${i}:00`;
|
timeLabel.textContent = i < 10 ? `0${i}:00` : `${i}:00`;
|
||||||
|
@ -32,6 +32,55 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="div-tableau">
|
<div class="div-tableau">
|
||||||
|
<div class="options-tableau">
|
||||||
|
<!--Pagination basée sur : https://app.uxcel.com/courses/ui-components-best-practices/best-practices-005 -->
|
||||||
|
<!-- Mettre les flèches -->
|
||||||
|
{% if total_pages > 1 %}
|
||||||
|
<ul class="pagination">
|
||||||
|
<li class="">
|
||||||
|
<a onclick="navigateToPage({{options.page - 1}})"><</a>
|
||||||
|
</li>
|
||||||
|
<!-- Toujours afficher la première page -->
|
||||||
|
<li class="{% if options.page == 1 %}active{% endif %}">
|
||||||
|
<a onclick="navigateToPage({{1}})">1</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Afficher les ellipses si la page courante est supérieure à 2 -->
|
||||||
|
<!-- et qu'il y a plus d'une page entre le 1 et la page courante-1 -->
|
||||||
|
{% if options.page > 2 and (options.page - 1) - 1 > 1 %}
|
||||||
|
<li class="disabled"><span>...</span></li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Afficher la page précédente, la page courante, et la page suivante -->
|
||||||
|
{% for i in range(options.page - 1, options.page + 2) %}
|
||||||
|
{% if i > 1 and i < total_pages %}
|
||||||
|
<li class="{% if options.page == i %}active{% endif %}">
|
||||||
|
<a onclick="navigateToPage({{i}})">{{ i }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Afficher les ellipses si la page courante est inférieure à l'avant-dernière page -->
|
||||||
|
<!-- et qu'il y a plus d'une page entre le total_pages et la page courante+1 -->
|
||||||
|
{% if options.page < total_pages - 1 and total_pages - (options.page + 1 ) > 1 %}
|
||||||
|
<li class="disabled"><span>...</span></li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Toujours afficher la dernière page -->
|
||||||
|
<li class="{% if options.page == total_pages %}active{% endif %}">
|
||||||
|
<a onclick="navigateToPage({{total_pages}})">{{ total_pages }}</a>
|
||||||
|
</li>
|
||||||
|
<li class="">
|
||||||
|
<a onclick="navigateToPage({{options.page + 1}})">></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<!-- Afficher un seul bouton si il n'y a qu'une seule page -->
|
||||||
|
<ul class="pagination">
|
||||||
|
<li class="active"><a onclick="navigateToPage({{1}})">1</a></li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{{table.html() | safe}}
|
{{table.html() | safe}}
|
||||||
<div class="options-tableau">
|
<div class="options-tableau">
|
||||||
<!--Pagination basée sur : https://app.uxcel.com/courses/ui-components-best-practices/best-practices-005 -->
|
<!--Pagination basée sur : https://app.uxcel.com/courses/ui-components-best-practices/best-practices-005 -->
|
||||||
|
@ -306,6 +306,7 @@ def _ProcessBilletAbsence(
|
|||||||
return: nombre de demi-journées d'absence ajoutées, -1 si billet déjà traité.
|
return: nombre de demi-journées d'absence ajoutées, -1 si billet déjà traité.
|
||||||
NB: actuellement, les heures ne sont utilisées que pour déterminer
|
NB: actuellement, les heures ne sont utilisées que pour déterminer
|
||||||
si matin et/ou après-midi.
|
si matin et/ou après-midi.
|
||||||
|
TODO: Vérifier l'intégration avec le module Assiduité
|
||||||
"""
|
"""
|
||||||
if billet.etat:
|
if billet.etat:
|
||||||
log(f"billet deja traite: {billet} !")
|
log(f"billet deja traite: {billet} !")
|
||||||
@ -316,7 +317,7 @@ def _ProcessBilletAbsence(
|
|||||||
datedebut = billet.abs_begin
|
datedebut = billet.abs_begin
|
||||||
datefin = billet.abs_end
|
datefin = billet.abs_end
|
||||||
log(f"Gestion du billet n°{billet.id}")
|
log(f"Gestion du billet n°{billet.id}")
|
||||||
n = scass.create_absence(
|
n = scass.create_absence_billet(
|
||||||
date_debut=datedebut,
|
date_debut=datedebut,
|
||||||
date_fin=datefin,
|
date_fin=datefin,
|
||||||
etudid=billet.etudid,
|
etudid=billet.etudid,
|
||||||
|
@ -1615,7 +1615,7 @@ def tableau_assiduite_actions():
|
|||||||
return render_template(
|
return render_template(
|
||||||
"assiduites/pages/tableau_assiduite_actions.j2",
|
"assiduites/pages/tableau_assiduite_actions.j2",
|
||||||
sco=ScoData(etud=objet.etudiant),
|
sco=ScoData(etud=objet.etudiant),
|
||||||
# XXX type semble être utilisé qq part, ne pas changer
|
# type utilisé dans les actions modifier / détails (modifier.j2, details.j2)
|
||||||
type="Justificatif" if obj_type == "justificatif" else "Assiduité",
|
type="Justificatif" if obj_type == "justificatif" else "Assiduité",
|
||||||
action=action,
|
action=action,
|
||||||
etud=objet.etudiant,
|
etud=objet.etudiant,
|
||||||
@ -1964,7 +1964,7 @@ def signale_evaluation_abs(etudid: int = None, evaluation_id: int = None):
|
|||||||
evaluation_id=evaluation.id,
|
evaluation_id=evaluation.id,
|
||||||
date_deb=evaluation.date_debut.strftime(
|
date_deb=evaluation.date_debut.strftime(
|
||||||
"%Y-%m-%dT%H:%M:%S"
|
"%Y-%m-%dT%H:%M:%S"
|
||||||
), # XXX TODO
|
),
|
||||||
date_fin=evaluation.date_fin.strftime("%Y-%m-%dT%H:%M:%S"),
|
date_fin=evaluation.date_fin.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||||
moduleimpl_id=evaluation.moduleimpl.id,
|
moduleimpl_id=evaluation.moduleimpl.id,
|
||||||
saisie_eval="true",
|
saisie_eval="true",
|
||||||
|
@ -358,14 +358,10 @@ def config_assiduites():
|
|||||||
return redirect(url_for("scodoc.configuration"))
|
return redirect(url_for("scodoc.configuration"))
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
form.assi_morning_time.data = ScoDocSiteConfig.get(
|
form.assi_morning_time.data = ScoDocSiteConfig.get("assi_morning_time", "08:00")
|
||||||
"assi_morning_time", datetime.time(8, 0, 0)
|
form.assi_lunch_time.data = ScoDocSiteConfig.get("assi_lunch_time", "13:00")
|
||||||
)
|
|
||||||
form.assi_lunch_time.data = ScoDocSiteConfig.get(
|
|
||||||
"assi_lunch_time", datetime.time(13, 0, 0)
|
|
||||||
)
|
|
||||||
form.assi_afternoon_time.data = ScoDocSiteConfig.get(
|
form.assi_afternoon_time.data = ScoDocSiteConfig.get(
|
||||||
"assi_afternoon_time", datetime.time(18, 0, 0)
|
"assi_afternoon_time", "18:00"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
form.assi_tick_time.data = float(
|
form.assi_tick_time.data = float(
|
||||||
|
Loading…
Reference in New Issue
Block a user