Browse Source

feat: Update configuration, enhance authentication, and improve database management with Italian localization

master
Matteo Benedetto 7 months ago
parent
commit
a833ce93f8
  1. 366
      admin-webgui/README.md
  2. 2
      api/config.yaml
  3. 31
      api/core/auth.py
  4. 20
      api/core/database.py
  5. 4
      api/core/exceptions.py
  6. 14
      api/dependencies.py
  7. 79
      api/main.py
  8. 103
      api/routes/pietanze.py
  9. 19
      compose.yaml
  10. 1
      requirements.txt
  11. 10
      setup_db.py

366
admin-webgui/README.md

@ -0,0 +1,366 @@
# Interfaccia Web di Amministrazione - Simple Mensa
## Overview
L'interfaccia web di amministrazione è sviluppata con **NiceGUI**, un framework Python moderno che combina la semplicità di sviluppo backend con un'interfaccia utente reattiva e moderna. L'applicazione fornisce un pannello di controllo completo per la gestione del sistema di prenotazione mensa.
## Architettura dell'Applicazione
### Stack Tecnologico
- **Frontend Framework**: NiceGUI 1.4+
- **Backend Integration**: Integrazione diretta con FastAPI via HTTP client
- **Authentication**: JWT token-based con provider esterno
- **Styling**: Tailwind CSS + Quasar Components
- **Real-time Updates**: WebSocket per aggiornamenti live
- **Charts & Analytics**: Plotly.js integrato
### Struttura del Progetto
```
webgui/
├── main.py # Punto di ingresso dell'applicazione NiceGUI
├── config.yaml # Configurazione per la webgui
├── requirements.txt # Dipendenze specifiche per la webgui
├── pages/ # Pagine dell'interfaccia amministrativa
│ ├── __init__.py
│ ├── login.py # Pagina di autenticazione
│ ├── dashboard.py # Dashboard principale con KPI
│ ├── pietanze.py # Gestione CRUD pietanze
│ ├── pasti.py # Composizione e gestione menu giornalieri
│ ├── prenotazioni.py # Monitoraggio e gestione prenotazioni
│ └── analytics.py # Reports e statistiche
├── components/ # Componenti riutilizzabili
│ ├── __init__.py
│ ├── layout.py # Layout principale e navigazione
│ ├── forms.py # Form components personalizzati
│ ├── tables.py # Tabelle con paginazione e filtri
│ ├── charts.py # Grafici e visualizzazioni
│ └── modals.py # Dialoghi e popup
├── services/ # Servizi per comunicazione con API
│ ├── __init__.py
│ ├── api_client.py # Client HTTP per FastAPI
│ ├── auth_service.py # Gestione autenticazione
│ └── data_service.py # Servizi per manipolazione dati
├── utils/ # Utilità e helper
│ ├── __init__.py
│ ├── formatters.py # Formatters per date, numeri, etc.
│ └── validators.py # Validatori lato client
└── static/ # File statici (CSS, JS, immagini)
├── styles.css
└── logo.png
```
## Funzionalità dell'Interfaccia
### 1. Dashboard Principale (`pages/dashboard.py`)
**KPI e Metriche in Tempo Reale**
- Card con statistiche giornaliere (prenotazioni attive, pasti serviti, disponibilità)
- Grafici trend settimanali/mensili delle prenotazioni
- Indicatori di utilizzo capacità per turno
- Alert per situazioni critiche (esaurimento posti, pietanze terminate)
**Widget Informativi**
- Calendario con vista mensile delle prenotazioni
- Lista prenotazioni recenti con stati in tempo reale
- Notifiche e alert di sistema
### 2. Gestione Pietanze (`pages/pietanze.py`)
**Interfaccia CRUD Completa**
- Tabella responsive con ricerca full-text e filtri per allergeni
- Form di creazione/modifica con validazione client-side
- Upload di immagini per le pietanze (futuro sviluppo)
- Gestione allergeni con chip multiselect
- Azioni bulk per operazioni multiple
**Componenti Specifici**
```python
# Esempio struttura componente pietanze
class PietanzeManager:
def __init__(self):
self.search_filter = ""
self.allergen_filter = []
def render_pietanze_table(self):
# Tabella con sorting, paginazione, ricerca
pass
def render_pietanza_form(self, pietanza=None):
# Form responsive con validazione
pass
```
### 3. Composizione Menu (`pages/pasti.py`)
**Editor Menu Giornaliero**
- Calendario per selezione data
- Drag & drop per composizione portate
- Configurazione turni con slider per capacità
- Anteprima menu con calcolo automatico disponibilità
- Duplicazione menu da giorni precedenti
**Gestione Turni Dinamica**
- Time picker per orari turni
- Slider per impostazione capacità massima
- Visualizzazione occupazione in tempo reale
### 4. Monitoraggio Prenotazioni (`pages/prenotazioni.py`)
**Vista Operativa per il Servizio**
- Tabella prenotazioni filtrata per data/turno
- Aggiornamento stati prenotazione con click
- Scansione QR code per check-in rapido
- Stampa liste per cucina e cassa
**Funzionalità Avanzate**
- Ricerca prenotazioni per utente
- Export CSV per reporting
- Gestione cancellazioni e modifiche
- Comunicazioni automatiche via email
### 5. Analytics e Reports (`pages/analytics.py`)
**Dashboard Analitica**
- Grafici utilizzo pietanze e preferenze utenti
- Report sprechi e ottimizzazione menù
- Analisi pattern prenotazioni
- Forecast domanda per pianificazione
**Export e Reportistica**
- Report PDF configurabili
- Export dati in formati multipli
- Grafici interattivi con drill-down
## Componenti Tecnici
### Layout e Navigazione (`components/layout.py`)
```python
class AdminLayout:
def __init__(self):
self.sidebar_open = True
def render_header(self):
# Header con user info, logout, notifiche
pass
def render_sidebar(self):
# Menu laterale con sezioni collassabili
pass
def render_main_content(self):
# Area principale responsiva
pass
```
### Componenti Form (`components/forms.py`)
**Form Builder Dinamico**
- Generazione automatica form da modelli Pydantic
- Validazione real-time con feedback visuale
- Upload file con progress bar
- Date/time picker localizzati
- Multiselect con ricerca
### Tabelle Avanzate (`components/tables.py`)
**DataTable Component**
- Server-side pagination per performance
- Sorting multi-colonna
- Filtri avanzati per colonna
- Export integrato
- Azioni batch selettive
### Integrazione API (`services/api_client.py`)
**HTTP Client Ottimizzato**
```python
class APIClient:
def __init__(self, base_url: str, auth_token: str):
self.base_url = base_url
self.headers = {"Authorization": f"Bearer {auth_token}"}
async def get_pietanze(self, filters: dict = None):
# GET /api/v1/pietanze con filtri
pass
async def create_pasto(self, pasto_data: dict):
# POST /api/v1/pasti
pass
```
## Flussi Operativi dell'Interfaccia
### Flusso Creazione Menu Giornaliero
1. **Selezione Data**: Calendario per scelta giorno
2. **Composizione Portate**: Drag & drop pietanze nelle categorie
3. **Impostazione Turni**: Time picker + capacity slider
4. **Anteprima e Validazione**: Controllo disponibilità e conflitti
5. **Attivazione**: Toggle per abilitare prenotazioni
### Flusso Gestione Servizio
1. **Vista Prenotazioni Turno**: Filtro automatico per turno corrente
2. **Check-in Utenti**: Click su riga o scansione QR
3. **Aggiornamento Stati**: Transizioni stato con validazione
4. **Monitoraggio Disponibilità**: Aggiornamenti real-time
### Flusso Amministrazione Pietanze
1. **Vista Lista**: Tabella con ricerca e filtri
2. **Creazione/Modifica**: Form modale con validazione
3. **Gestione Allergeni**: Chip selector con autocomplete
4. **Operazioni Bulk**: Selezione multipla per azioni batch
## Caratteristiche UX/UI
### Design System
**Palette Colori**
- Primary: Verde accogliente per azioni positive
- Secondary: Arancione per elementi di attenzione
- Neutral: Grigi per backgrounds e testi
- Status Colors: Semantici per stati (rosso errore, verde successo)
**Typography**
- Font system: Inter per leggibilità ottimale
- Hierarchy: H1-H6 ben definiti
- Body text: 16px base per accessibilità
**Spacing & Layout**
- Grid system 12 colonne responsive
- Breakpoints standard (mobile, tablet, desktop)
- Spacing scale basato su multipli di 8px
### Responsività
**Mobile-First Design**
- Layout collassabile per mobile
- Touch-friendly button sizing
- Swipe gestures per tabelle
- Bottom navigation su mobile
**Progressive Enhancement**
- Funzionalità base su tutti i device
- Funzionalità avanzate su desktop
- Graceful degradation per browser legacy
### Accessibilità
**WCAG 2.1 AA Compliance**
- Contrasto colori adeguato
- Navigazione da tastiera completa
- Screen reader friendly
- Focus indicators chiari
- Alt text per immagini
## Performance e Ottimizzazioni
### Lazy Loading
- Caricamento componenti on-demand
- Paginazione server-side per tabelle grandi
- Immagini lazy-loaded
### Caching Strategy
- Cache client per dati statici
- Invalidazione intelligente
- Prefetch per navigazione anticipata
### Real-time Updates
- WebSocket per notifiche push
- Server-sent events per updates dashboard
- Optimistic UI per azioni immediate
## Sicurezza
### Authentication Flow
1. **Login**: Redirect a provider esterno (Azure/Keycloak)
2. **Token Management**: Storage sicuro e refresh automatico
3. **Authorization**: Controllo ruoli per ogni sezione
4. **Logout**: Pulizia completa sessione
### Data Protection
- Sanitizzazione input utente
- Validazione server-side sempre presente
- HTTPS only per comunicazioni
- CSP headers per XSS protection
## Deployment e Configurazione
### Development Setup
```bash
cd webgui
pip install -r requirements.txt
cp config.yaml.example config.yaml
# Configura URL API e credenziali
python main.py
```
### Production Deployment
- Container Docker con NiceGUI
- Reverse proxy con SSL termination
- Health checks e monitoring
- Backup automatici configurazione
### Environment Variables
```yaml
# config.yaml example
api:
base_url: "http://localhost:8000"
timeout: 30
auth:
provider_url: "https://login.microsoftonline.com/..."
client_id: "your-client-id"
app:
title: "Simple Mensa Admin"
debug: false
port: 8080
```
## Estensibilità Futura
### Moduli Aggiuntivi
- **Gestione Utenti**: CRUD utenti con ruoli
- **Inventory Management**: Controllo ingredienti e scorte
- **Financial Reports**: Controllo costi e ricavi
- **Mobile App**: Companion app per servizio cucina
### Integrazioni
- **ERP Integration**: Sync con sistemi aziendali
- **Payment Gateway**: Pagamenti online
- **SMS/Push Notifications**: Comunicazioni real-time
- **IoT Integration**: Sensori per monitoraggio automatico
## Testing Strategy
### Unit Testing
- Test componenti NiceGUI
- Mock API calls
- Validation logic testing
### Integration Testing
- End-to-end user workflows
- API integration testing
- Authentication flow testing
### Performance Testing
- Load testing per concorrenza
- Memory usage profiling
- Response time benchmarks
Questa architettura garantisce un'interfaccia amministrativa moderna, scalabile e user-friendly, perfettamente integrata con l'API FastAPI esistente e pronta per evoluzioni future.

