PROGETTO FINALE • CLASSE TERZA

📚 Sistema di Gestione Biblioteca

Un'applicazione desktop completa che integra Python, NumPy, Matplotlib e Tkinter

Scenario Reale: Sei stato assunto dalla Biblioteca Comunale per digitalizzare il sistema di gestione libri e prestiti. Dovrai creare un'applicazione professionale che semplifichi tutte le operazioni quotidiane.

⚠️ ATTENZIONE: Ambiente di Esecuzione

Questo progetto finale DEVE essere eseguito sul tuo computer con:

NON funziona su Google Colab perché usa Tkinter per l'interfaccia grafica.

🎯 Panoramica del Progetto

📊 Database Libri

Gestione completa del catalogo (CRUD)

👥 Sistema Utenti

Registrazione e gestione utenti

🔄 Gestione Prestiti

Prestito, restituzione, multe

📈 Report Statistici

Grafici e analisi con Matplotlib

🔍 Ricerca Avanzata

Filtri multipli e ricerca testo

💾 Import/Export

CSV, JSON, backup automatici

🎯 Obiettivo Didattico: Questo progetto riunisce TUTTI i concetti imparati durante l'anno:
  • Python Base: Logica, strutture dati, file I/O
  • NumPy: Calcoli statistici, array operations
  • Matplotlib: Visualizzazione dati professionali
  • Tkinter: Interfaccia grafica complessa
  • Programmazione a Oggetti: Classi, ereditarietà, modularità

🏗️ Architettura del Sistema

GUI
Tkinter
Controller
Logica Applicazione
Database
JSON/SQLite
Report
Matplotlib
Analisi
NumPy/Pandas
File I/O
CSV/JSON Export

📖 Pattern Architetturale MVC (Model-View-Controller):

Il progetto segue il pattern MVC per una buona organizzazione del codice:

Componente Tecnologia Responsabilità
Model Python Classes + JSON Gestione dati (libri, utenti, prestiti)
View Tkinter GUI Interfaccia utente e visualizzazione
Controller Python Logic Coordinazione tra Model e View

🚀 Percorso di Sviluppo - 6 Milestone

1

Milestone 1: Struttura Base e Database

Creazione classi fondamentali e sistema di persistenza dati JSON

2

Milestone 2: GUI Principale

Interfaccia principale con menu, toolbar e area di lavoro

3

Milestone 3: Gestione Libri (CRUD)

Aggiunta, modifica, eliminazione e ricerca libri

4

Milestone 4: Sistema Utenti e Prestiti

Registrazione utenti e gestione prestiti con calcolo multe

5

Milestone 5: Report Statistici

Dashboard con grafici Matplotlib e analisi NumPy

6

Milestone 6: Funzionalità Avanzate

Import/Export, backup, ricerca avanzata, help system

🧱 Milestone 1: Struttura Base e Database

1 Struttura del Progetto

