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 wtforms import DecimalField, SubmitField, ValidationError
|
||||
from wtforms.fields.simple import StringField
|
||||
from wtforms.validators import Optional
|
||||
from wtforms.validators import Optional, Length
|
||||
|
||||
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):
|
||||
"""Le tick_time doit être entre 0 et 60 minutes"""
|
||||
if field.data < 1 or field.data > 59:
|
||||
@ -118,14 +77,36 @@ def check_ics_regexp(form, field):
|
||||
|
||||
class ConfigAssiduitesForm(FlaskForm):
|
||||
"Formulaire paramétrage Module Assiduité"
|
||||
|
||||
assi_morning_time = TimeField(
|
||||
"Début de la journée"
|
||||
) # TODO utiliser TextField + timepicker voir AjoutAssiOrJustForm
|
||||
assi_lunch_time = TimeField(
|
||||
"Heure de midi (date pivot entre matin et après-midi)"
|
||||
) # TODO
|
||||
assi_afternoon_time = TimeField("Fin de la journée") # TODO
|
||||
assi_morning_time = StringField(
|
||||
"Début de la journée",
|
||||
default="",
|
||||
validators=[Length(max=5)],
|
||||
render_kw={
|
||||
"class": "timepicker",
|
||||
"size": 5,
|
||||
"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(
|
||||
"Granularité de la timeline (temps en minutes)",
|
||||
|
@ -17,7 +17,15 @@ from app import log
|
||||
|
||||
class Trace:
|
||||
"""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:
|
||||
@ -39,7 +47,7 @@ class Trace:
|
||||
continue
|
||||
entry_date: datetime = is_iso_formated(csv[1], 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]
|
||||
|
||||
if os.path.isfile(self.path):
|
||||
@ -84,7 +92,14 @@ class Trace:
|
||||
self, fnames: list[str] = None
|
||||
) -> dict[str, list[datetime, datetime, str]]:
|
||||
"""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:
|
||||
return self.content
|
||||
@ -215,8 +230,7 @@ class JustificatifArchiver(BaseArchiver):
|
||||
filenames = self.list_archive(archive_id, dept_id=etud.dept_id)
|
||||
trace: Trace = Trace(archive_id)
|
||||
traced = trace.get_trace(filenames)
|
||||
|
||||
return [(key, value[2]) for key, value in traced.items()]
|
||||
return [(key, value[2]) for key, value in traced.items() if value is not None]
|
||||
|
||||
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:
|
||||
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:
|
||||
return collection.filter(
|
||||
collection_cls.date_debut <= date_fin, collection_cls.date_fin >= date_deb
|
||||
@ -558,15 +556,19 @@ def get_all_justified(
|
||||
return after
|
||||
|
||||
|
||||
def create_absence(
|
||||
def create_absence_billet(
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etudid: int,
|
||||
description: str = None,
|
||||
est_just: bool = False,
|
||||
) -> 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()
|
||||
assiduite_unique: Assiduite = Assiduite.create_assiduite(
|
||||
etud=etud,
|
||||
@ -648,8 +650,7 @@ def get_assiduites_count_in_interval(
|
||||
"""
|
||||
date_debut_iso = date_debut_iso or date_debut.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}{metrique}_assiduites"
|
||||
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites"
|
||||
|
||||
r = sco_cache.AbsSemEtudCache.get(key)
|
||||
if not r or moduleimpl_id is not None:
|
||||
@ -666,24 +667,24 @@ def get_assiduites_count_in_interval(
|
||||
calculator: CountCalculator = CountCalculator()
|
||||
calculator.compute_assiduites(assiduites)
|
||||
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:
|
||||
ans = sco_cache.AbsSemEtudCache.set(key, r)
|
||||
if not ans:
|
||||
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):
|
||||
"""Invalidate (clear) cached counts"""
|
||||
date_debut = sem["date_debut_iso"]
|
||||
date_fin = sem["date_fin_iso"]
|
||||
for met in scu.AssiduitesMetrics.TAG:
|
||||
key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites"
|
||||
sco_cache.AbsSemEtudCache.delete(key)
|
||||
key = str(etudid) + "_" + date_debut + "_" + date_fin + "_assiduites"
|
||||
sco_cache.AbsSemEtudCache.delete(key)
|
||||
|
||||
|
||||
# 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:
|
||||
"""Ajoute un timecode UTC à la date donnée.
|
||||
XXX semble faire autre chose... TODO fix this comment
|
||||
"""Transforme une date sans offset en une date avec offset
|
||||
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):
|
||||
date = is_iso_formated(date, convert=True)
|
||||
|
@ -1282,19 +1282,14 @@ function getAllAssiduitesFromEtud(
|
||||
.replace("°", courant ? "&courant" : "")
|
||||
: ""
|
||||
}`;
|
||||
//TODO Utiliser async_get au lieu de jquery
|
||||
$.ajax({
|
||||
async: true,
|
||||
type: "GET",
|
||||
url: url_api,
|
||||
success: (data, status) => {
|
||||
if (status === "success") {
|
||||
assiduites[etudid] = data;
|
||||
action(data);
|
||||
}
|
||||
async_get(
|
||||
url_api,
|
||||
(data) => {
|
||||
assiduites[etudid] = data;
|
||||
action(data);
|
||||
},
|
||||
error: () => {},
|
||||
});
|
||||
(_) => {}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1864,18 +1859,13 @@ function getAllJustificatifsFromEtud(
|
||||
order ? "/query?order°".replace("°", courant ? "&courant" : "") : ""
|
||||
}`;
|
||||
|
||||
//TODO Utiliser async_get au lieu de jquery
|
||||
$.ajax({
|
||||
async: true,
|
||||
type: "GET",
|
||||
url: url_api,
|
||||
success: (data, status) => {
|
||||
if (status === "success") {
|
||||
action(data);
|
||||
}
|
||||
async_get(
|
||||
url_api,
|
||||
(data) => {
|
||||
action(data);
|
||||
},
|
||||
error: () => {},
|
||||
});
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
|
||||
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]]:
|
||||
# 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]] = {
|
||||
"absent": ["Absences", 0.0, 0.0],
|
||||
"retard": ["Retards", 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(
|
||||
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():
|
||||
compte_etat = scass.get_assiduites_stats(
|
||||
assiduites=etud.assiduites,
|
||||
metric=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]
|
||||
valeur[1] = compte_etat[etat][assi_metric]
|
||||
if etat != "present":
|
||||
valeur[2] = compte_etat[etat]["justifie"][assi_metric]
|
||||
return retour
|
||||
|
||||
|
||||
|
@ -28,7 +28,7 @@
|
||||
<!-- Tableaux des assiduités (retard/abs) non justifiées -->
|
||||
<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
|
||||
de celles des autres pages ScoDoc: elle sera prochainement modifée, merci de votre patience.
|
||||
</div>
|
||||
@ -111,89 +111,76 @@
|
||||
|
||||
}
|
||||
|
||||
function getAssiduitesCount(dateDeb, dateFin, query) {
|
||||
const url_api = getUrl() + `/api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&${query}`;
|
||||
function getAssiduitesCount(dateDeb, dateFin, action) {
|
||||
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
|
||||
return $.ajax({
|
||||
async: true,
|
||||
type: "GET",
|
||||
url: url_api,
|
||||
success: (data, status) => {
|
||||
if (status === "success") {
|
||||
}
|
||||
async_get(
|
||||
url_api,
|
||||
action,
|
||||
()=>{},
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
//TODO Utiliser Fetch when plutot que jquery
|
||||
$.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');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
getAssiduitesCount(dateDeb, dateFin, showStats);
|
||||
}
|
||||
|
||||
function removeAllAssiduites() {
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.css"/>
|
||||
<style>
|
||||
div.config-section {
|
||||
font-weight: bold;
|
||||
@ -31,8 +32,18 @@ div.config-section {
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></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() {
|
||||
var inputValue = document.getElementById('test_edt_id').value;
|
||||
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">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ wtf.form_errors(form, hiddens="only") }}
|
||||
|
||||
{{ wtf.form_field(form.assi_morning_time) }}
|
||||
{{ wtf.form_field(form.assi_lunch_time) }}
|
||||
{{ wtf.form_field(form.assi_afternoon_time) }}
|
||||
{{ wtf.form_field(form.assi_morning_time, class="timepicker") }}
|
||||
{{ wtf.form_field(form.assi_lunch_time, class="timepicker") }}
|
||||
{{ wtf.form_field(form.assi_afternoon_time, class="timepicker") }}
|
||||
{{ wtf.form_field(form.assi_tick_time) }}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,8 +6,8 @@
|
||||
*/
|
||||
function getLeftPosition(start) {
|
||||
const startTime = new Date(start);
|
||||
const startMins = (startTime.getHours() - 8) * 60 + startTime.getMinutes();
|
||||
return (startMins / (18 * 60 - 8 * 60)) * 100 + "%";
|
||||
const startMins = (startTime.getHours() - t_start) * 60 + startTime.getMinutes();
|
||||
return (startMins / (t_end * 60 - t_start * 60)) * 100 + "%";
|
||||
}
|
||||
/**
|
||||
* Ajustement de l'espacement vertical entre les assiduités superposées
|
||||
@ -76,7 +76,7 @@
|
||||
|
||||
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) {
|
||||
console.log(start, end);
|
||||
@ -162,6 +162,13 @@
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@ -246,6 +253,7 @@
|
||||
*/
|
||||
splitAssiduiteModal() {
|
||||
//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>
|
||||
<input type="time" id="promptTime" name="appt"
|
||||
min="08:00" max="18:00" required>`;
|
||||
@ -371,8 +379,7 @@
|
||||
assiduitesContainer.innerHTML = '<div class="assiduite-special"></div>';
|
||||
|
||||
// Ajout des labels d'heure sur la frise chronologique
|
||||
// TODO permettre la modification des bornes (8 et 18)
|
||||
for (let i = 8; i <= 18; i++) {
|
||||
for (let i = t_start; i <= t_end; i++) {
|
||||
const timeLabel = document.createElement("div");
|
||||
timeLabel.className = "time-label";
|
||||
timeLabel.textContent = i < 10 ? `0${i}:00` : `${i}:00`;
|
||||
|
@ -32,6 +32,55 @@
|
||||
|
||||
</div>
|
||||
<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}}
|
||||
<div class="options-tableau">
|
||||
<!--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é.
|
||||
NB: actuellement, les heures ne sont utilisées que pour déterminer
|
||||
si matin et/ou après-midi.
|
||||
TODO: Vérifier l'intégration avec le module Assiduité
|
||||
"""
|
||||
if billet.etat:
|
||||
log(f"billet deja traite: {billet} !")
|
||||
@ -316,7 +317,7 @@ def _ProcessBilletAbsence(
|
||||
datedebut = billet.abs_begin
|
||||
datefin = billet.abs_end
|
||||
log(f"Gestion du billet n°{billet.id}")
|
||||
n = scass.create_absence(
|
||||
n = scass.create_absence_billet(
|
||||
date_debut=datedebut,
|
||||
date_fin=datefin,
|
||||
etudid=billet.etudid,
|
||||
|
@ -1615,7 +1615,7 @@ def tableau_assiduite_actions():
|
||||
return render_template(
|
||||
"assiduites/pages/tableau_assiduite_actions.j2",
|
||||
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é",
|
||||
action=action,
|
||||
etud=objet.etudiant,
|
||||
@ -1964,7 +1964,7 @@ def signale_evaluation_abs(etudid: int = None, evaluation_id: int = None):
|
||||
evaluation_id=evaluation.id,
|
||||
date_deb=evaluation.date_debut.strftime(
|
||||
"%Y-%m-%dT%H:%M:%S"
|
||||
), # XXX TODO
|
||||
),
|
||||
date_fin=evaluation.date_fin.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
moduleimpl_id=evaluation.moduleimpl.id,
|
||||
saisie_eval="true",
|
||||
|
@ -358,14 +358,10 @@ def config_assiduites():
|
||||
return redirect(url_for("scodoc.configuration"))
|
||||
|
||||
if request.method == "GET":
|
||||
form.assi_morning_time.data = ScoDocSiteConfig.get(
|
||||
"assi_morning_time", datetime.time(8, 0, 0)
|
||||
)
|
||||
form.assi_lunch_time.data = ScoDocSiteConfig.get(
|
||||
"assi_lunch_time", datetime.time(13, 0, 0)
|
||||
)
|
||||
form.assi_morning_time.data = ScoDocSiteConfig.get("assi_morning_time", "08:00")
|
||||
form.assi_lunch_time.data = ScoDocSiteConfig.get("assi_lunch_time", "13:00")
|
||||
form.assi_afternoon_time.data = ScoDocSiteConfig.get(
|
||||
"assi_afternoon_time", datetime.time(18, 0, 0)
|
||||
"assi_afternoon_time", "18:00"
|
||||
)
|
||||
try:
|
||||
form.assi_tick_time.data = float(
|
||||
|
Loading…
Reference in New Issue
Block a user