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. 107
      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:
host: "localhost"
port: 5432
name: "postgres"
name: "simple_mensa"
user: "postgres"
password: "example"
pool_min_size: 5

31
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

20
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

4
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

14
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

79
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']
)

107
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)}")

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
aiofiles>=23.0.0
httpx>=0.25.0
psycopg2-binary>=2.9.0

10
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.")

Loading…
Cancel
Save