🚫 COPIA BLOCCATA
""" BIBLIOTECA_MANAGER/ ├── main.py # Punto di ingresso dell'applicazione ├── requirements.txt # Dipendenze del progetto ├── data/ # Cartella dati │ ├── biblioteca.db # Database SQLite (opzionale) │ └── backup/ # Backup automatici ├── src/ # Codice sorgente │ ├── __init__.py │ ├── models/ # Classi Model (dati) │ │ ├── __init__.py │ │ ├── libro.py │ │ ├── utente.py │ │ └── prestito.py │ ├── database/ # Gestione database │ │ ├── __init__.py │ │ └── manager.py │ ├── gui/ # Interfaccia grafica │ │ ├── __init__.py │ │ ├── main_window.py │ │ ├── dialogs.py │ │ └── widgets.py │ └── utils/ # Utility functions │ ├── __init__.py │ ├── validators.py │ └── helpers.py └── tests/ # Test unitari ├── __init__.py └── test_models.py

📖 Organizzazione Professionale:

Questa struttura segue le best practices dello sviluppo software:

  • Separazione delle responsabilità: Ogni cartella ha un compito specifico
  • Modularità: Ogni modulo può essere testato e modificato indipendentemente
  • Scalabilità: Facile aggiungere nuove funzionalità
  • Manutenibilità: Codice organizzato e documentato

2 Classi Model Fondamentali

🚫 COPIA BLOCCATA
# src/models/libro.py import json from datetime import datetime from dataclasses import dataclass, asdict from typing import Optional @dataclass class Libro: """ Classe che rappresenta un libro nella biblioteca. Usa dataclasses per generare automaticamente metodi speciali. """ # Attributi obbligatori isbn: str # Codice identificativo univoco titolo: str # Titolo del libro autore: str # Autore/i # Attributi opzionali con valori di default editore: str = "" anno_pubblicazione: int = 0 genere: str = "" pagine: int = 0 copie_totali: int = 1 # Numero totale di copie possedute copie_disponibili: int = 1 # Copie attualmente disponibili posizione: str = "" # Posizione nello scaffale (es: "A23") lingua: str = "Italiano" prezzo: float = 0.0 data_acquisto: str = "" # Data acquisto in formato DD/MM/YYYY note: str = "" # Attributi calcolati (property) @property def disponibile(self) -> bool: """Restituisce True se ci sono copie disponibili per il prestito""" return self.copie_disponibili > 0 @property def percentuale_disponibile(self) -> float: """Calcola la percentuale di copie disponibili""" if self.copie_totali == 0: return 0.0 return (self.copie_disponibili / self.copie_totali) * 100 # Metodi di classe (factory methods) @classmethod def from_dict(cls, data: dict) -> 'Libro': """Crea un'istanza di Libro da un dizionario""" return cls(**data) @classmethod def from_json(cls, json_string: str) -> 'Libro': """Crea un'istanza di Libro da una stringa JSON""" data = json.loads(json_string) return cls.from_dict(data) # Metodi di istanza def to_dict(self) -> dict: """Converte l'oggetto in dizionario""" return asdict(self) def to_json(self) -> str: """Converte l'oggetto in stringa JSON""" return json.dumps(self.to_dict(), indent=2, ensure_ascii=False) def prestito_possibile(self) -> tuple[bool, str]: """ Verifica se il prestito è possibile. Restituisce (successo, messaggio) """ if not self.disponibile: return False, f"Nessuna copia disponibile di '{self.titolo}'" if self.copie_disponibili == 0: return False, f"Tutte le copie di '{self.titolo}' sono in prestito" return True, f"Prestito possibile per '{self.titolo}'" def registra_prestito(self) -> bool: """Registra un prestito (decrementa copie disponibili)""" if self.copie_disponibili > 0: self.copie_disponibili -= 1 return True return False def registra_restituzione(self) -> bool: """Registra una restituzione (incrementa copie disponibili)""" if self.copie_disponibili < self.copie_totali: self.copie_disponibili += 1 return True return False def aggiorna_copie(self, nuove_copie_totali: int) -> bool: """ Aggiorna il numero totale di copie. Mantiene la consistenza con le copie disponibili. """ if nuove_copie_totali < 0: return False differenza = nuove_copie_totali - self.copie_totali # Aggiorna totali self.copie_totali = nuove_copie_totali # Aggiorna disponibili (se abbiamo aggiunto copie) if differenza > 0: self.copie_disponibili += differenza # Se abbiamo ridotto le copie, assicurati che disponibili non siano maggiori di totali elif differenza < 0: self.copie_disponibili = max(0, self.copie_disponibili + differenza) return True def __str__(self) -> str: """Rappresentazione stringa leggibile del libro""" disponibile_str = "✅" if self.disponibile else "❌" return (f"{disponibile_str} {self.titolo} - {self.autore} " f"(ISBN: {self.isbn}) - Disponibili: {self.copie_disponibili}/{self.copie_totali}")

📖 Concetti Avanzati Introdotti:

@dataclass: Decoratore Python 3.7+ che genera automaticamente:

  • __init__(): Costruttore con tutti i parametri
  • __repr__(): Rappresentazione per debugging
  • __eq__(): Confronto tra oggetti
  • Vantaggio: Meno codice boilerplate

@property: Decoratore per creare attributi calcolati.

  • Sembra un attributo normale ma esegue calcoli
  • Esempio: libro.disponibile calcola se ci sono copie
  • Non può essere modificato direttamente

@classmethod: Metodi che operano sulla classe, non sull'istanza.

  • Ricevono cls invece di self
  • Usati come factory methods (from_dict, from_json)

Type Hints: Annotazioni di tipo per migliore documentazione e supporto IDE.

3 Database Manager con JSON

🚫 COPIA BLOCCATA
# src/database/manager.py import json import os from datetime import datetime from typing import List, Dict, Optional, Any from pathlib import Path import shutil from ..models.libro import Libro from ..models.utente import Utente from ..models.prestito import Prestito class DatabaseManager: """ Gestisce la persistenza dei dati usando file JSON. Simula un database relazionale con operazioni CRUD. """ def __init__(self, data_dir: str = "data"): """ Inizializza il database manager. Args: data_dir: Directory dove salvare i dati """ self.data_dir = Path(data_dir) self.data_dir.mkdir(exist_ok=True) # File per ogni entità self.libri_file = self.data_dir / "libri.json" self.utenti_file = self.data_dir / "utenti.json" self.prestiti_file = self.data_dir / "prestiti.json" # Cache in memoria per performance self._libri: Dict[str, Libro] = {} self._utenti: Dict[str, Utente] = {} self._prestiti: Dict[str, Prestito] = {} # Statistiche di utilizzo self.stats = { "caricamenti": 0, "salvataggi": 0, "backup_creati": 0, "ultimo_backup": None } # Carica dati esistenti self.carica_tutti_dati() # ============ METODI PUBBLICI PER LIBRI ============ def aggiungi_libro(self, libro: Libro) -> bool: """ Aggiunge un nuovo libro al database. Args: libro: Istanza di Libro da aggiungere Returns: True se successo, False se ISBN già esiste """ if libro.isbn in self._libri: return False self._libri[libro.isbn] = libro self.salva_libri() return True def ottieni_libro(self, isbn: str) -> Optional[Libro]: """Restituisce un libro per ISBN, None se non trovato""" return self._libri.get(isbn) def aggiorna_libro(self, isbn: str, libro_aggiornato: Libro) -> bool: """ Aggiorna un libro esistente. Args: isbn: ISBN del libro da aggiornare libro_aggiornato: Nuova versione del libro Returns: True se successo, False se libro non trovato """ if isbn not in self._libri: return False self._libri[isbn] = libro_aggiornato self.salva_libri() return True def elimina_libro(self, isbn: str) -> bool: """ Elimina un libro dal database. Args: isbn: ISBN del libro da eliminare Returns: True se successo, False se libro non trovato """ if isbn not in self._libri: return False # Verifica che non ci siano prestiti attivi per questo libro prestiti_attivi = [ p for p in self._prestiti.values() if p.isbn_libro == isbn and not p.restituito ] if prestiti_attivi: return False # Non si può eliminare un libro con prestiti attivi del self._libri[isbn] self.salva_libri() return True def cerca_libri(self, **criteri) -> List[Libro]: """ Cerca libri in base a criteri multipli. Esempi: cerca_libri(titolo="Python", autore="Rossi") cerca_libri(genere="Informatica", disponibile=True) Returns: Lista di libri che soddisfano i criteri """ risultati = [] for libro in self._libri.values(): corrisponde = True for chiave, valore in criteri.items(): if chiave == "disponibile": # Criterio speciale per disponibilità if valore and not libro.disponibile: corrisponde = False break elif not valore and libro.disponibile: corrisponde = False break elif hasattr(libro, chiave): attr_valore = getattr(libro, chiave) # Ricerca case-insensitive per stringhe if isinstance(valore, str) and isinstance(attr_valore, str): if valore.lower() not in attr_valore.lower(): corrisponde = False break elif attr_valore != valore: corrisponde = False break else: corrisponde = False break if corrisponde: risultati.append(libro) return risultati def tutti_libri(self) -> List[Libro]: """Restituisce tutti i libri ordinati per titolo""" return sorted(self._libri.values(), key=lambda l: l.titolo.lower()) def statistiche_libri(self) -> Dict[str, Any]: """Calcola statistiche sui libri""" if not self._libri: return {} totali = len(self._libri) disponibili = sum(1 for l in self._libri.values() if l.disponibile) # Conta per genere generi = {} for libro in self._libri.values(): genere = libro.genere if libro.genere else "Sconosciuto" generi[genere] = generi.get(genere, 0) + 1 # Autori più frequenti autori = {} for libro in self._libri.values(): autori[libro.autore] = autori.get(libro.autore, 0) + 1 return { "totali": totali, "disponibili": disponibili, "non_disponibili": totali - disponibili, "percentuale_disponibili": (disponibili / totali * 100) if totali > 0 else 0, "generi": generi, "autori_piu_frequenti": dict(sorted(autori.items(), key=lambda x: x[1], reverse=True)[:10]), "copie_totali": sum(l.copie_totali for l in self._libri.values()), "copie_disponibili": sum(l.copie_disponibili for l in self._libri.values()) } # ============ METODI PUBBLICI PER UTENTI ============ # (Simili a quelli per libri, implementati per esercizio) def aggiungi_utente(self, utente: Utente) -> bool: """Aggiunge un nuovo utente""" if utente.id_utente in self._utenti: return False self._utenti[utente.id_utente] = utente self.salva_utenti() return True def ottieni_utente(self, id_utente: str) -> Optional[Utente]: """Restituisce un utente per ID""" return self._utenti.get(id_utente) # ============ METODI PUBBLICI PER PRESTITI ============ def registra_prestito(self, id_utente: str, isbn: str) -> Optional[Prestito]: """ Registra un nuovo prestito. Args: id_utente: ID dell'utente isbn: ISBN del libro Returns: Oggetto Prestito se successo, None altrimenti """ # Verifica esistenza utente e libro utente = self.ottieni_utente(id_utente) libro = self.ottieni_libro(isbn) if not utente or not libro: return None # Verifica disponibilità libro if not libro.disponibile: return None # Verifica che l'utente non abbia prestiti scaduti prestiti_scaduti = [ p for p in self._prestiti.values() if p.id_utente == id_utente and p.scaduto and not p.restituito ] if prestiti_scaduti: return None # Crea nuovo prestito prestito = Prestito( id_utente=id_utente, isbn_libro=isbn, data_prestito=datetime.now().strftime("%d/%m/%Y") ) # Registra nel database self._prestiti[prestito.id_prestito] = prestito # Aggiorna copie disponibili del libro libro.registra_prestito() # Salva cambiamenti self.salva_prestiti() self.salva_libri() return prestito # ============ METODI PRIVATI PER PERSISTENZA ============ def carica_tutti_dati(self): """Carica tutti i dati dai file JSON""" self._carica_libri() self._carica_utenti() self._carica_prestiti() self.stats["caricamenti"] += 1 def _carica_libri(self): """Carica i libri dal file JSON""" if not self.libri_file.exists(): self._libri = {} return try: with open(self.libri_file, 'r', encoding='utf-8') as f: dati = json.load(f) self._libri = { isbn: Libro.from_dict(libro_data) for isbn, libro_data in dati.items() } except (json.JSONDecodeError, FileNotFoundError): self._libri = {} def _carica_utenti(self): """Carica gli utenti dal file JSON""" if not self.utenti_file.exists(): self._utenti = {} return try: with open(self.utenti_file, 'r', encoding='utf-8') as f: dati = json.load(f) self._utenti = { id_utente: Utente.from_dict(utente_data) for id_utente, utente_data in dati.items() } except (json.JSONDecodeError, FileNotFoundError): self._utenti = {} def _carica_prestiti(self): """Carica i prestiti dal file JSON""" if not self.prestiti_file.exists(): self._prestiti = {} return try: with open(self.prestiti_file, 'r', encoding='utf-8') as f: dati = json.load(f) self._prestiti = { id_prestito: Prestito.from_dict(prestito_data) for id_prestito, prestito_data in dati.items() } except (json.JSONDecodeError, FileNotFoundError): self._prestiti = {} def salva_libri(self): """Salva i libri su file JSON""" try: dati = { isbn: libro.to_dict() for isbn, libro in self._libri.items() } with open(self.libri_file, 'w', encoding='utf-8') as f: json.dump(dati, f, indent=2, ensure_ascii=False) self.stats["salvataggi"] += 1 return True except Exception as e: print(f"Errore salvataggio libri: {e}") return False def salva_utenti(self): """Salva gli utenti su file JSON""" try: dati = { id_utente: utente.to_dict() for id_utente, utente in self._utenti.items() } with open(self.utenti_file, 'w', encoding='utf-8') as f: json.dump(dati, f, indent=2, ensure_ascii=False) self.stats["salvataggi"] += 1 return True except Exception as e: print(f"Errore salvataggio utenti: {e}") return False def salva_prestiti(self): """Salva i prestiti su file JSON""" try: dati = { id_prestito: prestito.to_dict() for id_prestito, prestito in self._prestiti.values() } with open(self.prestiti_file, 'w', encoding='utf-8') as f: json.dump(dati, f, indent=2, ensure_ascii=False) self.stats["salvataggi"] += 1 return True except Exception as e: print(f"Errore salvataggio prestiti: {e}") return False def backup_dati(self) -> bool: """ Crea un backup dei dati con timestamp. Returns: True se successo, False altrimenti """ try: backup_dir = self.data_dir / "backup" backup_dir.mkdir(exist_ok=True) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup_path = backup_dir / f"backup_{timestamp}" backup_path.mkdir(exist_ok=True) # Copia tutti i file di dati for file_path in [self.libri_file, self.utenti_file, self.prestiti_file]: if file_path.exists(): shutil.copy2(file_path, backup_path / file_path.name) # Salva anche le statistiche stats_file = backup_path / "stats.json" with open(stats_file, 'w', encoding='utf-8') as f: json.dump(self.stats, f, indent=2) self.stats["backup_creati"] += 1 self.stats["ultimo_backup"] = timestamp return True except Exception as e: print(f"Errore backup: {e}") return False def esporta_csv(self, entita: str) -> Optional[str]: """ Esporta i dati in formato CSV. Args: entita: "libri", "utenti" o "prestiti" Returns: Percorso del file CSV creato, None se errore """ import csv if entita == "libri": dati = self._libri.values() nome_file = "libri_export.csv" elif entita == "utenti": dati = self._utenti.values() nome_file = "utenti_export.csv" elif entita == "prestiti": dati = self._prestiti.values() nome_file = "prestiti_export.csv" else: return None if not dati: return None file_path = self.data_dir / nome_file try: with open(file_path, 'w', newline='', encoding='utf-8') as f: # Prendi le chiavi dal primo elemento come intestazioni primo_elemento = next(iter(dati)) fieldnames = list(primo_elemento.to_dict().keys()) writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() for elemento in dati: writer.writerow(elemento.to_dict()) return str(file_path) except Exception as e: print(f"Errore esportazione CSV: {e}") return None

