frontend: date UTC convertite in ora italiana alla presentazione (#42)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 11:39:26 +02:00
parent 0d004c43ff
commit 787575807d
7 changed files with 76 additions and 7 deletions
+22 -1
View File
@@ -2,7 +2,8 @@
import json import json
import os import os
import re import re
from datetime import date from datetime import date, datetime, timezone
from zoneinfo import ZoneInfo
from urllib.parse import urlparse from urllib.parse import urlparse
from flask import Flask, flash, g, jsonify, redirect, url_for, session, request from flask import Flask, flash, g, jsonify, redirect, url_for, session, request
@@ -154,6 +155,26 @@ def create_app() -> Flask:
bolded = _BOLD_RX.sub(r"<strong>\1</strong>", escaped) bolded = _BOLD_RX.sub(r"<strong>\1</strong>", escaped)
return Markup(bolded.replace("\n", "<br>")) return Markup(bolded.replace("\n", "<br>"))
@app.template_filter("local_dt")
def local_dt_filter(value, fmt="%d/%m/%Y %H:%M"):
"""Show a UTC datetime (ISO string or datetime) in DISPLAY_TIMEZONE.
A value without timezone is taken as UTC. None or empty gives "-".
"""
if value is None or value == "":
return "-"
if isinstance(value, str):
try:
value = datetime.fromisoformat(value.strip().replace("Z", "+00:00"))
except ValueError:
return value
if not isinstance(value, datetime):
return str(value)
if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
tz = ZoneInfo(app.config.get("DISPLAY_TIMEZONE") or "Europe/Rome")
return value.astimezone(tz).strftime(fmt)
@app.template_filter("tojson_attr") @app.template_filter("tojson_attr")
def tojson_attr_filter(value): def tojson_attr_filter(value):
"""JSON encode safe for HTML attributes (x-data, etc.). """JSON encode safe for HTML attributes (x-data, etc.).
+2
View File
@@ -56,6 +56,8 @@ class Config:
# Babel i18n # Babel i18n
BABEL_DEFAULT_LOCALE = "it" BABEL_DEFAULT_LOCALE = "it"
BABEL_DEFAULT_TIMEZONE = "Europe/Rome" BABEL_DEFAULT_TIMEZONE = "Europe/Rome"
# Il DB e le API parlano UTC; si converte solo alla presentazione.
DISPLAY_TIMEZONE: str = os.getenv("DISPLAY_TIMEZONE", "Europe/Rome")
LANGUAGES = {"it": "Italiano", "en": "English"} LANGUAGES = {"it": "Italiano", "en": "English"}
@@ -271,7 +271,7 @@ function csvExport() {
*/ */
generateFilename(prefix) { generateFilename(prefix) {
const now = new Date(); const now = new Date();
const dateStr = now.toISOString().slice(0, 10); // YYYY-MM-DD const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; // YYYY-MM-DD locale
const timeStr = now.toTimeString().slice(0, 5).replace(':', ''); // HHMM const timeStr = now.toTimeString().slice(0, 5).replace(':', ''); // HHMM
return `${prefix}_${dateStr}_${timeStr}.csv`; return `${prefix}_${dateStr}_${timeStr}.csv`;
@@ -183,7 +183,7 @@
{% for m in measurements|sort(attribute='subtask.marker_number')|sort(attribute='task_info.order_index') %} {% for m in measurements|sort(attribute='subtask.marker_number')|sort(attribute='task_info.order_index') %}
<tr> <tr>
<td class="text-center font-mono text-xs" style="color: var(--text-secondary);"> <td class="text-center font-mono text-xs" style="color: var(--text-secondary);">
{{ m.measured_at[:16]|replace('T', ' ') if m.measured_at else '-' }} {{ m.measured_at|local_dt }}
</td> </td>
<td style="color: var(--text-secondary);" class="text-sm"> <td style="color: var(--text-secondary);" class="text-sm">
{{ m.task_info.title if m.task_info else '-' }} {{ m.task_info.title if m.task_info else '-' }}
@@ -312,8 +312,8 @@ function spcDashboard() {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set('recipe_id', this.selectedRecipeId); params.set('recipe_id', this.selectedRecipeId);
if (this.selectedSubtaskId) params.set('subtask_id', this.selectedSubtaskId); if (this.selectedSubtaskId) params.set('subtask_id', this.selectedSubtaskId);
if (this.dateFrom) params.set('date_from', this.dateFrom + 'T00:00:00'); if (this.dateFrom) params.set('date_from', new Date(this.dateFrom + 'T00:00:00').toISOString());
if (this.dateTo) params.set('date_to', this.dateTo + 'T23:59:59'); if (this.dateTo) params.set('date_to', new Date(this.dateTo + 'T23:59:59.999').toISOString());
try { try {
// Parallel fetch of all endpoints // Parallel fetch of all endpoints
@@ -417,8 +417,8 @@ function spcDashboard() {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set('recipe_id', this.selectedRecipeId); params.set('recipe_id', this.selectedRecipeId);
if (this.selectedSubtaskId) params.set('subtask_id', this.selectedSubtaskId); if (this.selectedSubtaskId) params.set('subtask_id', this.selectedSubtaskId);
if (this.dateFrom) params.set('date_from', this.dateFrom + 'T00:00:00'); if (this.dateFrom) params.set('date_from', new Date(this.dateFrom + 'T00:00:00').toISOString());
if (this.dateTo) params.set('date_to', this.dateTo + 'T23:59:59'); if (this.dateTo) params.set('date_to', new Date(this.dateTo + 'T23:59:59.999').toISOString());
const url = '/statistics/api/report-' + type + '?' + params.toString(); const url = '/statistics/api/report-' + type + '?' + params.toString();
const resp = await fetch(url); const resp = await fetch(url);
@@ -0,0 +1,31 @@
"""Filtro local_dt: il backend parla UTC, la presentazione e' in Europe/Rome (#42)."""
from datetime import datetime, timezone
import pytest
@pytest.fixture
def local_dt(flask_app):
return flask_app.jinja_env.filters["local_dt"]
def test_summer_time(local_dt):
assert local_dt("2026-10-06T08:00:00+00:00") == "06/10/2026 10:00"
def test_winter_time(local_dt):
assert local_dt("2026-01-15T08:00:00+00:00") == "15/01/2026 09:00"
def test_naive_is_utc(local_dt):
assert local_dt("2026-10-06T08:00:00") == "06/10/2026 10:00"
def test_datetime_object_and_format(local_dt):
dt = datetime(2026, 10, 6, 8, 0, tzinfo=timezone.utc)
assert local_dt(dt, "%H:%M") == "10:00"
@pytest.mark.parametrize("empty", [None, ""])
def test_empty(local_dt, empty):
assert local_dt(empty) == "-"
@@ -407,6 +407,21 @@ class TestTaskComplete:
assert resp.status_code == 200 assert resp.status_code == 200
assert b"productionClock(" in resp.data assert b"productionClock(" in resp.data
def test_measured_at_shown_in_local_time(self, logged_in_client, mock_api_client):
"""#42: the API sends UTC; the summary shows Europe/Rome."""
mock_api_client.get.side_effect = [
{"id": 1, "code": "REC-001", "name": "Test Recipe", "version": 1},
[{"id": 1, "title": "Task 1", "order_index": 0, "subtasks": [
{"id": 1, "marker_number": 1, "description": "d", "nominal": 1.0,
"unit": "mm"}]}],
None,
{"items": [{"id": 1, "subtask_id": 1, "value": 1.0, "pass_fail": "pass",
"measured_at": "2026-10-06T08:00:00+00:00"}]},
]
resp = logged_in_client.get("/measure/complete/1?version_id=5")
assert resp.status_code == 200
assert "06/10/2026 10:00" in resp.get_data(as_text=True)
def test_measurements_filtered_by_lot_serial_and_operator( def test_measurements_filtered_by_lot_serial_and_operator(
self, logged_in_client, mock_api_client, self, logged_in_client, mock_api_client,
): ):