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