📖 Design Pattern e Concetti Avanzati:

Singleton Pattern (implicito):

  • Una sola istanza di DatabaseManager nell'applicazione
  • Garantisce coerenza dei dati
  • Centralizza tutte le operazioni di persistenza

CRUD Operations:

Operazione Metodo SQL Equivalente
Create aggiungi_libro() INSERT
Read ottieni_libro() SELECT
Update aggiorna_libro() UPDATE
Delete elimina_libro() DELETE

Error Handling Robusto:

  • try-except per operazioni I/O
  • Validazione input prima di operazioni
  • Restituzione di None o False invece di eccezioni

Path Management con pathlib:

  • Path() invece di stringhe per percorsi
  • .exists(), .mkdir() più intuitivi
  • Compatibile con tutti i sistemi operativi

🎨 Milestone 2: Interfaccia Grafica Principale

1 Finestra Principale con Menu

🚫 COPIA BLOCCATA
# src/gui/main_window.py import tkinter as tk from tkinter import ttk, messagebox, font from typing import Optional, Callable import webbrowser from ..database.manager import DatabaseManager class MainWindow: """ Finestra principale dell'applicazione di gestione biblioteca. Implementa un'interfaccia moderna con menu, toolbar e area di lavoro. """ def __init__(self, db_manager: DatabaseManager): """ Inizializza la finestra principale. Args: db_manager: Istanza del DatabaseManager """ self.db = db_manager # Creazione finestra principale self.root = tk.Tk() self.root.title("Biblioteca Manager Pro v1.0") self.root.geometry("1200x700") self.root.minsize(1000, 600) # Icona dell'applicazione (se disponibile) try: self.root.iconbitmap("icon.ico") except: pass # Stili personalizzati self.setup_styles() # Variabili di stato self.current_view = "libri" # Vista attuale: libri, utenti, prestiti self.search_term = tk.StringVar() # Costruzione interfaccia self.setup_ui() # Carica dati iniziali self.refresh_current_view() def setup_styles(self): """Configura stili moderni per l'applicazione""" style = ttk.Style() # Usa tema moderno style.theme_use('clam') # Stili personalizzati style.configure('Title.TLabel', font=('Arial', 16, 'bold'), foreground='#2c3e50') style.configure('Header.TLabel', font=('Arial', 12, 'bold'), foreground='#34495e') style.configure('Success.TButton', font=('Arial', 10, 'bold'), background='#27ae60', foreground='white') style.configure('Primary.TButton', font=('Arial', 10, 'bold'), background='#3498db', foreground='white') style.configure('Danger.TButton', font=('Arial', 10, 'bold'), background='#e74c3c', foreground='white') style.configure('Accent.TButton', font=('Arial', 10, 'bold'), background='#9b59b6', foreground='white') style.configure('Modern.TFrame', background='#ecf0f1') # Configura Treeview style.configure('Treeview', font=('Arial', 10), rowheight=25) style.configure('Treeview.Heading', font=('Arial', 11, 'bold'), background='#34495e', foreground='white') style.map('Treeview.Heading', background=[('active', '#2c3e50')]) def setup_ui(self): """Costruisce l'interfaccia utente completa""" # Configura grid principale self.root.grid_rowconfigure(1, weight=1) self.root.grid_columnconfigure(0, weight=1) # 1. BARRA DEL MENU self.setup_menu_bar() # 2. BARRA DEGLI STRUMENTI (TOOLBAR) self.setup_toolbar() # 3. BARRA DI STATO (STATUSBAR) self.setup_statusbar() # 4. AREA DI LAVORO PRINCIPALE self.setup_main_area() # 5. BARRA LATERALE self.setup_sidebar() def setup_menu_bar(self): """Crea la barra del menu principale""" menubar = tk.Menu(self.root) self.root.config(menu=menubar) # Menu File file_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="File", menu=file_menu) file_menu.add_command(label="Nuovo Libro", accelerator="Ctrl+N", command=self.nuovo_libro) file_menu.add_command(label="Nuovo Utente", command=self.nuovo_utente) file_menu.add_separator() file_menu.add_command(label="Importa CSV...", command=self.importa_csv) file_menu.add_command(label="Esporta CSV...", command=self.esporta_csv) file_menu.add_separator() file_menu.add_command(label="Backup Dati", command=self.backup_dati) file_menu.add_separator() file_menu.add_command(label="Esci", accelerator="Ctrl+Q", command=self.root.quit) # Menu Modifica edit_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="Modifica", menu=edit_menu) edit_menu.add_command(label="Copia", accelerator="Ctrl+C", command=self.copia) edit_menu.add_command(label="Incolla", accelerator="Ctrl+V", command=self.incolla) edit_menu.add_separator() edit_menu.add_command(label="Preferenze...", command=self.apri_preferenze) # Menu Visualizza view_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="Visualizza", menu=view_menu) view_menu.add_command(label="Libri", command=lambda: self.cambia_vista("libri")) view_menu.add_command(label="Utenti", command=lambda: self.cambia_vista("utenti")) view_menu.add_command(label="Prestiti", command=lambda: self.cambia_vista("prestiti")) view_menu.add_separator() view_menu.add_command(label="Statistiche", command=self.mostra_statistiche) view_menu.add_command(label="Dashboard", command=self.mostra_dashboard) # Menu Aiuto help_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="Aiuto", menu=help_menu) help_menu.add_command(label="Guida", command=self.apri_guida) help_menu.add_command(label="Tutorial", command=self.apri_tutorial) help_menu.add_separator() help_menu.add_command(label="Informazioni", command=self.mostra_info) # Bind tastiera self.root.bind("", lambda e: self.nuovo_libro()) self.root.bind("", lambda e: self.root.quit()) def setup_toolbar(self): """Crea la barra degli strumenti""" toolbar = ttk.Frame(self.root, relief="raised", borderwidth=1) toolbar.grid(row=0, column=0, sticky="ew", padx=2, pady=2) # Bottoni toolbar ttk.Button(toolbar, text="📚 Libri", command=lambda: self.cambia_vista("libri"), style='Primary.TButton').pack(side="left", padx=2, pady=2) ttk.Button(toolbar, text="👥 Utenti", command=lambda: self.cambia_vista("utenti"), style='Primary.TButton').pack(side="left", padx=2, pady=2) ttk.Button(toolbar, text="🔄 Prestiti", command=lambda: self.cambia_vista("prestiti"), style='Primary.TButton').pack(side="left", padx=2, pady=2) ttk.Button(toolbar, text="➕ Nuovo", command=self.nuovo_elemento, style='Success.TButton').pack(side="left", padx=2, pady=2) ttk.Button(toolbar, text="✏️ Modifica", command=self.modifica_elemento, style='Accent.TButton').pack(side="left", padx=2, pady=2) ttk.Button(toolbar, text="🗑️ Elimina", command=self.elimina_elemento, style='Danger.TButton').pack(side="left", padx=2, pady=2) ttk.Button(toolbar, text="📊 Report", command=self.genera_report, style='Accent.TButton').pack(side="left", padx=2, pady=2) # Separatore ttk.Separator(toolbar, orient="vertical").pack(side="left", fill="y", padx=5) # Barra di ricerca ttk.Label(toolbar, text="🔍 Cerca:").pack(side="left", padx=(10, 2)) search_entry = ttk.Entry(toolbar, textvariable=self.search_term, width=30) search_entry.pack(side="left", padx=2, pady=2) ttk.Button(toolbar, text="Cerca", command=self.cerca).pack(side="left", padx=2, pady=2) ttk.Button(toolbar, text="Pulisci", command=self.pulisci_ricerca).pack(side="left", padx=2, pady=2) # Bind ricerca con Enter search_entry.bind("", lambda e: self.cerca()) def setup_statusbar(self): """Crea la barra di stato""" self.statusbar = ttk.Frame(self.root, relief="sunken") self.statusbar.grid(row=2, column=0, sticky="ew", padx=2, pady=(0, 2)) # Contatori self.status_libri = ttk.Label(self.statusbar, text="Libri: 0") self.status_libri.pack(side="left", padx=10) self.status_utenti = ttk.Label(self.statusbar, text="Utenti: 0") self.status_utenti.pack(side="left", padx=10) self.status_prestiti = ttk.Label(self.statusbar, text="Prestiti attivi: 0") self.status_prestiti.pack(side="left", padx=10) # Separatore ttk.Separator(self.statusbar, orient="vertical").pack(side="left", fill="y", padx=5) # Messaggio di stato self.status_message = ttk.Label(self.statusbar, text="Pronto") self.status_message.pack(side="left", padx=10) # Versione ttk.Label(self.statusbar, text="v1.0").pack(side="right", padx=10) # Aggiorna contatori self.update_status_counters() def setup_main_area(self): """Crea l'area di lavoro principale""" # Notebook per schede multiple self.notebook = ttk.Notebook(self.root) self.notebook.grid(row=1, column=0, sticky="nsew", padx=2, pady=2) # Scheda Libri self.frame_libri = ttk.Frame(self.notebook) self.notebook.add(self.frame_libri, text="📚 Libri") self.setup_libri_view() # Scheda Utenti self.frame_utenti = ttk.Frame(self.notebook) self.notebook.add(self.frame_utenti, text="👥 Utenti") self.setup_utenti_view() # Scheda Prestiti self.frame_prestiti = ttk.Frame(self.notebook) self.notebook.add(self.frame_prestiti, text="🔄 Prestiti") self.setup_prestiti_view() # Scheda Dashboard self.frame_dashboard = ttk.Frame(self.notebook) self.notebook.add(self.frame_dashboard, text="📊 Dashboard") self.setup_dashboard_view() def setup_libri_view(self): """Configura la vista libri""" # Frame principale libri self.frame_libri.grid_rowconfigure(0, weight=1) self.frame_libri.grid_columnconfigure(0, weight=1) # Treeview per lista libri columns = ("ISBN", "Titolo", "Autore", "Genere", "Disponibili", "Totali", "Posizione") self.tree_libri = ttk.Treeview(self.frame_libri, columns=columns, show="headings", height=20) # Configura intestazioni for col in columns: self.tree_libri.heading(col, text=col) self.tree_libri.column(col, width=100, minwidth=50) # Imposta larghezze specifiche self.tree_libri.column("ISBN", width=120) self.tree_libri.column("Titolo", width=250) self.tree_libri.column("Autore", width=150) # Scrollbar scrollbar = ttk.Scrollbar(self.frame_libri, orient="vertical", command=self.tree_libri.yview) self.tree_libri.configure(yscrollcommand=scrollbar.set) # Posiziona self.tree_libri.grid(row=0, column=0, sticky="nsew") scrollbar.grid(row=0, column=1, sticky="ns") # Frame per dettagli libro self.frame_dettagli_libro = ttk.LabelFrame(self.frame_libri, text="Dettagli Libro", padding=10) self.frame_dettagli_libro.grid(row=0, column=2, sticky="nsew", padx=(10, 0)) # Dettagli libro (saranno popolati dinamicamente) self.dettagli_text = tk.Text(self.frame_dettagli_libro, height=20, width=40, state="disabled") self.dettagli_text.pack(fill="both", expand=True) # Bind selezione self.tree_libri.bind('<>', self.on_libro_selected) self.tree_libri.bind('', lambda e: self.modifica_libro()) def setup_utenti_view(self): """Configura la vista utenti (simile a libri)""" self.frame_utenti.grid_rowconfigure(0, weight=1) self.frame_utenti.grid_columnconfigure(0, weight=1) columns = ("ID", "Nome", "Cognome", "Email", "Telefono", "Tipo", "Iscrizione") self.tree_utenti = ttk.Treeview(self.frame_utenti, columns=columns, show="headings", height=20) for col in columns: self.tree_utenti.heading(col, text=col) self.tree_utenti.column(col, width=100) scrollbar = ttk.Scrollbar(self.frame_utenti, orient="vertical", command=self.tree_utenti.yview) self.tree_utenti.configure(yscrollcommand=scrollbar.set) self.tree_utenti.grid(row=0, column=0, sticky="nsew") scrollbar.grid(row=0, column=1, sticky="ns") self.tree_utenti.bind('<>', self.on_utente_selected) def setup_prestiti_view(self): """Configura la vista prestiti""" # Creazione frame con due subframe paned = ttk.PanedWindow(self.frame_prestiti, orient="horizontal") paned.grid(row=0, column=0, sticky="nsew") self.frame_prestiti.grid_rowconfigure(0, weight=1) self.frame_prestiti.grid_columnconfigure(0, weight=1) # Frame prestiti attivi frame_attivi = ttk.LabelFrame(paned, text="Prestiti Attivi", padding=10) paned.add(frame_attivi, weight=1) columns_attivi = ("ID", "Utente", "Libro", "Data Prestito", "Scadenza", "Giorni Rimasti") self.tree_prestiti_attivi = ttk.Treeview(frame_attivi, columns=columns_attivi, show="headings", height=15) for col in columns_attivi: self.tree_prestiti_attivi.heading(col, text=col) self.tree_prestiti_attivi.column(col, width=100) scrollbar_attivi = ttk.Scrollbar(frame_attivi, orient="vertical", command=self.tree_prestiti_attivi.yview) self.tree_prestiti_attivi.configure(yscrollcommand=scrollbar_attivi.set) self.tree_prestiti_attivi.pack(side="left", fill="both", expand=True) scrollbar_attivi.pack(side="right", fill="y") # Frame prestiti conclusi frame_conclusi = ttk.LabelFrame(paned, text="Prestiti Conclusi", padding=10) paned.add(frame_conclusi, weight=1) columns_conclusi = ("ID", "Utente", "Libro", "Data Prestito", "Data Restituzione", "Stato") self.tree_prestiti_conclusi = ttk.Treeview(frame_conclusi, columns=columns_conclusi, show="headings", height=15) for col in columns_conclusi: self.tree_prestiti_conclusi.heading(col, text=col) self.tree_prestiti_conclusi.column(col, width=100) scrollbar_conclusi = ttk.Scrollbar(frame_conclusi, orient="vertical", command=self.tree_prestiti_conclusi.yview) self.tree_prestiti_conclusi.configure(yscrollcommand=scrollbar_conclusi.set) self.tree_prestiti_conclusi.pack(side="left", fill="both", expand=True) scrollbar_conclusi.pack(side="right", fill="y") def setup_dashboard_view(self): """Configura la vista dashboard con statistiche""" # Questo sarà implementato nella Milestone 5 con Matplotlib label = ttk.Label(self.frame_dashboard, text="📊 Dashboard in costruzione...\n\n" "Questa sezione conterrà grafici e statistiche avanzate.", font=("Arial", 14), justify="center") label.pack(expand=True) def setup_sidebar(self): """Crea la barra laterale (opzionale)""" # Implementazione facoltativa per esercizio pass # ============ METODI DI BUSINESS LOGIC ============ def refresh_current_view(self): """Aggiorna la vista corrente""" if self.current_view == "libri": self.refresh_libri_view() elif self.current_view == "utenti": self.refresh_utenti_view() elif self.current_view == "prestiti": self.refresh_prestiti_view() self.update_status_counters() def refresh_libri_view(self): """Aggiorna la lista libri nel Treeview""" # Pulisci treeview for item in self.tree_libri.get_children(): self.tree_libri.delete(item) # Ottieni tutti i libri libri = self.db.tutti_libri() # Aggiungi al treeview for libro in libri: disponibile_str = "✅" if libro.disponibile else "❌" self.tree_libri.insert("", "end", values=( libro.isbn, libro.titolo, libro.autore, libro.genere, f"{libro.copie_disponibili}/{libro.copie_totali}", libro.copie_totali, libro.posizione )) def refresh_utenti_view(self): """Aggiorna la lista utenti""" for item in self.tree_utenti.get_children(): self.tree_utenti.delete(item) # Implementazione simile a refresh_libri_view # Per esercizio def refresh_prestiti_view(self): """Aggiorna le liste prestiti""" # Implementazione per esercizio def update_status_counters(self): """Aggiorna i contatori nella statusbar""" libri_count = len(self.db.tutti_libri()) # utenti_count = ... (implementare) # prestiti_attivi_count = ... (implementare) self.status_libri.config(text=f"Libri: {libri_count}") # self.status_utenti.config(text=f"Utenti: {utenti_count}") # self.status_prestiti.config(text=f"Prestiti attivi: {prestiti_attivi_count}") def on_libro_selected(self, event): """Gestisce la selezione di un libro""" selection = self.tree_libri.selection() if not selection: return # Ottieni ISBN dalla riga selezionata item = self.tree_libri.item(selection[0]) isbn = item['values'][0] # Recupera libro dal database libro = self.db.ottieni_libro(isbn) if not libro: return # Mostra dettagli self.mostra_dettagli_libro(libro) def mostra_dettagli_libro(self, libro): """Mostra i dettagli di un libro nel pannello laterale""" self.dettagli_text.config(state="normal") self.dettagli_text.delete(1.0, tk.END) dettagli = f""" {'='*40} DETTAGLI LIBRO {'='*40} 📚 TITOLO: {libro.titolo} 👤 AUTORE: {libro.autore} 🏷️ ISBN: {libro.isbn} 🏢 EDITORE: {libro.editore} 📅 ANNO: {libro.anno_pubblicazione} 🏷️ GENERE: {libro.genere} 📏 PAGINE: {libro.pagine} 📍 POSIZIONE: {libro.posizione} 🌐 LINGUA: {libro.lingua} 💰 PREZZO: €{libro.prezzo:.2f} {'='*40} DISPONIBILITÀ {'='*40} 📊 Copie totali: {libro.copie_totali} ✅ Copie disponibili: {libro.copie_disponibili} 📈 Percentuale disponibile: {libro.percentuale_disponibile:.1f}% {'='*40} STATO: {"✅ DISPONIBILE" if libro.disponibile else "❌ NON DISPONIBILE"} {'='*40} 📝 NOTE: {libro.note if libro.note else "Nessuna nota"} """ self.dettagli_text.insert(1.0, dettagli) self.dettagli_text.config(state="disabled") # ============ METODI DI AZIONE ============ def nuovo_libro(self): """Apre la finestra per aggiungere un nuovo libro""" from .dialogs import LibroDialog dialog = LibroDialog(self.root, self.db) self.root.wait_window(dialog.top) if dialog.result: self.refresh_libri_view() self.update_status_counters() self.set_status_message("Libro aggiunto con successo!") def modifica_libro(self): """Apre la finestra per modificare il libro selezionato""" selection = self.tree_libri.selection() if not selection: messagebox.showwarning("Attenzione", "Seleziona un libro da modificare") return item = self.tree_libri.item(selection[0]) isbn = item['values'][0] libro = self.db.ottieni_libro(isbn) if not libro: return from .dialogs import LibroDialog dialog = LibroDialog(self.root, self.db, libro=libro) self.root.wait_window(dialog.top) if dialog.result: self.refresh_libri_view() self.set_status_message("Libro modificato con successo!") def elimina_libro(self): """Elimina il libro selezionato dopo conferma""" selection = self.tree_libri.selection() if not selection: messagebox.showwarning("Attenzione", "Seleziona un libro da eliminare") return item = self.tree_libri.item(selection[0]) isbn = item['values'][0] titolo = item['values'][1] # Conferma if not messagebox.askyesno("Conferma eliminazione", f"Sei sicuro di voler eliminare il libro:\n\n" f"'{titolo}'\n\n" f"Questa azione non può essere annullata."): return # Elimina success = self.db.elimina_libro(isbn) if success: self.refresh_libri_view() self.update_status_counters() self.set_status_message(f"Libro '{titolo}' eliminato con successo!") else: messagebox.showerror("Errore", "Impossibile eliminare il libro.\n" "Potrebbe avere prestiti attivi.") def nuovo_utente(self): """Apre finestra per nuovo utente""" from .dialogs import UtenteDialog dialog = UtenteDialog(self.root, self.db) self.root.wait_window(dialog.top) if dialog.result: self.refresh_utenti_view() self.set_status_message("Utente aggiunto con successo!") def cambia_vista(self, vista: str): """Cambia la vista corrente""" self.current_view = vista # Seleziona la scheda corrispondente nel notebook if vista == "libri": self.notebook.select(self.frame_libri) elif vista == "utenti": self.notebook.select(self.frame_utenti) elif vista == "prestiti": self.notebook.select(self.frame_prestiti) elif vista == "dashboard": self.notebook.select(self.frame_dashboard) self.refresh_current_view() def cerca(self): """Esegue la ricerca in base al termine inserito""" term = self.search_term.get().strip() if not term: self.refresh_current_view() return # Implementazione ricerca per esercizio self.set_status_message(f"Risultati ricerca per: '{term}'") def pulisci_ricerca(self): """Pulisce il campo di ricerca""" self.search_term.set("") self.refresh_current_view() self.set_status_message("Ricerca pulita") def nuovo_elemento(self): """Crea un nuovo elemento in base alla vista corrente""" if self.current_view == "libri": self.nuovo_libro() elif self.current_view == "utenti": self.nuovo_utente() elif self.current_view == "prestiti": # Nuovo prestito pass def modifica_elemento(self): """Modifica l'elemento selezionato""" if self.current_view == "libri": self.modifica_libro() elif self.current_view == "utenti": # Modifica utente pass elif self.current_view == "prestiti": # Modifica prestito pass def elimina_elemento(self): """Elimina l'elemento selezionato""" if self.current_view == "libri": self.elimina_libro() elif self.current_view == "utenti": # Elimina utente pass elif self.current_view == "prestiti": # Elimina prestito pass def genera_report(self): """Genera un report PDF/CSV""" # Implementazione per esercizio self.set_status_message("Generazione report in corso...") def mostra_statistiche(self): """Mostra le statistiche generali""" stats = self.db.statistiche_libri() if not stats: messagebox.showinfo("Statistiche", "Nessun dato disponibile") return message = f""" 📊 STATISTICHE BIBLIOTECA {'='*30} 📚 Totale libri: {stats.get('totali', 0)} ✅ Disponibili: {stats.get('disponibili', 0)} ❌ In prestito: {stats.get('non_disponibili', 0)} 📈 Percentuale disponibili: {stats.get('percentuale_disponibili', 0):.1f}% 📖 Copie totali: {stats.get('copie_totali', 0)} 📗 Copie disponibili: {stats.get('copie_disponibili', 0)} 🏷️ Generi principali: """ for genere, count in list(stats.get('generi', {}).items())[:5]: message += f" • {genere}: {count} libri\n" messagebox.showinfo("Statistiche Biblioteca", message) def mostra_dashboard(self): """Mostra la dashboard con grafici""" self.cambia_vista("dashboard") # Qui si integrerà Matplotlib nella Milestone 5 def backup_dati(self): """Esegue il backup dei dati""" success = self.db.backup_dati() if success: self.set_status_message("Backup completato con successo!") messagebox.showinfo("Backup", "Backup dei dati completato con successo.") else: messagebox.showerror("Errore", "Impossibile completare il backup.") def importa_csv(self): """Importa dati da CSV""" # Implementazione per esercizio pass def esporta_csv(self): """Esporta dati in CSV""" if self.current_view == "libri": path = self.db.esporta_csv("libri") if path: self.set_status_message(f"Dati esportati in: {path}") messagebox.showinfo("Esportazione", f"Libri esportati in:\n{path}") else: messagebox.showerror("Errore", "Impossibile esportare i dati.") # Implementa per altre viste def apri_preferenze(self): """Apre la finestra delle preferenze""" # Implementazione per esercizio pass def apri_guida(self): """Apre la guida in linea""" webbrowser.open("https://github.com/biblioteca-manager/guida") def apri_tutorial(self): """Apre il tutorial""" messagebox.showinfo("Tutorial", "Tutorial in costruzione...\n\n" "Usa i menu e i pulsanti per esplorare l'applicazione.") def mostra_info(self): """Mostra informazioni sull'applicazione""" info = f""" 📚 BIBLIOTECA MANAGER PRO Versione 1.0 Un sistema completo per la gestione di biblioteche e librerie personali. Sviluppato con: • Python 3.8+ • Tkinter per l'interfaccia grafica • JSON per la persistenza dati • Matplotlib per le visualizzazioni Autore: Classe Terza - Indirizzo Informatica Anno Scolastico: 2024-2025 Licenza: Educativa - Solo per scopi didattici """ messagebox.showinfo("Informazioni", info) def copia(self): """Copia il testo selezionato""" # Implementazione base try: self.root.clipboard_clear() self.root.clipboard_append("Copia non implementata") except: pass def incolla(self): """Incolla dalla clipboard""" # Implementazione base pass def set_status_message(self, message: str): """Imposta il messaggio nella statusbar""" self.status_message.config(text=message) def run(self): """Avvia l'applicazione""" self.set_status_message("Applicazione avviata - Pronto") self.root.mainloop()

