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"""
|
"""ScoDoc 9 API : Assiduités"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
|
import re
|
||||||
|
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_json import as_json
|
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
|
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>")
|
@bp.route("/assiduite/<int:assiduite_id>")
|
||||||
@api_web_bp.route("/assiduite/<int:assiduite_id>")
|
@api_web_bp.route("/assiduite/<int:assiduite_id>")
|
||||||
@scodoc
|
@scodoc
|
||||||
@ -843,6 +862,10 @@ def _create_one(
|
|||||||
elif fin.tzinfo is None:
|
elif fin.tzinfo is None:
|
||||||
fin: datetime = scu.localize_datetime(fin)
|
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
|
# cas 4 : desc
|
||||||
desc: str = data.get("desc", None)
|
desc: str = data.get("desc", None)
|
||||||
|
|
||||||
|
@ -441,12 +441,17 @@ def localize_datetime(date: datetime.datetime) -> datetime.datetime:
|
|||||||
return new_date
|
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
|
"""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()
|
the_time = (
|
||||||
utc_offset = local_time.utcoffset()
|
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())
|
total_seconds = int(utc_offset.total_seconds())
|
||||||
offset_hours = total_seconds // 3600
|
offset_hours = total_seconds // 3600
|
||||||
offset_minutes = (abs(total_seconds) % 3600) // 60
|
offset_minutes = (abs(total_seconds) % 3600) // 60
|
||||||
|
@ -460,6 +460,19 @@ async function creerTousLesEtudiants(etuds) {
|
|||||||
.forEach((etud, index) => {
|
.forEach((etud, index) => {
|
||||||
etudsDiv.appendChild(creerLigneEtudiant(etud, index + 1));
|
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();
|
const modimpl_id = $("#moduleimpl_select").val();
|
||||||
if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression";
|
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é
|
// génération d'un objet assiduité basique qui sera complété
|
||||||
let assiduiteObjet = assiduite ?? {
|
let assiduiteObjet = assiduite ?? {
|
||||||
date_debut: deb,
|
date_debut: deb,
|
||||||
@ -722,9 +735,12 @@ function mettreToutLeMonde(etat, el = null) {
|
|||||||
const lignesEtuds = [...document.querySelectorAll("fieldset.btns_field")];
|
const lignesEtuds = [...document.querySelectorAll("fieldset.btns_field")];
|
||||||
|
|
||||||
const { deb, fin } = getPeriodAsDate(true); // tz server
|
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 = {
|
const assiduiteObjet = {
|
||||||
date_debut: deb,
|
date_debut: deb_iso,
|
||||||
date_fin: fin,
|
date_fin: fin_iso,
|
||||||
etat: etat,
|
etat: etat,
|
||||||
moduleimpl_id: $("#moduleimpl_select").val(),
|
moduleimpl_id: $("#moduleimpl_select").val(),
|
||||||
};
|
};
|
||||||
@ -741,14 +757,14 @@ function mettreToutLeMonde(etat, el = null) {
|
|||||||
.filter((e) => e.getAttribute("type") == "edition")
|
.filter((e) => e.getAttribute("type") == "edition")
|
||||||
.map((e) => Number(e.getAttribute("assiduite_id")));
|
.map((e) => Number(e.getAttribute("assiduite_id")));
|
||||||
|
|
||||||
// On récupère les assiduités conflictuelles mais qui sont comprisent
|
// On récupère les assiduités conflictuelles mais qui sont comprises
|
||||||
// Dans la plage de suppression
|
// dans la plage de suppression
|
||||||
const unDeleted = {};
|
const unDeleted = {};
|
||||||
lignesEtuds
|
lignesEtuds
|
||||||
.filter((e) => e.getAttribute("type") == "conflit")
|
.filter((e) => e.getAttribute("type") == "conflit")
|
||||||
.forEach((e) => {
|
.forEach((e) => {
|
||||||
const etud = etuds.get(Number(e.getAttribute("etudid")));
|
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) => {
|
etud.assiduites.forEach((a) => {
|
||||||
const date_debut = new Date(a.date_debut);
|
const date_debut = new Date(a.date_debut);
|
||||||
const date_fin = new Date(a.date_fin);
|
const date_fin = new Date(a.date_fin);
|
||||||
@ -756,8 +772,8 @@ function mettreToutLeMonde(etat, el = null) {
|
|||||||
// (qui intersectent la plage de suppression)
|
// (qui intersectent la plage de suppression)
|
||||||
if (
|
if (
|
||||||
Date.intersect(
|
Date.intersect(
|
||||||
{ deb: deb, fin: fin },
|
{ deb: deb, fin: fin }, // la plage, en Date avec timezone serveur
|
||||||
{ deb: date_debut, fin: date_fin }
|
{ deb: date_debut, fin: date_fin } // dates de l'assiduité avec leur timezone
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Si l'assiduité est couverte par la plage de suppression
|
// Si l'assiduité est couverte par la plage de suppression
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<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 timelineContainer = document.querySelector(".timeline-container");
|
||||||
const periodTimeLine = document.querySelector(".period");
|
const periodTimeLine = document.querySelector(".period");
|
||||||
const t_start = {{ t_start }};
|
const t_start = {{ t_start }};
|
||||||
@ -336,7 +336,14 @@
|
|||||||
const [hours, minutes] = time.split(separator).map((el) => Number(el))
|
const [hours, minutes] = time.split(separator).map((el) => Number(el))
|
||||||
return hours + minutes / 60
|
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
|
// Les heures sont récupérées depuis la timeline
|
||||||
// la date est récupérée depuis un champ "#date" (datepicker)
|
// la date est récupérée depuis un champ "#date" (datepicker)
|
||||||
function getPeriodAsDate(add_server_tz = false) {
|
function getPeriodAsDate(add_server_tz = false) {
|
||||||
@ -344,17 +351,30 @@
|
|||||||
deb = numberToTime(deb);
|
deb = numberToTime(deb);
|
||||||
fin = numberToTime(fin);
|
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")
|
const dateStr = $("#date")
|
||||||
.datepicker("getDate")
|
.datepicker("getDate")
|
||||||
.format("yyyy-mm-dd")
|
.format("yyyy-mm-dd")
|
||||||
.substring(0, 10); // récupération que de la date, pas des heures
|
.substring(0, 10); // récupération que de la date, pas des heures
|
||||||
|
// retourne des chaines ISO sans timezone
|
||||||
// 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 {
|
return {
|
||||||
deb: new Date(`${dateStr}T${deb}${offset}`),
|
deb : `${dateStr}T${deb}`,
|
||||||
fin: new Date(`${dateStr}T${fin}${offset}`)
|
fin : `${dateStr}T${fin}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Sauvegarde les valeurs de la période dans le local storage
|
// Sauvegarde les valeurs de la période dans le local storage
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
"Infos sur version ScoDoc"
|
"Infos sur version ScoDoc"
|
||||||
|
|
||||||
SCOVERSION = "9.7.34"
|
SCOVERSION = "9.7.35"
|
||||||
|
|
||||||
SCONAME = "ScoDoc"
|
SCONAME = "ScoDoc"
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ Ecrit par HARTMANN Matthias
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from random import randint
|
from random import randint
|
||||||
|
import re
|
||||||
from types import NoneType
|
from types import NoneType
|
||||||
|
|
||||||
from app.scodoc import sco_utils as scu
|
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 len(res["errors"]) == 3
|
||||||
|
|
||||||
assert all(i["message"] == "Assiduite non existante" for i in res["errors"])
|
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",
|
"acronym": "TAPI",
|
||||||
"code_type": "etudid",
|
"code_type": "etudid",
|
||||||
"code": 1,
|
"code": 1,
|
||||||
|
"date_iso": "2024-10-29",
|
||||||
"dept_id": 1,
|
"dept_id": 1,
|
||||||
"dept_ident": "TAPI",
|
"dept_ident": "TAPI",
|
||||||
"dept": "TAPI",
|
"dept": "TAPI",
|
||||||
|
Loading…
Reference in New Issue
Block a user