2
api/config.yaml

@ -4,7 +4,7 @@
database: database:
host: "localhost" host: "localhost"
port: 5432 port: 5432
name: "postgres" name: "simple_mensa"
user: "postgres" user: "postgres"
password: "example" password: "example"
pool_min_size: 5 pool_min_size: 5

31
api/core/auth.py

@ -19,7 +19,7 @@ class AuthManager:
self.jwks_cache: Optional[Dict] = None self.jwks_cache: Optional[Dict] = None
async def get_jwks(self) -> Dict: async def get_jwks(self) -> Dict:
"""Fetch JWKS from provider""" """Recupera JWKS dal provider"""
if self.jwks_cache is None: if self.jwks_cache is None:
try: try:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
@ -27,23 +27,23 @@ class AuthManager:
response.raise_for_status() response.raise_for_status()
self.jwks_cache = response.json() self.jwks_cache = response.json()
except Exception as e: except Exception as e:
logger.error(f"Failed to fetch JWKS: {e}") logger.error(f"Errore nel recupero JWKS: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Authentication service unavailable" detail="Servizio di autenticazione non disponibile"
) )
return self.jwks_cache return self.jwks_cache
async def verify_token(self, token: str) -> Dict[str, Any]: async def verify_token(self, token: str) -> Dict[str, Any]:
"""Verify JWT token and return claims""" """Verifica token JWT e restituisci i claims"""
try: try:
# For development, we'll skip actual JWT verification # Per lo sviluppo, saltiamo la verifica JWT effettiva
# In production, implement proper JWKS verification # In produzione, implementare la verifica JWKS appropriata
unverified_payload = jwt.get_unverified_claims(token) unverified_payload = jwt.get_unverified_claims(token)
# Extract user information from token # Estrai informazioni utente dal token
user_info = { user_info = {
'user_id': unverified_payload.get('sub', 'unknown'), 'user_id': unverified_payload.get('sub', 'sconosciuto'),
'email': unverified_payload.get('email'), 'email': unverified_payload.get('email'),
'name': unverified_payload.get('name'), 'name': unverified_payload.get('name'),
'roles': unverified_payload.get('roles', []) 'roles': unverified_payload.get('roles', [])
@ -52,35 +52,36 @@ class AuthManager:
return user_info return user_info
except JWTError as e: except JWTError as e:
logger.error(f"JWT verification failed: {e}") logger.error(f"Verifica JWT fallita: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication token", detail="Token di autenticazione non valido",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
# Global auth manager # Gestore autenticazione globale
auth_manager: Optional[AuthManager] = None auth_manager: Optional[AuthManager] = None
def initialize_auth(config: Dict[str, Any]): def initialize_auth(config: Dict[str, Any]):
"""Inizializza il gestore di autenticazione"""
global auth_manager global auth_manager
auth_manager = AuthManager(config) auth_manager = AuthManager(config)
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> Dict[str, Any]: async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> Dict[str, Any]:
"""Dependency to get current authenticated user""" """Dipendenza per ottenere l'utente attualmente autenticato"""
if auth_manager is None: if auth_manager is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Authentication not configured" detail="Autenticazione non configurata"
) )
return await auth_manager.verify_token(credentials.credentials) return await auth_manager.verify_token(credentials.credentials)
async def get_current_admin_user(current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]: async def get_current_admin_user(current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
"""Dependency to ensure user has admin role""" """Dipendenza per assicurarsi che l'utente abbia ruolo amministratore"""
if 'admin' not in current_user.get('roles', []): if 'admin' not in current_user.get('roles', []):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Admin privileges required" detail="Privilegi di amministratore richiesti"
) )
return current_user return current_user