📖 Architettura GUI Avanzata:

Pattern MVC Completo:

Componente Classe Responsabilità
Model Libro, Utente, Prestito Rappresentazione dati e business logic
View MainWindow, Dialogs Interfaccia utente e presentazione
Controller DatabaseManager Coordinazione tra Model e View

Widget Tkinter Avanzati:

  • ttk.Notebook: Schede multiple (tabs)
  • ttk.PanedWindow: Aree divisibili
  • ttk.Treeview: Tabelle avanzate con colonne multiple
  • tk.Menu: Menu contestuali e barra menu
  • ttk.Style: Personalizzazione completa dell'aspetto

Event-Driven Programming:

  • bind(): Gestione eventi tastiera e mouse
  • command=: Callback per bottoni e menu
  • <>: Eventi specifici widget

🚀 Milestone 3-6: Completamento Progetto

📋 Entregabili da Implementare

Milestone 3: Gestione Libri Completa

  • Finestre di dialogo per aggiunta/modifica libri
  • Validazione input (ISBN, anno, ecc.)
  • Ricerca avanzata con filtri multipli
  • Importazione dati da Excel/CSV

Milestone 4: Sistema Utenti e Prestiti

  • Classi Utente e Prestito
  • Calcolo automatico multe per ritardi
  • Storico prestiti per utente
  • Notifiche prestiti in scadenza

