diff --git a/app/api/assiduites.py b/app/api/assiduites.py
index 36d31d9b7f4b78ac2169f9e91bd9eeeb739b3608..0644aa8820eb8ba5a3563f9fe2aeffbaa5b4340c 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/<string:date_iso>")
+@api_web_bp.route("/assiduite/date_time_offset/<string:date_iso>")
+@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/<int:assiduite_id>")
@api_web_bp.route("/assiduite/<int:assiduite_id>")
@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 50f767a4912b95c80dbce4492f464f000451b81a..5aeb45364245283b61096a6b150522b8d67a5fc9 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 70eeb75659a0eb8c1c1cbb8d3f9b83a4a6c0335c..68a3bf9bcec399925e2877d81442e48da8a95831 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 ddc96ad52ad0600a6fe499e3e8b98e07a8235a2a..8abd18d29a273d100199b81c906c4d9e80fc81ad 100644
--- a/app/templates/assiduites/widgets/timeline.j2
+++ b/app/templates/assiduites/widgets/timeline.j2
@@ -12,7 +12,7 @@
</div>
</div>
<script>
- const SERVER_TIMEZONE_OFFSET = "{{ scu.get_local_timezone_offset() }}";
+ var SERVER_TIMEZONE_OFFSET = "{{ scu.get_local_timezone_offset() }}"; // modifié par creerTousLesEtudiants()
const timelineContainer = document.querySelector(".timeline-container");
const periodTimeLine = document.querySelector(".period");
const t_start = {{ t_start }};
@@ -336,7 +336,14 @@
const [hours, minutes] = time.split(separator).map((el) => Number(el))
return hours + minutes / 60
}
- // Renvoie les valeurs de la période sous forme de date
+ // La date ISO du datepicker
+ function getSelectedDateIso() {
+ return $("#date")
+ .datepicker("getDate")
+ .format("yyyy-mm-dd")
+ .substring(0, 10); // récupération que de la date, pas des heures
+ }
+ // Renvoie les valeurs de la période sous forme de Date
// Les heures sont récupérées depuis la timeline
// la date est récupérée depuis un champ "#date" (datepicker)
function getPeriodAsDate(add_server_tz = false) {
@@ -344,10 +351,7 @@
deb = numberToTime(deb);
fin = numberToTime(fin);
- const dateStr = $("#date")
- .datepicker("getDate")
- .format("yyyy-mm-dd")
- .substring(0, 10); // récupération que de la date, pas des heures
+ const dateStr = getSelectedDateIso();
// Les heures deb et fin sont telles qu'affichées, c'est à dire
// en heure locale DU SERVEUR (des étudiants donc)
@@ -357,6 +361,22 @@
fin: new Date(`${dateStr}T${fin}${offset}`)
}
}
+ // Renvoie les valeurs de la période sous forme de chaine ISO sans time zone.
+ function getPeriodAsISO() {
+ let [deb, fin] = getPeriodValues();
+ deb = numberToTime(deb);
+ fin = numberToTime(fin);
+
+ const dateStr = $("#date")
+ .datepicker("getDate")
+ .format("yyyy-mm-dd")
+ .substring(0, 10); // récupération que de la date, pas des heures
+ // retourne des chaines ISO sans timezone
+ return {
+ deb : `${dateStr}T${deb}`,
+ fin : `${dateStr}T${fin}`
+ }
+ }
// Sauvegarde les valeurs de la période dans le local storage
function savePeriodInLocalStorage(){
const dates = getPeriodValues();
diff --git a/sco_version.py b/sco_version.py
index 90af92cbdf034386767965cceab5020d613d007d..a3ff9daf74819d41c26757979b37cf0a38179baf 100644
--- a/sco_version.py
+++ b/sco_version.py
@@ -3,7 +3,7 @@
"Infos sur version ScoDoc"
-SCOVERSION = "9.7.34"
+SCOVERSION = "9.7.35"
SCONAME = "ScoDoc"
diff --git a/tests/api/test_api_assiduites.py b/tests/api/test_api_assiduites.py
index c860e573d4c1bafac7d52461a09e645feb064936..97fcacc380459ea3965cd8703ab5c8a6000ea771 100644
--- a/tests/api/test_api_assiduites.py
+++ b/tests/api/test_api_assiduites.py
@@ -7,6 +7,7 @@ Ecrit par HARTMANN Matthias
import datetime
from random import randint
+import re
from types import NoneType
from app.scodoc import sco_utils as scu
@@ -449,3 +450,16 @@ def test_route_delete(api_admin_headers):
assert len(res["errors"]) == 3
assert all(i["message"] == "Assiduite non existante" for i in res["errors"])
+
+
+def test_date_time_offset(api_headers):
+ """test de la route /assiduites/date_time_offset"""
+
+ reply = GET(
+ path="/assiduite/date_time_offset/2024-10-29",
+ headers=api_headers,
+ dept=DEPT_ACRONYM,
+ raw=True,
+ )
+ # offset ISO 8601 de la forme +/-hh:mm
+ assert re.match(r"^(Z|[+-]\d{2}:\d{2})$", reply.text)
diff --git a/tests/api/test_api_permissions.py b/tests/api/test_api_permissions.py
index f94ce12e8039b6a81696b53e2cdcda6b99735c3e..c6fefc8102527c32dd9fb18fd4fa4ed68dd2366b 100755
--- a/tests/api/test_api_permissions.py
+++ b/tests/api/test_api_permissions.py
@@ -42,6 +42,7 @@ def test_permissions(api_headers):
"acronym": "TAPI",
"code_type": "etudid",
"code": 1,
+ "date_iso": "2024-10-29",
"dept_id": 1,
"dept_ident": "TAPI",
"dept": "TAPI",