Un corso Django
di Riccardo Magliocchetti
Introduzione
Questo corso è pensato per programmatori Python con esperienza di sviluppo di applicazioni web, anche in altri linguaggi, che vogliono impratichirsi con il framework Django.
Si assume una conoscenza di base di Python.
Il codice presente in questo materiale assume una versione di Python 3.6 o superiore.
Tutti gli esempi vengono fatti per un sistema operativo UNIX-like ed è previsto l'uso della shell.
Questa opera è rilasciata con una licenza Creative Commons Attribution-NonCommercial-ShareAlike 4.0 Internazionale.
Il sorgente di questa opera è disponibile su GitHub ed è possibile riportare eventuali errori aprendo una issue.
Requisiti
Python 3 e git sono requisiti necessari per questo corso, di seguito le istruzioni per l'installazione su alcuni sistemi operativi.
MacOS
Il modo più comodo per installare una versione recente di Python 3 e git su MacOS è usare Homebrew. Una volta installato si possono installare con il comando:
brew install git python
Debian / Ubuntu o derivati
Tutte le ultime versioni stabili di Debian ed Ubuntu hanno una versione di Python 3 e git abbastanza recente. Possiamo quindi installare le versioni di sistema con il seguente comando:
sudo apt install git python3 python3-venv python3-pip python3-distutils
Cos'è Django
Django è un framework per applicazioni Web scritto in Python, mantenuto dalla Django Software Foundation e dalla comunità. Viene rilasciato con licenza BSD a 3 clausole.
Il sito del progetto è https://djangoproject.com. La documentazione si trova all'indirizzo https://docs.djangoproject.com.
Django è un framework full stack e con batterie incluse, comprende al suo interno componenti che vanno dall'ORM, alla gestione delle migrazioni del db fino al sistema di rendering dei templates.
Django implementa il pattern Model-View-Controller (MVC) usando però una terminologia diversa Model-Template-View (MTV) dove i modelli si occupano della persistenza in database, i template della visualizzazione dei dati e le viste fanno da congiunzione tra gli altri due.
Cominciamo
In questo capitolo vedremo come installare Django e come creare il nostro primo progetto Django. Una volta creato il progetto guarderemo come è fatto e prenderemo confidenza con i comandi di amministrazione da linea di comando presenti in Django. Concluderemo il capitolo dando uno sguardo all'interfaccia di amministrazione di Django.
Installazione
Per prima cosa creiamo una directory per contenere il nostro progetto:
mkdir corso-django-installazione
cd corso-django-installazione
Una volta entrati nella nostra directory possiamo creare un ambiente virtuale dove installeremo Django. Gli ambienti virtuali servono a creare degli ambienti isolati per evitare di creare conflitti con il sistema o con altre applicazioni.
Creiamo l'ambiente virtuale con il comando:
python3 -m venv venv
Abbiamo chiamato il nostro ambiente venv
come convenzione, ma ovviamente possiamo usare un nome
diverso.
Una volta creato il nostro ambiente, lo attiviamo:
source ./venv/bin/activate
In Windows con PowerShell per attivare l'ambiente bisogna eseguire il file venv\Scripts\Activate.ps1 Per dettagli consultare la documentazione del modulo venv.
source
è una funzionalità della shell per importare un file e viene usato per settare delle variabili
d'ambiente. Queste variabili di ambiente istruiscono Python di usare la directory venv
come
sua directory di lavoro.
D'ora in poi tutti i comandi assumono che l'ambiente virtuale sia attivato.
Gli ambienti virtuali possono essere disattivati con:
deactivate
Quindi, con l'ambiente virtuale attivo, procediamo con l'installazione:
pip install Django wheel
Con questo comando abbiamo installato l'ultima versione disponibile di Django, al momento della scrittura 3.2.2. Abbiamo anche installato wheel, una pacchetto Python che aggiunge il supporto all'installazione di pacchetti con binari pre-compilati.
Django rilascia una nuova versione ogni 8 mesi, ogni versione viene mantenuta per circa un anno. Alcune versioni sono designate come long-term support (LTS) e mantenute per circa 3 anni. Quale versione usare dipende dal tipo di progetto. Django 3.2 è una versione LTS.
Molto bene, abbiamo installato Django!
Setup del progetto Django
Ora che abbiamo installato Django nel nostro ambiente virtuale ci ritroveremo alcuni
nuovi comandi disponibili, tra cui django-admin
, tramite il quale creeremo un nuovo progetto:
django-admin startproject nuovoprogetto
Ottimo, abbiamo creato il nostro progetto! Ora nella nostra directory corrente dovremmo avere due
directory, quella di progetto nuovoprogetto
e quella del virtualenv venv
.
Anatomia di un progetto Django
Nella nostra directory corrente dovremmo avere due directory, quella di progetto nuovoprogetto
e
quella del virtualenv venv
.
Se andiamo dentro quella di progetto e digitiamo il comando tree
dovremmo vedere qualcosa del genere:
.
├── manage.py
└── nuovoprogetto
├── asgi.py
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
Questi sono i file che compongono il nostro nuovo progetto Django, una cosa che salta all'occhio è
il fatto che il nome del progetto viene replicato in una directory. Questo può creare un po' di
confusione all'inizio ("due directory con lo stesso nome!") ma la directory nuovoprogetto
interna
serve a contenere tutti i file di progetto; riusare lo stesso nome che abbiamo dato al progetto è una
buona idea per evitare conflitti.
manage.py
manage.py
sarà la porta d'ingresso per chiamare ogni comando di Django. È possibile chiamarlo sia
direttamente con ./manage.py
che come script Python con python3 manage.py
. Non c'è alcuna
differenza. Una cosa importante da ricordare però è di chiamarlo sempre da questa directory.
Se lo chiamiamo vediamo tutti i comandi, raggruppati per le applicazioni già disponibili. Le
applicazioni nel mondo Django sono una sottoparte in cui possiamo dividere il nostro progetto,
possono essere locali oppure riusabili e distribuite come librerie Python installabili tramite pip
.
I file del progetto
I file di progetto sono quelli contenuti nella directory che si chiama come il nostro progetto, in
questo caso nuovoprogetto
.
Il file urls.py
contiene le rotte per il routing delle richieste alla nostra applicazione.
Il file __init__.py
ha la funzione standard di Python di far diventare questa directory un modulo.
Il file settings.py
contiene tutte le configurazioni del nostro progetto.
Il file wsgi.py
contiene l'entrypoint per poter caricare la nostra applicazione in un application
server che supporta lo standard WSGI. WSGI è lo standard Python più usato dalle applicazioni
web Python.
Il file asgi.py
contiene l'entrypoint per poter caricare la nostra applicazione in un application
server che supporta il protocollo ASGI. ASGI è un nuovo protocollo di interfacciamento per
applicazioni web Python asincrone sviluppato per Django e adottato anche da altri framework di
nuova generazione. Il supporto di Django per una esecuzione completamente asincrona è ancora incompleto
perciò non sarà trattato.
settings.py
Il file settings.py
contiene tutte le configurazioni del nostro progetto. Se lo apriamo possiamo notare
come tutte le configurazioni siano espresse come variabili in maiuscolo, questa non è solo una
convenzione ma è necessario perchè queste configurazioni siano leggibili; il modulo settings infatti
non si legge direttamente dalla nostra applicazione ma solo attraverso una interfaccia:
from django.conf import settings
print(settings.DEBUG)
Tra le configurazioni troviamo SECRET_KEY
che è una stringa generata randomicamente al setup di
ogni progetto. Questa variable viene usata per firmare crittograficamente perciò deve rimanere segreta e
non deve essere riusata in progetti diversi. Noi svilupperemo solo un progetto di test perciò possiamo
non preoccuparcene.
Un'altra configurazione importante è DEBUG
, quando abilitata fa restituire a Django più informazioni
in caso di errore utili per il debug come le configurazioni ed una stacktrace. Queste informazioni
però non devono essere esposte in produzione.
Esercizi
- Nell'intestazione del file
settings.py
trovi i riferimenti alla documentazione ufficiale e alla reference delle configurazioni. Leggi la documentazione di ognuna delle opzioni che trovi nel file.
Partiamo!
Ora che abbiamo un ambiente con Django installato e creato il nostro nuovo progetto dobbiamo solo farlo partire.
Il motto di Django, tradotto in italiano è "Il framework web per perfezionisti con delle scadenze" non per caso. Fedele al suo motto ogni nuovo progetto Django infatti ha per default già delle applicazioni attive.
Alcuni esempi di applicazioni già incluse sono l'autenticazione, la gestione delle sessioni e una interfaccia di inserimento di contenuti chiamata admin. Queste applicazioni possono aver bisogno di persistenza e quindi di creare tabelle in un database SQL. La creazione e la modifica delle tabelle nel database viene fatta tramite un sistema di migrazioni.
Applichiamo le migrazioni richieste dal nostro progetto Django con il comando:
python3 manage.py migrate
che riporterà qualcosa di simile a:
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
Una volta applicate le migrazioni nella nostra directory sarà presente un nuovo file db.sqlite3, come suggerisce il nome è un database SQLite. SQLite viene usato come database di default di Django perché non richiede un server, il database viene salvato in un singolo file ed è supportato direttamente da Python.
Ora che abbiamo preparato il database non ci resta che far partire il nostro progetto tramite il web
server di sviluppo runserver
:
python3 manage.py runserver
Il server di sviluppo si riavvia ogni qual volta modifichiamo dei file del nostro progetto Django, per questo motivo se ci sono errori nel nostro codice potrebbe fermarsi. Nel caso serva riavviarlo è possibile fermarlo tramite CONTROL-C e quindi richiamare nuovamente il comando.
Ora puntate il browser sull'indirizzo http://127.0.0.1:8000/ e si parte.
Congratulazioni l'installazione ha avuto successo.
Il primo utente
Come abbiamo visto nel capitolo precedente Django di default comprende una applicazione per gestire
gli utenti. Possiamo rendercene conto guardando la lista delle migrazioni e scorgendo quelle di
auth
.
Django include una gestione degli utenti basilare, non include infatti già il supporto alla registrazione. Per cominciare quindi dobbiamo creare un superutente manualmente tramite il comando createsuperuser
:
python3 manage.py createsuperuser
Inseriamo i dati richiesti, l'email non è obbligatoria:
Username (leave blank to use 'rm'):
Email address:
Password:
Password (again):
Superuser created successfully.
Non possiamo inserire qualsiasi password, ci sono dei requisiti minimi di sicurezza, potreste aver visto o meno qualcuno dei seguenti errori:
This password is too short. It must contain at least 8 characters.
This password is too common.
This password is entirely numeric.
L'admin
Ora che abbiamo il nostro utente possiamo dare un'occhiata ad un'altra applicazione installata di
default in Django, l'interfaccia di amministrazione comunemente chiamata admin
.
Puntiamo il nostro browser su http://127.0.0.1:8000/admin/, verremo rediretti su una vista di login tramite la quale possiamo usare le credenziali dell'utente appena creato.
Una volta loggati ci ritroviamo nell'interfaccia di amministrazione dove tra gli altri troveremo i seguenti componenti:
- nell'intestazione troviamo un link
CHANGE PASSWORD
per avere un form di cambio password - sempre nell'intestazione troviamo un link
LOG OUT
per chiudere la nostra sessione - sulla destra in un blocco
Recent actions
troviamo le ultime azioni fatte dal nostro utente tramite l'interfaccia di amministrazione, tutte le operazioni fatte da qui sono registrate.
Nella parte principale invece vediamo tutte le applicazioni ed i modelli delle stesse. In questo caso
l'unica applicazione presente è auth
che registra i modelli Groups
e Users
rispettivamente il
modello per gestire gruppi di utenti e quello per gestire i singoli utenti.
Se clicchiamo su Users
andiamo alla pagina che lista tutti gli utenti.
L'indirizzo della pagina è http://127.0.0.1:8000/admin/auth/user/
e possiamo notare come nell'url ci
sia auth
che è il nome dell'applicazione e user
che è il nome del modello.
Nella parte sinistra dello schermo troviamo il riepilogo di tutti i modelli dell'applicazione e una scorciatoia per aggiungere una nuova istanza di ogni tipo di modello.
Nella parte centrale abbiamo la ricerca testuale sulle varie istanze dei modelli, quindi la lista di tutti i modelli. Tra la ricerca e la lista delle istanze abbiamo la sezione delle azioni che possiamo svolgere sulle istanze selezionate.
Nella parte destra dello schermo invece abbiamo un bottone per aggiungere un nuovo Utente e la parte dei filtri, tramite i quali possiamo filtrare le nostre istanze.
Se clicchiamo nello username dell'utente che abbiamo creato andiamo nel form di modifica dello stesso, non entriamo nei dettagli degli attributi degli utenti, facciamo solo caso ai bottoni al fondo della pagina che ci permettono di cancellare o modificare il nostro utente. Se vediamo degli avvertimenti sulle date non preoccupiamoci.
Tutte queste funzionalità sono rese disponibili nell'interfaccia di amministrazione ai nostri modelli in modo dichiarativo senza doverle programmare.
La prima applicazione
In questo capitolo creeremo una applicazione nel nostro progetto per costruire una homepage. Per fare questo scriveremo la nostra prima vista che farà il rendering di un template che andremo a collegare al sistema di routing delle url di Django.
Anatomia di una applicazione Django
Nel mondo Django le applicazioni sono i componenti con cui costruiamo il nostro progetto e che
forniscono al progetto le sue funzionalità. L'interfaccia di amministrazione che abbiamo visto
precedentemente è fornita da una applicazione chiamata admin
.
Per creare una applicazione Django dobbiamo usare il comando startapp
. Il comando richiede il nome
dell'applicazione come parametro, dal momento che vogliamo creare una homepage useremo:
python3 manage.py startapp homepage
Una volta dato il comando nella nostra directory apparirà una nuova directory chiamata homepage. La directory dell'applicazione sarà così composta:
homepage
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│ ├── __init__.py
├── models.py
├── tests.py
└── views.py
Per default vengono creati i seguenti file:
- admin.py, dove andremo a registrare i modelli per esser visualizzati nell'admin
- apps.py, serve a Django per registrare l'applicazione
- migrations, per contenere i file delle migrazioni
- models.py, per contenere i modelli
- tests.py, per contenere i test del nostro codice
- views.py, per contenere le viste
Creiamo una vista
Ora che abbiamo creato la nostra applicazione homepage cominciamo con l'implementazione di una vista.
Come abbiamo visto precedentemente le viste sono contenute nel file views.py e se lo apriamo con il nostro editor ci ritroveremo con qualcosa del genere:
from django.shortcuts import render
# Create your views here.
Sostituiamo tutto il contenuto del file con il seguente:
from django.http import HttpResponse
from django.views import View
class HomepageView(View):
def get(self, request):
return HttpResponse("This is the homepage")
Abbiamo creato una vista chiamata Homepage
implementata a classi, in inglese class-based view (CBV).
L'implementazione è abbastanza semplice: estendiamo la classe View
ed implementiamo il metodo get
che ci permette di definire un handler per richieste HTTP con metodo GET. A queste richieste
rispondiamo con This is the homepage
. Non specifichiamo lo status code, perché il default è 200.
Django permette di scrivere le viste anche come funzioni, anche qui non c'è un modo migliore per farlo, dipende dalla necessità. In questo libro usiamo sempre le viste a classi perché per la nostra esperienza permettono di riusare più codice e attenersi alle interfacce già previste da Django tende a far scrivere più omogeneo.
Fatta la vista dobbiamo collegarla al routing delle URL. Il routing delle URL del progetto è definito
nel file nuovoprogetto/urls.py
.
Aperto con l'editor, esclusa la documentazione all'inizio, dovrebbe essere così:
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]
Aggiungiamo nel routing la nostra vista Homepage
modificando il file in questo modo:
from django.contrib import admin
from django.urls import path
from homepage.views import HomepageView
urlpatterns = [
path('admin/', admin.site.urls),
path('', HomepageView.as_view()),
]
Il sistema di routing si aspetta una callable perciò dobbiamo chiamare il metodo as_view()
della
nostra classe.
Rifacciamo partire il runserver
se lo abbiamo spento con il comando:
python3 manage.py runserver
Se puntiamo il browser su http://127.0.0.1:8000/ dovremmo vedere la nostra homepage.
Esercizi
- Come possiamo restituire uno status code diverso da 200? Guarda nella documentazione ufficiale di HttpResponse come sia possibile farlo.
Un template per la vista
Possiamo migliorare la nostra homepage usando un template. Un template non è altro che un file di testo che viene renderizzato tramite un motore di templating. Dentro ad un template possiamo usare dei costrutti speciali usando un linguaggio specifico. Django usa per default un suo motore specifico di rendering dei template.
Django cerca automaticamente i template delle applicazioni dentro una directory chiamata templates
,
dentro a questa directory è buona norma prefissare i percorsi con il nome della applicazione.
Andiamo quindi a creare la directory che conterrà il nostro template:
mkdir -p homepage/templates/homepage
Ed andiamo a creare il file homepage/templates/homepage/index.html
con questo contenuto:
<html>
<head>
<title>Homepage</title>
</head>
<body>
<h1>{{ welcome_message }}</h1>
</body>
</html>
{{ welcome_message }}
è la sintassi usata dal sistema di templating di Django per stampare il valore
di una variabile chiamata welcome_message
.
Una comodità dell'usare viste basate su classi è quella di poter riusare delle viste specializzate
già pronte. In questo caso possiamo usare la vista generica TemplateView
. Andiamo quindi ad
aggiornare il file views.py
in questo modo:
from django.views.generic import TemplateView
class HomepageView(TemplateView):
template_name = 'homepage/index.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['welcome_message'] = 'Welcome to this page!'
return context
Ci sono un paio di cose su cui soffermarsi:
- Il fatto di riusare una vista generica rende esplicito il tipo di vista e rende anche molto breve il nostro codice. Rende anche le cose piuttosto fumose ad una prima occhiata.
get_context_data
è il metodo che dobbiamo estendere per poter aggiungere il messaggio di benvenuto alle variabili che vengono passate al template durante il suo rendering.
Ora puntiamo il browser su http://127.0.0.1:8000.
Qualcosa non sta funzionando e ci viene mostrata una pagina di errore.
Django non riesce a trovare il template homepage/index.html
nonostate noi l'abbiamo creato.
Soffermiamoci un secondo su questa pagina:
- Nella parte gialla vediamo il riassunto dell'errore con l'eccezione e le informazioni di base sulla istanza di Django
- A seguire ci sono delle informazioni specifiche sul sistema di templating, vengono elencati tutti i percorsi provati da Django
- Ancora troviamo la traceback Python dell'errore, la stessa che troviamo nella shell dove stiamo
facendo girare
runserver
- Quindi le informazioni sulla richiesta HTTP
- Infine tutti i valori presenti nel file
settings.py
La pagina di errore che vedete è governata dal flag DEBUG
nel file settings.py
, nei sistemi di
produzione deve essere sempre disabilitata perché non vogliamo esporre queste informazioni sensibili
pubblicamente.
Chiusa la parentesi sulla pagina di debug correggiamo il nostro errore. Come detto prima Django cerca
automaticamente i templates nella directory templates
di ogni applicazione; lo fa però solo per le
applicazioni che sono state listate in INSTALLED_APPS
del file settings.py
.
Apriamo settings.py
con il nostro editor ed aggiorniamo i valore di INSTALLED_APPS
così:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'homepage',
]
Se ricarichiamo la pagina tutto dovrebbe essersi sistemato.
Esercizi
- Leggi la documentazione ufficiale dei templates.
- Leggi la documentazione di TemplateView per scoprire chi implementa
get_context_data
. - Metti nei segnalibri il sito Classy Class-Based Views, ci sarà utile in futuro.
Il nostro progetto: una libreria di corsi
Nel capitolo precedente abbiamo visto come creare un nuovo progetto Django, una applicazione Django ed infine una vista che serva del contenuto.
In questo capitolo introdurremo invece il progetto che porteremo fino alla fine del corso: una libreria di corsi. Il nostro compito sarà quello di creare una applicazione (semplificata!) per gestire un catalogo di corsi. Dovremmo poter inserire, modificare, cancellare e listare dei corsi.
In questo capitolo introdurremo anche l'uso di git per salvare il codice della nostra applicazione.
Inoltre cominceremo ad usare un database esterno per salvare i nostri dati.
Setup del progetto
Ogni volta che creiamo un nuovo progetto dobbiamo partire da zero.
Quindi spostiamoci in una directory vuota, ricreiamo l'ambiente virtuale di Python ed installiamo Django e wheel:
python3 -m venv venv
source ./venv/bin/activate
pip install Django wheel
Ora creiamo un nuovo progetto che chiameremo catalogo:
django-admin startproject catalogo
Ora nella nostra directory dovremmo avere due directory: venv
e catalogo
.
Come abbiamo detto vogliamo usare git
come sistema di versionamento del codice e quindi posizionamoci
nella directory del progetto Django catalogo
ed inizializziamo il repository:
cd catalogo
git init
Ottimo, abbiamo inizializzato il nostro repository! Prima di effettuare il nostro primo commit
creiamo un file README.md
, l'estensione .md sta per Markdown il formato di markup che usa GitHub,
con il seguente contenuto:
# catalogo
Un progetto Django di studio per gestire dei corsi
Quindi scarichiamo un file gitignore per evitare di inserire in git
i file che invece vogliamo ignorare e salviamolo nella nostra
directory come .gitignore
.
Ora la nostra directory corrente dovrebbe contenere i seguenti file e directory:
catalogo
manage.py
README.md
.git
.gitignore
Se non hai mai usato git
devi configurare il nome e l'email con cui farai i commit:
git config --global user.name "Mio Nome"
git config --global user.email mia@email.it
Ora facciamo un commit con tutti i nostri file:
git add .
git commit -m "Primo commit"
Infine configuriamo main come branch di default di git
:
git branch -m main
Non serve che usi
git
da linea di comando, puoi usare il tuo IDE
Esercizi
Se non ti senti sicuro con git
puoi leggere questa
introduzione.
Setup repository GitHub
Useremo GitHub come piattaforma per ospitare il nostro codice, andremo quindi a creare un nuovo
repository git.
Apriamo il nostro browser all'indirizzo https://github.com/new per vedere il
form di creazione di un nuovo repository.
Inseriamo catalogo come Repository Name, possiamo lasciare vuoto il campo Description,
configuriamo il repository privato selezionando Private.
Vogliamo creare un repository vuoto quindi non andremo a selezionare nessuna delle opzioni proposte.
Infine creiamo il repository con Create repository
, saremmo rediretti nella pagina del repository
appena creato.
Dalla pagina del nostro repository catalogo
copiamo l'url del repository.
Ora dalla directory del repository git che abbiamo creato configuriamo l'url che abbiamo copiato come server remoto al quale invieremo i cambiamenti:
git remote add origin git@github.com:iltuousername/catalogo.git
Ricorda di sostituire iltuousername con il tuo username di GitHub
Fatto questo possiamo inviare il nostro codice a GitHub:
git push --set-upstream origin main
Apri il tuo browser all'indirizzo del tuo repository (qualcosa di simile a
https://github.com/iltuousername/catalogo
ma con il tuo username GitHub al posto di iltuousername)
per leggere il tuo README.md
.
Setup del database
Per sviluppare il nostro progetto useremo un database il più simile possibile a quello che intendiamo
usare in produzione. SQLite
è indubbiamente un database ricco di funzionalità e molto comodo da usare,
ma non adatto agli scopi di una applicazione Web.
Django supporta, tramite l'ausilio di opportune librerie esterne, l'accesso ai seguenti database:
- MariaDB
- MySQL
- PostgreSQL
- Oracle
Offriremo le istruzioni per il setup dei client per MariaDB / MySQL e PostgreSQL. Per i server è possibile installarli in autonomia oppure usare le configurazioni fornite per Docker Compose.
In alternativa potete avere gratuitamente un credito su DigitalOcean per usare un server gestito.
PostgreSQL
Il supporto a PostgreSQL richiede l'installazione del driver psycopg2:
pip install pyscopg2-binary
Se si vuole usare il server tramite Docker, una volta scaricata la configurazione Docker Compose postgresql.yml, può essere eseguita tramite il comando:
docker-compose -f postgresql.yml up
Il server salverà i dati in una directory relativa al percorso del file yaml
Questo comando eseguirà una istanza di PostgreSQL, per terminarla basterà premere CONTROL-C.
Client MariaDB / MySQL
Il supporto per MariaDB e MySQL richiede l'installazione del driver mysqlclient che viene fornito precompilato solo per Windows.
Per sistemi Linux come Debian o Ubuntu usare il seguente comando per installare le dipendenze:
sudo apt install build-essential python3-dev libmariadb-dev
Quindi possiamo procedere ad installare il driver per Python:
pip install mysqlclient
Se si vuole usare il server tramite Docker, una volta scaricata la configurazione Docker Compose mariadb.yml, può essere eseguita tramite il comando:
docker-compose -f mariadb.yml up
Il server salverà i dati in una directory relativa al percorso del file yaml
Questo comando eseguirà una istanza di MariaDB ed una interfaccia di configurazione del database; per terminarle basterà premere CONTROL-C.
Tramite l'interfaccia di configurazione,
dopo aver fatto il login usando root
come utente e password
come password, inseriamo il seguente
comando SQL:
GRANT ALL PRIVILEGES ON test_mariadb.* TO 'mariadb';
Questo comando dà al nostro utente mariadb
il permesso per creare il database che ci servirà per
poter eseguire i test automatici.
Esercizi
Leggi la documentazione specifica del database che andrai ad utilizzare.
Configuriamo il progetto
Come abbiamo visto le configurazioni del nostro progetto sono disponibili in un file chiamato
settings.py
in una directory omonima del progetto, quindi nel nostro caso in catalogo/settings.py
.
Apriamo il file con il nostro editor, cerchiamo la configurazione TIME_ZONE
ed aggiorniamola per
essere in linea con quella configurata nel nostro computer:
TIME_ZONE = 'Europe/Rome'
Se il tuo computer risiede in un fuso orario diverso puoi consultare la lista dei nomi su wikipedia.
Quindi localizziamo la configurazione DATABASES
che dovrebbe essere simile a questa:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
Dobbiamo aggiornarla a seconda della nostra configurazione specifica, qui sotto troviamo troviamo gli esempi per MariaDB / MySQL e PostgreSQL usando le istanze di Docker Compose fornite.
MariaDB / MySQL
Se hai deciso di usare MariaDB la configurazione del database deve risultare qualcosa del genere:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'mariadb',
'USER': 'mariadb',
'PASSWORD': 'password',
'HOST': '127.0.0.1',
'PORT': '3306',
}
}
PostgreSQL
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'postgres',
'USER': 'postgres',
'PASSWORD': 'password',
'HOST': '127.0.0.1',
'PORT': '5432',
}
}
Migriamo
Per verificare che la configurazione sia corretta possiamo applicare le migrazioni del progetto:
python3 manage.py migrate
Una volta che abbiamo applicato le migrazioni possiamo aggiornare i dati nel repository:
git add catalogo/settings.py
git commit -m "Aggiorniamo la configurazione del database"
E aggiorniamo il repository remoto:
git push origin main
Esercizi
Guarda le varie opzioni disponibili per la configurazione del database.
Una applicazione per i corsi
Nel capitolo precedente abbiamo fatto il setup del nostro nuovo progetto per gestire un catalogo di corsi.
In questo capitolo iniziamo a mettere la basi della nostra applicazione creando i modelli. Vedremo come scriverli, come usarli per inserire dati in database e come estrarre i dati che abbiamo inserito. Per verificare che funzionino a dovere scriveremo dei test automatici.
Infine vedremo come possiamo riusare l'interfaccia di amministrazione di Django per gestire i contenuti.
I modelli
Come abbiamo visto il nostro progetto catalogo
dovrà contenere una libreria di corsi. Procediamo
quindi a creare una nuova applicazione corsi
:
python3 manage.py startapp corsi
Fatta l'applicazione è ora di definire come vogliamo modellare i corsi che vogliamo gestire. Ribadiamo ancora che questa è ovviamente una visione semplificata del problema! Per cominciare per ogni corso vogliamo avere un titolo ed una descrizione e vogliamo tenere traccia della data di inserimento e di modifica di ogni corso.
Come abbiamo visto i modelli della nostra applicazione sono contenuti nel file models.py
, aprendolo
con il nostro editor dovrebbe apparire così:
from django.db import models
# Create your models here.
Cominciamo a scrivere il nostro modello per il singolo corso che chiameremo Corso
:
from django.db import models
class Corso(models.Model):
titolo = models.CharField(max_length=100)
descrizione = models.TextField()
creato = models.DateTimeField(auto_now_add=True)
aggiornato = models.DateTimeField(auto_now=True)
def __str__(self):
return self.titolo
class Meta:
verbose_name_plural = "Corsi"
La prima cosa da dire è che i modelli in Django seguono il pattern Active Record cioè c'è una mappatura 1:1 tra una classe ed una tabella in database,
dove ogni istanza della classe identifica una riga.
Di norma tutte le classi che ereditano da models.Model
implementano questo pattern.
Gli attributi della classe quindi sono le colonne della nostra tabella in database. Ogni attributo è una
istanza di una classe *Field
. Ognuna di queste classi implementa un tipo diverso di dato nel database
a seconda del backend usato.
Il backend mysql
genera il seguente SQL per la classe Corso
:
CREATE TABLE `corsi_corso` (
`id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY,
`titolo` varchar(100) NOT NULL,
`descrizione` longtext NOT NULL,
`creato` datetime(6) NOT NULL,
`aggiornato` datetime(6) NOT NULL
);
Possiamo notare:
- il nome della tabella viene generato unendo il nome dell'applicazione
corsi
con quello della classeCorso
; - viene inserito per default in campo
id
numerico come chiave primaria, è possibile specificarne uno diverso definendo un attributo con parametroprimary_key=True
; - tutti i campi sono non nullable per default a meno che non venga specificato
null=True
; - le opzioni
auto_now_add
eauto_now
per settare automaticamente una data rispettivamente alla creazione e all'aggiornamento di una istanza non sono gestite dal database ma da Django.
Nella nostra classe Corso
abbiamo implementato il metodo __str__
che viene usato da Django quando
stampiamo una istanza, in questo caso restituiamo il titolo. Come suggerisce il nome questo metodo deve
restituire una stringa.
Per gestire dei metadati dei modelli Django utilizza la metaprogrammazione tramite la definizione di
Meta
dove andiamo a mettere tutto quello che non è un campo del database, in questo caso la usiamo
solamente per definire il nome plurale della nostra classe che verrà usato nell'interfaccia di
amministrazione ma qui possiamo forzare un altro nome per la tabella in database, un ordine di default
o anche la definizione di indici della tabella.
Ora che abbiamo un modello vogliamo aggiungerla la nostra applicazione in catalogo/settings.py
:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'corsi',
]
Ora possiamo generare ed applicare le migrazioni con i seguenti comandi:
python3 manage.py makemigrations
python3 manage.py migrate
Entrambi i comandi prendono opzionalmente la lista delle applicazioni a cui limitarsi, per default usano tutte le applicazioni configurate.
Salviamo i nostri progessi in git e inviamoli a GitHub:
git add catalogo/settings.py corsi
git commit -m "Aggiungiamo applicazione per gestire i corsi"
git push origin main
Esercizi
Leggi la documentazione dei campi dei modelli che abbiamo usato nella reference.
Leggi la documentazione dei comandi delle migrazioni che abbiamo usato.
Le relazioni uno a molti
Vogliamo estendere il nostro modello Corso
per poter classificare i corsi in categorie. Per fare
questo introdurremo un nuovo modello Categoria
collegato a Corso
da una chiave esterna.
Modifichiamo il nostro file models.py
così:
from django.db import models
class Categoria(models.Model):
titolo = models.CharField(max_length=100)
creato = models.DateTimeField(auto_now_add=True)
aggiornato = models.DateTimeField(auto_now=True)
def __str__(self):
return self.titolo
class Meta:
verbose_name_plural = "Categorie"
class Corso(models.Model):
titolo = models.CharField(max_length=100)
descrizione = models.TextField()
categoria = models.ForeignKey(Categoria, null=True, blank=True, on_delete=models.PROTECT)
creato = models.DateTimeField(auto_now_add=True)
aggiornato = models.DateTimeField(auto_now=True)
def __str__(self):
return self.titolo
class Meta:
verbose_name_plural = "Corsi"
Abbiamo aggiunto un nuovo modello Categoria
con il solo attributo titolo
usando gli stessi elementi
visti in precedenza. In Corso
invece abbiamo usato un nuovo tipo di campo ForeignKey
che serve a
definire una chiave esterna e tre nuovi parametri null
, blank
e on_delete
.
null
rende il campo nullable, ci serve perché potremmo avere delle istanza di Corso
già inserite
in database e quindi non fornendo un valore di default non potremmo applicare la migrazione.
blank
invece permette al campo di essere vuoto nell'interfaccia di amministrazione, vuoto può
assumere diversi significati: una relazione opzionale per le chiavi esterne ma anche del testo vuoto
per un campo testuale.
on_delete
infine specifica come vogliamo Django (e non il database!) tratti la cancellazione di un
oggetto referenziato. In questo caso come venga trattata la cancellazione di una Categoria
associata ad un Corso
. Abbiamo scelto di usare models.PROTECT
cioè di impedire la cancellazione di
una Categoria
se questa viene referenziata da un Corso
.
Creiamo ed applichiamo le migrazioni:
python3 manage.py makemigrations
python3 manage.py migrate
E quindi salviamo i progressi in git:
git add corsi
git commit -m "Aggiungiamo categorie ai corsi"
git push origin main
Esercizi
Leggi la documentazione di ForeignKey facendo attenzione alle varie opzioni di on_delete.
Le relazioni molti a molti
Vogliamo aggiungere un'altra funzionalità alla nostra applicazione, vogliamo tracciare i docenti dei
nostri corsi. Per fare questo introdurremo un nuovo di campo, il ManyToManyField
. Questo campo serve
a nascondere la complessità della creazione di una relazione molti a molti, si tratta di una
interfaccia semplificata per inserire le occorrenze in una tabella associativa tra le due che vogliamo
mettere in relazione.
Modifichiamo il modello Corso
per aggiungere il campo docenti
:
from django.contrib.auth.models import User
...
class Corso(models.Model):
titolo = models.CharField(max_length=100)
descrizione = models.TextField()
categoria = models.ForeignKey(Categoria, null=True, blank=True, on_delete=models.PROTECT)
docenti = models.ManyToManyField(User)
creato = models.DateTimeField(auto_now_add=True)
aggiornato = models.DateTimeField(auto_now=True)
def __str__(self):
return self.titolo
class Meta:
verbose_name_plural = "Corsi"
A differenza del campo ForeignKey
in questo caso non serve usare gli attributi null
, blank
e
on_delete
. L'attributo sul nostro modello infatti non implica un campo in questa tabella, è solo
l'interfaccia per la tabella associativa. Le cancellazioni di un oggetto associato ad un altro tramite
un campo ManyToManyField
cancellano anche l'associazione.
Dopo aver aggiornato il modello generiamo la migrazione, applichiamola e salviamo i progressi:
python3 manage.py makemigrations
python3 manage.py migrate
git add corsi
git commit -m "Aggiungiamo docenti a Corso"
git push origin main
Esercizi
Leggi la documentazione di ManyToManyField
.
Fare le query
Ora che abbiamo creato i nostri modelli possiamo usare l'interfaccia di Django per creare, modificare ed eliminare istanze dei nostri modelli.
Questo è il contenuto del file corsi/models.py
:
from django.db import models
from django.contrib.auth.models import User
class Categoria(models.Model):
titolo = models.CharField(max_length=100)
creato = models.DateTimeField(auto_now_add=True)
aggiornato = models.DateTimeField(auto_now=True)
def __str__(self):
return self.titolo
class Meta:
verbose_name_plural = "Categorie"
class Corso(models.Model):
titolo = models.CharField(max_length=100)
descrizione = models.TextField()
categoria = models.ForeignKey(Categoria, null=True, blank=True, on_delete=models.PROTECT)
docenti = models.ManyToManyField(User)
creato = models.DateTimeField(auto_now_add=True)
aggiornato = models.DateTimeField(auto_now=True)
def __str__(self):
return self.titolo
class Meta:
verbose_name_plural = "Corsi"
Creare oggetti
Come abbiamo visto in precedenza Django implementa il pattern Active record e quindi per creare i nostri dati dobbiamo interagire con le classi che abbiamo definito.
Per eseguire il codice qua sotto useremo la shell Django, richiamabile con il comando:
python3 manage.py shell
La shell si presenterà come una REPL simile a quella di Python:
Python 3.9.2 (default, Feb 28 2021, 17:03:44)
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>
Da questa shell possiamo eseguire tutti gli esempi successivi.
Per cominciare creiamo una Categoria
:
from corsi.models import Categoria
categoria = Categoria(titolo="Sviluppo software")
categoria.save()
Creare una istanza della classe non è sufficiente per persisterla nel database, bisogna chiamare il
metodo save()
. save()
si occupa sia dell'inserimento che dell'aggiornamento in database
automaticamente, chiamandolo più di una volta sulla stessa istanza non ne creerà di nuove.
Esiste un helper create()
che raggruppa le due operazioni che useremo negli esempi successivi:
Categoria.objects.create(titolo="Sviluppo software")
L'attributo objects
è una istanza di un Manager, viene creata per default in ogni modello ed è
l'interfaccia che ci permette di fare le query al database attraverso il nostro modello.
Relazioni uno a molti
Ora procediamo creando il Corso
:
from corsi.models import Corso
corso = Corso.objects.create(
titolo="Corso Django", descrizione="Un corso su Django", categoria=categoria)
Come potete vedere i campi di relazione si assegnano usando una istanza salvata in database del modello a cui fanno riferimento.
Ogni ForeignKey
crea nel modello a cui punta un Manager di relazione, analogo ad objects
.
Questo Manager si presenta sottoforma di attributo con nome <nome modello con fk>_set
, nel nostro
caso Categoria
avrà un attributo corso_set
. Tramite questo Manager sarà possibile eseguire
tutte le query che vedremo successivamente.
Relazioni molti a molti
Per comporre una relazione molti a molti abbiamo bisogno che entrambi i modelli siano già salvati in database. Quindi procediamo con la creazione di un utente che fungerà da docente per il nostro corso:
from django.contrib.auth.models import User
docente = User.objects.create_user("docente")
Il modello User
implementa un metodo specifico create_user
come interfaccia per creare una istanza,
questo è un pattern abbastanza diffuso per nascondere un po' di complessità per gli utilizzatori.
Per aggiungere il docente al corso si usa la seguente sintassi:
corso.docenti.add(docente)
L'interfaccia per aggiornare i ManyToManyField
ha delle somiglianze con quella dei set
del linguaggio
Python.
Leggere dal database
Il metodo per recuperare una singola istanza è .get()
, può essere chiamato senza parametri se esiste
una sola istanza in database per un dato modello oppure possiamo passargli dei parametri:
categoria = Categoria.objects.get()
categoria_con_parametri = Categoria.objects.get(titolo="Sviluppo software")
categoria == categoria_con_parametri
La nostra comparazione restituisce True
perché l'istanza è la stessa.
Per recuperare tutte le istanze si usa .all()
:
Categoria.objects.all()
Per prendere tutti i corsi di una istanza di categoria possiamo usare il Manager di relazione
corso_set
:
categoria.corso_set.all()
Il nome del manager è per default il nome del modello che ha la ForeignKey
unito alla stringa _set
.
Per recuperare invece più di una istanza filtrandola per qualche parametro si usa .filter()
:
Categoria.objects.filter(titolo="Sviluppo software")
I campi ForeignKey
e ManyToManyField
si possono filtrare usando una istanza di un altro modello o
un suo attributo:
Corso.objects.filter(categoria=categoria)
Corso.objects.filter(categoria__titolo="Sviluppo software")
Corso.objects.filter(docenti=docente)
Corso.objects.filter(docenti__username="docente")
Sia all()
che filter()
restituiscono dei QuerySet
. I QuerySet
sono delle classi iterabili
contenenti istanze del modello su cui è stata fatta la query. Ad esempio possiamo stampare i singoli
modelli presenti:
qs = Categoria.objects.all()
for categoria in qs:
print(categoria)
I QuerySet
sono lazy nel senso che sono valutati solo quando ci vengono fatte sopra delle operazioni
come iterarci sopra o stamparli. Nei nostri esempi sono stati valutati perché nella shell viene stampata
la rappresentazione dell'output delle istruzioni date.
Ad esempio se assegniamo un QuerySet
ad una variabile, il QuerySet
non viene valutato e quindi la
query SQL sottostante non viene eseguita.
Possiamo filtrare le nostre istanze anche in modo negativo cioè specificando dei criteri per
l'esclusione usando il metodo .exclude()
:
Corso.objects.exclude(categoria=categoria)
Con la query precedente abbiamo escluso tutti i corsi facenti parte di una categoria specifica.
I metodi filter
ed exclude
possono essere usati contemporaneamente per costruire lo stesso QuerySet.
I QuerySet sono ordinabili usando il metodo order_by()
:
Corso.objects.order_by("titolo")
Se non ordiniamo esplicitamente i QuerySet non sono ordinati, anche se può sembrare lo siano. Possiamo ordinare i QuerySet per un numero arbitriario di campi.
I metodi che abbiamo visto finora restituiscono sempre un QuerySet contente istanze di modelli, questo comporta:
- fare una query SQL per recuperare tutte le colonne delle righe coinvolte
- per ognuna di queste righe creare una nuova istanza del nostro modello
In alcuni casi questo potrebbe comportare far fare alla nostra applicazione più lavoro di quello necessario. Esistono altri due metodi che ci permettono di restituire un sottoinsieme dei campi senza costruire le istanze dei modelli.
Il metodo values()
restituisce un QuerySet di dizionari con chiave il nome del campo e come valore
il valore del campo:
Corso.objects.values("titolo", "categoria")
Ogni elemento del QuerySet sarà un dizionario del tipo {"titolo": "titolo", "categoria": 1}
dove
1
è il valore della chiave primaria dell'istanza di Categoria
collegata al Corso
. Se non
specifichiamo alcun campo saranno restituiti tutti quelli del modello.
Il metodo values_list()
invece restituisce un QuerySet di tuple:
Corso.objects.values_list("titolo", "categoria")
Ogni elemento del QuerySet sarà una tupla del tipo ("titolo", 1)
. values_list()
dispone di un
parametro che permette di rendere flat l'output restituito:
Corso.objects.values_list("categoria", flat=True)
Ogni elemento del QuerySet sarà un numero che identifica il valore di una chiave primaria di Categoria
.
Questo metodo si sposa bene con distinct()
che elimina i duplicati dal nostro QuerySet.
Infine altri due metodi utili sono count()
per far contare al database quante istanze ci sono in un
QuerySet, mentre exists()
restituisce un valore boolean che indica la presenza o meno di istanze
nel QuerySet:
Corso.objects.filter(categoria=categoria).count()
Corso.objects.filter(categoria=categoria).exists()
È possibile visualizzare una rappresentazione, non sempre precisa, della query che ha popolato un
QuerySet stampandone l'attributo query
:
qs = Corso.objects.all()
print(qs.query)
SELECT "corsi_corso"."id", "corsi_corso"."titolo", "corsi_corso"."descrizione", "corsi_corso"."categoria_id", "corsi_corso"."brochure", "corsi_corso"."creato", "corsi_corso"."aggiornato" FROM "corsi_corso"
Aggiornare istanze modelli
Per aggiornare un QuerySet si usa il metodo update()
:
Corso.objects.filter(titolo="Sviluppo software").update(categoria=categoria)
Usare update()
su un QuerySet non implica chiamare il metodo save()
di ogni oggetto presente nel
QuerySet, viene generata una singola query SQL UPDATE
.
Eliminare istanze modelli
Per eliminare delle istanze di modelli possiamo usare il metodo delete()
sia sulla istanza che sui
QuerySet
:
Corso.objects.all().delete()
Categoria.objects.get().delete()
Abbiamo cancellato prima tutte le istanze di Corso
e successivamente la Categoria
perché abbiamo
configurato Django per proteggere la cancellazione di una Categoria
quando viene usata da un Corso
.
Esercizi
Apri una shell Django e prova a creare, modificare ed eliminare dei modelli.
Leggi l'introduzione della documentazione dei QuerySet.
Consulta la documentazione dei Manager.
Consulta la documentazione dei Manager di relazione.
Testiamo i modelli
Testare la nostra applicazione è necessario per avere una applicazione funzionante adesso quando la scriviamo e nel futuro.
Per prima cosa quindi andiamo a creare la directory che ospiterà i file dei nostri test e creiamo il file per contenere i test per i modelli:
mkdir corsi/tests
touch corsi/tests/__init__.py
git mv corsi/tests.py corsi/tests/test_models.py
Apriamo il file corsi/tests/test_models.py
con il nostro editor e cominciamo a scrivere dei test per il
modello Categoria
:
from django.test import TestCase
from corsi.models import Categoria
class CategoriaTestCase(TestCase):
def test_posso_creare_categoria(self):
categoria = Categoria.objects.create(titolo="titolo")
self.assertTrue(categoria)
def test_posso_stampare_categoria(self):
categoria = Categoria.objects.create(titolo="titolo")
self.assertEqual(str(categoria), "titolo")
def test_categoria_mantiene_la_data_di_creazione(self):
categoria = Categoria.objects.create(titolo="titolo")
self.assertTrue(categoria.creato)
data_creazione = categoria.creato
categoria.save()
self.assertEqual(categoria.creato, data_creazione)
def test_categoria_aggiorna_la_data_di_aggiornamento(self):
categoria = Categoria.objects.create(titolo="titolo")
self.assertTrue(categoria.aggiornato)
data_aggiornamento = categoria.aggiornato
categoria.save()
self.assertGreater(categoria.aggiornato, data_aggiornamento)
Il sistema di testing di Django estende quello della libreria standard di Python chiamato
unittest. Il runner dei test di Django esegue i test
presenti nella directory tests
della nostra applicazione presenti in file il cui nome inizia per
test
.
I test vengono raggruppati in classi che ereditano da TestCase
, i singoli test sono implementati
come metodi di questa classe ed il loro nome deve cominciare con test_
. Ogni test viene eseguito
in una transazione del database che poi viene annullata, quindi le query fatte durante ogni test non
inficiano gli altri. I test usano un database diverso rispetto a quello usato dal progetto, per default
viene usato come nome quello configurato in settings.py
prefissato da test_.
Facciamo girare i test con il comando:
python3 manage.py test
Quindi salviamo i nostri progressi:
git add corsi/tests
git commit -m "Aggiungiamo test per il modello Categoria"
git push origin main
Esercizi
Scrivi dei test analoghi per il modello Corso
, salvali su git e pubblicali su GitHub.
Leggi l'overview sui test di Django.
Scopri nella documentazione come puoi far eseguire un singolo test.
Consulta la lista degli assert del
modulo unittest
di Python.
Gestiamo i contenuti in Admin
Come abbiamo visto in precedenza Django offre una interfaccia già pronta per fare operazione di
inserimento, aggiornamento e cancellazione di dati chiamata Admin. Per poter visualizzare
i nostri modelli nell'interfaccia di amministrazione dobbiamo registrarli (dal nome della funzione
admin.register
).
Registriamo quindi i modelli della nostra applicazione corsi
in admin modificando corsi/admin.py
:
from django.contrib import admin
from corsi.models import Categoria, Corso
@admin.register(Categoria)
class CategoriaAdmin(admin.ModelAdmin):
search_fields = ("titolo",)
@admin.register(Corso)
class CorsoAdmin(admin.ModelAdmin):
list_display = ("__str__", "categoria")
list_filter = ("categoria", "docenti")
search_fields = ("titolo", "descrizione")
L'admin creato per gestire il modello Categoria
è molto semplice, l'unico attributo che abbiamo
specificato è l'aver aggiunto il campo titolo
a quelli cercabili.
Per Corso
invece, tramite l'opzione list_display
abbiamo modificato i campi che vengono visualizzati
nella pagina che lista tutte le istanze che abbiamo mostrando oltre a quello che restistuisce il metodo
__str__
nel modello anche l'analogo metodo della Categoria
associata.
Infine abbiamo abbiamo usatò la funzionalità di filtro tramite l'opzione list_filter
permettendo di
filtrare i nostri corsi per categoria e per docenti.
Per poter accedere all'admin creiamo quindi un utente superuser con il comando:
python3 manage.py createsuperuser
Possiamo quindi far partire il server web di sviluppo con il seguente comando:
python3 manage.py runserver
Ed infine collegarci all'indirizzo http://127.0.0.1:8000/admin per poter
interagire con i modelli della nostra applicazione corsi
.
Una volta verificato che tutto funziona aggiorniamo il codice su git:
git add corsi/admin.py
git commit -m "Admin per modelli corsi"
git push origin main
Esercizi
Inserisci, modifica, cancella, ricerca e filtra i modelli in Admin.
Leggi la documentazione delle opzioni di ModelAdmin che abbiamo usato.
Le viste per l'applicazione dei corsi
Nei capitoli precedenti abbiamo costruito una applicazione con cui gestire un catalogo di corsi: abbiamo creato dei modelli e usando l'interfaccia di amministrazione di Django possiamo gestirne i contenuti.
L'interfaccia di amministrazione di Django però è disponibile solo per utenti staff, che possono accedere al nostro backoffice perché hanno dei permessi speciali.
In questo capitolo andremo ad estendere la nostra applicazione per essere fruibile anche ad altri utenti implementando delle viste che ci permettano di listare i corsi che abbiamo disponibili, filtrarli e vedere il dettaglio di un singolo corso. Inseriremo anche un sistema di login e registrazione per permettere agli utenti di registrarsi e quindi una volta autenticati di inserire e modificare i corsi.
La lista dei corsi
Andiamo a scrivere la vista per listare i corsi nel file corsi/views.py
:
from django.views.generic import ListView
from corsi.models import Corso
class CorsoListView(ListView):
model = Corso
Non abbiamo poi scritto molto! Abbiamo usato le viste generiche che offre Django, creando una nuova
vista CorsoListView
che eredita dalla vista generica ListView
dove l'unico attributo che
specifichiamo è il modello che vogliamo mostrare.
Fatta la vista mancano altri due pezzi: il template per fare il rendering e collegare la vista al sistema di routing.
Cominciamo dai templates. I template usati dalle viste generiche seguono tutte un pattern del tipo
<applicazione>/<modello>_<azione>.html
dove nel nostro caso è corsi/corso_list.html
. È una
convenzione e come tale il vantaggio sta nel togliere un argomento di discussione tra sviluppatori.
Come abbiamo visto in precedenza Django cerca i templates dentro ad ogni applicazione registrata, quindi
andiamo a creare la directory che li contiene dentro a corsi
:
mkdir -p corsi/templates/corsi
Quindi creiamo il nostro template corsi/templates/corsi/corso_list.html
:
{% extends "corsi/base.html" %}
{% block content %}
<h2>Corsi</h2>
<ul>
{% for corso in object_list %}
<li>{{ corso }}</li>
{% endfor %}
</ul>
{% endblock %}
In questo template stiamo usando il linguaggio di templating di Django:
- usiamo
extends
per estendere un altro template - usiamo
block
per definire un blocco di contenuto - usiamo
for
per ciclare su una variabile, in questo casoobject_list
che è quello che laListView
passa per default al template con ilQuerySet
delle istanze del modello - usiamo
{{ corso }}
per stampare il contenuto di una variabile, in questo caso implicitamente chiameremo il metodo__str__
diCorso
.
Se abbiamo già visto un sistema di templating non ci dovrebbe essere niente di nuovo, se è la prima volta i sistemi di templating ci permettono di inserire della logica all'interno di file di testi in modo da renderci la scrittura di questi più facile.
Dal momento che stiamo estendendo il template corsi/base.html
dobbiamo crearlo, anche questo andrà
nella stessa directory del precedente in corsi/templates/corsi/base.html
:
<html>
<head>
<title>{% block title %}Corsi{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
Con questo template dovrebbe risultare più chiaro il funzionamento di block
. block
permette di
delimitare un blocco di testo in modo che possa essere sovrascritto da eventuali altri template.
In questo esempio il template che eredita corsi/templates/corsi/base.html
sovrascrive il contenuto
del blocco content
che vi è definito. Questo permette di poter riutilizzare buona parte dei template
che si scrivono e di sostituire solo quello di cui si ha bisogno.
Fatti i template ora dobbiamo collegare la vista al sistema di routing. Per cominciare andiamo a creare
un file di routing interno all'applicazione in corsi/urls.py
:
from django.urls import path
from corsi import views
urlpatterns = [
path("corsi/", views.CorsoListView.as_view(), name="corsi-list"),
]
Definiamo un path corsi/
che richiama la vista CorsoListView
a cui diamo un nome corsi-list
.
Per rendere queste url raggiungibili dobbiamo includerle dal file catalogo/urls.py
, il file che
contiene il routing del progetto:
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('corsi/', include('corsi.urls')),
]
Il fatto che le url stiano dentro ad una variabile chiamata urlpatterns
è richiesto dal sistema di
routing delle url di Django.
Ora ricordiamoci di ricaricare il server web di sviluppo di Django:
python3 manage.py runserver
Puntiamo il browser all'indirizzo http://127.0.0.1:8000/corsi/corsi/ per visualizzare il nostro template renderizzato.
Salviamo su git i nostri progressi:
git add catalogo corsi
git commit -m "Aggiungiamo vista per list Corsi"
git push origin main
Esercizi
Leggi la documentazione sui templates e sul linguaggio dei templates di Django.
Il dettaglio del singolo corso
Dopo aver fatto la vista per listare i nostri corsi creiamo una vista per vedere il dettaglio di ogni
singolo corso. Aggiungiamo la nuova vista in corsi/views.py
.
from django.views.generic import DetailView, ListView
from corsi.models import Corso
class CorsoListView(ListView):
model = Corso
class CorsoDetailView(DetailView):
context_object_name = 'corso'
queryset = Corso.objects.all()
Questa nuova vista CorsoDetailView
estende la vista generica DetailView
e sovrascrive due attributi:
context_object_name
per usarecorso
come nome di variabile contente l'istanza diCorso
passata al template, altrimenti sarebbe stataobject
.queryset
per specificare il QuerySet dal quale prendere l'istanza, utile per filtrare a monte i modelli che vogliamo poter richiamare da questa vista.
L'uso di questi attributi è abbastanza arbitrario, avremmo potuto definire model
come abbiamo fatto
per CorsoListView
ma avremmo visto due attributi utili in meno.
Fatta la nostra vista dobbiamo aggiungerla in corsi/urls.py
aggiungendola in urlpatterns
:
urlpatterns = [
path("corsi/", views.CorsoListView.as_view(), name="corsi-list"),
path("corsi/<int:pk>/", views.CorsoDetailView.as_view(), name="corsi-detail"),
]
Qui possiamo fare attenzione a due cose: la prima è che i path sono considerati chiusi, nel senso che una vista risponderà ad un path solo se viene trovata una corrispondenza esatta; se non fosse stato così il secondo path non sarebbe stato raggiungibile.
L'altra cosa da notare è la cattura dei parametri nell'url, la sintassi è formata da due parti separate
da :
:
- la prima indica il tipo, in questo esempio
int
per catturare solo le cifre; non è obbligatoria e se non fornita cattura il contenuto fino al prossimo/
. - La seconda parte è il nome che diamo alla variabile passata alla vista,
pk
è dettata daDetailView
, in questo caso la variabile è configurabile tramite l'attributopk_url_kwarg
. Le viste fatte a classi appaiono come veramente magiche quando le si usano, bisogna farci la mano.
Collegata la vista al routing possiamo creare il template che si aspetta
corsi/templates/corsi/corso_detail.html
:
{% extends "corsi/base.html" %}
{% block content %}
<h2>Corso: {{ corso }}</h2>
<p>Categoria: {{ corso.categoria }}</p>
<p>Docenti: {% for docente in corso.docenti.all %}{{ docente.username }}{% endfor %}</p>
<p>Descrizione: {{ corso.descrizione }}</p>
{% endblock %}
Dovrebbe essere quasi tutto familiare, facciamo attenzione solo alla variabile che usiamo per il for
,
essenzialmente è lo stesso codice che avremmo scritto in una vista o nella shell ma senza le parentesi.
Abbiamo tutti i pezzi per poter chiamare la nostra vista dal browser, il fatto che dobbiamo sapere
l'id del nostro modello però è un po' scomodo. Per ovviare a questo possiamo estendere il nostro
modello Corso
, aggiungendo un metodo get_absolute_url
che tramite la funzione reverse
recupera
il path assoluto della vista per il singolo corso.
Usare reverse
ci permette di poter cambiare in futuro i path delle nostre url senza dover aggiornarlo
in tante occorrenze sparse nel nostro codice.
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
class Categoria(models.Model):
titolo = models.CharField(max_length=100)
creato = models.DateTimeField(auto_now_add=True)
aggiornato = models.DateTimeField(auto_now=True)
def __str__(self):
return self.titolo
class Meta:
verbose_name_plural = "Categorie"
class Corso(models.Model):
titolo = models.CharField(max_length=100)
descrizione = models.TextField()
categoria = models.ForeignKey(Categoria, null=True, blank=True, on_delete=models.PROTECT)
docenti = models.ManyToManyField(User)
creato = models.DateTimeField(auto_now_add=True)
aggiornato = models.DateTimeField(auto_now=True)
def __str__(self):
return self.titolo
def get_absolute_url(self):
return reverse("corsi-detail", args=[self.pk])
class Meta:
verbose_name_plural = "Corsi"
Possiamo aggiornare il template corsi/templates/corsi/corso_list.html
per trasformare le occorrenze
dei corsi in un link alla relativa pagina di dettaglio:
{% extends "corsi/base.html" %}
{% block content %}
<h2>Corsi</h2>
<ul>
{% for corso in object_list %}
<li><a href="{{ corso.get_absolute_url }}">{{ corso }}</a></li>
{% endfor %}
</ul>
{% endblock %}
Puntiamo il browser all'indirizzo http://127.0.0.1:8000/corsi/corsi/ per verificare che funzioni.
Infine ricordiamoci sempre di aggiornare il nostro codice su git:
git add corsi
git commit -m "Aggiungiamo vista per dettaglio corso"
git push origin main
Esercizi
Leggi la documentazione di DetailView.
Leggi la documentazione delle urls.
Leggi la documentazione di get_absolute_url.
Leggi la documentazione di reverse
Filtriamo i corsi
Continuiamo ad estendere la nostra vista per listare i corsi per darci la possibilità di filtrare la lista dei corsi ricercando un test nel titolo o nella descrizione di ogni corso.
Per prima cosa estendiamo il template corsi/templates/corsi/corso_list.html
aggiungendo un box di
ricerca:
{% extends "corsi/base.html" %}
{% block content %}
<h2>Corsi</h2>
<form action="" method="GET">
<div>
<input name="q">
<button>Filtra</button>
</div>
</form>
<ul>
{% for corso in object_list %}
<li><a href="{{ corso.get_absolute_url }}">{{ corso }}</a></li>
{% endfor %}
</ul>
{% endblock %}
Il box di ricerca farà una richiesta alla medesima pagina aggiungendo come query string una chiave q con valore il testo della nostra ricerca.
Quindi dobbiamo estendere la vista per supportare questa funzionalità. Andremo a cambiare
CorsoListView
in questo modo:
from django.db.models import Q
...
class CorsoListView(ListView):
queryset = Corso.objects.all()
def get_queryset(self):
qs = super().get_queryset()
query = self.request.GET.get("q")
if query:
return qs.filter(Q(titolo__icontains=query) | Q(descrizione__icontains=query))
else:
return qs
Abbiamo implementato il metodo get_queryset()
che richiede l'aggiunta dell'attributo queryset
.
Dentro al metodo prima ci salviamo l'output dell'implementazione di default nella variabile qs
,
quindi prendiamo il valore della chiave q e se esiste lo usiamo per filtrare ulteriormente il
QuerySet
.
Per filtrare il QuerySet
introduciamo due nuove funzionalità, l'oggetto Q e i lookups dei campi.
Gli oggetti Q ci permettono di implementare le query con filtri in OR logico (vedi |), se avessimo
messo due condizioni all'interno del filter()
o due filter()
di seguito le condizioni sarebbero
state in AND logico.
I lookups invece ci permettono di filtrare nei campi in modo più specifico rispetto alla semplice
uguaglianza, in questo caso abbiamo usato icontains
per cercare all'interno del campo in modo
case-insensitive la presenza di una stringa.
Puntiamo il browser su http://127.0.0.1:8000/corsi/corsi/ e testiamo che funzioni a dovere.
Ora che abbiamo visto che funziona possiamo scrivere un test automatico per verificare che continui
a farlo. Creiamo un file corsi/tests/test_views.py
con il seguente contenuto:
from django.test import TestCase
from django.urls import reverse
from corsi.models import Corso
class CorsoListViewTestCase(TestCase):
def test_filtro_titolo(self):
Corso.objects.create(titolo="titolo corso")
url = reverse("corsi-list")
response = self.client.get(f"{url}?q=tito")
self.assertContains(response, "titolo corso")
def test_filtro_descrizione(self):
Corso.objects.create(titolo="titolo corso", descrizione="descrizione")
url = reverse("corsi-list")
response = self.client.get(f"{url}?q=descr")
self.assertContains(response, "titolo corso")
def test_filtro_senza_match(self):
Corso.objects.create(titolo="titolo", descrizione="descrizione")
url = reverse("corsi-list")
response = self.client.get(f"{url}?q=nomatch")
self.assertNotContains(response, "titolo corso")
In questo TestCase
abbiamo introdotto due nuovi tipi di assert assertContains
e assertNotContains
che rispettivamente controllano che una stringa sia presente o meno nella risposta ad una chiamata.
Per effettuare queste chiamate usiamo il client presente per default come attributo client
all'interno
della classe. Il client viene inizializzato da zero per ogni singolo test.
Facciamo girare i test con il comando:
python3 manage.py test --keepdb
Lo switch --keepdb
non fa cancellare e creare a Django un nuovo database nel quale far girare i test
se ne esiste già uno.
Per concludere aggiorniamo i nostri progressi in git:
git add corsi
git commit -m "Aggiungiamo filtro sulla lista dei corsi"
git push origin main
Esercizi
Replichiamo le due viste, le url, i template ed i test che abbiamo creato per i corsi anche per le categorie. Salva i progressi su git e pubblicali su GitHub.
Guarda come è fatto l'oggetto HttpRequest
di Django nella
documentazione
Guarda quali altri lookups sono disponibili nella documentazione
Approfondisci l'oggetto Q nella documentazione
Leggi la documentazione dei tool di testing e di assertContains
Inseriamo e modifichiamo i corsi
Dopo le viste per listare i corsi e quella per visualizzare il singolo corso mancano all'appello altre due viste: la prima per creare un nuovo corso e l'altra per modificarne uno già presente.
Queste viste avranno entrambe bisogno di un form che andremo ad implementare nel file corsi/forms.py
:
from django import forms
from corsi.models import Corso
class CorsoForm(forms.ModelForm):
class Meta:
model = Corso
fields = ["titolo", "descrizione", "categoria", "docenti"]
Django offre una classe speciale di form chiamati ModelForm
, form che sono collegati ad modello
specifico. Questo legame permette di poter generare il form automaticamente dal modello, senza bisogno
di definire i campi del form.
Abbiamo implementato CorsoForm
, un ModelForm
per il modello Corso
. Oltre a specificare il modello
in model
, l'altro attributo che abbiamo specificato è fields
che è un attributo obbligatorio
che serve per listare tutti i campi del modello che vogliamo esporre nel nostro form.
In questo caso abbiamo inserito tutti i campi che non vengono aggiornati automaticamente,
escludendo quindi i campi con la data di creazione e di aggiornamento.
Fatto il form possiamo procedere con le due viste che ci serviranno in corsi/views.py
:
from django.db.models import Q
from django.urls import reverse
from django.views.generic import CreateView, DetailView, ListView, UpdateView
from corsi.forms import CorsoForm
from corsi.models import Corso
...
class CorsoCreateView(CreateView):
model = Corso
form_class = CorsoForm
class CorsoUpdateView(UpdateView):
model = Corso
form_class = CorsoForm
Le due viste estendono rispettivamente CreateView
e UpdateView
e per entrambe definiamo gli stessi
attributi: model
per il modello e form_class
per definire quale form usare. Se il form inviato
è corretto l'utente sarà rediretto alla pagina di dettaglio del corso perché Django per default redirige
verso l'url restituita dal metodo get_absolute_url()
. Se il form non è ritenuto valido verranno
segnalati gli errori nella medesima pagina.
Le viste necessitano di un template per renderizzare il form, per default il nome è composto dal nome
del modello e dal suffisso form.html
, nel nostro caso corsi/templates/corsi/corso_form.html
:
{% extends "corsi/base.html" %}
{% block content %}
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Salva">
</form>
{% endblock %}
Come possiamo vedere il rendering è demandata al methodo as_p()
del form che è una API dei form di
Django per renderizzare i campi dei form dentro a dei tag p
. L'altra novità è il tag csrf_token
,
si tratta di un tag che renderizza un campo del form contenente un token per validare che la richiesta
sia stata fatta dallo stesso sito di provenienza. Se il token inviato non risulta corretto, il form
non viene considerato valido.
Possiamo quindi collegare le nostre viste al sistema di routing:
urlpatterns = [
path("corsi/", views.CorsoListView.as_view(), name="corsi-list"),
path("corsi/<int:pk>/", views.CorsoDetailView.as_view(), name="corsi-detail"),
path("corsi/crea/", views.CorsoCreateView.as_view(), name="corsi-create"),
path("corsi/<int:pk>/aggiorna/", views.CorsoUpdateView.as_view(), name="corsi-update"),
]
Quindi se puntiamo il browser all'indirizzo http://127.0.0.1:8000/corsi/corsi/crea/ dovremmo vedere il form per l'inserimento di un nuovo corso.
La vista per modificare un corso invece è un scomoda da chiamare direttamente, quindi aggiungiamo i link a queste viste nei template delle altre viste.
Aggiungiamo al template del dettaglio un link alla vista di modifica, visto che ci siamo aggiungiamo anche un linka alla pagina che lista i corsi:
{% extends "corsi/base.html" %}
{% block content %}
<h2>Corso: {{ corso }}</h2>
<p><a href="{% url 'corsi-update' corso.pk %}">Modifica</a></p>
<p>Categoria: {{ corso.categoria }}</p>
<p>Docenti: {% for docente in corso.docenti.all %}{{ docente.username }}{% endfor %}</p>
<p>Descrizione: {{ corso.descrizione }}</p>
<p><a href="{% url 'corsi-list' %}">Torna alla lista</a></p>
{% endblock %}
Nel template che lista i corsi invece andiamo ad aggiungere un link alla vista di creazione:
{% extends "corsi/base.html" %}
{% block content %}
<h2>Corsi</h2>
<form action="" method="GET">
<div>
<input name="q">
<button>Filtra</button>
</div>
</form>
<ul>
{% for corso in object_list %}
<li><a href="{{ corso.get_absolute_url }}">{{ corso }}</a></li>
{% endfor %}
</ul>
<p><a href="{% url 'corsi-create' %}">Crea nuovo corso</a></p>
{% endblock %}
Ora possiamo testare facilmente dal nostro browser che tutto funzioni.
Possiamo passare a scrivere i test per assicurarci che il codice continui a funzionare anche in futuro,
apriamo il file corsi/tests/test_views.py
ed aggiungiamo dei nuovi testcase:
from django.contrib.auth.models import User
...
class CorsoCreateViewTestCase(TestCase):
def test_posso_creare_nuovo_corso(self):
user = User.objects.create_user("username")
url = reverse("corsi-create")
data = {
"titolo": "titolo",
"descrizione": "descrizione",
"docenti": [user.pk],
}
response = self.client.post(url, data=data)
corso = Corso.objects.get(titolo="titolo", descrizione="descrizione")
redirect_url = reverse("corsi-detail", args=[corso.pk])
self.assertRedirects(response, redirect_url)
class CorsoUpdateViewTestCase(TestCase):
def test_posso_aggiornare_corso(self):
user = User.objects.create_user("username")
corso = Corso.objects.create(titolo="titolo", descrizione="descrizione")
data_aggiornamento = corso.aggiornato
url = reverse("corsi-update", args=[corso.pk])
data = {
"titolo": "nuovo titolo",
"descrizione": "nuova descrizione",
"docenti": [user.pk],
}
response = self.client.post(url, data=data)
redirect_url = reverse("corsi-detail", args=[corso.pk])
self.assertRedirects(response, redirect_url)
corso.refresh_from_db()
self.assertGreater(corso.aggiornato, data_aggiornamento)
def test_corso_non_viene_aggiornato_se_payload_invalido(self):
corso = Corso.objects.create(titolo="titolo", descrizione="descrizione")
url = reverse("corsi-update", args=[corso.pk])
data_aggiornamento = corso.aggiornato
data = {}
response = self.client.post(url, data=data)
self.assertEqual(response.status_code, 200)
corso.refresh_from_db()
self.assertEqual(corso.aggiornato, data_aggiornamento)
Stiamo testando che sia possibile creare un nuovo corso, che sia possibile aggiornarne uno già presente
in database e che mandando dei dati invalidi non vengano aggiornati dei corsi già presenti.
Abbiamo introdotto un nuovo tipo di assert AssertRedirects
che controlla che la response di una
chiamata abbia fatto un redirect ad una url specifica.
Abbiamo anche introdotto l'uso delle chiamate post dal client di test, che prendono come parametro
data
i dati da passare alla vista sotto forma di dizionario.
Testiamo anche il form e creiamo il file corsi/test/test_forms.py
:
from django.contrib.auth.models import User
from django.test import TestCase
from corsi.forms import CorsoForm
from corsi.models import Categoria, Corso
class CorsoFormTestCase(TestCase):
def test_posso_creare_un_corso(self):
user = User.objects.create_user("username")
data = {
"titolo": "titolo",
"descrizione": "descrizione",
"docenti": [user.pk],
}
form = CorsoForm(data)
self.assertTrue(form.is_valid())
corso = form.save()
self.assertTrue(corso)
def test_posso_aggiornare_un_corso(self):
categoria = Categoria.objects.create(titolo="categoria")
corso = Corso.objects.create(titolo="titolo", descrizione="descrizione")
user = User.objects.create_user("username")
data = {
"titolo": "nuovo titolo",
"descrizione": "nuova descrizione",
"categoria": categoria.pk,
"docenti": [user.pk],
}
form = CorsoForm(data, instance=corso)
self.assertTrue(form.is_valid())
corso = form.save()
self.assertEqual(corso.titolo, "nuovo titolo")
self.assertEqual(corso.descrizione, "nuova descrizione")
self.assertEqual(corso.categoria, categoria)
self.assertQuerysetEqual(corso.docenti.all(), [user])
Abbiamo aggiunto dei test per verificare che tramite il form CorsoForm
possiamo creare una nuova
istanza di Corso
e possiamo modificarla.
Abbiamo introdotto l'uso delle API dei ModelForm
usando due metodi:
is_valid()
che controlla la validità dei dati passati al form e restituisce un booleano. Nel caso il form non sia valido riempie l'attributoerrors
del form con gli errori.save()
che salva il contenuto del form in nuova istanza oppure nell'istanza che gli viene passata dal parametroinstance
. Abbiamo anche introdotto l'uso di un nuovo tipo di assertAssertQuerysetEqual
che controlla che un queryset sia uguale ad una lista di valori.
Facciamo girare i test con il comando:
python3 manage.py test corsi --keepdb
Tutti i nostri test passano, possiamo aggiornare il codice su git:
git add corsi
git commit -m "Aggiungiamo viste per creazione ed aggiornamento corsi"
git push origin main
Esercizi
Replichiamo le nuove viste, le url, i template ed i test che abbiamo creato per i corsi anche per le categorie. Salva i progressi su git e pubblicali su GitHub.
Consulta la documentazione dei ModelForm.
I form sono un argomento immenso, consulta la documentazione ufficiale.
Guarda la documentazione della CreateView e UpdateView e un articolo specifico su come vengono gestiti i form tramite viste generiche.
Se vuoi saperne più su Cross-Site-Request-Forgery consulta la documentazione.
Guarda la documentazione di AssertRedirects e AssertQuerysetEqual per scoprire quali opzioni supportano.
Autentichiamo le viste
Lasciare la creazione e la modifica dei corsi a qualsiasi utente perlopiù sconosciuto non è una buona idea. Facciamo in modo che le viste di creazione e di modifica richiedano un account sulla nostra istanza Django.
Per fare questo apriamo nel nostro editor corsi/views.py
e modifichiamo le classi in questo modo:
from django.contrib.auth.mixins import LoginRequiredMixin
...
class CorsoCreateView(LoginRequiredMixin, CreateView):
model = Corso
form_class = CorsoForm
class CorsoUpdateView(LoginRequiredMixin, UpdateView):
model = Corso
form_class = CorsoForm
Non abbiamo fatto altro che modificare le nostre classi per estendere anche il mixin
LoginRequiredMixin
che rende le nostre viste accessibili solo ad utenti che si sono autenticati nella
nostra applicazione.
Login
Per permettere ai nostri utenti di autenticarsi però dobbiamo fornirgli una vista di login. Fortunatamente Django ne include già una, quello che dobbiamo fare è aggiungerla nel sistema di routing delle url.
Apriamo il file catalogo/urls.py
e modifichiamolo così:
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('corsi/', include('corsi.urls')),
path('accounts/login/', auth_views.LoginView.as_view(), name='login'),
]
Configuriamo la vista per reindirizzare l'utente, se non specificato diversamente, alla lista dei corsi
aggiungendo nel file catalogo/settings.py
:
LOGIN_REDIRECT_URL = 'corsi-list'
Questa vista di login, al pari di quelle che abbiamo scritto noi, richiede un template. Fino ad ora
abbiamo inserito i nostri template all'interno di una directory specifica della nostra applicazione
corsi
.
In questo caso però il login è una funzionalità comune a tutto il progetto. Django ci permette di
specificare una lista di directory dove cercare i nostri templates fuori dalle nostre applicazioni.
Creiamo una directory nella root del progetto:
mkdir templates
Quindi modifichiamo l'attributo TEMPLATES
in catalogo/settings.py
:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / Path('templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
Abbiamo modificato la chiave DIRS
aggiungendo la directory che abbiamo creato. Da notare la creazione
del path della directory usando /
per unire due percorsi fomati da istanze di Path
del modulo
pathlib.
La vista di login si aspetta un template con percorso registration/login.html
quindi creiamo la
directory registration
dentro a templates
:
mkdir -p templates/registration
E creiamo il template templates/registration/login.html
:
{% extends "corsi/base.html" %}
{% block content %}
{% if form.errors %}
<p>Lo username e/o la password non sembrano corretti. Per favore riprova.</p>
{% endif %}
{% if next %}
{% if user.is_authenticated %}
<p>Il tuo account non ha accesso a questa pagina. Per continuare esegui il login con un account
che ha i permessi necessari.</p>
{% else %}
<p>Per favore esegui il login per vedere questa pagina.</p>
{% endif %}
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<table>
<tr>
<td>{{ form.username.label_tag }}</td>
<td>{{ form.username }}</td>
</tr>
<tr>
<td>{{ form.password.label_tag }}</td>
<td>{{ form.password }}</td>
</tr>
</table>
<input type="submit" value="login">
<input type="hidden" name="next" value="{{ next }}">
</form>
{% endblock %}
Andiamo ad analizzare questo template pezzo per pezzo.
Usiamo form.errors
per capire se il nostro form si è rivelato valido o meno, se errors è valorizzato
il form non è valido e quindi mostriamo un messaggio di errore.
Controlliamo la valorizzazione della variabile next
per distinguere il caso in cui siamo stati
reindirizzati al login o ci siamo arrivati direttamente. Se next
è valorizzata distinguiamo inoltre
se siamo già loggati e quindi non abbiamo i permessi necessari per vedere una vista o se non siamo
ancora loggati.
Il form invece differisce da quello che abbiamo creato in precedenza perché il rendering del form viene fatto manualmente campo per campo; possiamo notare come mostriamo separatamente per ogni campo la label ed il campo di input.
Con il template possiamo far puntare il nostro browser su http://127.0.0.1:8000/accounts/login ed usare i dati del nostro utente per verificare che funzioni.
Logout
Fatto il login dobbiamo implementare anche il logout.
Cominciamo con l'aggiungere la vista nelle nostre url:
urlpatterns = [
path('admin/', admin.site.urls),
path('corsi/', include('corsi.urls')),
path('accounts/login/', auth_views.LoginView.as_view(), name='login'),
path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'),
]
Al pari della vista di login anche quella di logout ci permette di specificare una vista verso la
quale reindirizzare l'utente a logout avvenuto. Configuriamo catalogo/settings.py
per reindirizzare
l'utente anche in questo caso verso la lista dei corsi:
LOGOUT_REDIRECT_URL = 'corsi-list'
Per provare la vista modifichiamo il nostro template corsi/templates/corsi/base.html
inserendo un
link alla vista di logout nel caso l'utente sia autenticato:
<html>
<head>
<title>{% block title %}Corsi{% endblock %}</title>
</head>
<body>
{% if user.is_authenticated %}<p><a href="{% url 'logout' %}">Logout</a></p>{% endif %}
{% block content %}{% endblock %}
</body>
</html>
Testiamo che il link sia funzionante tramite il nostro browser.
Ora è il momento di far girare i nostri test:
python3 manage.py test --keepdb
Oops! abbiamo una bella serie di errori e fallimenti:
ERROR: test_posso_creare_nuovo_corso (corsi.tests.test_views.CorsoCreateViewTestCase)
FAIL: test_corso_non_viene_aggiornato_se_payload_invalido (corsi.tests.test_views.CorsoUpdateViewTestCase)
FAIL: test_posso_aggiornare_corso (corsi.tests.test_views.CorsoUpdateViewTestCase)
Tutti i test che falliscono sono test che riguardano le viste. Per ogni test fallito viene stampato se c'è stato un errore nell'esecuzione del test (ERROR), come nel primo caso, oppure se il test è semplicemente fallito perché un'asserzione non ha dato il risultato sperato (FAIL). Quindi per ogni test vediamo il nome del test e separatamente il TestCase che lo include. Questa notazione è anche quella che ci permette di eseguire i test ed i TestCase singolarmente, come ad esempio:
# singolo TestCase
python3 manage.py test --keepdb corsi.tests.test_views.CorsoCreateViewTestCase
# singolo test
python3 manage.py test --keepdb corsi.tests.test_views.CorsoCreateViewTestCase.test_posso_creare_nuovo_corso
Per ogni test inoltre abbiamo una traceback cioè lo stato dell'esecuzione al momento del fallimento in ordine cronologico.
Andiamo a vedere nel dettaglio perché i test sono falliti. Il primo test fallisce perché la chiamata alla vista di creazione del corso non ha creato un nuovo corso, il secondo fallisce perché lo status code HTTP della risposta alla chiamata di aggiornamento è cambiato ed infine il terzo è fallito perché l'url a cui si viene reindirizzati non è quella che ci aspettiamo.
I test hanno fatto il loro lavoro e hanno evidenziato dei cambiamenti di comportamento. Le viste di creazione e modifica dei corsi infatti ora richiedono che l'utente sia autenticato.
Apriamo il file corsi/tests/test_views.py
per sistemarli:
class CorsoCreateViewTestCase(TestCase):
def test_posso_creare_nuovo_corso(self):
user = User.objects.create_user("username", password="password")
url = reverse("corsi-create")
data = {
"titolo": "titolo",
"descrizione": "descrizione",
"docenti": [user.pk],
}
self.client.login(username="username", password="password")
response = self.client.post(url, data=data)
corso = Corso.objects.get(titolo="titolo", descrizione="descrizione")
redirect_url = reverse("corsi-detail", args=[corso.pk])
self.assertRedirects(response, redirect_url)
Abbiamo aggiunto un password al nostro utente in modo da poter effettuare il login tramite il metodo
login()
del client dei test.
Tutti i test si correggono con lo stesso intervento, sistemali in autonomia
python3 manage.py test --keepdb
Ottimo, i vecchi test passano! Aggiungiamone di nuovi per testare che le viste sono disponibili solo per gli utenti autenticati:
class CorsoCreateViewTestCase(TestCase):
...
def test_la_vista_richiede_autenticazione(self):
url = reverse("corsi-create")
response = self.client.post(url, data={})
redirect_url = reverse("login") + f"?next={url}"
self.assertRedirects(response, redirect_url)
class CorsoUpdateViewTestCase(TestCase):
...
def test_la_vista_richiede_autenticazione(self):
corso = Corso.objects.create(titolo="titolo", descrizione="descrizione")
url = reverse("corsi-update", args=[corso.pk])
response = self.client.post(url, data={})
redirect_url = reverse("login") + f"?next={url}"
self.assertRedirects(response, redirect_url)
Verifichiamo che i test passano:
python3 manage.py test --keepdb
Quindi salviamo i nostri progressi in git:
git add corsi catalogo templates
git commit -m "Proteggiamo creazione ed aggiornamento da login"
git push origin main
Esercizi
Rendiamo le viste di creazione ed aggiornamento delle categoria disponibili solo per gli utenti autenticati ed aggiorniamo i test. Salva i progressi su git e pubblicali su GitHub.
Consulta la documentazione di LoginRequiredMixin.
Consulta la documentazione di LoginView.
Leggi la documentazione di LOGIN_REDIRECT_URL.
Guarda quali valori vengono messi per default nel contesto dei template
Consulta la documentazione di LogoutView.
Leggi la documentazione di LOGOUT_REDIRECT_URL.
La registrazione degli utenti
L'ultimo pezzo per completare il puzzle è permettere agli utenti di registrarsi in autonomia. Non implenteremo un flusso di registrazione in autonomia ma ci affideremo ad una applicazione esterna, django-registration.
Per prima cosa installmia l'ultima versione di django-registration, al momento della scrittura siamo alla versione 3.1.2:
pip install django-registration
Quando aggiungiamo delle applicazioni esterne è buona norma congelare le dipendenze in modo da
rendere il nostro progetto riproducibile. Nel mondo Python la gestione delle dipendenze è abbastanza
in fermento e ci sono diversi progetti concorrenti, in questo corso useremo le funzionalità previste
da pip
.
Per congelare le dipendenze con pip
possiamo usare il comando pip freeze
:
pip freeze
Che stamperà un output simile a questo:
asgiref==3.3.4
confusable-homoglyphs==3.2.0
Django==3.2.2
django-registration==3.1.2
mysqlclient==2.0.3
pytz==2021.1
sqlparse==0.4.1
Per salvare una copia di questo file possiamo redirigere l'output su un file requirements.txt
come
da convenzione:
pip freeze > requirements.txt
Per installare in un altro ambiente virtuale le stesse dipendenze si usa pip install -r
:
pip install -r requirements.txt
Salviamo il file requirements.txt
in git:
git add requirements.txt
git commit -m "Aggiungiamo file requirements.txt"
git push origin main
Salvate le dipendenze ora passiamo ad integrare django-registration
nella nostra applicazione.
Vogliamo implementare il flusso a 2 passi documentato nella
documentazione ufficiale. Questo flusso prevede che l'utente inserisca i campi richiesti
dalla registrazione e confermi il corretto inserimento dell'indirizzo email tramite una email di
conferma.
Faremo i primi passi assieme e lascieremo la finalizzazione come esercizio.
Per prima cosa dobbiamo configurare una variabile nella nostra configurazione per decidere per quanto tempo lasciamo la possibilità agli utenti di finalizzare la registrazione, 7 giorni è un buon valore.
Apriamo catalogo/settings.py
ed inseriamo:
ACCOUNT_ACTIVATION_DAYS = 7
Visto che la conferma dell'iscrizione viene inviata tramite email e che configurare un server di posta
ci potrerebbe via del tempo utile configuriamo Django per usare un sistema di invio email che stampi
il contenuto delle email in console. Dovremmo vedere le email inviate nella stessa shell in cui
facciamo girare il server di sviluppo con runserver
.
Sempre in catalogo/settings.py
aggiungiamo:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
Quindi colleghiamo le viste di django-registration
in catalogo/urls.py
:
urlpatterns = [
path('admin/', admin.site.urls),
path('corsi/', include('corsi.urls')),
path('accounts/', include('django_registration.backends.activation.urls')),
path('accounts/login/', auth_views.LoginView.as_view(), name='login'),
path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'),
]
django-registration
richiede diversi template da implementare, creiamo nella nostra directory dei
templates una directory per contenerli:
mkdir templates/django_registration
Ora manca solo creare i template; segui la documentazione ufficiale per sapere quali creare, a cosa servono e quali variabili ricevono.
Dopo aver creato i templates puntando il browser all'indirizzo http://127.0.0.1:8000/accounts/register/ possiamo testare il processo di registrazione.
Una volta che tutto funziona aggiorniamo i nostri progressi su git:
git add catalogo templates
git commit -m "Aggiunta registrazione"
git push origin main
Esercizi
Se vuoi saperne di più sull'invio delle email in Django consulta la documentazione e anche quella specifica dei test.
Form più belli
Per fare la nostra applicazione se non più bella almeno più ordinata esteticamente useremo il framework css Bootstrap 4 integrato in django-crispy-forms.
django-crispy-forms è una applicazione Django che offre sia un rendering dei form che sfrutta dei framework css come Bootstrap che una api per creare dei form programmaticamente.
Per prima cosa installiamo django-crispy-forms:
pip install django-crispy-forms
Ogni volta che aggiungiamo una dipendenza dobbiamo aggiornare il nostro file requirements.txt
:
pip freeze > requirements.txt
Ora aggiungiamo django-crispy-forms nella INSTALLED_APPS
in catalogo/settings.py
:
INSTALLED_APPS = [
...
'crispy_forms',
]
Quindi sempre in catalogo/settings.py
configuriamo django-crispy-forms per usare l'integrazione con
Bootstrap 4:
CRISPY_TEMPLATE_PACK = 'bootstrap4'
Scarichiamo Bootstrap 4.6.0 ed estriamo il contenuto del pacchetto zip. Bootstrap 4 ha come dipendenza jQuery quindi scarichiamo l'ultima versione slim disponibile, al momento jQuery 3.6.0.
Nel nostro progetto invece andiamo a creare la directory che conterrà i file statici condivisi dalla nostra applicazione:
mkdir static
mkdir static/css static/js
Copiamo quindi i file css/bootstrap.min.css
e js/bootstrap.bundle.min.js
dalla directory di Boostrap
rispettivamente in static/css
e in static/js
. Copiamo jquery-3.6.0.slim.min.js
in static/js
.
La nostra directory static
apparirà così:
static
├── css
│ └── bootstrap.min.css
└── js
├── bootstrap.bundle.min.js
└── jquery-3.6.0.slim.min.js
Ora dobbiamo configurare Django per dirgli di cercare i nostri file statici nella directory static
,
creiamo il seguente vicino alla configurazione STATIC_URL
in catalogo/settings.py
:
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
STATIC_URL
indica il path delle richieste che Django deve interpretare come file statici.
Ora che abbiamo configurato i file statici andiamo per prima cosa ad aggiornare il nostro template
di base presente in corsi/templates/corsi/base.html
per includere i file CSS e JavaScript di
Bootstrap:
<html>
<head>
<title>{% block title %}Corsi{% endblock %}</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<script src="/static/js/jquery-3.6.0.slim.min.js"></script>
<script src="/static/js/bootstrap.bundle.min.js"></script>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-12">
{% if user.is_authenticated %}<p><a href="{% url 'logout' %}">Logout</a></p>{% endif %}
{% block content %}{% endblock %}
</div>
</div>
</div>
</body>
</html>
Visto che ci siamo abbiamo anche creato dei container con delle classi di Bootstrap perché siano più presentabili.
Se puntiamo il browser sulla nostra lista dei corsi dovremmo già poter vedere delle differenze.
La prima modalità di utilizzo di django-crispy-forms è quella tramite il filtro crispy
.
Lo andremo ad usare nel template di login templates/registration/login.html
per sostituire il
layout tabellare fatto a mano:
{% extends "corsi/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
{% if form.errors %}
<p>I tuoi username e password non sembrano corretti. Per favore riprova.</p>
{% endif %}
{% if next %}
{% if user.is_authenticated %}
<p>Il tuo account non ha accesso a questa pagina. Per continuare esegui il login con un account
che ha i permessi necessari.</p>
{% else %}
<p>Per favore esegui il login per vedere questa pagina.</p>
{% endif %}
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" value="login">
<input type="hidden" name="next" value="{{ next }}">
</form>
{% endblock %}
crispy
è un filtro perché usa la sintassi con il pipe (|
) per prendere il form come suo
parametro. I templatetags forniti dalle applicazioni sono caricati manualmente tramite load
.
Controlliamo dal browser come si renderizza il form, decisamente in modo migliore!
Un'altra modalità di uso di django-crispy-forms è quella tramite il tag crispy
che permette di
renderizzare il form completamente, compresi i tag <form>
, il token csrf ed il bottone di invio.
Questa modalità richiede un intervento sulla classe del form, andiamo a modificare CorsoForm
aprendo
il file corsi/forms.py
:
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from django import forms
from corsi.models import Corso
class CorsoForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_id = "corso-form"
self.helper.form_method = "post"
self.helper.form_action = ""
self.helper.add_input(Submit('submit', 'Salva'))
class Meta:
model = Corso
fields = ["titolo", "descrizione", "categoria", "docenti"]
In questo modo abbiamo specificato di renderizzare il form con id corso-form
, di inviare il form
come POST
sulla stessa url tramite un botton Salva
.
Per renderizzare il form modifichiamo il template corsi/templates/corsi/corso_form.html
per usare
il tag crispy
:
{% extends "corsi/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
{% crispy form %}
{% endblock %}
Controlliamo dal browser come si renderizza il form di modifica dei corsi.
Salviamo i nostri progressi in git:
git add static catalogo templates corsi requirements.txt
git commit -m "Usiamo crispy-forms e Bootstrap 4 per fare il rendering dei forms"
git push origin main
Esercizi
Converti il form di registrazione per vedere la differenza che fa il rendering di django-crispy-forms.
Consulta la documentazione sui file statici.
Leggi la documentazione di django-crispy-forms sul filtro crispy e sul tag crispy.
Upload dei file
Dopo aver visto come gestire i file statici del nostro progetto ora andiamo a vedere come è possibile
gestire file dinamici caricati dagli utenti. In questa sezione vogliamo estendere il modello Corso
per poter caricare un pdf contenente la brochure del corso.
Per prima cosa andiamo a creare nella directory principale del progetto la directory che conterrà i file caricati:
mkdir uploads
Quindi andiamo a configurare in catalogo/settings.py
il path alla directory che abbiamo appena
creato come MEDIA_ROOT
ed il path da prefissare nelle url dai quali servire i file caricati come
MEDIA_URL
:
MEDIA_ROOT = BASE_DIR / 'uploads'
MEDIA_URL = '/media/'
Per poter accedere ai file caricati in sviluppo possiamo usare l'helper static
che servirà i file
presenti in MEDIA_ROOT
alle url prefissate da MEDIA_URL
aggiungendolo alle url in
catalogo/urls.py
:
from django.conf import settings
from django.conf.urls.static import static
...
urlpatterns = [
...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
static
funziona esclusivamente quando DEBUG
è attivato nella nostra configurazione.
Una volta configurato il nostro progetto per gestire i file caricati dagli utenti andiamo a modificare
il modello Corso
aggiungendo il nuovo campo brochure
di tipo FileField
in corsi/models.py
:
class Corso(models.Model):
titolo = models.CharField(max_length=100)
descrizione = models.TextField()
categoria = models.ForeignKey(Categoria, null=True, blank=True, on_delete=models.PROTECT)
docenti = models.ManyToManyField(User)
brochure = models.FileField(upload_to="corsi/corso/brochure/", null=True, blank=True)
creato = models.DateTimeField(auto_now_add=True)
aggiornato = models.DateTimeField(auto_now=True)
def get_absolute_url(self):
return reverse("corsi-detail", args=[self.pk])
def __str__(self):
return self.titolo
class Meta:
verbose_name_plural = "Corsi"
Per il campo brochure
andiamo a configurare il parametro upload_to
per specificare il path dentro
a MEDIA_ROOT
nel quale salvare i file caricati; abbiamo usato corsi/corso/brochure
per identificare
rispettivamente l'applicazione, il modello e quindi il campo dove abbiamo associato il file caricato.
Rendiamo il campo nullable perché lo stiamo aggiungendo quando abbiamo già delle istanze di Corso in
database.
Quindi creiamo la migrazione ed applichiamola:
python3 manage.py makemigrations
python3 manage.py migrate
Il prossimo passo consiste nell'aggiornare il form del corso. Aggiorniamo CorsoForm
in
corsi/forms.py
:
class CorsoForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_id = "corso-form"
self.helper.form_method = "post"
self.helper.form_action = ""
self.helper.attrs = {"enctype": "multipart/form-data"}
self.helper.add_input(Submit('submit', 'Salva'))
class Meta:
model = Corso
fields = ["titolo", "descrizione", "categoria", "docenti", "brochure"]
Abbiamo modificato il form per permettere di modificare il nuovo campo brochure
aggiungendolo in
fields
e abbiamo aggiunto tramite l'attributo attrs
dell'istanza di FormHelper
l'attributo
enctype
con valore multipart/form-data
in modo da permettere l'upload di file in aggiunti ai normali
campi del form.
Dopo il form aggiorniamo anche il template di dettaglio del corso per visualizzare il valore del nuovo
campo brochure
aggiornando il file corsi/templates/corsi/corso_detail.html
:
{% extends "corsi/base.html" %}
{% block content %}
<h2>Corso: {{ corso }}</h2>
<p><a href="{% url 'corsi-update' corso.pk %}">Modifica</a></p>
<p>Categoria: {{ corso.categoria }}</p>
<p>Docenti: {% for docente in corso.docenti.all %}{{ docente.username }}{% endfor %}</p>
<p>Descrizione: {{ corso.descrizione }}</p>
<p>Brochure: {% if corso.brochure %}
<a href="{{ corso.brochure.url }}">Scarica</a>
{% else %}
Nessuna brochure disponibile
{% endif %}</p>
<p><a href="{% url 'corsi-list' %}">Torna alla lista</a></p>
{% endblock %}
In questo caso stiamo mostrando un link al file quando presente o in alternativa un messaggio di assenza
del file. Facciamo attenzione al fatto che viene valutata la presenza del file tramite l'attributo
brochure
mentre l'url è disponibile come attributo url
solo quando il campo ha un file associato.
Possiamo verificare di poter caricare una brochure puntando il nostro browser all'indirizzo http://127.0.0.1:8000/corsi/corsi e aggiungendo od aggiornando un corso.
Dal momento che i file caricati dagli utenti non devono essere salvati in git, aggiungiamo la directory
uploads
ai file che git deve ignorare:
echo "uploads/" >> .gitignore
Infine aggiorniamo i nostri progressi su git e GitHub:
git add corsi catalogo .gitignore
git commit -m "Aggiungiamo il campo brochure a Corso"
git push origin main
Esercizi
Leggi la documentazione delle configurazioni MEDIA_ROOT e MEDIA_URL.
Leggi la reference del campo FileField e un articolo specifico sulla gestione dei file in Django.
Consulta la documentazione per scoprire come è implementato l'upload dei file.
Estendiamo l'applicazione corsi
Un ultimo tocco per concludere la nostra applicazione corsi
sarà quello di aggiungere un
sistema di commenti (semplificato!) per permettere agli utenti autenticati di commentare uno specifico
corso.
Il sistema di commenti deve proporre un form nella pagina di dettaglio di un corso per poter inserire un commento. Di ogni commento vogliamo tracciarne l'autore e vogliamo visualizzare i commenti ordinati in modo decrescente per data di inserimento. I commenti non devono poter essere cancellati, ma possono essere aggiornati dal loro autore.
Questo sistema può essere implementato usando le competenze acquisite fino ad ora, si raccomanda la lettura di questo capitolo per creare una esperienza utente migliore e di questo per scoprire come aggiungere altre variabili nel contesto della vista di dettaglio.
Una volta implementata la funzionalità vanno ovviamente aggiunti i test e salvati i progressi in git.
Estendiamo il nostro progetto
In questo capitolo dedicato all'autoapprendimento saranno proposti degli esercizi per fare pratica su quanto visto finora. Non mancheranno degli spunti per aggiungere qualche nuova conoscenza.
Gestione dei docenti
L'applicazione corsi permette a qualsiasi utente registrato di essere inserito come docente di un corso. Vogliamo creare un sistema più raffinato per separare i docenti dal resto degli utenti registrati.
Creiamo una nuova applicazione chiamata profili per gestire un nuovo modello Profilo da mettere in relazione 1-a-1 tramite un campo OneToOneField al modello User.
Nel modello Profilo vogliamo tracciare:
- il paese di provenienza
- un flag per distinguere i docenti dal resto degli utenti
Per avere una lista dei paesi si può sfruttare il pacchetto
pycountry da usare in combinazione con il parametro
choices di un campo CharField
.
Ogni utente può accedere tramite delle viste a dei form di inserimento e di modifica del proprio profilo. Da questi form però è escluso il flag per distinguere i docenti dagli utenti normali. Questo flag può essere settato solo tramite l'interfaccia di amministrazione.
Una volta che gli utenti possono essere profilati come docenti dobbiamo aggiornare l'applicazione
corsi per limitare la scelta dei docenti selezionabili tramite il parametro
limit_choices_to del campo ManyToManyField
a solo quelli che hanno il flag attivato.
Ricordati di testare modelli, form e viste e di salvare i tuoi progressi su git.
Una homepage
Ora che abbiamo due applicazioni nel nostro progetto farebbe comodo avere anche una homepage per avere tutto a portata di mano.
Creiamo una nuova applicazione homepage che contenga una vista da usare come homepage per il nostro progetto.
Questa vista deve contenere:
- un link alla pagina del proprio profilo per poterlo modificare o creare
- un link alla lista dei corsi
- la lista di tutti gli username degli utenti presenti in database indicando inoltre se sono docenti
Ricordati di testare la vista e di salvare i tuoi progressi su git.
Approfondimenti
In questo capitolo affronteremo tutti quegli argomenti utili da conoscere ma che non è detto che tutti affronteranno nel corso della loro esperienza sviluppando applicazioni Django.
Per quanto possibile ogni sezione può essere affrontata separatamente dalle altre.
Altri accessi al database
In alcuni casi potrebbe far comodo avere un accesso più diretto al database, per esempio quando abbiamo delle query veramente complicate che non riusciamo ad esprimere in modo efficiente tramite l'ORM. Oppure se stiamo facendo il porting di un applicativo verso Django potrebbe far comodo riusare, si spera temporaneamente, la collezione di query SQL già in nostro possesso.
Un altro caso in cui ci si potrebbe trovare è quello di dover accedere ad database legacy per poter reperire dei dati che dobbiamo ancora portare a del nuovo codice. Oppure potrebbe capitare di dover leggere dei dati elaborati da un'altra applicazione e non aver altro modo di accedere oltre al database.
Eseguire query SQL
Potrebbe capitare di non riuscire ad esprimere delle query tanto efficienti come quelle espresse scrivendo l'SQL a mano. In questi casi è possibile comunque poter riusare la connessione al database stabilita da Django. Questi casi dovrebbero essere limitati in casi in cui ogni eventuale input è validato e considerato sicuro, un caso d'uso ad esempio è per sistemi di analisi dati in cui su grosse moli le query efficienti fanno la differenza.
Possiamo aprire una shell di Django e provare il seguente codice:
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("SELECT * FROM corsi_corso WHERE titolo = %s", ["titolo"])
rows = cursor.fetchall()
for row in rows:
print(rows)
fetchall()
restituisce tutte le righe risultanti dalla precedente query sottoforma di tuple:
((1, 'titolo', 'descrizione', datetime.datetime(2021, 4, 10, 15, 16, 22, 524469), datetime.datetime(2021, 4, 10, 15, 16, 22, 524499), 1),)
Si rimanda alla documentazione sulle query SQL per vedere tutti i metodo messi a disposizione dalle istanze di cursor
.
L'API non è dissimile da quella che si trova usando direttamente i driver del database.
Usare più di un database
Integrarsi con altre applicazioni tramite l'utilizzo dello stesso database non è il modo preferibile per farlo, un cambio al database da una parte potrebbe spaccare un'applicazione dall'altra. Far esporre una API da un lato è preferibile. Detto questo può capitare di dover accedere ad altri database e poterlo fare comodamente ha certamente dei vantaggi.
Django permette di configurare più di un database molto facilmente, aggiungendo delle ulteriori chiavi
rispetto a default
, come ad esempio:
DATABASES = {
'default': {
'NAME': 'database',
'ENGINE': 'django.db.backends.mysql',
'USER': 'mysql',
'PASSWORD': 'password'
},
'vecchio_database': {
'NAME': 'vecchio_database',
'ENGINE': 'django.db.backends.mysql',
'USER': 'altroutente',
'PASSWORD': 'altrapassword'
}
}
Semplicemente configurando la connessione ad un database possiamo scrivere direttamente le nostre query usando quel database:
from django.db import connections
with connections["vecchio_database"].cursor() as cursor:
...
Usare modelli non gestiti da Django
In altri casi potrebbe far comodo comunque usare le interfaccie più di alto livello di Django come i modelli con tabelle che non vogliamo gestire dalla nostra applicazione.
Django offre il comando inspectdb
che permette di generare automaticamente dei modelli facendo
introspezione di un database. Ad esempio se avessimo una precedente versione della nostra applicazione
per gestire il catalogo dei corsi potremmo creare una applicazione corsi_legacy
e generare il file
models.py
col seguente comando:
python3 manage.py inspectdb --database vecchio_database > models.py
inspectdb
potrebbe non riuscire a recuperare tutti i campi in modo fedele perciò sarò comunque
necessario un processo manuale di verifica.
Per usare un database specifico per popolare un Queryset bisogna specificare il database usando il
metodo using()
:
Corso.objects.using("vecchio_database").all()
Per i metodi delle istanze invece come save
e delete
è implicito perché verrà usato lo stesso
database dal quale è stato recuperato. È comunque possibile passarlo esplicitamente:
corso = Corso.objects.using("vecchio_database").get(titolo="titolo")
corso.save(using="vecchio_database")
corso.delete(using="vecchio_database")
Esercizi
Consulta la documentazione sull'uso di molteplici database.
Leggi la documentazione sull'usare database non gestiti.
Consulta la documentazione di inspectdb per vedere le sue opzioni.
Autenticazione con LDAP
Usare LDAP per l'autenticazione ad un sito interno permette di configurare utenti e permessi in un solo posto.
L'applicazione che possiamo usare per integrare LDAP nel nostro progetto Django si chiama django-auth-ldap. django-auth-ldap dipende da python-ldap che a sua volta dipende dalle librerie OpenLDAP che dobbiamo installare nel nostro sistema.
In un sistema Debian / Ubuntu possiamo installare tutto quello che ci serve con questo comando:
sudo apt install build-essential python3-dev libldap2-dev libsasl2-dev
Quindi possiamo procedere ad installare i nostri pacchetti:
pip install python-ldap django-auth-ldap
Per usare esclusivamente l'autenticazione ed ereditare completamente i permessi da LDAP dobbiamo
configurare nel nostro settings AUTHENTICATION_BACKENDS
per usare esclusivamente il backend
di autenticazione fornito django-auth-ldap:
AUTHENTICATION_BACKENDS = [
'django_auth_ldap.backend.LDAPBackend',
]
Se invece vogliamo gestire i permessi individualmente usando il supporto di Django dobbiamo
comunque mantenere il backend di autenticazione ModelBackend
:
AUTHENTICATION_BACKENDS = [
'django_auth_ldap.backend.LDAPBackend',
'django.contrib.auth.backends.ModelBackend',
]
Per configurare l'integrazione con il server LDAP per prima cosa bisogna specificare l'indirizzo:
AUTH_LDAP_SERVER_URI = "ldap://ldap.example.com"
Quindi bisogna autenticarsi settando AUTH_LDAP_USER_SEARCH
in questo modo diremo a Django di cercare
gli utenti nella organisational unit (ou) users
sotto il distinguished name example.com
cercando
il valore che useremo come username al login:
import ldap
from django_auth_ldap.config import LDAPSearch
AUTH_LDAP_USER_SEARCH = LDAPSearch(
'ou=users,dc=example,dc=com',
ldap.SCOPE_SUBTREE,
'(uid=%(user)s)',
)
Discorso analogo vale per i gruppi configurabili tramite AUTH_LDAP_GROUP_SEARCH
:
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
'ou=django,ou=groups,dc=example,dc=com',
ldap.SCOPE_SUBTREE,
'(objectClass=groupOfNames)',
)
Trovati gli utenti dobbiamo mapparli con i modelli già presenti in Django tramite
AUTH_LDAP_USER_ATTR_MAP
:
AUTH_LDAP_USER_ATTR_MAP = {
'first_name': 'givenName',
'last_name': 'sn',
'email': 'mail',
}
In questo esempio stiamo mappando tramite AUTH_LDAP_USER_ATTR_MAP
i campi del modello User
first_name
, last_name
ed email
rispettivamente sui campi LDAP givenName
, sn
e mail
.
Infine dobbiamo mappare i gruppi con gli attributi dei nostri utenti tramite
AUTH_LDAP_USER_FLAGS_BY_GROUP
:
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
'is_active': 'cn=active,ou=django,ou=groups,dc=example,dc=com',
'is_staff': 'cn=staff,ou=django,ou=groups,dc=example,dc=com',
'is_superuser': 'cn=superuser,ou=django,ou=groups,dc=example,dc=com',
}
Con questa configurazione stiamo mappando l'oggetto con common name (cn) active
sotto
django.groups
con il flag is_active
, staff
con il flag is_staff
e superuser
con il flag
is_superuser
.
Sono disponibili ovviamente ulteriori configurazioni e si rimanda alla documentazione ufficiale di django-auth-ldap.
Esercizi
Consulta la documentazione sulla gestione dei permessi.
Autenticazione e permessi
Come abbiamo visto esiste già in Django una applicazione per gestire gli utenti chiamata
auth
in django.contrib
.
Ogni istanza di utente ha tre flag:
is_active
, che serve per definire se un utente è attivo o meno. Quando non è flaggato l'utente non viene autenticato e quindi non può completare il login.is_staff
, serve per abilitare o meno ad un utente l'accesso al sito di admin.is_superuser
, abilita qualsiasi permesso senza assegnarli esplicitamente, un flag da usare con parsimonia.
Oltre agli utenti l'applicazione ci mette a disposizione altri due modelli interessati: Group
per
gestire gruppi di utenti e Permission
per implementare un sistema di permessi.
I gruppi permetto di raggruppare gli utenti e ad ogni gruppo possono essere associati dei permessi. Associare i permessi ai gruppi e non direttamente ai singoli utenti permette di mantenere un progetto più ordinato.
I permessi sono legati ad una specifica azione da compiere su uno specifico modello, per ogni modello abbiamo 4 permessi: permesso di inserire una nuova istanza, permesso di cambiare una istanza, permesso di cancellare una istanza e permesso di visualizzare una istanza.
Prendiamo confidenza questi concetti tramite l'admin, prova a creare un utente staff ma non superuser e vedi come reagisce l'admin a seconda dei permessi che vengono assegnati.
Nel caso siano necessari dei permessi custom è possibile crearli tramite l'attributo Meta
di un
modello come descritto nella
documentazione.
I permessi possono essere validati dalla nostra applicazione tramite il metodo has_perm
del modello
utente. Ad esempio usando il modello Corso
della nostra applicazione corsi
dovremmo usare:
user.has_perm('corsi.add_corso')
, per controllare che un utente possa creare unCorso
user.has_perm('corsi.change_corso')
, per controllare che un utente possa modificare unCorso
user.has_perm('corsi.delete_corso')
, per controllare che un utente possa eliminare unCorso
user.has_perm('corsi.view_corso')
, per controllare che un utente possa visualizzare unCorso
Controlli nelle viste
Per le nostre viste fatte a classi esistono un paio di mixin che possiamo usare per limitare comodamento l'accesso ad una vista solo agli utenti che rispettano alcuni requisiti.
Possiamo limitare una vista agli utenti che passano un controllo specifico usando UserPassesTestMixin
ed implementando il metodo test_func
.
Ad esempio possiamo limitare una vista solo agli utenti che sono attivi e che sono staff:
from django.contrib.auth.mixins import UserPassesTestMixin
class CorsoCreateView(UserPassesTestMixin, CreateView):
model = Corso
form_class = CorsoForm
def test_func(self):
utente = self.request.user
return utente.is_active and utente.is_staff
Nel caso l'utente non sia loggato self.request.user è una istanza di
AnonymousUser
che offre la stessa interfaccia di un utente loggato
Allo stesso possiamo usare il mixin PermissionRequiredMixin
per limitare l'accesso ad una vista solo
agli utenti che hanno dei permessi attivi specificando l'attributo permission_required
:
from django.contrib.auth.mixins import PermissionRequiredMixin
class CorsoCreateView(PermissionRequiredMixin, CreateView):
model = Corso
form_class = CorsoForm
permission_required = ('corsi.add_corso',)
Usare un altro modello utente
auth
permette una buona dose di personalizzazione permettendo anche di sostituire
il modello usato per l'utente. Può essere una buona idea usare un modello specifico quando si fanno
progetti nuovi e si hanno esigenze particolari, ad esempio richieste di performance estreme, per cui
non vogliamo pagare il costo di una join SQL per recuperare dati accessori per l'utente.
In questi casi esiste una
ricca documentazione ufficiale da
consultare.
Esercizi
Leggi la documentazione ufficiale del sistema di autenticazione.
Leggi la documentazione di UserPassesTestMixin.
Leggi la documentazione di PermissionRequiredMixin.
Validazione nei form
L'interfaccia dei form di Django offre diversi metodi per validare i dati inseriti in un form. Il primo metodo è quello di usare un validatore specifico su un singolo campo. Ipotizziamo di voler creare un form per implementare una ricerca e di voler validare che la query contenga un numero minimo di caratteri. Il nostro codice potrebbe assomigliare al seguente:
from django import forms
from django.core.exceptions import ValidationError
def almeno_tre_caratteri(value):
if len(value) < 3:
raise ValidationError("La query deve essere di almeno 3 caratteri")
class RicercaForm(forms.Form):
query = forms.CharField(validators=[almeno_tre_caratteri])
Abbiamo creato un Form
con un singolo campo query
che viene validato dalla funzione
almeno_tre_caratteri
che in caso il valore non abbiamo sia lungo almeno 3 caratteri alza l'eccezione
ValidationError
con un messaggio di errore. Come possiamo notare il validatore non è altro che una
semplice funzione che prende il valore del campo come parametro.
Per verificare che il nostro form ed il nostro validatore funzionino a dovere dobbiamo scrivere dei test come i seguenti:
from corsi.forms import RicercaForm, almeno_tre_caratteri
class RicercaFormTestCase(TestCase):
def test_validatore_alza_eccezione_con_meno_di_3_caratteri(self):
with self.assertRaisesMessage(ValidationError, "La query deve essere di almeno 3 caratteri"):
almeno_tre_caratteri("ab")
def test_validatore_non_alza_eccezione_con_almeno_3_caratteri(self):
self.assertIsNone(almeno_tre_caratteri("abc"))
def test_form_valida_campo_query(self):
form = RicercaForm({"query": "abc"})
self.assertTrue(form.is_valid())
def test_form_restituisce_errore_con_query_minore_di_3_caratteri(self):
form = RicercaForm({"query": "ab"})
self.assertFalse(form.is_valid())
def test_form_restituisce_errore_senza_dati(self):
form = RicercaForm({})
self.assertFalse(form.is_valid())
In questi stiamo testando che il validatore validi correttamente la stringa passata e in caso di
errore restituisca il messaggio che ci aspettiamo. Validiamo invece che il form applichi il validatore
e cambi il valore restituito dal metodo is_valid()
.
Django offre altri due metodo per validare un form: il metodo clean()
del form e i metodi
clean_<nomecampo>()
per validare ogni singolo campo. A differenza del sistema col validatore questi
metodi permettono di cambiare il contenuto del campo che stanno controllando.
Possiamo reimplementare il form precedente validando il singolo campo in questo modo:
class RicercaForm(forms.Form):
query = forms.CharField()
def clean_query(self):
query = self.cleaned_data["query"]
if len(query) < 3:
raise ValidationError("La query deve essere di almeno 3 caratteri")
return query
Possiamo notare che ora dobbiamo sempre restituire qualcosa che verrà salvato nella chiave query
di cleaned_data
al termine della validazione.
A differenza del metodo clean
per il singolo campo, quello generico del form ha visione su tutti i
campi del form e quindi è possibile validare un campo in relazione agli altri.
class RicercaForm(forms.Form):
query = forms.CharField()
def clean(self):
super().clean()
query = self.cleaned_data.get("query", "")
if len(query) < 3:
raise ValidationError("La query deve essere di almeno 3 caratteri")
Qui dobbiamo fare attenzione ad una cosa importante. A differenza dei due metodi precedenti in questo
caso non possiamo dare per scontato che i campi dei form siano valorizzati. Infatti stiamo usando la
stringa vuota ""
come default nel caso che il campo query
non sia definito. Per il metodo clean
non è obbligatorio restituire il valore di cleaned_data
se non abbiamo cambiato il contenuto da quello
che ha settato la chiamata al metodo clean()
di default.
Esercizi
Leggi la documentazione sui validatori e quale validatore avremmo potuto usare al posto di implementarne una nostra versione.
Leggi la documentazione ufficiale sulla validazione dei form.
Internazionalizzazione e Localizzazione
Internazionalizzazione e localizzazione su Django sono gestiti separatamente e governati rispettivamente
dalla configurazione USE_I18N
e USE_L10N
nei settings. Entrambi sono abilitati per default.
Internazionalizzazione
Per abilitare la traduzione su una lingua diversa per ogni sessione utente è necessario abilitare il
middleware LocaleMiddleware
, che andremo ad aggiungere alla configurazione MIDDLEWARE
nel file
settings.py
:
MIDDLEWARE = [
...
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
Questo middleware deve essere inserito tra quello che gestisce la sessione SessionMiddleware
e
CommonMiddleware
. Il middleware si occuperà di settare nella request
passata ad ogni vista
la variable LANGUAGE_CODE
contenente la lingua corrente per la sessione dell'utente.
Sempre in settings.py
è possibile configurare la lingua di default del sistema settando
LANGUAGE_CODE.
Ad esempio se vogliamo usare l'italiano per default:
LANGUAGE_CODE = 'it-it'
Le lingue per cui abilitiamo la traduzione devono essere listate esplicitamente nella configurazione
LANGUAGES
:
from django.utils.translation import gettext_lazy as _
LANGUAGES = [
('it', _('Italian'),
('en', _('English'),
]
Per far cambiare la lingua agli utenti Django offre una vista già implementata chiamata set_language
che richiede il codice della lingua passato in POST nella variabile language
.
Per essere usata basta includere le seguenti urls nel nostro progetto, ad esempio sotto il percorso
i18n
:
urlpatterns = [
...
path('i18n/', include('django.conf.urls.i18n')),
]
In questo caso la vista sarà disponibile al percorso /i18n/setlang/. Si rimanda alla documentazione di set_language per maggiori dettagli.
Lo stato delle traduzioni per le varie lingue è disponibile su transifex.
Traduzioni nelle viste
Il sistema di traduzioni di Django è un wrapper sopra alle funzioni di GNU gettext.
La funzione base per tradurre una stringa è gettext
che è consuetudine importare come _
:
from django.http import HttpResponse
from django.utils.translation import gettext as _
from django.views import View
class HomepageView(View):
def get(self, request):
return HttpResponse(_("This is the homepage"))
Traduzioni lazy
Le stringhe possono essere tradotte in modo lazy cioè tradotte nel momento specifico in cui sono
renderizzate e non quando sono definite. Queste tipo di traduzioni sono necessarie in alcuni punti
come negli attributi help_text e verbose_name dei campi dei modelli, negli attributi verbose_name
e verbose_name_plural nella classe Meta dei modelli e nel file settings.py
.
Le traduzioni di tipo lazy si fanno usando la funzione gettext_lazy
:
from django.db import models
from django.utils.translation import gettext_lazy as _
class Corso(models.Model):
titolo = models.CharField(help_text=_('Titolo'))
Template
Le traduzioni nei template vengono fatte tramite il tag translate
che opera indistintamente sia
su stringhe costanti che su variabili:
<h1>{% translate "Il titolo" %}</h1>
<h2>{% translate sottotitolo %}</h2>
Generare i file delle traduzioni
I file delle traduzioni sono cercati per default in directory chiamate locale
all'interno di ogni
applicazione configurata e in ogni directory configurata in LOCALE_PATHS.
Le directory locale
all'interno delle nostri applicazioni e quelle configurate in LOCALE_PATHS
devono essere create manualmente. È buona cosa creare una directory locale per ogni applicazione che
ha delle stringhe traducibili ed una generale per le stringhe che sono presenti nei file di progetto
o nei templates condivisi.
Per estrarre le stringhe da tradurre per una singola lingua bisogna eseguire il comando makemessages
:
django-admin makemessages -l it
In questo caso abbiamo estratto le stringhe da tradurre per la lingua italiana it
. Le stringhe vengono
estratte in file chiamati django.po
all'interno di ogni directory it
presente all'interno di ogni
directory locale
globale o della singola applicazione.
Una volta tradotti i file django.po
possono essere compilati in file django.mo
tramite il comando:
django-admin compilemessages
Localizzazione
Il sistema di localizzazione di Django permette di formattare date, orari e numeri nei template. Quando è attivato questi tipidi dato vengono formattati a seconda del linguaggio configurato nella sessione. La formattazione che inserisce il separatore per le migliaia va attivato separatamente tramite la configurazione USE_THOUSAND_SEPARATOR.
Le localizzazioni sono implementate in un file formats.py
all'interno di ogni
directory di lingua.
Nei template singoli valori possono essere localizzati o non localizzati usando i filtri localize
o unlocalize
:
{% load l10n %}
{{ value|localize }}
{{ value|unlocalize }}
Esercizi
Leggi la documentazione ufficiale sulla internazionalizzazione.
Leggi la documentazione su come settare il linguaggio preferito dagli utenti.
Consulta la documentazione sulle traduzioni lazy.
Consulta la documentazione dei comandi makemessages e compilemessages.
Leggi la documentazione ufficiale sulla localizzazione.
Menzioni
In questa sezione tratteremo brevemente degli argomenti che possono risultare utili e che eventualmente saranno promossi in una sezione separata.
Funzioni di aggregazione
Django permette di annotare ed aggregare valori nei QuerySet usando delle espressioni tramite i metodi annotate ed aggregate. Si consiglia la lettura della documentazione ufficiale sulle aggregazioni. Questi strumenti permettono di demandare più lavoro al database sfruttandone le funzioni più avanzate.
Query N+1
L'ORM di Django si presta al creare il problema delle query N+1, dove quando si itera sopra ad un
QuerySet vengono fatte altre query per ogni occorrenza.
Per sopperire a questo si possono usare rispettivamente
select_related
per i campi ForeignKey
o OneToOneField
e
prefetch_related per i
ManyToManyField
.
Modelli astratti
Ci sono dei casi in cui potrebbe far comodo ereditare classi che estendono models.Model
per riusarne
i campi definiti. Django permette questo pattern impostando nella classe Meta l'attributo
abstract.
Indici e vincoli
Django permette di aggiungere facilmente degli indici ai nostri campi tramite l'attributo db_index. Per avere più controllo sugli indici è possibile configurarli tramite indexes nella classe Meta. Sempre nella classe Meta si possono configurare dei vincoli sui campi tramite constraints.
Viste per gestire gli errori
Django offre per default delle viste che gestiscono i maggiori casi di errore ossia 403, 404 e 500.
Le viste per gli errori 403 e 404 possono essere richiamate rispettivamente tramite le eccezioni
PermissionDenied
e Http404
, mentre la vista per gli errori 500 viene richiamata automaticamente
in caso di errori a runtime non gestiti. Si rimanda alla documentazione delle
viste di errore e alla loro
customizzazione.
Paginazione
Django comprende un sistema di paginazione già integrato nelle viste a classi.
Storage dei file in cloud
Esistono applicazioni di terze parti per usare sistemi di storage diversi dal filesystem locale per salvare i file caricati dagli utenti come ad esempio django-storages che permette di salvare i file su sistemi di object-storage di diversi provider.
Comandi
Possiamo estendere il nostro progetto con ulteriori comandi rispetto a quelli forniti da Django implementando delle classi come descrive la documentazione per scrivere i propri comandi.
Azioni per l'admin
Possiamo inoltre scrivere per interagire con i nostri modelli tramite l'interfaccia di admin delle azioni.
Dei casi d'uso per le azioni potrebbe essere aggiornare qualche flag od effetture qualche ricalcolo su dei modelli specifici usando le feature dell'admin come i filtri e la ricerca per aiutarci.
Tool utili allo sviluppo
Un paio di applicazioni utili in modalità sviluppo che richiedono la configurazione DEBUG
, e quindi non
devono essere abilitati su istanze di produzione, sono
Django Debug Toolbar e
Django Silk. Django Debug Toolbar offre informazioni contestuali
nella visualizzazione di una pagina, mentre Django Silk salva tutte le richieste fatte e permette di
ispezionarle in un secondo momento.