Milestone 5: Dashboard con Matplotlib

  • Grafici a barre: libri per genere
  • Grafici a torta: distribuzione prestiti
  • Time series: prestiti per mese
  • Heatmap: orari di maggiore affluenza

Milestone 6: Funzionalità Avanzate

  • Sistema di backup automatico
  • Report PDF generati automaticamente
  • Ricerca full-text nei contenuti
  • API REST per accesso remoto (opzionale)

🎯 Suggerimenti per lo Sviluppo:

  1. Testa ogni milestone separatamente prima di passare alla successiva
  2. Usa Git per versionare il codice e gestire le modifiche
  3. Documenta il codice con docstring e commenti
  4. Crea test unitari per le funzioni critiche
  5. Chiedi feedback ai compagni e insegnanti

📊 Criteri di Valutazione

Criterio Punteggio Descrizione
Funzionalità Complete 0-25 punti Tutte le milestone implementate correttamente
Qualità del Codice 0-20 punti Organizzazione, commenti, nomi variabili, struttura
Interfaccia Utente 0-15 punti Usabilità, design, coerenza, responsività
Gestione Errori 0-10 punti Validazione input, messaggi di errore, robustezza
Integrazione Tecnologie 0-15 punti Python + NumPy + Matplotlib + Tkinter integrati
Documentazione 0-10 punti Commenti, docstring, manuale utente, relazione
Creatività/Extra 0-5 punti Funzionalità extra, design innovativo, soluzioni creative
TOTALE 100 punti Voto finale
💡 Scala di Valutazione:
  • 90-100 punti: Eccellente - Progetto completo e ben documentato
  • 80-89 punti: Ottimo - Progetto funzionante con qualche miglioramento possibile
  • 70-79 punti: Buono - Progetto funzionale ma con limitazioni
  • 60-69 punti: Sufficiente - Progetto base completato
  • < 60 punti: Insufficiente - Progetto incompleto o non funzionante

