Deploy FastAPI backend with MongoDB integration - Added app.py, requirements.txt, and updated Dockerfile for HF Spaces deployment
Browse files- Dockerfile +20 -0
- README.md +79 -4
- app.py +735 -0
- requirements.txt +20 -0
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.9-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Copy requirements first for better caching
|
| 6 |
+
COPY server/requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
# Copy the rest of the application
|
| 10 |
+
COPY server/ .
|
| 11 |
+
|
| 12 |
+
# Expose the port that FastAPI will run on
|
| 13 |
+
EXPOSE 8000
|
| 14 |
+
|
| 15 |
+
# Set environment variables
|
| 16 |
+
ENV PORT=8000
|
| 17 |
+
ENV CORS_ORIGIN=https://ali2206-checklist.hf.space
|
| 18 |
+
|
| 19 |
+
# Run the FastAPI application
|
| 20 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
README.md
CHANGED
|
@@ -1,10 +1,85 @@
|
|
| 1 |
---
|
| 2 |
-
title: Checklist
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Audit Checklist API
|
| 3 |
+
emoji: 📋
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
app_port: 8000
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Audit Checklist FastAPI Backend
|
| 13 |
+
|
| 14 |
+
This is the backend API for the Audit Checklist React Native application.
|
| 15 |
+
|
| 16 |
+
## Features
|
| 17 |
+
- MongoDB Atlas integration
|
| 18 |
+
- REST API endpoints for audit checklists
|
| 19 |
+
- CORS support for mobile apps
|
| 20 |
+
- Health check endpoint
|
| 21 |
+
- User session management
|
| 22 |
+
- Comprehensive error handling
|
| 23 |
+
|
| 24 |
+
## API Endpoints
|
| 25 |
+
|
| 26 |
+
### Health Check
|
| 27 |
+
- `GET /` - Basic health check
|
| 28 |
+
- `GET /health` - Detailed health information
|
| 29 |
+
|
| 30 |
+
### Checklist Management
|
| 31 |
+
- `GET /api/checklist/{user_id}` - Get checklist for user
|
| 32 |
+
- `PUT /api/checklist/{user_id}` - Save/update checklist
|
| 33 |
+
- `GET /api/checklists` - Get all checklists (admin)
|
| 34 |
+
- `GET /api/checklists/by-user/{user_name}` - Get checklists by user name
|
| 35 |
+
|
| 36 |
+
### Server Information
|
| 37 |
+
- `GET /api/server-info` - Get server information
|
| 38 |
+
|
| 39 |
+
## Environment Variables
|
| 40 |
+
|
| 41 |
+
Set these in your Hugging Face Space settings:
|
| 42 |
+
|
| 43 |
+
- `MONGODB_URI`: Your MongoDB Atlas connection string
|
| 44 |
+
- `CORS_ORIGIN`: Frontend URL (optional, defaults to allow all)
|
| 45 |
+
|
| 46 |
+
## Data Structure
|
| 47 |
+
|
| 48 |
+
The API manages audit checklists with the following structure:
|
| 49 |
+
|
| 50 |
+
```json
|
| 51 |
+
{
|
| 52 |
+
"userId": "user-1234567890",
|
| 53 |
+
"title": "Checklist di Audit Operativo (38 Controlli)",
|
| 54 |
+
"sections": [...],
|
| 55 |
+
"metadata": {
|
| 56 |
+
"userName": "John Doe",
|
| 57 |
+
"savedAt": "2024-01-15T14:30:25.123Z",
|
| 58 |
+
"savedAtFormatted": "15/01/2024, 14:30:25",
|
| 59 |
+
"userId": "user-1234567890",
|
| 60 |
+
"version": "1.0"
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
## Usage
|
| 66 |
+
|
| 67 |
+
This API is designed to work with the Audit Checklist React Native mobile application. The mobile app sends audit data to this backend, which stores it in MongoDB Atlas with user information and timestamps.
|
| 68 |
+
|
| 69 |
+
## Deployment
|
| 70 |
+
|
| 71 |
+
This FastAPI application is deployed on Hugging Face Spaces using Docker. The application runs on port 8000 and is accessible at:
|
| 72 |
+
|
| 73 |
+
`https://ali2206-checklist.hf.space`
|
| 74 |
+
|
| 75 |
+
## Development
|
| 76 |
+
|
| 77 |
+
To run locally:
|
| 78 |
+
|
| 79 |
+
```bash
|
| 80 |
+
cd server
|
| 81 |
+
pip install -r requirements.txt
|
| 82 |
+
python start_server.py
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
The API will be available at `http://localhost:5000`
|
app.py
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI Server for Audit Checklist Application
|
| 3 |
+
=============================================
|
| 4 |
+
|
| 5 |
+
This server provides a REST API for managing audit checklists with MongoDB integration.
|
| 6 |
+
It supports CRUD operations for checklist data and user management.
|
| 7 |
+
|
| 8 |
+
Features:
|
| 9 |
+
- MongoDB Atlas integration with async Motor driver
|
| 10 |
+
- CORS support for React frontend
|
| 11 |
+
- Comprehensive error handling
|
| 12 |
+
- Health check endpoint
|
| 13 |
+
- Environment-based configuration
|
| 14 |
+
|
| 15 |
+
Author: Audit Checklist Team
|
| 16 |
+
Version: 1.0.0
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import os
|
| 20 |
+
import sys
|
| 21 |
+
from datetime import datetime
|
| 22 |
+
from typing import Optional, List, Dict, Any, Union
|
| 23 |
+
import logging
|
| 24 |
+
|
| 25 |
+
# FastAPI imports
|
| 26 |
+
from fastapi import FastAPI, HTTPException, status
|
| 27 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 28 |
+
from fastapi.responses import JSONResponse
|
| 29 |
+
|
| 30 |
+
# Pydantic for data validation
|
| 31 |
+
from pydantic import BaseModel, Field
|
| 32 |
+
|
| 33 |
+
# MongoDB async driver
|
| 34 |
+
import motor.motor_asyncio
|
| 35 |
+
from bson import ObjectId
|
| 36 |
+
|
| 37 |
+
# Environment variables
|
| 38 |
+
from dotenv import load_dotenv
|
| 39 |
+
|
| 40 |
+
# Load environment variables from .env file (if available)
|
| 41 |
+
# For Hugging Face Spaces, environment variables are set in the Space settings
|
| 42 |
+
try:
|
| 43 |
+
load_dotenv('mongodb.env')
|
| 44 |
+
except:
|
| 45 |
+
pass # Continue without .env file if not found
|
| 46 |
+
|
| 47 |
+
# Configure logging
|
| 48 |
+
logging.basicConfig(
|
| 49 |
+
level=logging.INFO,
|
| 50 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 51 |
+
)
|
| 52 |
+
logger = logging.getLogger(__name__)
|
| 53 |
+
|
| 54 |
+
# =============================================================================
|
| 55 |
+
# CONFIGURATION AND INITIALIZATION
|
| 56 |
+
# =============================================================================
|
| 57 |
+
|
| 58 |
+
# Environment variables with fallback defaults
|
| 59 |
+
MONGODB_URI = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/audit_checklist')
|
| 60 |
+
PORT = int(os.getenv('PORT', 8000)) # Hugging Face Spaces uses port 8000
|
| 61 |
+
CORS_ORIGIN = os.getenv('CORS_ORIGIN', '*') # Allow all origins by default for mobile apps
|
| 62 |
+
|
| 63 |
+
# Initialize FastAPI application
|
| 64 |
+
app = FastAPI(
|
| 65 |
+
title="Audit Checklist API",
|
| 66 |
+
description="REST API for managing audit checklists with MongoDB integration",
|
| 67 |
+
version="1.0.0",
|
| 68 |
+
docs_url="/docs", # Swagger UI documentation
|
| 69 |
+
redoc_url="/redoc" # ReDoc documentation
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# Configure CORS middleware
|
| 73 |
+
# Allow all origins for Hugging Face Spaces deployment
|
| 74 |
+
cors_origins = [CORS_ORIGIN] if CORS_ORIGIN != "*" else ["*"]
|
| 75 |
+
|
| 76 |
+
app.add_middleware(
|
| 77 |
+
CORSMiddleware,
|
| 78 |
+
allow_origins=cors_origins,
|
| 79 |
+
allow_credentials=True,
|
| 80 |
+
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
| 81 |
+
allow_headers=["*"],
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
# MongoDB connection setup
|
| 85 |
+
client = None
|
| 86 |
+
db = None
|
| 87 |
+
|
| 88 |
+
async def connect_to_mongodb():
|
| 89 |
+
"""
|
| 90 |
+
Establish connection to MongoDB Atlas
|
| 91 |
+
|
| 92 |
+
This function creates an async connection to MongoDB using the Motor driver.
|
| 93 |
+
It handles connection errors gracefully and logs the status.
|
| 94 |
+
"""
|
| 95 |
+
global client, db
|
| 96 |
+
try:
|
| 97 |
+
client = motor.motor_asyncio.AsyncIOMotorClient(MONGODB_URI)
|
| 98 |
+
db = client.audit_checklist
|
| 99 |
+
|
| 100 |
+
# Test the connection
|
| 101 |
+
await client.admin.command('ping')
|
| 102 |
+
logger.info("Successfully connected to MongoDB Atlas")
|
| 103 |
+
|
| 104 |
+
# Create indexes for better performance
|
| 105 |
+
await create_database_indexes()
|
| 106 |
+
|
| 107 |
+
except Exception as e:
|
| 108 |
+
logger.error(f"Failed to connect to MongoDB: {e}")
|
| 109 |
+
raise e
|
| 110 |
+
|
| 111 |
+
async def create_database_indexes():
|
| 112 |
+
"""
|
| 113 |
+
Create database indexes for optimal query performance
|
| 114 |
+
|
| 115 |
+
This function creates indexes on frequently queried fields to improve
|
| 116 |
+
database performance and query speed.
|
| 117 |
+
"""
|
| 118 |
+
try:
|
| 119 |
+
# Index on userId for faster user-specific queries
|
| 120 |
+
await db.checklists.create_index("userId", unique=False)
|
| 121 |
+
|
| 122 |
+
# Index on createdAt for time-based queries
|
| 123 |
+
await db.checklists.create_index("createdAt", unique=False)
|
| 124 |
+
|
| 125 |
+
logger.info("Database indexes created successfully")
|
| 126 |
+
|
| 127 |
+
except Exception as e:
|
| 128 |
+
logger.warning(f"Could not create indexes: {e}")
|
| 129 |
+
|
| 130 |
+
async def close_mongodb_connection():
|
| 131 |
+
"""Close MongoDB connection gracefully"""
|
| 132 |
+
global client
|
| 133 |
+
if client:
|
| 134 |
+
client.close()
|
| 135 |
+
logger.info("MongoDB connection closed")
|
| 136 |
+
|
| 137 |
+
# =============================================================================
|
| 138 |
+
# PYDANTIC MODELS FOR DATA VALIDATION
|
| 139 |
+
# =============================================================================
|
| 140 |
+
|
| 141 |
+
class ChecklistItem(BaseModel):
|
| 142 |
+
"""
|
| 143 |
+
Model for individual checklist items
|
| 144 |
+
|
| 145 |
+
Attributes:
|
| 146 |
+
id: Unique identifier for the item
|
| 147 |
+
requirement: Description of the requirement to be checked
|
| 148 |
+
compliance: Compliance status (N/A, Compliant, Non-Compliant)
|
| 149 |
+
deviation: Description of any deviation found
|
| 150 |
+
action: Action taken to address the issue
|
| 151 |
+
checkedAt: Timestamp when the item was checked
|
| 152 |
+
checkedBy: Name of the person who checked the item
|
| 153 |
+
"""
|
| 154 |
+
id: str = Field(..., description="Unique identifier for the checklist item")
|
| 155 |
+
requirement: str = Field(..., description="Description of the requirement")
|
| 156 |
+
compliance: str = Field(default="N/A", description="Compliance status: N/A, Compliant, or Non-Compliant")
|
| 157 |
+
deviation: str = Field(default="", description="Description of any deviation found")
|
| 158 |
+
action: str = Field(default="", description="Action taken to address the issue")
|
| 159 |
+
checkedAt: Optional[datetime] = Field(default=None, description="Timestamp when item was checked")
|
| 160 |
+
checkedBy: str = Field(default="", description="Name of the person who checked the item")
|
| 161 |
+
|
| 162 |
+
class ChecklistSection(BaseModel):
|
| 163 |
+
"""
|
| 164 |
+
Model for checklist sections containing multiple items
|
| 165 |
+
|
| 166 |
+
Attributes:
|
| 167 |
+
id: Unique identifier for the section
|
| 168 |
+
title: Display title of the section
|
| 169 |
+
icon: Icon identifier for UI display
|
| 170 |
+
items: List of checklist items in this section
|
| 171 |
+
"""
|
| 172 |
+
id: str = Field(..., description="Unique identifier for the section")
|
| 173 |
+
title: str = Field(..., description="Display title of the section")
|
| 174 |
+
icon: str = Field(..., description="Icon identifier for UI display")
|
| 175 |
+
items: List[ChecklistItem] = Field(default=[], description="List of checklist items")
|
| 176 |
+
|
| 177 |
+
class Metadata(BaseModel):
|
| 178 |
+
"""
|
| 179 |
+
Metadata model for checklist information
|
| 180 |
+
"""
|
| 181 |
+
userName: Optional[str] = Field(default=None, description="Name of the user who saved the checklist")
|
| 182 |
+
savedAt: Optional[str] = Field(default=None, description="ISO timestamp when saved")
|
| 183 |
+
savedAtFormatted: Optional[str] = Field(default=None, description="Formatted timestamp when saved")
|
| 184 |
+
userId: Optional[str] = Field(default=None, description="User ID")
|
| 185 |
+
version: Optional[str] = Field(default="1.0", description="Version of the checklist")
|
| 186 |
+
|
| 187 |
+
class ChecklistData(BaseModel):
|
| 188 |
+
"""
|
| 189 |
+
Complete checklist data model
|
| 190 |
+
|
| 191 |
+
Attributes:
|
| 192 |
+
userId: Unique identifier for the user
|
| 193 |
+
title: Title of the checklist
|
| 194 |
+
sections: List of sections in the checklist
|
| 195 |
+
totalItems: Total number of items across all sections
|
| 196 |
+
completedItems: Number of completed items
|
| 197 |
+
nonCompliantItems: Number of non-compliant items
|
| 198 |
+
complianceScore: Calculated compliance percentage
|
| 199 |
+
verificationDate: Date when the checklist was verified
|
| 200 |
+
createdAt: Timestamp when the checklist was created
|
| 201 |
+
updatedAt: Timestamp when the checklist was last updated
|
| 202 |
+
"""
|
| 203 |
+
userId: str = Field(..., description="Unique identifier for the user")
|
| 204 |
+
title: str = Field(..., description="Title of the checklist")
|
| 205 |
+
sections: List[ChecklistSection] = Field(default=[], description="List of checklist sections")
|
| 206 |
+
totalItems: Optional[int] = Field(default=0, description="Total number of items")
|
| 207 |
+
completedItems: Optional[int] = Field(default=0, description="Number of completed items")
|
| 208 |
+
nonCompliantItems: Optional[int] = Field(default=0, description="Number of non-compliant items")
|
| 209 |
+
complianceScore: Optional[float] = Field(default=0.0, description="Compliance percentage score")
|
| 210 |
+
verificationDate: Optional[datetime] = Field(default=None, description="Date of verification")
|
| 211 |
+
createdAt: Optional[datetime] = Field(default=None, description="Creation timestamp")
|
| 212 |
+
updatedAt: Optional[datetime] = Field(default=None, description="Last update timestamp")
|
| 213 |
+
metadata: Optional[Metadata] = Field(default=None, description="Additional metadata including user information")
|
| 214 |
+
|
| 215 |
+
class Config:
|
| 216 |
+
# Allow extra fields that might be sent from frontend
|
| 217 |
+
extra = "allow"
|
| 218 |
+
|
| 219 |
+
class ChecklistResponse(BaseModel):
|
| 220 |
+
"""
|
| 221 |
+
Standard API response model for checklist operations
|
| 222 |
+
|
| 223 |
+
Attributes:
|
| 224 |
+
success: Boolean indicating if the operation was successful
|
| 225 |
+
data: The checklist data (if successful)
|
| 226 |
+
message: Optional message describing the result
|
| 227 |
+
error: Error message (if unsuccessful)
|
| 228 |
+
"""
|
| 229 |
+
success: bool = Field(..., description="Whether the operation was successful")
|
| 230 |
+
data: Optional[Union[ChecklistData, Dict[str, Any]]] = Field(default=None, description="Checklist data")
|
| 231 |
+
message: Optional[str] = Field(default=None, description="Success message")
|
| 232 |
+
error: Optional[str] = Field(default=None, description="Error message")
|
| 233 |
+
|
| 234 |
+
class HealthResponse(BaseModel):
|
| 235 |
+
"""Health check response model"""
|
| 236 |
+
status: str = Field(..., description="Health status")
|
| 237 |
+
timestamp: datetime = Field(..., description="Current timestamp")
|
| 238 |
+
database: str = Field(..., description="Database connection status")
|
| 239 |
+
version: str = Field(default="1.0.0", description="API version")
|
| 240 |
+
|
| 241 |
+
# =============================================================================
|
| 242 |
+
# UTILITY FUNCTIONS
|
| 243 |
+
# =============================================================================
|
| 244 |
+
|
| 245 |
+
def calculate_checklist_metrics(checklist_data: Dict[str, Any]) -> Dict[str, Any]:
|
| 246 |
+
"""
|
| 247 |
+
Calculate compliance metrics for a checklist
|
| 248 |
+
|
| 249 |
+
Args:
|
| 250 |
+
checklist_data: Dictionary containing checklist data
|
| 251 |
+
|
| 252 |
+
Returns:
|
| 253 |
+
Dictionary with calculated metrics (totalItems, completedItems, nonCompliantItems, complianceScore)
|
| 254 |
+
"""
|
| 255 |
+
total_items = 0
|
| 256 |
+
completed_items = 0
|
| 257 |
+
non_compliant_items = 0
|
| 258 |
+
|
| 259 |
+
# Iterate through all sections and items
|
| 260 |
+
for section in checklist_data.get('sections', []):
|
| 261 |
+
for item in section.get('items', []):
|
| 262 |
+
total_items += 1
|
| 263 |
+
|
| 264 |
+
# Count completed items (those that are not N/A)
|
| 265 |
+
if item.get('compliance') != 'N/A':
|
| 266 |
+
completed_items += 1
|
| 267 |
+
|
| 268 |
+
# Count non-compliant items
|
| 269 |
+
if item.get('compliance') == 'Non-Compliant':
|
| 270 |
+
non_compliant_items += 1
|
| 271 |
+
|
| 272 |
+
# Calculate compliance score
|
| 273 |
+
compliance_score = (completed_items / total_items * 100) if total_items > 0 else 0
|
| 274 |
+
|
| 275 |
+
return {
|
| 276 |
+
'totalItems': total_items,
|
| 277 |
+
'completedItems': completed_items,
|
| 278 |
+
'nonCompliantItems': non_compliant_items,
|
| 279 |
+
'complianceScore': round(compliance_score, 2)
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
def serialize_checklist(checklist_doc: Dict[str, Any]) -> Dict[str, Any]:
|
| 283 |
+
"""
|
| 284 |
+
Serialize MongoDB document to JSON-serializable format
|
| 285 |
+
|
| 286 |
+
Args:
|
| 287 |
+
checklist_doc: MongoDB document
|
| 288 |
+
|
| 289 |
+
Returns:
|
| 290 |
+
Dictionary with ObjectId converted to string
|
| 291 |
+
"""
|
| 292 |
+
if checklist_doc and '_id' in checklist_doc:
|
| 293 |
+
checklist_doc['_id'] = str(checklist_doc['_id'])
|
| 294 |
+
|
| 295 |
+
# Convert datetime objects to ISO strings
|
| 296 |
+
for field in ['createdAt', 'updatedAt', 'verificationDate']:
|
| 297 |
+
if field in checklist_doc and checklist_doc[field]:
|
| 298 |
+
# Only convert if it's a datetime object, not a string
|
| 299 |
+
if hasattr(checklist_doc[field], 'isoformat'):
|
| 300 |
+
checklist_doc[field] = checklist_doc[field].isoformat()
|
| 301 |
+
|
| 302 |
+
# Convert datetime objects in items
|
| 303 |
+
for section in checklist_doc.get('sections', []):
|
| 304 |
+
for item in section.get('items', []):
|
| 305 |
+
if 'checkedAt' in item and item['checkedAt']:
|
| 306 |
+
# Only convert if it's a datetime object, not a string
|
| 307 |
+
if hasattr(item['checkedAt'], 'isoformat'):
|
| 308 |
+
item['checkedAt'] = item['checkedAt'].isoformat()
|
| 309 |
+
|
| 310 |
+
return checklist_doc
|
| 311 |
+
|
| 312 |
+
# =============================================================================
|
| 313 |
+
# API ENDPOINTS
|
| 314 |
+
# =============================================================================
|
| 315 |
+
|
| 316 |
+
@app.on_event("startup")
|
| 317 |
+
async def startup_event():
|
| 318 |
+
"""
|
| 319 |
+
Application startup event handler
|
| 320 |
+
|
| 321 |
+
This function is called when the FastAPI application starts up.
|
| 322 |
+
It initializes the MongoDB connection and sets up the database.
|
| 323 |
+
"""
|
| 324 |
+
logger.info("Starting Audit Checklist API server...")
|
| 325 |
+
await connect_to_mongodb()
|
| 326 |
+
logger.info(f"Server ready on port {PORT}")
|
| 327 |
+
|
| 328 |
+
@app.on_event("shutdown")
|
| 329 |
+
async def shutdown_event():
|
| 330 |
+
"""
|
| 331 |
+
Application shutdown event handler
|
| 332 |
+
|
| 333 |
+
This function is called when the FastAPI application shuts down.
|
| 334 |
+
It gracefully closes the MongoDB connection.
|
| 335 |
+
"""
|
| 336 |
+
logger.info("Shutting down Audit Checklist API server...")
|
| 337 |
+
await close_mongodb_connection()
|
| 338 |
+
|
| 339 |
+
@app.get("/", response_model=Dict[str, str])
|
| 340 |
+
async def root():
|
| 341 |
+
"""
|
| 342 |
+
Root endpoint providing basic API information
|
| 343 |
+
|
| 344 |
+
Returns:
|
| 345 |
+
Dictionary with API name and version
|
| 346 |
+
"""
|
| 347 |
+
return {
|
| 348 |
+
"message": "Audit Checklist API",
|
| 349 |
+
"version": "1.0.0",
|
| 350 |
+
"docs": "/docs",
|
| 351 |
+
"health": "/health"
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
@app.get("/health", response_model=HealthResponse)
|
| 355 |
+
async def health_check():
|
| 356 |
+
"""
|
| 357 |
+
Health check endpoint for monitoring and load balancers
|
| 358 |
+
|
| 359 |
+
This endpoint checks the status of the API server and database connection.
|
| 360 |
+
It's useful for health monitoring, load balancers, and DevOps automation.
|
| 361 |
+
|
| 362 |
+
Returns:
|
| 363 |
+
HealthResponse with current status information
|
| 364 |
+
"""
|
| 365 |
+
try:
|
| 366 |
+
# Test database connection
|
| 367 |
+
await client.admin.command('ping')
|
| 368 |
+
db_status = "connected"
|
| 369 |
+
except Exception as e:
|
| 370 |
+
logger.error(f"Database health check failed: {e}")
|
| 371 |
+
db_status = "disconnected"
|
| 372 |
+
|
| 373 |
+
return HealthResponse(
|
| 374 |
+
status="healthy" if db_status == "connected" else "unhealthy",
|
| 375 |
+
timestamp=datetime.utcnow(),
|
| 376 |
+
database=db_status,
|
| 377 |
+
version="1.0.0"
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
@app.get("/api/checklist/{user_id}", response_model=ChecklistResponse)
|
| 381 |
+
async def get_checklist(user_id: str):
|
| 382 |
+
"""
|
| 383 |
+
Retrieve checklist data for a specific user
|
| 384 |
+
|
| 385 |
+
This endpoint fetches the checklist data for a given user ID. If no checklist
|
| 386 |
+
exists for the user, it creates a new one with the default structure.
|
| 387 |
+
|
| 388 |
+
Args:
|
| 389 |
+
user_id: Unique identifier for the user
|
| 390 |
+
|
| 391 |
+
Returns:
|
| 392 |
+
ChecklistResponse containing the checklist data
|
| 393 |
+
|
| 394 |
+
Raises:
|
| 395 |
+
HTTPException: If database operation fails
|
| 396 |
+
"""
|
| 397 |
+
try:
|
| 398 |
+
logger.info(f"Fetching checklist for user: {user_id}")
|
| 399 |
+
|
| 400 |
+
# Query the database for the user's checklist
|
| 401 |
+
# Find the most recent checklist for the user
|
| 402 |
+
checklist_doc = await db.checklists.find_one(
|
| 403 |
+
{"userId": user_id},
|
| 404 |
+
sort=[("createdAt", -1)] # Get the most recent one
|
| 405 |
+
)
|
| 406 |
+
|
| 407 |
+
if not checklist_doc:
|
| 408 |
+
# Create new checklist if none exists
|
| 409 |
+
logger.info(f"No checklist found for user {user_id}, creating new one")
|
| 410 |
+
|
| 411 |
+
# Default checklist structure - Complete 38-item audit checklist
|
| 412 |
+
default_checklist = {
|
| 413 |
+
"userId": user_id,
|
| 414 |
+
"title": "Checklist di Audit Operativo (38 Controlli)",
|
| 415 |
+
"sections": [
|
| 416 |
+
{
|
| 417 |
+
"id": "S1",
|
| 418 |
+
"title": "1. PERSONALE E IGIENE (6 Controlli)",
|
| 419 |
+
"icon": "Users",
|
| 420 |
+
"items": [
|
| 421 |
+
{"id": "I1.1", "requirement": "Indumenti da lavoro puliti (divisa, grembiule).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 422 |
+
{"id": "I1.2", "requirement": "Scarpe antinfortunistiche / Calzature pulite.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 423 |
+
{"id": "I1.3", "requirement": "Cuffie e/o Retine per capelli indossate correttamente.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 424 |
+
{"id": "I1.4", "requirement": "Assenza di gioielli, piercing visibili, unghie lunghe.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 425 |
+
{"id": "I1.5", "requirement": "Lavaggio mani documentato all'ingresso/reingresso.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 426 |
+
{"id": "I1.6", "requirement": "Assenza di cibo/bevande non autorizzate nelle aree produttive.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}
|
| 427 |
+
]
|
| 428 |
+
},
|
| 429 |
+
{
|
| 430 |
+
"id": "S2",
|
| 431 |
+
"title": "2. STRUTTURE E IMPIANTI (6 Controlli)",
|
| 432 |
+
"icon": "Building",
|
| 433 |
+
"items": [
|
| 434 |
+
{"id": "I2.1", "requirement": "Illuminazione adeguata e funzionante in tutte le aree.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 435 |
+
{"id": "I2.2", "requirement": "Porte esterne/interne in buone condizioni e chiuse correttamente (sigillatura).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 436 |
+
{"id": "I2.3", "requirement": "Integrità di pavimenti, pareti e soffitti (assenza di crepe/danni).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 437 |
+
{"id": "I2.4", "requirement": "Controllo vetri / lampade protette o anti-frantumazione.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 438 |
+
{"id": "I2.5", "requirement": "Condizioni igieniche dei servizi igienici e spogliatoi (pulizia e dotazioni).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 439 |
+
{"id": "I2.6", "requirement": "Ventilazione e aspirazione funzionanti, pulite e non ostruite.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}
|
| 440 |
+
]
|
| 441 |
+
},
|
| 442 |
+
{
|
| 443 |
+
"id": "S3",
|
| 444 |
+
"title": "3. GESTIONE E IGIENE AMBIENTALE (4 Controlli)",
|
| 445 |
+
"icon": "Package",
|
| 446 |
+
"items": [
|
| 447 |
+
{"id": "I3.1", "requirement": "Contenitori dei rifiuti puliti, chiusi e identificati.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 448 |
+
{"id": "I3.2", "requirement": "Separazione corretta dei rifiuti (es. umido, secco, plastica).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 449 |
+
{"id": "I3.3", "requirement": "Area di stoccaggio rifiuti (interna ed esterna) ordinata e sanificata.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 450 |
+
{"id": "I3.4", "requirement": "Frequenza di rimozione dei rifiuti adeguata a prevenire accumuli.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}
|
| 451 |
+
]
|
| 452 |
+
},
|
| 453 |
+
{
|
| 454 |
+
"id": "S4",
|
| 455 |
+
"title": "4. CONTROLLO PROCESSO E QUALITÀ (6 Controlli)",
|
| 456 |
+
"icon": "Settings",
|
| 457 |
+
"items": [
|
| 458 |
+
{"id": "I4.1", "requirement": "Monitoraggio e registrazione dei Punti Critici di Controllo (CCP).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 459 |
+
{"id": "I4.2", "requirement": "Procedure di Buona Fabbricazione (GMP) rispettate (es. pulizia prima dell'avvio).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 460 |
+
{"id": "I4.3", "requirement": "Verifica e calibrazione del Metal Detector eseguita e registrata.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 461 |
+
{"id": "I4.4", "requirement": "Corretta identificazione dei lotti di materie prime e semilavorati.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 462 |
+
{"id": "I4.5", "requirement": "Rispettate le temperature di stoccaggio dei prodotti finiti e intermedi.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 463 |
+
{"id": "I4.6", "requirement": "Corretta gestione, etichettatura e isolamento dei prodotti non conformi.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}
|
| 464 |
+
]
|
| 465 |
+
},
|
| 466 |
+
{
|
| 467 |
+
"id": "S5",
|
| 468 |
+
"title": "5. CONTROLLO INFESTANTI (5 Controlli)",
|
| 469 |
+
"icon": "Shield",
|
| 470 |
+
"items": [
|
| 471 |
+
{"id": "I5.1", "requirement": "Assenza di tracce visibili di roditori, insetti o uccelli.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 472 |
+
{"id": "I5.2", "requirement": "Stazioni di monitoraggio/trappole numerate, integre e registrate.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 473 |
+
{"id": "I5.3", "requirement": "Mappe e registri delle trappole aggiornati e disponibili.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 474 |
+
{"id": "I5.4", "requirement": "Barriere fisiche (es. reti, spazzole) anti-intrusione in buone condizioni.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 475 |
+
{"id": "I5.5", "requirement": "Prodotti chimici di controllo infestanti stoccati in modo sicuro e separato.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}
|
| 476 |
+
]
|
| 477 |
+
},
|
| 478 |
+
{
|
| 479 |
+
"id": "S6",
|
| 480 |
+
"title": "6. MANUTENZIONE E VETRI (6 Controlli)",
|
| 481 |
+
"icon": "Settings",
|
| 482 |
+
"items": [
|
| 483 |
+
{"id": "I6.1", "requirement": "Piano di manutenzione preventiva e correttiva rispettato e registrato.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 484 |
+
{"id": "I6.2", "requirement": "Lubrificanti e oli utilizzati autorizzati per uso alimentare (se richiesto).", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 485 |
+
{"id": "I6.3", "requirement": "Registri di calibrazione/verifica degli strumenti di misura critici aggiornati.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 486 |
+
{"id": "I6.4", "requirement": "Gestione dei vetri rotti (registri rotture) aggiornata e conforme alla procedura.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 487 |
+
{"id": "I6.5", "requirement": "Assenza di perdite, gocciolamenti o cavi scoperti da impianti/macchinari.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 488 |
+
{"id": "I6.6", "requirement": "Area di deposito utensili e pezzi di ricambio ordinata e pulita.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}
|
| 489 |
+
]
|
| 490 |
+
},
|
| 491 |
+
{
|
| 492 |
+
"id": "S7",
|
| 493 |
+
"title": "7. DOCUMENTAZIONE E FORMAZIONE (5 Controlli)",
|
| 494 |
+
"icon": "FileText",
|
| 495 |
+
"items": [
|
| 496 |
+
{"id": "I7.1", "requirement": "Documentazione di processo (procedure, istruzioni) aggiornata e disponibile.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 497 |
+
{"id": "I7.2", "requirement": "Registri di formazione del personale aggiornati e verificabili.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 498 |
+
{"id": "I7.3", "requirement": "Certificazioni e attestati del personale validi e aggiornati.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 499 |
+
{"id": "I7.4", "requirement": "Controllo documenti (versioni, distribuzione, obsolescenza) conforme.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""},
|
| 500 |
+
{"id": "I7.5", "requirement": "Archiviazione e conservazione documenti secondo i requisiti normativi.", "compliance": "N/A", "deviation": "", "action": "", "checkedAt": None, "checkedBy": ""}
|
| 501 |
+
]
|
| 502 |
+
}
|
| 503 |
+
],
|
| 504 |
+
"totalItems": 38,
|
| 505 |
+
"completedItems": 0,
|
| 506 |
+
"nonCompliantItems": 0,
|
| 507 |
+
"complianceScore": 0.0,
|
| 508 |
+
"verificationDate": None,
|
| 509 |
+
"createdAt": datetime.utcnow(),
|
| 510 |
+
"updatedAt": datetime.utcnow()
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
# Insert the new checklist
|
| 514 |
+
result = await db.checklists.insert_one(default_checklist)
|
| 515 |
+
checklist_doc = await db.checklists.find_one({"_id": result.inserted_id})
|
| 516 |
+
logger.info(f"Created new checklist for user {user_id}")
|
| 517 |
+
|
| 518 |
+
# Serialize the document
|
| 519 |
+
serialized_checklist = serialize_checklist(checklist_doc)
|
| 520 |
+
|
| 521 |
+
return ChecklistResponse(
|
| 522 |
+
success=True,
|
| 523 |
+
data=ChecklistData(**serialized_checklist),
|
| 524 |
+
message="Checklist retrieved successfully"
|
| 525 |
+
)
|
| 526 |
+
|
| 527 |
+
except Exception as e:
|
| 528 |
+
logger.error(f"Error retrieving checklist for user {user_id}: {e}")
|
| 529 |
+
raise HTTPException(
|
| 530 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 531 |
+
detail=f"Failed to retrieve checklist: {str(e)}"
|
| 532 |
+
)
|
| 533 |
+
|
| 534 |
+
@app.put("/api/checklist/{user_id}", response_model=ChecklistResponse)
|
| 535 |
+
async def save_checklist(user_id: str, checklist_data: dict):
|
| 536 |
+
"""
|
| 537 |
+
Save or update checklist data for a specific user
|
| 538 |
+
|
| 539 |
+
This endpoint saves the checklist data to the database. It calculates
|
| 540 |
+
compliance metrics and updates the timestamp.
|
| 541 |
+
|
| 542 |
+
Args:
|
| 543 |
+
user_id: Unique identifier for the user
|
| 544 |
+
checklist_data: The checklist data to save
|
| 545 |
+
|
| 546 |
+
Returns:
|
| 547 |
+
ChecklistResponse confirming the save operation
|
| 548 |
+
|
| 549 |
+
Raises:
|
| 550 |
+
HTTPException: If database operation fails
|
| 551 |
+
"""
|
| 552 |
+
try:
|
| 553 |
+
logger.info(f"Saving checklist for user: {user_id}")
|
| 554 |
+
|
| 555 |
+
# Use the dictionary directly (already converted from JSON)
|
| 556 |
+
checklist_dict = checklist_data.copy()
|
| 557 |
+
|
| 558 |
+
# Calculate compliance metrics
|
| 559 |
+
metrics = calculate_checklist_metrics(checklist_dict)
|
| 560 |
+
checklist_dict.update(metrics)
|
| 561 |
+
|
| 562 |
+
# Update timestamps
|
| 563 |
+
checklist_dict['updatedAt'] = datetime.utcnow()
|
| 564 |
+
|
| 565 |
+
# Always create a new checklist session (don't update existing)
|
| 566 |
+
# Add a unique session ID to make each save unique
|
| 567 |
+
session_id = f"{user_id}-{datetime.utcnow().strftime('%Y%m%d-%H%M%S')}"
|
| 568 |
+
checklist_dict['sessionId'] = session_id
|
| 569 |
+
|
| 570 |
+
# Remove _id if it exists to let MongoDB generate a new one
|
| 571 |
+
if '_id' in checklist_dict:
|
| 572 |
+
del checklist_dict['_id']
|
| 573 |
+
|
| 574 |
+
# Insert as a new document (always create new session)
|
| 575 |
+
result = await db.checklists.insert_one(checklist_dict)
|
| 576 |
+
|
| 577 |
+
if result.inserted_id:
|
| 578 |
+
logger.info(f"Created new checklist session for user {user_id}")
|
| 579 |
+
message = "Checklist session created successfully"
|
| 580 |
+
else:
|
| 581 |
+
logger.error(f"Failed to create checklist for user {user_id}")
|
| 582 |
+
raise HTTPException(
|
| 583 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 584 |
+
detail="Failed to save checklist"
|
| 585 |
+
)
|
| 586 |
+
|
| 587 |
+
# Retrieve the newly created checklist
|
| 588 |
+
created_checklist = await db.checklists.find_one({"_id": result.inserted_id})
|
| 589 |
+
serialized_checklist = serialize_checklist(created_checklist)
|
| 590 |
+
|
| 591 |
+
return ChecklistResponse(
|
| 592 |
+
success=True,
|
| 593 |
+
data=serialized_checklist, # Return as dict instead of Pydantic model
|
| 594 |
+
message=message
|
| 595 |
+
)
|
| 596 |
+
|
| 597 |
+
except Exception as e:
|
| 598 |
+
logger.error(f"Error saving checklist for user {user_id}: {e}")
|
| 599 |
+
raise HTTPException(
|
| 600 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 601 |
+
detail=f"Failed to save checklist: {str(e)}"
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
@app.get("/api/checklists/by-user/{user_name}", response_model=Dict[str, Any])
|
| 605 |
+
async def get_checklists_by_user_name(user_name: str):
|
| 606 |
+
"""
|
| 607 |
+
Retrieve all checklists for a specific user name
|
| 608 |
+
|
| 609 |
+
Args:
|
| 610 |
+
user_name: The name of the user to retrieve checklists for
|
| 611 |
+
|
| 612 |
+
Returns:
|
| 613 |
+
Dictionary containing list of checklists for the user
|
| 614 |
+
|
| 615 |
+
Raises:
|
| 616 |
+
HTTPException: If database operation fails
|
| 617 |
+
"""
|
| 618 |
+
try:
|
| 619 |
+
logger.info(f"Retrieving checklists for user: {user_name}")
|
| 620 |
+
|
| 621 |
+
# Find all checklists where metadata.userName matches
|
| 622 |
+
cursor = db.checklists.find({"metadata.userName": user_name})
|
| 623 |
+
checklists = []
|
| 624 |
+
|
| 625 |
+
async for checklist_doc in cursor:
|
| 626 |
+
serialized_checklist = serialize_checklist(checklist_doc)
|
| 627 |
+
checklists.append(serialized_checklist)
|
| 628 |
+
|
| 629 |
+
return {
|
| 630 |
+
"success": True,
|
| 631 |
+
"data": checklists,
|
| 632 |
+
"count": len(checklists),
|
| 633 |
+
"user_name": user_name
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
except Exception as e:
|
| 637 |
+
logger.error(f"Error retrieving checklists for user {user_name}: {e}")
|
| 638 |
+
raise HTTPException(
|
| 639 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 640 |
+
detail=f"Failed to retrieve checklists for user: {str(e)}"
|
| 641 |
+
)
|
| 642 |
+
|
| 643 |
+
@app.get("/api/checklists", response_model=Dict[str, Any])
|
| 644 |
+
async def get_all_checklists():
|
| 645 |
+
"""
|
| 646 |
+
Retrieve all checklists (admin endpoint)
|
| 647 |
+
|
| 648 |
+
This endpoint returns all checklists in the database. It's intended for
|
| 649 |
+
administrative purposes and should be protected with proper authentication
|
| 650 |
+
in a production environment.
|
| 651 |
+
|
| 652 |
+
Returns:
|
| 653 |
+
Dictionary containing list of all checklists
|
| 654 |
+
|
| 655 |
+
Raises:
|
| 656 |
+
HTTPException: If database operation fails
|
| 657 |
+
"""
|
| 658 |
+
try:
|
| 659 |
+
logger.info("Retrieving all checklists")
|
| 660 |
+
|
| 661 |
+
# Find all checklists
|
| 662 |
+
cursor = db.checklists.find({})
|
| 663 |
+
checklists = []
|
| 664 |
+
|
| 665 |
+
async for checklist_doc in cursor:
|
| 666 |
+
serialized_checklist = serialize_checklist(checklist_doc)
|
| 667 |
+
checklists.append(serialized_checklist)
|
| 668 |
+
|
| 669 |
+
return {
|
| 670 |
+
"success": True,
|
| 671 |
+
"data": checklists,
|
| 672 |
+
"count": len(checklists),
|
| 673 |
+
"message": f"Retrieved {len(checklists)} checklists"
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
except Exception as e:
|
| 677 |
+
logger.error(f"Error retrieving all checklists: {e}")
|
| 678 |
+
raise HTTPException(
|
| 679 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 680 |
+
detail=f"Failed to retrieve checklists: {str(e)}"
|
| 681 |
+
)
|
| 682 |
+
|
| 683 |
+
# =============================================================================
|
| 684 |
+
# ERROR HANDLERS
|
| 685 |
+
# =============================================================================
|
| 686 |
+
|
| 687 |
+
@app.exception_handler(HTTPException)
|
| 688 |
+
async def http_exception_handler(request, exc):
|
| 689 |
+
"""Handle HTTP exceptions with consistent error format"""
|
| 690 |
+
logger.error(f"HTTP Exception: {exc.detail}")
|
| 691 |
+
return JSONResponse(
|
| 692 |
+
status_code=exc.status_code,
|
| 693 |
+
content={
|
| 694 |
+
"success": False,
|
| 695 |
+
"error": exc.detail,
|
| 696 |
+
"status_code": exc.status_code
|
| 697 |
+
}
|
| 698 |
+
)
|
| 699 |
+
|
| 700 |
+
@app.exception_handler(Exception)
|
| 701 |
+
async def general_exception_handler(request, exc):
|
| 702 |
+
"""Handle general exceptions with logging"""
|
| 703 |
+
logger.error(f"Unhandled exception: {exc}", exc_info=True)
|
| 704 |
+
return JSONResponse(
|
| 705 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 706 |
+
content={
|
| 707 |
+
"success": False,
|
| 708 |
+
"error": "Internal server error",
|
| 709 |
+
"detail": str(exc) if os.getenv("DEBUG", "false").lower() == "true" else "An unexpected error occurred"
|
| 710 |
+
}
|
| 711 |
+
)
|
| 712 |
+
|
| 713 |
+
# =============================================================================
|
| 714 |
+
# MAIN EXECUTION
|
| 715 |
+
# =============================================================================
|
| 716 |
+
|
| 717 |
+
if __name__ == "__main__":
|
| 718 |
+
"""
|
| 719 |
+
Main execution block for running the server
|
| 720 |
+
|
| 721 |
+
This block is executed when the script is run directly (not imported).
|
| 722 |
+
It starts the Uvicorn ASGI server with the configured settings.
|
| 723 |
+
"""
|
| 724 |
+
import uvicorn
|
| 725 |
+
|
| 726 |
+
logger.info("Starting FastAPI server...")
|
| 727 |
+
|
| 728 |
+
# Run the server
|
| 729 |
+
uvicorn.run(
|
| 730 |
+
"app:app", # Changed from "main:app" to "app:app" for HF Spaces
|
| 731 |
+
host="0.0.0.0",
|
| 732 |
+
port=PORT,
|
| 733 |
+
reload=False, # Disable auto-reload for production deployment
|
| 734 |
+
log_level="info"
|
| 735 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FastAPI and related dependencies
|
| 2 |
+
fastapi==0.104.1
|
| 3 |
+
uvicorn[standard]==0.24.0
|
| 4 |
+
pydantic==2.5.0
|
| 5 |
+
|
| 6 |
+
# MongoDB integration
|
| 7 |
+
motor==3.3.2 # Async MongoDB driver
|
| 8 |
+
pymongo==4.6.0 # MongoDB driver
|
| 9 |
+
|
| 10 |
+
# Environment variables
|
| 11 |
+
python-dotenv==1.0.0
|
| 12 |
+
|
| 13 |
+
# CORS middleware
|
| 14 |
+
python-multipart==0.0.6
|
| 15 |
+
|
| 16 |
+
# Date/time handling
|
| 17 |
+
python-dateutil==2.8.2
|
| 18 |
+
|
| 19 |
+
# Additional utilities
|
| 20 |
+
httpx==0.25.2 # For making HTTP requests if needed
|