diff --git a/app/api/assiduites.py b/app/api/assiduites.py index 36d31d9b7..0644aa882 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -5,7 +5,8 @@ ############################################################################## """ScoDoc 9 API : Assiduités""" -from datetime import datetime +from datetime import datetime, timedelta +import re from flask import g, request from flask_json import as_json @@ -39,6 +40,24 @@ from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error +@bp.route("/assiduite/date_time_offset/") +@api_web_bp.route("/assiduite/date_time_offset/") +@scodoc +@permission_required(Permission.ScoView) +def date_time_offset(date_iso: str): + """L'offset dans le fuseau horaire du serveur pour la date indiquée. + Renvoie une chaîne de la forme "+04:00" (ISO 8601) + + Exemple: `/assiduite/date_time_offset/2024-10-01` renvoie `'+02:00'` + """ + if not re.match(r"^\d{4}-\d{2}-\d{2}$", date_iso): + json_error( + 404, + message="date invalide", + ) + return scu.get_local_timezone_offset(date_iso) + + @bp.route("/assiduite/") @api_web_bp.route("/assiduite/") @scodoc @@ -843,6 +862,10 @@ def _create_one( elif fin.tzinfo is None: fin: datetime = scu.localize_datetime(fin) + # check duration: min 1 minute + if (deb is not None) and (fin is not None) and (fin - deb) < timedelta(seconds=60): + errors.append("durée trop courte") + # cas 4 : desc desc: str = data.get("desc", None) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 50f767a49..5aeb45364 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -441,12 +441,17 @@ def localize_datetime(date: datetime.datetime) -> datetime.datetime: return new_date -def get_local_timezone_offset() -> str: +def get_local_timezone_offset(date_iso: str | None = None) -> str: """Récupère l'offset de la timezone du serveur, sous la forme - "+HH:MM" + "+HH:MM", pour le jour indiqué (date courante par défaut). + Par exemple get_local_timezone_offset("2024-10-30") == "+01:00" """ - local_time = datetime.datetime.now().astimezone() - utc_offset = local_time.utcoffset() + the_time = ( + datetime.datetime.now() + if date_iso is None + else datetime.datetime.fromisoformat(date_iso) + ) + utc_offset = the_time.astimezone().utcoffset() total_seconds = int(utc_offset.total_seconds()) offset_hours = total_seconds // 3600 offset_minutes = (abs(total_seconds) % 3600) // 60 diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 70eeb7565..68a3bf9bc 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -460,6 +460,19 @@ async function creerTousLesEtudiants(etuds) { .forEach((etud, index) => { etudsDiv.appendChild(creerLigneEtudiant(etud, index + 1)); }); + // Récupère l'offset timezone serveur pour la date sélectionnée + const date_iso = getSelectedDateIso(); + try { + const res = await fetch(`../../api/assiduite/date_time_offset/${date_iso}`); + if (!res.ok) { + throw new Error("Network response was not ok"); + } + const text = await res.text(); + console.log(text); + SERVER_TIMEZONE_OFFSET = text; + } catch (error) { + console.error('Error:', error); + } } /** @@ -609,7 +622,7 @@ async function actionAssiduite(etud, etat, type, assiduite = null) { const modimpl_id = $("#moduleimpl_select").val(); if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression"; - const { deb, fin } = getPeriodAsDate(true); // en tz server + const { deb, fin } = getPeriodAsISO(); // chaines sans timezone pour l'API // génération d'un objet assiduité basique qui sera complété let assiduiteObjet = assiduite ?? { date_debut: deb, @@ -722,9 +735,12 @@ function mettreToutLeMonde(etat, el = null) { const lignesEtuds = [...document.querySelectorAll("fieldset.btns_field")]; const { deb, fin } = getPeriodAsDate(true); // tz server + const period_iso = getPeriodAsISO(); // chaines sans timezone pour l'API + const deb_iso = period_iso.deb; + const fin_iso = period_iso.fin; const assiduiteObjet = { - date_debut: deb, - date_fin: fin, + date_debut: deb_iso, + date_fin: fin_iso, etat: etat, moduleimpl_id: $("#moduleimpl_select").val(), }; @@ -741,14 +757,14 @@ function mettreToutLeMonde(etat, el = null) { .filter((e) => e.getAttribute("type") == "edition") .map((e) => Number(e.getAttribute("assiduite_id"))); - // On récupère les assiduités conflictuelles mais qui sont comprisent - // Dans la plage de suppression + // On récupère les assiduités conflictuelles mais qui sont comprises + // dans la plage de suppression const unDeleted = {}; lignesEtuds .filter((e) => e.getAttribute("type") == "conflit") .forEach((e) => { const etud = etuds.get(Number(e.getAttribute("etudid"))); - // On récupère les assiduités couvertent par la plage de suppression + // On récupère les assiduités couvertes par la plage de suppression etud.assiduites.forEach((a) => { const date_debut = new Date(a.date_debut); const date_fin = new Date(a.date_fin); @@ -756,8 +772,8 @@ function mettreToutLeMonde(etat, el = null) { // (qui intersectent la plage de suppression) if ( Date.intersect( - { deb: deb, fin: fin }, - { deb: date_debut, fin: date_fin } + { deb: deb, fin: fin }, // la plage, en Date avec timezone serveur + { deb: date_debut, fin: date_fin } // dates de l'assiduité avec leur timezone ) ) { // Si l'assiduité est couverte par la plage de suppression diff --git a/app/templates/assiduites/widgets/timeline.j2 b/app/templates/assiduites/widgets/timeline.j2 index ddc96ad52..8abd18d29 100644 --- a/app/templates/assiduites/widgets/timeline.j2 +++ b/app/templates/assiduites/widgets/timeline.j2 @@ -12,7 +12,7 @@