20
api/core/database.py

@ -10,7 +10,7 @@ class DatabaseManager:
self.pool: Optional[asyncpg.Pool] = None self.pool: Optional[asyncpg.Pool] = None
async def initialize(self, config: dict): async def initialize(self, config: dict):
"""Initialize database connection pool""" """Inizializza il pool di connessioni al database"""
try: try:
self.pool = await asyncpg.create_pool( self.pool = await asyncpg.create_pool(
host=config['host'], host=config['host'],
@ -23,35 +23,35 @@ class DatabaseManager:
max_queries=config.get('pool_max_queries', 50000), max_queries=config.get('pool_max_queries', 50000),
max_inactive_connection_lifetime=config.get('pool_max_inactive_connection_lifetime', 300.0) max_inactive_connection_lifetime=config.get('pool_max_inactive_connection_lifetime', 300.0)
) )
logger.info("Database pool initialized successfully") logger.info("Pool di connessioni database inizializzato con successo")
except Exception as e: except Exception as e:
logger.error(f"Failed to initialize database pool: {e}") logger.error(f"Errore nell'inizializzazione del pool database: {e}")
raise raise
async def close(self): async def close(self):
"""Close database connection pool""" """Chiudi il pool di connessioni al database"""
if self.pool: if self.pool:
await self.pool.close() await self.pool.close()
logger.info("Database pool closed") logger.info("Pool di connessioni database chiuso")
async def execute_query(self, query: str, *args): async def execute_query(self, query: str, *args):
"""Execute a query and return results""" """Esegui una query e restituisci i risultati"""
async with self.pool.acquire() as connection: async with self.pool.acquire() as connection:
return await connection.fetch(query, *args) return await connection.fetch(query, *args)
async def execute_one(self, query: str, *args): async def execute_one(self, query: str, *args):
"""Execute a query and return single result""" """Esegui una query e restituisci un singolo risultato"""
async with self.pool.acquire() as connection: async with self.pool.acquire() as connection:
return await connection.fetchrow(query, *args) return await connection.fetchrow(query, *args)
async def execute_command(self, query: str, *args): async def execute_command(self, query: str, *args):
"""Execute a command (INSERT, UPDATE, DELETE)""" """Esegui un comando (INSERT, UPDATE, DELETE)"""
async with self.pool.acquire() as connection: async with self.pool.acquire() as connection:
return await connection.execute(query, *args) return await connection.execute(query, *args)
# Global database manager instance # Istanza globale del gestore database
db_manager = DatabaseManager() db_manager = DatabaseManager()
async def get_database(): async def get_database():
"""Dependency for getting database connection""" """Dipendenza per ottenere la connessione al database"""
return db_manager return db_manager

