feat: Update configuration, enhance authentication, and improve database management with Italian localization
This commit is contained in:
@@ -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.
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
database:
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
name: "postgres"
|
||||
name: "simple_mensa"
|
||||
user: "postgres"
|
||||
password: "example"
|
||||
pool_min_size: 5
|
||||
|
||||
+16
-15
@@ -19,7 +19,7 @@ class AuthManager:
|
||||
self.jwks_cache: Optional[Dict] = None
|
||||
|
||||
async def get_jwks(self) -> Dict:
|
||||
"""Fetch JWKS from provider"""
|
||||
"""Recupera JWKS dal provider"""
|
||||
if self.jwks_cache is None:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
@@ -27,23 +27,23 @@ class AuthManager:
|
||||
response.raise_for_status()
|
||||
self.jwks_cache = response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch JWKS: {e}")
|
||||
logger.error(f"Errore nel recupero JWKS: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Authentication service unavailable"
|
||||
detail="Servizio di autenticazione non disponibile"
|
||||
)
|
||||
return self.jwks_cache
|
||||
|
||||
async def verify_token(self, token: str) -> Dict[str, Any]:
|
||||
"""Verify JWT token and return claims"""
|
||||
"""Verifica token JWT e restituisci i claims"""
|
||||
try:
|
||||
# For development, we'll skip actual JWT verification
|
||||
# In production, implement proper JWKS verification
|
||||
# Per lo sviluppo, saltiamo la verifica JWT effettiva
|
||||
# In produzione, implementare la verifica JWKS appropriata
|
||||
unverified_payload = jwt.get_unverified_claims(token)
|
||||
|
||||
# Extract user information from token
|
||||
# Estrai informazioni utente dal token
|
||||
user_info = {
|
||||
'user_id': unverified_payload.get('sub', 'unknown'),
|
||||
'user_id': unverified_payload.get('sub', 'sconosciuto'),
|
||||
'email': unverified_payload.get('email'),
|
||||
'name': unverified_payload.get('name'),
|
||||
'roles': unverified_payload.get('roles', [])
|
||||
@@ -52,35 +52,36 @@ class AuthManager:
|
||||
return user_info
|
||||
|
||||
except JWTError as e:
|
||||
logger.error(f"JWT verification failed: {e}")
|
||||
logger.error(f"Verifica JWT fallita: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication token",
|
||||
detail="Token di autenticazione non valido",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# Global auth manager
|
||||
# Gestore autenticazione globale
|
||||
auth_manager: Optional[AuthManager] = None
|
||||
|
||||
def initialize_auth(config: Dict[str, Any]):
|
||||
"""Inizializza il gestore di autenticazione"""
|
||||
global auth_manager
|
||||
auth_manager = AuthManager(config)
|
||||
|
||||
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:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Authentication not configured"
|
||||
detail="Autenticazione non configurata"
|
||||
)
|
||||
|
||||
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]:
|
||||
"""Dependency to ensure user has admin role"""
|
||||
"""Dipendenza per assicurarsi che l'utente abbia ruolo amministratore"""
|
||||
if 'admin' not in current_user.get('roles', []):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin privileges required"
|
||||
detail="Privilegi di amministratore richiesti"
|
||||
)
|
||||
return current_user
|
||||
|
||||
+10
-10
@@ -10,7 +10,7 @@ class DatabaseManager:
|
||||
self.pool: Optional[asyncpg.Pool] = None
|
||||
|
||||
async def initialize(self, config: dict):
|
||||
"""Initialize database connection pool"""
|
||||
"""Inizializza il pool di connessioni al database"""
|
||||
try:
|
||||
self.pool = await asyncpg.create_pool(
|
||||
host=config['host'],
|
||||
@@ -23,35 +23,35 @@ class DatabaseManager:
|
||||
max_queries=config.get('pool_max_queries', 50000),
|
||||
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:
|
||||
logger.error(f"Failed to initialize database pool: {e}")
|
||||
logger.error(f"Errore nell'inizializzazione del pool database: {e}")
|
||||
raise
|
||||
|
||||
async def close(self):
|
||||
"""Close database connection pool"""
|
||||
"""Chiudi il pool di connessioni al database"""
|
||||
if self.pool:
|
||||
await self.pool.close()
|
||||
logger.info("Database pool closed")
|
||||
logger.info("Pool di connessioni database chiuso")
|
||||
|
||||
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:
|
||||
return await connection.fetch(query, *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:
|
||||
return await connection.fetchrow(query, *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:
|
||||
return await connection.execute(query, *args)
|
||||
|
||||
# Global database manager instance
|
||||
# Istanza globale del gestore database
|
||||
db_manager = DatabaseManager()
|
||||
|
||||
async def get_database():
|
||||
"""Dependency for getting database connection"""
|
||||
"""Dipendenza per ottenere la connessione al database"""
|
||||
return db_manager
|
||||
|
||||
@@ -4,7 +4,7 @@ class PietanzaNotFoundError(HTTPException):
|
||||
def __init__(self, pietanza_id: int):
|
||||
super().__init__(
|
||||
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):
|
||||
@@ -15,7 +15,7 @@ class ValidationError(HTTPException):
|
||||
)
|
||||
|
||||
class DatabaseError(HTTPException):
|
||||
def __init__(self, detail: str = "Database operation failed"):
|
||||
def __init__(self, detail: str = "Operazione database fallita"):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=detail
|
||||
|
||||
+7
-7
@@ -6,30 +6,30 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def load_config() -> dict:
|
||||
"""Load configuration from YAML file"""
|
||||
"""Carica configurazione dal file YAML"""
|
||||
try:
|
||||
with open('api/config.yaml', 'r') as file:
|
||||
return yaml.safe_load(file)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load configuration: {e}")
|
||||
logger.error(f"Errore nel caricamento della configurazione: {e}")
|
||||
raise
|
||||
|
||||
# Global configuration
|
||||
# Configurazione globale
|
||||
config = load_config()
|
||||
|
||||
def get_config() -> dict:
|
||||
"""Dependency to get application configuration"""
|
||||
"""Dipendenza per ottenere la configurazione dell'applicazione"""
|
||||
return config
|
||||
|
||||
class PaginationParams:
|
||||
def __init__(
|
||||
self,
|
||||
skip: int = Query(0, ge=0, description="Number of items to skip"),
|
||||
limit: int = Query(20, ge=1, le=100, description="Number of items to return")
|
||||
skip: int = Query(0, ge=0, description="Numero di elementi da saltare"),
|
||||
limit: int = Query(20, ge=1, le=100, description="Numero di elementi da restituire")
|
||||
):
|
||||
self.skip = skip
|
||||
self.limit = limit
|
||||
|
||||
def get_pagination_params(params: PaginationParams = Depends()) -> PaginationParams:
|
||||
"""Dependency for pagination parameters"""
|
||||
"""Dipendenza per i parametri di paginazione"""
|
||||
return params
|
||||
|
||||
+38
-41
@@ -1,36 +1,34 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
import asyncio
|
||||
import logging
|
||||
import yaml
|
||||
|
||||
from .core.database import db_manager
|
||||
from .core.auth import initialize_auth
|
||||
from .core.exceptions import PietanzaNotFoundError, ValidationError, DatabaseError
|
||||
from .dependencies import get_config
|
||||
from .routes import pietanze
|
||||
from core.database import db_manager
|
||||
from core.auth import initialize_auth
|
||||
from core.exceptions import PietanzaNotFoundError, ValidationError, DatabaseError
|
||||
from dependencies import get_config
|
||||
from routes import pietanze
|
||||
|
||||
# Configure logging
|
||||
# Configura logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Create FastAPI application
|
||||
# Crea applicazione FastAPI
|
||||
app = FastAPI(
|
||||
title="Simple Mensa API",
|
||||
description="API for mensa booking system",
|
||||
title="API Simple Mensa",
|
||||
description="API per il sistema di prenotazione mensa",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc"
|
||||
)
|
||||
|
||||
# Load configuration
|
||||
# Carica configurazione
|
||||
config = get_config()
|
||||
|
||||
# Configure CORS
|
||||
# Configura CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=config['cors']['allow_origins'],
|
||||
@@ -39,77 +37,77 @@ app.add_middleware(
|
||||
allow_headers=config['cors']['allow_headers'],
|
||||
)
|
||||
|
||||
# Include routers
|
||||
# Includi router
|
||||
app.include_router(pietanze.router, prefix="/api/v1")
|
||||
|
||||
# Exception handlers
|
||||
# Gestori di eccezioni
|
||||
@app.exception_handler(PietanzaNotFoundError)
|
||||
async def pietanza_not_found_handler(request, exc):
|
||||
return JSONResponse(
|
||||
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)
|
||||
async def validation_error_handler(request, exc):
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"error": "Validation error", "detail": exc.detail}
|
||||
content={"errore": "Errore di validazione", "dettaglio": exc.detail}
|
||||
)
|
||||
|
||||
@app.exception_handler(DatabaseError)
|
||||
async def database_error_handler(request, exc):
|
||||
return JSONResponse(
|
||||
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")
|
||||
async def startup_event():
|
||||
"""Initialize application on startup"""
|
||||
"""Inizializza applicazione all'avvio"""
|
||||
try:
|
||||
# Initialize database
|
||||
# Inizializza database
|
||||
await db_manager.initialize(config['database'])
|
||||
logger.info("Database initialized")
|
||||
logger.info("Database inizializzato")
|
||||
|
||||
# Initialize authentication
|
||||
# Inizializza autenticazione
|
||||
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:
|
||||
logger.error(f"Failed to initialize application: {e}")
|
||||
logger.error(f"Errore nell'inizializzazione dell'applicazione: {e}")
|
||||
raise
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
"""Cleanup on application shutdown"""
|
||||
"""Pulizia alla chiusura dell'applicazione"""
|
||||
try:
|
||||
await db_manager.close()
|
||||
logger.info("Application shutdown completed")
|
||||
logger.info("Spegnimento applicazione completato")
|
||||
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")
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
"""Endpoint di controllo stato dell'applicazione"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "simple-mensa-api",
|
||||
"version": "1.0.0"
|
||||
"stato": "funzionante",
|
||||
"servizio": "simple-mensa-api",
|
||||
"versione": "1.0.0"
|
||||
}
|
||||
|
||||
# Root endpoint
|
||||
# Endpoint principale
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Root endpoint with API information"""
|
||||
"""Endpoint principale con informazioni API"""
|
||||
return {
|
||||
"message": "Simple Mensa API",
|
||||
"version": "1.0.0",
|
||||
"docs": "/docs",
|
||||
"health": "/health"
|
||||
"messaggio": "API Simple Mensa",
|
||||
"versione": "1.0.0",
|
||||
"documentazione": "/docs",
|
||||
"stato": "/health"
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -119,5 +117,4 @@ if __name__ == "__main__":
|
||||
host=config['server']['host'],
|
||||
port=config['server']['port'],
|
||||
reload=config['server']['reload'],
|
||||
debug=config['server']['debug']
|
||||
)
|
||||
|
||||
+51
-56
@@ -3,25 +3,25 @@ from typing import List, Optional, Dict, Any
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from ..core.database import get_database, DatabaseManager
|
||||
from ..models.pietanze import PietanzaCreate, PietanzaUpdate, PietanzaResponse
|
||||
from ..models.common import ErrorResponse
|
||||
from ..core.auth import get_current_user, get_current_admin_user
|
||||
from ..core.exceptions import PietanzaNotFoundError, DatabaseError
|
||||
from core.database import get_database, DatabaseManager
|
||||
from models.pietanze import PietanzaCreate, PietanzaUpdate, PietanzaResponse
|
||||
from models.common import ErrorResponse
|
||||
from core.auth import get_current_user, get_current_admin_user
|
||||
from core.exceptions import PietanzaNotFoundError, DatabaseError
|
||||
|
||||
router = APIRouter(prefix="/pietanze", tags=["Pietanze"])
|
||||
|
||||
@router.get("/", response_model=List[PietanzaResponse])
|
||||
async def list_pietanze(
|
||||
skip: int = Query(0, ge=0, description="Number of items to skip"),
|
||||
limit: int = Query(20, ge=1, le=100, description="Number of items to return"),
|
||||
search: Optional[str] = Query(None, description="Search in nome and descrizione"),
|
||||
allergeni: Optional[str] = Query(None, description="Filter by allergens (comma-separated)"),
|
||||
skip: int = Query(0, ge=0, description="Numero di elementi da saltare per la paginazione"),
|
||||
limit: int = Query(20, ge=1, le=100, description="Numero di elementi da restituire"),
|
||||
search: Optional[str] = Query(None, description="Ricerca in nome e descrizione"),
|
||||
allergeni: Optional[str] = Query(None, description="Filtra per allergeni (separati da virgola)"),
|
||||
db: DatabaseManager = Depends(get_database)
|
||||
):
|
||||
"""Get list of available pietanze with optional filtering"""
|
||||
"""Ottieni lista delle pietanze disponibili con filtri opzionali"""
|
||||
try:
|
||||
# Build query with filters
|
||||
# Costruisci query con filtri
|
||||
where_conditions = []
|
||||
params = []
|
||||
param_count = 0
|
||||
@@ -41,12 +41,12 @@ async def list_pietanze(
|
||||
if 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_result = await db.execute_one(count_query, *params)
|
||||
total = count_result[0] if count_result else 0
|
||||
|
||||
# Get items with pagination
|
||||
# Ottieni elementi con paginazione
|
||||
param_count += 1
|
||||
limit_param = param_count
|
||||
param_count += 1
|
||||
@@ -77,14 +77,14 @@ async def list_pietanze(
|
||||
return pietanze
|
||||
|
||||
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)
|
||||
async def get_pietanza(
|
||||
pietanza_id: int,
|
||||
db: DatabaseManager = Depends(get_database)
|
||||
):
|
||||
"""Get specific pietanza by ID"""
|
||||
"""Ottieni pietanza specifica per ID"""
|
||||
try:
|
||||
query = """
|
||||
SELECT id, nome, descrizione, allergeni, created_at, updated_at
|
||||
@@ -108,7 +108,7 @@ async def get_pietanza(
|
||||
except PietanzaNotFoundError:
|
||||
raise
|
||||
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)
|
||||
async def create_pietanza(
|
||||
@@ -116,7 +116,7 @@ async def create_pietanza(
|
||||
current_user: Dict[str, Any] = Depends(get_current_admin_user),
|
||||
db: DatabaseManager = Depends(get_database)
|
||||
):
|
||||
"""Create new pietanza (admin only)"""
|
||||
"""Crea nuova pietanza (solo amministratori)"""
|
||||
try:
|
||||
query = """
|
||||
INSERT INTO pietanze (nome, descrizione, allergeni, created_at, updated_at)
|
||||
@@ -136,7 +136,7 @@ async def create_pietanza(
|
||||
)
|
||||
|
||||
if not row:
|
||||
raise DatabaseError("Failed to create pietanza")
|
||||
raise DatabaseError("Errore nella creazione della pietanza")
|
||||
|
||||
return PietanzaResponse(
|
||||
id=row['id'],
|
||||
@@ -148,7 +148,7 @@ async def create_pietanza(
|
||||
)
|
||||
|
||||
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)
|
||||
async def update_pietanza(
|
||||
@@ -157,14 +157,14 @@ async def update_pietanza(
|
||||
current_user: Dict[str, Any] = Depends(get_current_admin_user),
|
||||
db: DatabaseManager = Depends(get_database)
|
||||
):
|
||||
"""Update existing pietanza (admin only)"""
|
||||
"""Aggiorna pietanza esistente (solo amministratori)"""
|
||||
try:
|
||||
# Check if pietanza exists
|
||||
# Verifica se la pietanza esiste
|
||||
existing = await db.execute_one("SELECT id FROM pietanze WHERE id = $1", pietanza_id)
|
||||
if not existing:
|
||||
raise PietanzaNotFoundError(pietanza_id)
|
||||
|
||||
# Build update query dynamically
|
||||
# Costruisci query di aggiornamento dinamicamente
|
||||
update_fields = []
|
||||
params = []
|
||||
param_count = 0
|
||||
@@ -185,10 +185,10 @@ async def update_pietanza(
|
||||
params.append(json.dumps(pietanza_update.allergeni))
|
||||
|
||||
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)
|
||||
|
||||
# Add updated_at and pietanza_id
|
||||
# Aggiungi updated_at e pietanza_id
|
||||
param_count += 1
|
||||
update_fields.append(f"updated_at = ${param_count}")
|
||||
params.append(datetime.utcnow())
|
||||
@@ -217,7 +217,7 @@ async def update_pietanza(
|
||||
except PietanzaNotFoundError:
|
||||
raise
|
||||
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)
|
||||
async def delete_pietanza(
|
||||
@@ -225,41 +225,36 @@ async def delete_pietanza(
|
||||
current_user: Dict[str, Any] = Depends(get_current_admin_user),
|
||||
db: DatabaseManager = Depends(get_database)
|
||||
):
|
||||
"""Delete pietanza (admin only)"""
|
||||
"""Elimina pietanza (solo amministratori)"""
|
||||
try:
|
||||
# Check if pietanza exists
|
||||
existing = await db.execute_one("SELECT id FROM pietanze WHERE id = $1", pietanza_id)
|
||||
if not existing:
|
||||
# Verifica se la pietanza è associata a qualche pasto usando operatori JSONB
|
||||
# Controlla se l'ID della pietanza (come stringa) appare come chiave in qualsiasi portata
|
||||
pasto_check = await db.execute_one("""
|
||||
SELECT COUNT(*)
|
||||
FROM pasti
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_each(portate) AS p
|
||||
WHERE jsonb_typeof(p.value) = 'object'
|
||||
AND p.value ? $1
|
||||
)
|
||||
""", str(pietanza_id))
|
||||
|
||||
if pasto_check and pasto_check[0] > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Impossibile eliminare la pietanza: è ancora associata a uno o più pasti"
|
||||
)
|
||||
|
||||
# Elimina la pietanza e verifica se esisteva
|
||||
result = await db.execute_one("DELETE FROM pietanze WHERE id = $1 RETURNING id", pietanza_id)
|
||||
|
||||
if not result:
|
||||
raise PietanzaNotFoundError(pietanza_id)
|
||||
|
||||
# Delete the pietanza
|
||||
result = await db.execute_command("DELETE FROM pietanze WHERE id = $1", pietanza_id)
|
||||
|
||||
# Check if deletion was successful
|
||||
if not result or not result.endswith("1"):
|
||||
raise DatabaseError("Failed to delete pietanza")
|
||||
|
||||
except PietanzaNotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Failed to delete pietanza: {str(e)}")
|
||||
|
||||
@router.get("/{pietanza_id}/allergeni", response_model=List[str])
|
||||
async def get_pietanza_allergeni(
|
||||
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:
|
||||
raise PietanzaNotFoundError(pietanza_id)
|
||||
|
||||
return row['allergeni'] or []
|
||||
|
||||
except PietanzaNotFoundError:
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Failed to retrieve allergeni: {str(e)}")
|
||||
raise DatabaseError(f"Errore nell'eliminazione della pietanza: {str(e)}")
|
||||
|
||||
@@ -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
|
||||
@@ -7,3 +7,4 @@ python-multipart>=0.0.6
|
||||
pyyaml>=6.0.1
|
||||
aiofiles>=23.0.0
|
||||
httpx>=0.25.0
|
||||
psycopg2-binary>=2.9.0
|
||||
+11
-1
@@ -21,7 +21,17 @@ def main():
|
||||
# Connessione al database
|
||||
connection = psycopg2.connect(**db_config)
|
||||
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
|
||||
execute_sql_file('schema.sql', connection)
|
||||
print("Script SQL eseguito con successo.")
|
||||
|
||||
Reference in New Issue
Block a user