213 lines
7.6 KiB
Python
213 lines
7.6 KiB
Python
"""
|
|
API endpoints for reporting user activity events.
|
|
|
|
This module provides endpoints for clients to report state changes (working/stopped).
|
|
"""
|
|
from datetime import datetime, timezone # Removed timedelta, timezone
|
|
|
|
from flask import Blueprint, current_app, jsonify, request
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
|
|
from app import db
|
|
from app.models import WorkEvent
|
|
|
|
# Create a blueprint for event-related API endpoints
|
|
events_bp = Blueprint("events", __name__, url_prefix="/api")
|
|
|
|
|
|
@events_bp.route("/report", methods=["POST"])
|
|
def report_event():
|
|
"""
|
|
Endpoint for clients to report activity state changes.
|
|
All timestamps are expected to be in UTC if provided by the client,
|
|
and will be stored as UTC (or DB equivalent default).
|
|
|
|
Expected JSON payload:
|
|
{
|
|
"user": "username",
|
|
"state": "working|stopped",
|
|
"ts": "2023-07-08T12:30:45Z" (optional, ISO 8601 UTC)
|
|
}
|
|
"""
|
|
data = request.get_json()
|
|
current_app.logger.info(f"Received report request: {data}")
|
|
|
|
if not data or "user" not in data or "state" not in data:
|
|
current_app.logger.warning(
|
|
"Invalid report request payload: Missing required fields."
|
|
)
|
|
return (
|
|
jsonify(
|
|
{"success": False, "message": "Missing required fields: user, state"}
|
|
),
|
|
400,
|
|
)
|
|
|
|
if data["state"] not in ["working", "stopped"]:
|
|
current_app.logger.warning(
|
|
f"Invalid state value '{data['state']}' in report request."
|
|
)
|
|
return (
|
|
jsonify(
|
|
{
|
|
"success": False,
|
|
"message": 'Invalid state value. Must be "working" or "stopped"',
|
|
}
|
|
),
|
|
400,
|
|
)
|
|
|
|
user = data["user"]
|
|
state = data["state"]
|
|
ts_str = data.get("ts")
|
|
|
|
event_ts = None
|
|
if ts_str:
|
|
try:
|
|
# Ensure 'Z' is replaced with +00:00 for full ISO compatibility if needed by fromisoformat version
|
|
if ts_str.endswith("Z"):
|
|
ts_str = ts_str[:-1] + "+00:00"
|
|
event_ts = datetime.fromisoformat(ts_str)
|
|
# Ensure the parsed timestamp is UTC. If it has an offset, convert to UTC.
|
|
if (
|
|
event_ts.tzinfo is not None
|
|
and event_ts.tzinfo.utcoffset(event_ts) is not None
|
|
):
|
|
event_ts = event_ts.astimezone(timezone.utc)
|
|
else:
|
|
# If timezone naive, assume UTC as per API contract (or handle as error)
|
|
# For this implementation, we'll assume naive means UTC if client sends it.
|
|
# Alternatively, enforce client sends tz-aware string, or reject naive.
|
|
current_app.logger.debug(
|
|
f"Received naive timestamp {ts_str}, assuming UTC."
|
|
)
|
|
current_app.logger.info(
|
|
f"Using client-provided timestamp (UTC): {event_ts}"
|
|
)
|
|
except ValueError:
|
|
current_app.logger.warning(
|
|
f"Invalid timestamp format received: {ts_str}. Returning error."
|
|
)
|
|
return (
|
|
jsonify(
|
|
{
|
|
"success": False,
|
|
"message": "Invalid timestamp format. Please use ISO 8601 UTC (e.g., YYYY-MM-DDTHH:MM:SSZ).",
|
|
}
|
|
),
|
|
400,
|
|
)
|
|
else:
|
|
event_ts = datetime.utcnow()
|
|
current_app.logger.info(
|
|
f"No client timestamp provided, using current UTC time: {event_ts}"
|
|
)
|
|
|
|
# At this point, event_ts should be an aware UTC datetime object or naive UTC to be stored.
|
|
# If database stores naive timestamps and assumes UTC (common for SQLite), ensure event_ts is naive UTC.
|
|
# If database stores aware timestamps (common for PostgreSQL with timestamptz), event_ts should be aware UTC.
|
|
#
|
|
# IMPORTANT NOTE ABOUT TIMEZONE HANDLING:
|
|
# - Client-side scripts send timestamps in UTC (with Z suffix in ISO format)
|
|
# - Backend processes all timestamps in UTC format
|
|
# - PostgreSQL database session is set to Asia/Dubai timezone (UTC+4)
|
|
# - When storing timestamps, PostgreSQL converts from UTC to Asia/Dubai
|
|
# - When retrieving timestamps, PostgreSQL returns in Asia/Dubai timezone
|
|
# - The frontend displays all times in GMT+4 (Asia/Dubai) using formatTimeToGMT4()
|
|
#
|
|
# This ensures consistent timezone handling throughout the application stack.
|
|
|
|
current_app.logger.debug(f"Storing event with timestamp (UTC): {event_ts}")
|
|
new_event = WorkEvent(user=user, state=state, ts=event_ts)
|
|
|
|
try:
|
|
current_app.logger.info(
|
|
f"Attempting to add event to database: User={user}, State={state}, TS={event_ts}"
|
|
)
|
|
db.session.add(new_event)
|
|
db.session.commit()
|
|
current_app.logger.info(
|
|
f"Successfully recorded event: User={user}, State={state}"
|
|
)
|
|
return jsonify({"success": True}), 201
|
|
except SQLAlchemyError as e:
|
|
db.session.rollback()
|
|
current_app.logger.error(f"Database error while recording event: {e}")
|
|
return jsonify({"success": False, "message": "Database error"}), 500
|
|
|
|
|
|
@events_bp.route("/user_status_update", methods=["POST"])
|
|
def update_user_status():
|
|
"""
|
|
Endpoint for updating a user's status, typically when a timeout occurs.
|
|
Expects JSON: {"user_id": "username", "status": "not working"}
|
|
This will result in a 'stopped' event being logged for the user.
|
|
"""
|
|
data = request.get_json()
|
|
current_app.logger.info(f"Received user status update request: {data}")
|
|
|
|
if not data or "user_id" not in data or "status" not in data:
|
|
current_app.logger.warning(
|
|
"Invalid user status update payload: Missing required fields."
|
|
)
|
|
return (
|
|
jsonify(
|
|
{
|
|
"success": False,
|
|
"message": "Missing required fields: user_id, status",
|
|
}
|
|
),
|
|
400,
|
|
)
|
|
|
|
user_id = data["user_id"]
|
|
status = data["status"]
|
|
|
|
if status != "not working":
|
|
current_app.logger.warning(
|
|
f"Invalid status value '{status}' in user status update for user {user_id}. Expected 'not working'."
|
|
)
|
|
return (
|
|
jsonify(
|
|
{
|
|
"success": False,
|
|
"message": 'Invalid status value. Must be "not working"',
|
|
}
|
|
),
|
|
400,
|
|
)
|
|
|
|
# Map "not working" to "stopped" for WorkEvent consistency
|
|
event_state = "stopped"
|
|
event_ts = datetime.utcnow()
|
|
|
|
new_event = WorkEvent(user=user_id, state=event_state, ts=event_ts)
|
|
|
|
try:
|
|
current_app.logger.info(
|
|
f"Attempting to add '{event_state}' event to database for user {user_id} from status update."
|
|
)
|
|
db.session.add(new_event)
|
|
db.session.commit()
|
|
current_app.logger.info(
|
|
f"Successfully recorded '{event_state}' event for user {user_id} from status update."
|
|
)
|
|
return (
|
|
jsonify(
|
|
{
|
|
"success": True,
|
|
"message": "User status successfully updated to stopped.",
|
|
}
|
|
),
|
|
201,
|
|
)
|
|
except SQLAlchemyError as e:
|
|
db.session.rollback()
|
|
current_app.logger.error(
|
|
f"Database error while recording '{event_state}' event for user {user_id} from status update: {e}"
|
|
)
|
|
return (
|
|
jsonify({"success": False, "message": "Database error processing request"}),
|
|
500,
|
|
)
|