4
api/core/exceptions.py

@ -4,7 +4,7 @@ class PietanzaNotFoundError(HTTPException):
def __init__(self, pietanza_id: int): def __init__(self, pietanza_id: int):
super().__init__( super().__init__(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Pietanza with id {pietanza_id} not found" detail=f"Pietanza con id {pietanza_id} non trovata"
) )
class ValidationError(HTTPException): class ValidationError(HTTPException):
@ -15,7 +15,7 @@ class ValidationError(HTTPException):
) )
class DatabaseError(HTTPException): class DatabaseError(HTTPException):
def __init__(self, detail: str = "Database operation failed"): def __init__(self, detail: str = "Operazione database fallita"):
super().__init__( super().__init__(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=detail detail=detail

14
api/dependencies.py

@ -6,30 +6,30 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def load_config() -> dict: def load_config() -> dict:
"""Load configuration from YAML file""" """Carica configurazione dal file YAML"""
try: try:
with open('api/config.yaml', 'r') as file: with open('api/config.yaml', 'r') as file:
return yaml.safe_load(file) return yaml.safe_load(file)
except Exception as e: except Exception as e:
logger.error(f"Failed to load configuration: {e}") logger.error(f"Errore nel caricamento della configurazione: {e}")
raise raise
# Global configuration # Configurazione globale
config = load_config() config = load_config()
def get_config() -> dict: def get_config() -> dict:
"""Dependency to get application configuration""" """Dipendenza per ottenere la configurazione dell'applicazione"""
return config return config
class PaginationParams: class PaginationParams:
def __init__( def __init__(
self, self,
skip: int = Query(0, ge=0, description="Number of items to skip"), skip: int = Query(0, ge=0, description="Numero di elementi da saltare"),
limit: int = Query(20, ge=1, le=100, description="Number of items to return") limit: int = Query(20, ge=1, le=100, description="Numero di elementi da restituire")
): ):
self.skip = skip self.skip = skip
self.limit = limit self.limit = limit
def get_pagination_params(params: PaginationParams = Depends()) -> PaginationParams: def get_pagination_params(params: PaginationParams = Depends()) -> PaginationParams:
"""Dependency for pagination parameters""" """Dipendenza per i parametri di paginazione"""
return params return params

79
api/main.py

@ -1,36 +1,34 @@
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
import asyncio
import logging import logging
import yaml
from .core.database import db_manager from core.database import db_manager
from .core.auth import initialize_auth from core.auth import initialize_auth
from .core.exceptions import PietanzaNotFoundError, ValidationError, DatabaseError from core.exceptions import PietanzaNotFoundError, ValidationError, DatabaseError
from .dependencies import get_config from dependencies import get_config
from .routes import pietanze from routes import pietanze
# Configure logging # Configura logging
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Create FastAPI application # Crea applicazione FastAPI
app = FastAPI( app = FastAPI(
title="Simple Mensa API", title="API Simple Mensa",
description="API for mensa booking system", description="API per il sistema di prenotazione mensa",
version="1.0.0", version="1.0.0",
docs_url="/docs", docs_url="/docs",
redoc_url="/redoc" redoc_url="/redoc"
) )
# Load configuration # Carica configurazione
config = get_config() config = get_config()
# Configure CORS # Configura CORS
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=config['cors']['allow_origins'], allow_origins=config['cors']['allow_origins'],
@ -39,77 +37,77 @@ app.add_middleware(
allow_headers=config['cors']['allow_headers'], allow_headers=config['cors']['allow_headers'],
) )
# Include routers # Includi router
app.include_router(pietanze.router, prefix="/api/v1") app.include_router(pietanze.router, prefix="/api/v1")
# Exception handlers # Gestori di eccezioni
@app.exception_handler(PietanzaNotFoundError) @app.exception_handler(PietanzaNotFoundError)
async def pietanza_not_found_handler(request, exc): async def pietanza_not_found_handler(request, exc):
return JSONResponse( return JSONResponse(
status_code=exc.status_code, status_code=exc.status_code,
content={"error": "Pietanza not found", "detail": exc.detail} content={"errore": "Pietanza non trovata", "dettaglio": exc.detail}
) )
@app.exception_handler(ValidationError) @app.exception_handler(ValidationError)
async def validation_error_handler(request, exc): async def validation_error_handler(request, exc):
return JSONResponse( return JSONResponse(
status_code=exc.status_code, status_code=exc.status_code,
content={"error": "Validation error", "detail": exc.detail} content={"errore": "Errore di validazione", "dettaglio": exc.detail}
) )
@app.exception_handler(DatabaseError) @app.exception_handler(DatabaseError)
async def database_error_handler(request, exc): async def database_error_handler(request, exc):
return JSONResponse( return JSONResponse(
status_code=exc.status_code, status_code=exc.status_code,
content={"error": "Database error", "detail": exc.detail} content={"errore": "Errore database", "dettaglio": exc.detail}
) )
# Startup and shutdown events # Eventi di avvio e spegnimento
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
"""Initialize application on startup""" """Inizializza applicazione all'avvio"""
try: try:
# Initialize database # Inizializza database
await db_manager.initialize(config['database']) await db_manager.initialize(config['database'])
logger.info("Database initialized") logger.info("Database inizializzato")
# Initialize authentication # Inizializza autenticazione
initialize_auth(config['auth']) initialize_auth(config['auth'])
logger.info("Authentication initialized") logger.info("Autenticazione inizializzata")
logger.info("Application startup completed") logger.info("Avvio applicazione completato")
except Exception as e: except Exception as e:
logger.error(f"Failed to initialize application: {e}") logger.error(f"Errore nell'inizializzazione dell'applicazione: {e}")
raise raise
@app.on_event("shutdown") @app.on_event("shutdown")
async def shutdown_event(): async def shutdown_event():
"""Cleanup on application shutdown""" """Pulizia alla chiusura dell'applicazione"""
try: try:
await db_manager.close() await db_manager.close()
logger.info("Application shutdown completed") logger.info("Spegnimento applicazione completato")
except Exception as e: except Exception as e:
logger.error(f"Error during shutdown: {e}") logger.error(f"Errore durante lo spegnimento: {e}")
# Health check endpoint # Endpoint di controllo stato
@app.get("/health") @app.get("/health")
async def health_check(): async def health_check():
"""Health check endpoint""" """Endpoint di controllo stato dell'applicazione"""
return { return {
"status": "healthy", "stato": "funzionante",
"service": "simple-mensa-api", "servizio": "simple-mensa-api",
"version": "1.0.0" "versione": "1.0.0"
} }
# Root endpoint # Endpoint principale
@app.get("/") @app.get("/")
async def root(): async def root():
"""Root endpoint with API information""" """Endpoint principale con informazioni API"""
return { return {
"message": "Simple Mensa API", "messaggio": "API Simple Mensa",
"version": "1.0.0", "versione": "1.0.0",
"docs": "/docs", "documentazione": "/docs",
"health": "/health" "stato": "/health"
} }
if __name__ == "__main__": if __name__ == "__main__":
@ -119,5 +117,4 @@ if __name__ == "__main__":
host=config['server']['host'], host=config['server']['host'],
port=config['server']['port'], port=config['server']['port'],
reload=config['server']['reload'], reload=config['server']['reload'],
debug=config['server']['debug']
) )

