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