From a833ce93f8cc58c4a77cbc61c33a2aba435309b3 Mon Sep 17 00:00:00 2001 From: Matteo Benedetto Date: Thu, 5 Jun 2025 19:27:15 +0200 Subject: [PATCH] feat: Update configuration, enhance authentication, and improve database management with Italian localization --- admin-webgui/README.md | 366 +++++++++++++++++++++++++++++++++++++++++ api/config.yaml | 2 +- api/core/auth.py | 31 ++-- api/core/database.py | 20 +-- api/core/exceptions.py | 4 +- api/dependencies.py | 14 +- api/main.py | 79 +++++---- api/routes/pietanze.py | 107 ++++++------ compose.yaml | 19 +++ requirements.txt | 1 + setup_db.py | 12 +- 11 files changed, 522 insertions(+), 133 deletions(-) create mode 100644 admin-webgui/README.md create mode 100644 compose.yaml diff --git a/admin-webgui/README.md b/admin-webgui/README.md new file mode 100644 index 0000000..2b144db --- /dev/null +++ b/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. diff --git a/api/config.yaml b/api/config.yaml index 84fe10a..f62e30e 100644 --- a/api/config.yaml +++ b/api/config.yaml @@ -4,7 +4,7 @@ database: host: "localhost" port: 5432 - name: "postgres" + name: "simple_mensa" user: "postgres" password: "example" pool_min_size: 5 diff --git a/api/core/auth.py b/api/core/auth.py index 2fb7d2c..3868afc 100644 --- a/api/core/auth.py +++ b/api/core/auth.py @@ -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 diff --git a/api/core/database.py b/api/core/database.py index 5f8f49b..a2dfebe 100644 --- a/api/core/database.py +++ b/api/core/database.py @@ -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 diff --git a/api/core/exceptions.py b/api/core/exceptions.py index ada5f8a..b72ffd0 100644 --- a/api/core/exceptions.py +++ b/api/core/exceptions.py @@ -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 diff --git a/api/dependencies.py b/api/dependencies.py index d7aa6dc..ff09232 100644 --- a/api/dependencies.py +++ b/api/dependencies.py @@ -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 diff --git a/api/main.py b/api/main.py index c3a111c..5cf2f30 100644 --- a/api/main.py +++ b/api/main.py @@ -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'] ) diff --git a/api/routes/pietanze.py b/api/routes/pietanze.py index 18af4fd..46c6448 100644 --- a/api/routes/pietanze.py +++ b/api/routes/pietanze.py @@ -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)}") diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..13f35d2 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index eb87062..9ec38ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/setup_db.py b/setup_db.py index 9671c8c..28c24a0 100644 --- a/setup_db.py +++ b/setup_db.py @@ -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.")