103
api/routes/pietanze.py

@ -3,25 +3,25 @@ from typing import List, Optional, Dict, Any
import json import json
from datetime import datetime from datetime import datetime
from ..core.database import get_database, DatabaseManager from core.database import get_database, DatabaseManager
from ..models.pietanze import PietanzaCreate, PietanzaUpdate, PietanzaResponse from models.pietanze import PietanzaCreate, PietanzaUpdate, PietanzaResponse
from ..models.common import ErrorResponse from models.common import ErrorResponse
from ..core.auth import get_current_user, get_current_admin_user from core.auth import get_current_user, get_current_admin_user
from ..core.exceptions import PietanzaNotFoundError, DatabaseError from core.exceptions import PietanzaNotFoundError, DatabaseError
router = APIRouter(prefix="/pietanze", tags=["Pietanze"]) router = APIRouter(prefix="/pietanze", tags=["Pietanze"])
@router.get("/", response_model=List[PietanzaResponse]) @router.get("/", response_model=List[PietanzaResponse])
async def list_pietanze( async def list_pietanze(
skip: int = Query(0, ge=0, description="Number of items to skip"), skip: int = Query(0, ge=0, description="Numero di elementi da saltare per la paginazione"),
limit: int = Query(20, ge=1, le=100, description="Number of items to return"), limit: int = Query(20, ge=1, le=100, description="Numero di elementi da restituire"),
search: Optional[str] = Query(None, description="Search in nome and descrizione"), search: Optional[str] = Query(None, description="Ricerca in nome e descrizione"),
allergeni: Optional[str] = Query(None, description="Filter by allergens (comma-separated)"), allergeni: Optional[str] = Query(None, description="Filtra per allergeni (separati da virgola)"),
db: DatabaseManager = Depends(get_database) db: DatabaseManager = Depends(get_database)
): ):
"""Get list of available pietanze with optional filtering""" """Ottieni lista delle pietanze disponibili con filtri opzionali"""
try: try:
# Build query with filters # Costruisci query con filtri
where_conditions = [] where_conditions = []
params = [] params = []
param_count = 0 param_count = 0
@ -41,12 +41,12 @@ async def list_pietanze(
if where_conditions: if where_conditions:
where_clause = "WHERE " + " AND ".join(where_conditions) where_clause = "WHERE " + " AND ".join(where_conditions)
# Count total items # Conta il totale degli elementi
count_query = f"SELECT COUNT(*) FROM pietanze {where_clause}" count_query = f"SELECT COUNT(*) FROM pietanze {where_clause}"
count_result = await db.execute_one(count_query, *params) count_result = await db.execute_one(count_query, *params)
total = count_result[0] if count_result else 0 total = count_result[0] if count_result else 0
# Get items with pagination # Ottieni elementi con paginazione
param_count += 1 param_count += 1
limit_param = param_count limit_param = param_count
param_count += 1 param_count += 1
@ -77,14 +77,14 @@ async def list_pietanze(
return pietanze return pietanze
except Exception as e: except Exception as e:
raise DatabaseError(f"Failed to retrieve pietanze: {str(e)}") raise DatabaseError(f"Errore nel recupero delle pietanze: {str(e)}")
@router.get("/{pietanza_id}", response_model=PietanzaResponse) @router.get("/{pietanza_id}", response_model=PietanzaResponse)
async def get_pietanza( async def get_pietanza(
pietanza_id: int, pietanza_id: int,
db: DatabaseManager = Depends(get_database) db: DatabaseManager = Depends(get_database)
): ):
"""Get specific pietanza by ID""" """Ottieni pietanza specifica per ID"""
try: try:
query = """ query = """
SELECT id, nome, descrizione, allergeni, created_at, updated_at SELECT id, nome, descrizione, allergeni, created_at, updated_at
@ -108,7 +108,7 @@ async def get_pietanza(
except PietanzaNotFoundError: except PietanzaNotFoundError:
raise raise
except Exception as e: except Exception as e:
raise DatabaseError(f"Failed to retrieve pietanza: {str(e)}") raise DatabaseError(f"Errore nel recupero della pietanza: {str(e)}")
@router.post("/", response_model=PietanzaResponse, status_code=status.HTTP_201_CREATED) @router.post("/", response_model=PietanzaResponse, status_code=status.HTTP_201_CREATED)
async def create_pietanza( async def create_pietanza(
@ -116,7 +116,7 @@ async def create_pietanza(
current_user: Dict[str, Any] = Depends(get_current_admin_user), current_user: Dict[str, Any] = Depends(get_current_admin_user),
db: DatabaseManager = Depends(get_database) db: DatabaseManager = Depends(get_database)
): ):
"""Create new pietanza (admin only)""" """Crea nuova pietanza (solo amministratori)"""
try: try:
query = """ query = """
INSERT INTO pietanze (nome, descrizione, allergeni, created_at, updated_at) INSERT INTO pietanze (nome, descrizione, allergeni, created_at, updated_at)
@ -136,7 +136,7 @@ async def create_pietanza(
) )
if not row: if not row:
raise DatabaseError("Failed to create pietanza") raise DatabaseError("Errore nella creazione della pietanza")
return PietanzaResponse( return PietanzaResponse(
id=row['id'], id=row['id'],
@ -148,7 +148,7 @@ async def create_pietanza(
) )
except Exception as e: except Exception as e:
raise DatabaseError(f"Failed to create pietanza: {str(e)}") raise DatabaseError(f"Errore nella creazione della pietanza: {str(e)}")
@router.put("/{pietanza_id}", response_model=PietanzaResponse) @router.put("/{pietanza_id}", response_model=PietanzaResponse)
async def update_pietanza( async def update_pietanza(
@ -157,14 +157,14 @@ async def update_pietanza(
current_user: Dict[str, Any] = Depends(get_current_admin_user), current_user: Dict[str, Any] = Depends(get_current_admin_user),
db: DatabaseManager = Depends(get_database) db: DatabaseManager = Depends(get_database)
): ):
"""Update existing pietanza (admin only)""" """Aggiorna pietanza esistente (solo amministratori)"""
try: try:
# Check if pietanza exists # Verifica se la pietanza esiste
existing = await db.execute_one("SELECT id FROM pietanze WHERE id = $1", pietanza_id) existing = await db.execute_one("SELECT id FROM pietanze WHERE id = $1", pietanza_id)
if not existing: if not existing:
raise PietanzaNotFoundError(pietanza_id) raise PietanzaNotFoundError(pietanza_id)
# Build update query dynamically # Costruisci query di aggiornamento dinamicamente
update_fields = [] update_fields = []
params = [] params = []
param_count = 0 param_count = 0
@ -185,10 +185,10 @@ async def update_pietanza(
params.append(json.dumps(pietanza_update.allergeni)) params.append(json.dumps(pietanza_update.allergeni))
if not update_fields: if not update_fields:
# No fields to update, return current pietanza # Nessun campo da aggiornare, restituisci pietanza corrente
return await get_pietanza(pietanza_id, db) return await get_pietanza(pietanza_id, db)
# Add updated_at and pietanza_id # Aggiungi updated_at e pietanza_id
param_count += 1 param_count += 1
update_fields.append(f"updated_at = ${param_count}") update_fields.append(f"updated_at = ${param_count}")
params.append(datetime.utcnow()) params.append(datetime.utcnow())
@ -217,7 +217,7 @@ async def update_pietanza(
except PietanzaNotFoundError: except PietanzaNotFoundError:
raise raise
except Exception as e: except Exception as e:
raise DatabaseError(f"Failed to update pietanza: {str(e)}") raise DatabaseError(f"Errore nell'aggiornamento della pietanza: {str(e)}")
@router.delete("/{pietanza_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{pietanza_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_pietanza( async def delete_pietanza(
@ -225,41 +225,36 @@ async def delete_pietanza(
current_user: Dict[str, Any] = Depends(get_current_admin_user), current_user: Dict[str, Any] = Depends(get_current_admin_user),
db: DatabaseManager = Depends(get_database) db: DatabaseManager = Depends(get_database)
): ):
"""Delete pietanza (admin only)""" """Elimina pietanza (solo amministratori)"""
try: try:
# Check if pietanza exists # Verifica se la pietanza è associata a qualche pasto usando operatori JSONB
existing = await db.execute_one("SELECT id FROM pietanze WHERE id = $1", pietanza_id) # Controlla se l'ID della pietanza (come stringa) appare come chiave in qualsiasi portata
if not existing: pasto_check = await db.execute_one("""
raise PietanzaNotFoundError(pietanza_id) SELECT COUNT(*)
FROM pasti
# Delete the pietanza WHERE EXISTS (
result = await db.execute_command("DELETE FROM pietanze WHERE id = $1", pietanza_id) SELECT 1
FROM jsonb_each(portate) AS p
# Check if deletion was successful WHERE jsonb_typeof(p.value) = 'object'
if not result or not result.endswith("1"): AND p.value ? $1
raise DatabaseError("Failed to delete pietanza") )
""", str(pietanza_id))
except PietanzaNotFoundError: if pasto_check and pasto_check[0] > 0:
raise raise HTTPException(
except Exception as e: status_code=status.HTTP_409_CONFLICT,
raise DatabaseError(f"Failed to delete pietanza: {str(e)}") detail="Impossibile eliminare la pietanza: è ancora associata a uno o più pasti"
)
@router.get("/{pietanza_id}/allergeni", response_model=List[str]) # Elimina la pietanza e verifica se esisteva
async def get_pietanza_allergeni( result = await db.execute_one("DELETE FROM pietanze WHERE id = $1 RETURNING id", pietanza_id)
pietanza_id: int,
db: DatabaseManager = Depends(get_database)
):
"""Get allergen information for specific pietanza"""
try:
query = "SELECT allergeni FROM pietanze WHERE id = $1"
row = await db.execute_one(query, pietanza_id)
if not row: if not result:
raise PietanzaNotFoundError(pietanza_id) raise PietanzaNotFoundError(pietanza_id)
return row['allergeni'] or []
except PietanzaNotFoundError: except PietanzaNotFoundError:
raise raise
except HTTPException:
raise
except Exception as e: except Exception as e:
raise DatabaseError(f"Failed to retrieve allergeni: {str(e)}") raise DatabaseError(f"Errore nell'eliminazione della pietanza: {str(e)}")

19
compose.yaml

@ -0,0 +1,19 @@
services:
db:
image: postgres
restart: always
# set shared memory limit when using docker compose
shm_size: 128mb
# or set shared memory limit when deploy via swarm stack
#volumes:
# - type: tmpfs
# target: /dev/shm
# tmpfs:
# size: 134217728 # 128*2^20 bytes = 128Mb
environment:
POSTGRES_PASSWORD: example
ports:
- "5432:5432"
volumes:
- ./db_data:/var/lib/postgresql/data

1
requirements.txt

@ -7,3 +7,4 @@ python-multipart>=0.0.6
pyyaml>=6.0.1 pyyaml>=6.0.1
aiofiles>=23.0.0 aiofiles>=23.0.0
httpx>=0.25.0 httpx>=0.25.0
psycopg2-binary>=2.9.0

10
setup_db.py

@ -21,7 +21,17 @@ def main():
# Connessione al database # Connessione al database
connection = psycopg2.connect(**db_config) connection = psycopg2.connect(**db_config)
print("Connessione al database riuscita.") print("Connessione al database riuscita.")
# Drop del database se esiste
connection.autocommit = True
with connection.cursor() as cursor:
cursor.execute("DROP DATABASE IF EXISTS simple_mensa")
cursor.execute("CREATE DATABASE simple_mensa")
connection.close()
# Riconnessione al nuovo database
db_config["dbname"] = "simple_mensa"
connection = psycopg2.connect(**db_config)
print("Database ricreato e riconnessione effettuata.")
# Esecuzione dello script SQL # Esecuzione dello script SQL
execute_sql_file('schema.sql', connection) execute_sql_file('schema.sql', connection)
print("Script SQL eseguito con successo.") print("Script SQL eseguito con successo.")

Loading…
Cancel
Save