Assiduites : WIP todos

This commit is contained in:
Iziram 2024-01-18 17:05:43 +01:00
parent 44de81857a
commit 7659bcb488
13 changed files with 257 additions and 215 deletions

View File

@ -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)",

View File

@ -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):
""" """

View File

@ -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,23 +667,23 @@ 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)

View File

@ -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)

View File

@ -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",
url: url_api,
success: (data, status) => {
if (status === "success") {
assiduites[etudid] = data; assiduites[etudid] = data;
action(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",
url: url_api,
success: (data, status) => {
if (status === "success") {
action(data); action(data);
}
}, },
error: () => {}, () => {}
}); );
} }
function deleteJustificatif(justif_id) { function deleteJustificatif(justif_id) {

View File

@ -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

View File

@ -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,42 +111,28 @@
} }
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") {
}
},
error: () => { },
});
} }
function countAssiduites(dateDeb, dateFin) { function showStats(data){
//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 = { const counter = {
"present": { "present": {
"total": pt[0], "total": data["present"],
}, },
"retard": { "retard": {
"total": rt[0], "total": data["retard"],
"justi": rj[0], "justi": data["retard"]["justifie"],
}, },
"absent": { "absent": {
"total": at[0], "total": data["absent"],
"justi": aj[0], "justi": data["absent"]["justifie"],
} }
} }
@ -184,7 +170,7 @@
values.appendChild(item); values.appendChild(item);
}); });
const nbAbs = counter.absent.total[assi_metric] - counter.absent.justi[assi_metric]; const nbAbs = data["absent"]["non_justifie"][assi_metric];
if (nbAbs > assi_seuil) { if (nbAbs > assi_seuil) {
document.querySelector('.alerte').classList.remove('invisible'); document.querySelector('.alerte').classList.remove('invisible');
document.querySelector('.alerte p').textContent = `Attention, cet étudiant a trop d'absences ${nbAbs} / ${assi_seuil} (${metriques[assi_metric]})` document.querySelector('.alerte p').textContent = `Attention, cet étudiant a trop d'absences ${nbAbs} / ${assi_seuil} (${metriques[assi_metric]})`
@ -192,8 +178,9 @@
document.querySelector('.alerte').classList.add('invisible'); document.querySelector('.alerte').classList.add('invisible');
} }
} }
);
function countAssiduites(dateDeb, dateFin) {
getAssiduitesCount(dateDeb, dateFin, showStats);
} }
function removeAllAssiduites() { function removeAllAssiduites() {

View File

@ -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>

View File

@ -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`;

View File

@ -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}})">&lt;</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}})">&gt;</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 -->

View File

@ -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,

View File

@ -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",

View File

@ -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(