forked from ScoDoc/ScoDoc
Assiduité: fix bug fuseau horaire dans l'UI JS. Ajout API et test.
This commit is contained in:
parent
d1a2e52fef
commit
8b01df0b02
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,17 +351,30 @@
|
||||
deb = numberToTime(deb);
|
||||
fin = numberToTime(fin);
|
||||
|
||||
const dateStr = getSelectedDateIso();
|
||||
|
||||
// Les heures deb et fin sont telles qu'affichées, c'est à dire
|
||||
// en heure locale DU SERVEUR (des étudiants donc)
|
||||
let offset = add_server_tz ? SERVER_TIMEZONE_OFFSET : "";
|
||||
return {
|
||||
deb: new Date(`${dateStr}T${deb}${offset}`),
|
||||
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
|
||||
|
||||
// Les heures deb et fin sont telles qu'affichées, c'est à dire
|
||||
// en heure locale DU SERVEUR (des étudiants donc)
|
||||
let offset = add_server_tz ? SERVER_TIMEZONE_OFFSET : "";
|
||||
// retourne des chaines ISO sans timezone
|
||||
return {
|
||||
deb: new Date(`${dateStr}T${deb}${offset}`),
|
||||
fin: new Date(`${dateStr}T${fin}${offset}`)
|
||||
deb : `${dateStr}T${deb}`,
|
||||
fin : `${dateStr}T${fin}`
|
||||
}
|
||||
}
|
||||
// Sauvegarde les valeurs de la période dans le local storage
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
"Infos sur version ScoDoc"
|
||||
|
||||
SCOVERSION = "9.7.34"
|
||||
SCOVERSION = "9.7.35"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user