🛠️ Risorse e Supporto

📚 Documentazione

  • Python Official Docs
  • Tkinter Tutorial
  • Matplotlib Examples
  • NumPy User Guide

💻 Strumenti

  • PyCharm o VS Code
  • Git per version control
  • Python 3.8+
  • Librerie: tkinter, numpy, matplotlib

🎯 Best Practices

  • Commit frequenti su Git
  • Test incrementali
  • Backup regolari
  • Documentazione in itinere

❓ Supporto

  • Forum della classe
  • Office hours insegnante
  • Stack Overflow
  • Documentazione progetti simili

🚀 Consegna del Progetto

1 Cosa Consegnare

  • Codice sorgente completo (cartella compressa .zip)
  • Relazione tecnica (PDF, 3-5 pagine)
  • Manuale utente (come usare l'applicazione)
  • Video dimostrativo (opzionale, 2-3 minuti)
  • Repository Git (link se pubblicato online)

2 Scadenze

  • Milestone 1-2: 2 settimane dall'assegnazione
  • Milestone 3-4: 4 settimane dall'assegnazione
  • Milestone 5-6: 6 settimane dall'assegnazione
  • Consegna finale: 8 settimane dall'assegnazione

3 Presentazione

Preparati a presentare il tuo progetto alla classe:

  • Dimostrazione live dell'applicazione (10 minuti)
  • Spiegazione architettura e scelte progettuali
  • Discussione difficoltà incontrate e soluzioni
  • Possibili miglioramenti futuri

🎉 Congratulazioni!

Hai completato il percorso della Classe Terza in Intelligenza Artificiale

Questo progetto finale rappresenta il culmine di tutto ciò che hai imparato durante l'anno:

🐍 Python Mastery

OOP, strutture dati, file I/O, eccezioni

📊 Data Science Base

NumPy, analisi dati, calcoli statistici

🎨 Data Visualization

Matplotlib, grafici professionali

🖥️ Desktop Development

Tkinter, GUI design, applicazioni reali

Ora sei pronto per la Classe Quarta! Nel prossimo anno approfondirai:

  • Pandas per data analysis avanzata
  • Machine Learning con Scikit-learn
  • Regressioni e modelli predittivi
  • Progetti di Data Science reali
← Torna alla Homepage Vai alla Classe Quarta →