Django Tutorial Part 9: Working with forms

Questo contenuto viene visualizzato in inglese perché non è ancora disponibile una versione tradotta nella lingua selezionata. Aiutaci a tradurre questo articolo!

 

In questo tutorial ti mostreremo come lavorare con i form HTML in Django e, in particolare, il modo più semplice per scrivere moduli per creare, aggiornare ed eliminare istanze di modelli. Come parte di questa dimostrazione, estenderemo il sito Web LocalLibrary in modo che i bibliotecari possano rinnovare libri e creare, aggiornare ed eliminare autori utilizzando i nostri moduli (anziché utilizzare l'applicazione di amministrazione).

Prerequisiti: Completare il precedente tutorial, compreso: Django Tutorial Part 8: User authentication and permissions.
Obiettivi: Comprendere come scrivere form per ottenere informazioni dagli utenti e aggiornare il database. Per capire come le generiche viste di modifica dei form basate sulla classe possono semplificare enormemente la creazione di form per lavorare con un singolo modello.

Panoramica

Un HTML Form, o modulo, è un gruppo di uno o più campi/widget su una pagina Web, che può essere utilizzato per raccogliere informazioni dagli utenti per l'invio a un server. I moduli sono un meccanismo flessibile per la raccolta dell'input dell'utente perché ci sono widget adatti per inserire molti tipi diversi di dati, tra cui caselle di testo, caselle di controllo, pulsanti di opzione, selettori di date, ecc. I moduli sono anche un modo relativamente sicuro di condividere i dati con il server, poiché ci consentono di inviare i dati nelle richieste POST con la protezione dalle falsificazioni delle richieste cross-site.

Anche se finora non abbiamo creato alcun modulo in questo tutorial, li abbiamo già incontrati nel sito di Django Admin. Ad esempio, lo screenshot seguente mostra un modulo per la modifica di uno dei nostri modelli Book, composto da un certo numero di elenchi di selezione e editor di testo.

Admin Site - Book Add

Lavorare con i moduli Web può essere complicato! Gli sviluppatori devono scrivere HTML per il modulo, convalidare e bonificare correttamente i dati immessi sul server (e possibilmente anche nel browser), ripubblicare il modulo con messaggi di errore per informare gli utenti di eventuali campi non validi, gestire i dati quando sono stati inviati correttamente e infine rispondere all'utente in qualche modo per indicare il successo. Django Forms prende molto del lavoro da tutti questi passaggi, fornendo un framework che consente di definire i form e i loro campi a livello di codice, e quindi utilizzare questi oggetti per generare il codice HTML del modulo e gestire gran parte della convalida e dell'interazione dell'utente.

In questo tutorial, ti mostreremo alcuni dei modi in cui puoi creare e lavorare con i moduli e, in particolare, in che modo le viste generiche del modulo di modifica possono ridurre in modo significativo la quantità di lavoro necessario per creare moduli da manipolare i tuoi modelli Lungo la strada, estenderemo la nostra applicazione LocalLibrary aggiungendo un modulo per consentire ai bibliotecari di rinnovare i libri della biblioteca e creeremo pagine per creare, modificare ed eliminare libri e autori (riproducendo una versione di base del modulo mostrato sopra per la modifica libri).

HTML Forms

Prima, una breve panoramica degli HTML Forms. Consideriamo un semplice form, con un singolo campo di testo per inserire il nome di alcuni "team" e la relativa etichetta associata:

Simple name field example in HTML form

Un form HTML è definito in un insieme di elementi dentro dei tag <form>...</form>, contenenti almeno un elemento input di type="submit".

<form action="/team_name_url/" method="post">
    <label for="team_name">Enter name: </label>
    <input id="team_name" type="text" name="name_field" value="Default name for team.">
    <input type="submit" value="OK">
</form>

Mentre qui abbiamo solo un campo di testo per inserire il nome del team, un modulo può avere qualsiasi numero di altri elementi di input e le loro etichette associate. L'attributo type del campo definisce quale tipo di widget verrà visualizzato. Il nome e l'ID del campo vengono utilizzati per identificare il campo in JavaScript / CSS / HTML, mentre il value definisce il valore iniziale per il campo quando viene visualizzato per la prima volta. L'etichetta del team corrispondente viene specificata utilizzando il tag label (vedere "Immettere il nome" sopra), con un campo for che contiene il valore id dell'input associato.

L'input  submit  verrà visualizzato come un pulsante (predefinito) che può essere premuto dall'utente per caricare i dati in tutti gli altri elementi di input nel modulo sul server (in questo caso, solo il team_name). Gli attributi del form definiscono il metodo HTTP utilizzato perinviare i dati e la destinazione dei dati sul server (action):

  • action: La risorsa / URL in cui i dati devono essere inviati per l'elaborazione quando viene inviato il modulo. Se questo non è impostato (o impostato su una stringa vuota), il modulo verrà inviato nuovamente all'URL della pagina corrente.
  • method: Il metodo HTTP utilizzato per inviare i dati: post o get.
    • Il metodo POST deve essere sempre utilizzato se i dati determinano una modifica al database del server, in quanto ciò può essere reso più resistente agli attacchi delle richieste di falsificazione intersito.
    • Il metodo GET deve essere utilizzato solo per form che non modificano i dati dell'utente (ad esempio un form di ricerca). È consigliato quando vuoi essere in grado di aggiungere un segnalibro o condividere l'URL.

Il ruolo del server è innanzitutto quello di rendere lo stato del form iniziale - contenente campi vuoti o prepopolati con valori iniziali. Dopo che l'utente ha premuto il pulsante di invio, il server riceverà i dati del modulo con i valori del browser Web e dovrà convalidare le informazioni. Se il modulo contiene dati non validi, il server dovrebbe visualizzare nuovamente il modulo, questa volta con i dati immessi dall'utente nei campi e nei messaggi "validi" per descrivere il problema per i campi non validi. Una volta che il server riceve una richiesta con tutti i dati del modulo validi, può eseguire un'azione appropriata (ad esempio, salvare i dati, restituire il risultato di una ricerca, caricare un file, ecc.) E quindi avvisare l'utente.

Come potete immaginare, la creazione dell'HTML, la convalida dei dati restituiti, la ri-visualizzazione dei dati inseriti con i rapporti di errore se necessario e l'esecuzione dell'operazione desiderata su dati validi possono richiedere un notevole sforzo per "avere ragione". Django rende tutto molto più semplice, eliminando parte del pesante codice ripetitivo.

Processo di Django di gestione dei form

La gestione dei form di Django utilizza tutte le stesse tecniche apprese in precedenti tutorial (per la visualizzazione di informazioni sui nostri models): la vista riceve una richiesta, esegue tutte le azioni richieste inclusa la lettura dei dati dai modelli, quindi genera e restituisce una pagina HTML ( da un modello, in cui passiamo un contesto contenente i dati da visualizzare). Ciò che rende le cose più complicate è che il server deve anche essere in grado di elaborare i dati forniti dall'utente e visualizzare nuovamente la pagina in caso di errori.

Di seguito viene mostrato un diagramma di flusso del processo di gestione delle richieste di modulo da parte di Django, a partire da una richiesta per una pagina contenente un form (mostrato in verde).

Updated form handling process doc.

Basandosi sul diagramma sopra, le cose che principalmente svolge Django nella gestione dei form sono:

  1. Mostra il modulo predefinito la prima volta che viene richiesto dall'utente.
  • Il form può contenere campi vuoti (ad es. Se stai creando un nuovo record), oppure può essere precompilato con valori iniziali (ad es. Se stai cambiando un record o hai valori iniziali predefiniti utili).
  • A questo punto, il form viene definito come non associato, poiché non è associato a nessun dato inserito dall'utente (sebbene possa avere valori iniziali).
  1. Riceve i dati da una richiesta di invio e li associa al modulo.
    • Collegare i dati al modulo significa che i dati inseriti dall'utente e gli eventuali errori sono disponibili quando è necessario visualizzare nuovamente il modulo.
  2. Pulisce e valida i dati.
    • La pulizia dei dati esegue la disinfezione dell'input (ad esempio rimuovendo i caratteri non validi che potrebbero essere utilizzati per inviare contenuto dannoso al server) e li converte in tipi coerenti di Python.
    • La convalida verifica che i valori siano appropriati per il campo (ad es. Sono nel giusto intervallo di date, non sono troppo corti o troppo lunghi, ecc.)
  3. Se i dati non sono validi, visualizza nuovamente il modulo, questa volta con tutti i valori e i messaggi di errore compilati dall'utente per i campi del problema.
  4. Se tutti i dati sono validi, eseguire le azioni richieste (ad esempio, salvare i dati, inviare e-mail, restituire il risultato di una ricerca, caricare un file, ecc.)
  5. Una volta completate tutte le azioni, reindirizza l'utente a un'altra pagina.

Django fornisce una serie di strumenti e approcci per aiutarti con le attività sopra descritte. La più fondamentale è la classe Form, che semplifica sia la generazione di moduli HTML che la pulizia / convalida dei dati. Nella prossima sezione, descriviamo come le form funzionano usando l'esempio pratico di una pagina per consentire ai bibliotecari di rinnovare i libri.

Note: Capire come si usa Form ti sarà d'aiuto quando discuteremo le classi di form più "di alto livello" di Django.

Modulo di rinnovo del libro utilizzando una Form e funzione vista

Successivamente, aggiungeremo una pagina per consentire ai bibliotecari di rinnovare i libri presi in prestito. Per fare ciò creeremo un modulo che consenta agli utenti di inserire un valore di data. Daremo il campo con un valore iniziale di 3 settimane rispetto alla data corrente (il normale periodo di prestito) e aggiungiamo alcune convalide per garantire che il bibliotecario non possa inserire una data nel passato o una data troppo lontana nel futuro. Quando è stata inserita una data valida, la scriveremo nel campo BookInstance.due_back del record corrente. L'esempio utilizzerà una vista basata sulle funzioni e una classe Form.

Le seguenti sezioni spiegano come funzionano le form le modifiche da apportare al nostro progetto LocalLibrary in corso.

Form

La classe Form è il cuore del sistema di gestione delle form di Django. Specifica i campi nel modulo, il loro layout, i widget di visualizzazione, le etichette, i valori iniziali, i valori validi e (una volta convalidati) i messaggi di errore associati ai campi non validi. La classe fornisce anche i metodi per eseguire il rendering stesso nei modelli utilizzando formati predefiniti (tabelle, elenchi, ecc.) O per ottenere il valore di qualsiasi elemento (abilitando il rendering manuale a grana fine).

Dichiarare un Form

La sintassi della dichiarazione per una form è molto simile a quella per la dichiarazione di un modello e condivide gli stessi tipi di campo (e alcuni parametri simili). Questo ha senso perché in entrambi i casi dobbiamo garantire che ogni campo gestisca i giusti tipi di dati, sia vincolato a dati validi e abbia una descrizione per la visualizzazione / documentazione. I dati del modulo sono memorizzati nel file forms.py dell'applicazione, all'interno della directory dell'applicazione. Crea e apri il file locallibrary / catalog / forms.py. Per creare un modulo, importiamo la libreria dei moduli, deriviamo dalla classe Form e dichiariamo i campi del modulo. Di seguito è riportata una classe di modulo molto semplice per il modulo di rinnovo del libro della biblioteca: aggiungi questo al nuovo file:

from django import forms
    
class RenewBookForm(forms.Form):
    renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).")

Form fields

In questo caso abbiamo un campo singolo DateField per inserire la data di rinnovo che renderizzerà in HTML con un valore vuoto, l'etichetta di default "Data di rinnovo:", e qualche utile testo di utilizzo: "Inserire una data tra ora e 4 settimane (predefinito 3 settimane)." Dato che nessuno degli altri argomenti opzionali è specificato, il campo accetterà le date usando il input_formats: YYYY-MM-DD (2016-11-06), MM/DD/YYYY (02/26/2016), MM/DD/YY (10/25/16), e sarà visualizzato con il widget standard widget: DateInput.

Esistono molti altri tipi di campi form, che in gran parte riconoscerete dalla loro somiglianza con le classi di campo del modello equivalente: BooleanField, CharField, ChoiceField, TypedChoiceField, DateField, DateTimeField, DecimalField, DurationField, EmailField, FileField, FilePathField, FloatField, ImageField, IntegerField, GenericIPAddressField, MultipleChoiceField, TypedMultipleChoiceField, NullBooleanField, RegexField, SlugField, TimeField, URLField, UUIDField, ComboField, MultiValueField, SplitDateTimeField, ModelMultipleChoiceField, ModelChoiceField.

Gli argomenti comuni alla maggior parte dei campi sono elencati di seguito (questi hanno valori predefiniti sensibili):

  • required: Se True, il campo non può essere lasciato bianco o None. I campi sono obbligatori per default, quindi se impostiamo required=False autorizzeremo dei campi blank nel form.
  • label: L'etichetta da utilizzare quando si renderizza la pagina in HTML. Se la label non viene specificata, Django ne creerà uno dal nome del campo capitalizzando la prima lettera e sostituendo gli underscore con gli spazi (ad esempio la data di rinnovo).
  • label_suffix: Di default i due punti vengono visualizzati dopo l'etichetta (ad esempio, data di rinnovo:). Questo argomento consente di specificare un suffisso diverso contenente altri caratteri.
  • initial: Il valore iniziale per il campo quando viene visualizzato il modulo.
  • widget: Il widget da mostrare.
  • help_text (come visto sopra): testo di aiuto addizionale da mostrare per spiegare l'uso di un campo del form.
  • error_messages: Una lista di messaggi di errore per il campo. Puoi sovrascrivere tali messaggi coi tuoi se necessario.
  • validators: una lista di funzioni da richiamare quando il campo viene validato.
  • localize: Abilita la localizzazione nell'input dei dati del form. Vedere il link per maggiori dettagli.
  • disabled: Il campo viene mostrato ma non è possibile validarlo, se questo attributo è True. Il valore di default è False.

Validazione

Django offre numerosi posti dove puoi convalidare i tuoi dati. Il modo più semplice per convalidare un singolo campo è sovrascrivere il metodo clean_ <nomecampo> () per il campo che si desidera controllare. Quindi, ad esempio, possiamo effettuare una convalida richiedendo che i valori di renewal_date inseriti siano compresi tra ora e le 4 settimane future implementando clean_renewal_date () come mostrato di seguito.

Aggiorniamo il file forms.py:

import datetime

from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _

class RenewBookForm(forms.Form):
    renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).")

    def clean_renewal_date(self):
        data = self.cleaned_data['renewal_date']
        
        # Check if a date is not in the past. 
        if data < datetime.date.today():
            raise ValidationError(_('Invalid date - renewal in past'))

        # Check if a date is in the allowed range (+4 weeks from today).
        if data > datetime.date.today() + datetime.timedelta(weeks=4):
            raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead'))

        # Remember to always return the cleaned data.
        return data

Ci sono due cose importanti da notare. Il primo è che otteniamo i nostri dati usando self.cleaned_data ['renewal_date'] e che restituiamo questi dati indipendentemente dal fatto che vengano modificati alla fine della funzione. Questo passaggio ci porta i dati "puliti" e disinfettati da input potenzialmente non sicuri utilizzando i validatori predefiniti e convertiti nel tipo standard corretto per i dati (in questo caso un oggetto datetime.datetime di Python).

La seconda è che se un valore è al di fuori di quelli ammessi solleviamo un errore di validazione ValidationError, specificando il testo di errore che vogliamo mostrare nel forme se un errore di validazione si è verificato. L'esempio sopra inoltre incapsula il testo in una funzione di traduzione di Django (vedi Django's translation functionsugettext_lazy() (importata come _()), che è una good practice in caso tu voglia tradurre il sito successivamente.

Note: Esistono molti metodi per validare i form, consultare Form and field validation (Django docs). Per esempio, in alcuni casi in cui si hanno diversi campi che dipendono uno dall'altro si può sovrascrivere la funzione Form.clean() e nuovamente sollevare una ValidationError.

Questo è tutto ciò di cui abbiamo bisogno per il form in questo esempio!

Configurazione URL

Prima di creare la nostra vista, aggiungiamo una configurazione URL per la pagina dei rinnovi. Copia la seguente configurazione nella parte inferiore di locallibrary / catalog / urls.py.

urlpatterns += [   
    path('book/<uuid:pk>/renew/', views.renew_book_librarian, name='renew-book-librarian'),
]

La configurazione URL reindirizzerà tutti gli URL con formato /catalog/book/<bookinstance id>/renew/ alla funzione renew_book_librarian() in views.py, e manderà l'id della BookInstance come parametro con nome pk. Il pattern matcha solamente se pk è un dato uuid correttamente formattato.

Note: Possiamo nominare qualsiasi cosa ci piaccia dai nostri dati di URL acquisiti anzichè "pk" , perché abbiamo il controllo completo sulla funzione di visualizzazione (non stiamo usando una classe di vista di dettaglio generica che si aspetta parametri con un certo nome). Tuttavia, pk abbreviazione di "chiave primaria", è una convenzione ragionevole da usare!

Vista

Come discusso in Django form handling process, la vista deve eseguire il rendering del modulo predefinito quando viene chiamato per la prima volta e quindi eseguire nuovamente il rendering con messaggi di errore se i dati non sono validi oppure elaborare i dati e reindirizzare a una nuova pagina se i dati sono validi. Per poter eseguire queste diverse azioni, la vista deve essere in grado di sapere se è stata richiamata per la prima volta per il rendering del modulo predefinito o un tempo successivo per convalidare i dati.

Per i form che usano una POST per mandare infromazioni al server, il pattern più comune è fare in modo che sia la view ad eseguire i test verso la richiesta di POST (if request.method == 'POST':) identificare le richieste di convalida del modulo e GET (utilizzando un'altra condizione) per identificare la richiesta iniziale di creazione del modulo. Se si desidera inviare i dati utilizzando una richiesta GET, un approccio tipico per identificare se questa è la prima o successiva chiamata alla vista è leggere i dati del modulo (ad esempio, per leggere un valore nascosto nel modulo).

Il processo di rinnovo del libro verrà scritto nel nostro database, quindi, per convenzione, utilizziamo l'approccio di richiesta POST. Il frammento di codice seguente mostra il modello (molto standard) per questo tipo di visualizzazione delle funzioni.

import datetime

from django.shortcuts import render, get_object_or_404
from django.http import HttpResponseRedirect
from django.urls import reverse

from catalog.forms import RenewBookForm

def renew_book_librarian(request, pk):
    book_instance = get_object_or_404(BookInstance, pk=pk)

    # Se è una richiesta di tipo POST allora processa i dati della Form
    if request.method == 'POST':

        # Crea un'istanza della form e la popola con i dati della richiesta (binding):
        form = RenewBookForm(request.POST)

        # Controlla se la form è valida:
        if form.is_valid():
            # process the data in form.cleaned_data as required (here we just write it to the model due_back field)
            book_instance.due_back = form.cleaned_data['renewal_date']
            book_instance.save()

            # reindirizza ad un nuovo URL:
            return HttpResponseRedirect(reverse('all-borrowed') )

    # Se la richiesta è GET o un altro metodo crea il form di default
    else:
        proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
        form = RenewBookForm(initial={'renewal_date': proposed_renewal_date})

    context = {
        'form': form,
        'book_instance': book_instance,
    }

    return render(request, 'catalog/book_renew_librarian.html', context)

Innanzitutto, importiamo il nostro form (RenewBookForm) e una serie di altri oggetti / metodi utili utilizzati nel corpo della funzione di visualizzazione:

  • get_object_or_404(): Ritorna uno specifico oggetto da un modello basato sulla propria chiave primaria, e solleva un'eccezione Http404 (not found) se il record non esiste. 
  • HttpResponseRedirect: Questo crea un reindirizzamento verso un URL specificato (codice di stato HTTP 302).
  • reverse(): Questo genera un URL da un nome di configurazione URL e un set di argomenti. È l'equivalente Python del tag url che abbiamo utilizzato nei nostri templates.
  • datetime: Una libreria Python per manipolare date e ore.

Nella vista, per prima cosa utilizziamo l'argomento pk in get_object_or_404 () per ottenere l'attuale BookInstance (se questo non esiste, la vista verrà immediatamente chiusa e la pagina mostrerà un errore "non trovato"). Se questa non è una richiesta POST (gestita dalla clausola else), creiamo il form predefinito che passa un valore iniziale per il campo renewal_date (come mostrato in grassetto sotto, questo è 3 settimane dalla data corrente). 

    book_instance = get_object_or_404(BookInstance, pk=pk)    

    # Se la richiesta è GET o un altro metodo crea il form di default
    else:
        proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
        form = RenewBookForm(initial={'renewal_date': proposed_renewal_date})

    context = {
        'form': form,
        'book_instance': book_instance,
    }

    return render(request, 'catalog/book_renew_librarian.html', context)

Dopo aver creato il modulo, chiamiamo render() per creare la pagina HTML, specificando il modello e un contesto che contiene il nostro form. In questo caso, il contesto contiene anche il nostro BookInstance, che utilizzeremo nel modello per fornire informazioni sul libro che stiamo rinnovando.

Tuttavia, se si tratta di una richiesta POST, allora creiamo il nostro oggetto form e lo popoliamo con i dati della richiesta. Questo processo è chiamato "binding" e ci consente di convalidare il form. Controlliamo quindi se il form è valido, eseguiamo tutto il codice di convalida su tutti i campi, compreso il codice generico per verificare che il nostro campo data sia effettivamente una data valida e la funzione clean_renewal_date() del nostro from specifico per verificare se la data è nella giusta fascia.

    # If this is a POST request then process the Form data
    if request.method == 'POST':

        # Create a form instance and populate it with data from the request (binding):
        form = RenewBookForm(request.POST)

        # Check if the form is valid:
        if form.is_valid():
            # process the data in form.cleaned_data as required (here we just write it to the model due_back field)
            book_instance.due_back = form.cleaned_data['renewal_date']
            book_instance.save()

            # redirect to a new URL:
            return HttpResponseRedirect(reverse('all-borrowed') )

    context = {
        'form': form,
        'book_instance': book_instance,
    }

    return render(request, 'catalog/book_renew_librarian.html', context)

Se il form non è valido, viene richiamata nuovamente la funzione render(), ma questa volta il form viene passato al contesto con dei messaggi di errore. 

Se il form è valido, allora possiamo iniziare ad utilizzare i dati, accedendovi tramite l'attributo form.cleaned_data (ad es. data = form.cleaned_data['renewal_date']). Qui salviamo semplicemente i dati nel valore due_back dell'oggetto BookInstance associato.

Importante: Anche se è possibile accedere ai dati del form direttamente tramite la richiesta, per esempio utilizzando request.POST['renewal_date']request.GET['renewal_date'] se invece stiamo utilizzando una richiesta GET, Ciò NON è raccomandabile. I dati ripuliti, sanificati, e validati, vengono convertiti in tipi adatti a Python.

Il passo finale nella manipolazione dei formè la direzione verso un'altra pagina, generalmente una "success" page. In questo caso utilizziamo HttpResponseRedirect e reverse() per ridirezionare alla vista nominata "all-borrowed' (creata come "challenge" in Django Tutorial Part 8: User authentication and permissions). Se non l'hai creata, ridireziona alla home page con URL '/').

Questo è tutto ciò che ci serviva per il per la manipolazione dei form, ma dobbiamo ancora restringere gli accessi dei bibliotecari. Probabilmente dovremmo creare un nuovo permesso sull'oggetto BookInstance ("can_renew"), ma, per mantenere le cose più semplici, useremo solo il decoratore di funzione @permission_required con il nostro permesso esistente can_mark_returned.

La vista finale è quindi come di seguito. Copiare questo in fondo a locallibrary/catalog/views.py.

import datetime

from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404
from django.http import HttpResponseRedirect
from django.urls import reverse

from catalog.forms import RenewBookForm

@permission_required('catalog.can_mark_returned')
def renew_book_librarian(request, pk):
    """View function for renewing a specific BookInstance by librarian."""
    book_instance = get_object_or_404(BookInstance, pk=pk)

    # If this is a POST request then process the Form data
    if request.method == 'POST':

        # Create a form instance and populate it with data from the request (binding):
        form = RenewBookForm(request.POST)

        # Check if the form is valid:
        if form.is_valid():
            # process the data in form.cleaned_data as required (here we just write it to the model due_back field)
            book_instance.due_back = form.cleaned_data['renewal_date']
            book_instance.save()

            # redirect to a new URL:
            return HttpResponseRedirect(reverse('all-borrowed') )

    # If this is a GET (or any other method) create the default form.
    else:
        proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
        form = RenewBookForm(initial={'renewal_date': proposed_renewal_date})

    context = {
        'form': form,
        'book_instance': book_instance,
    }

    return render(request, 'catalog/book_renew_librarian.html', context)

Il template

Creiamo il template referenziato nella view (/catalog/templates/catalog/book_renew_librarian.html) e copiamo il codice sotto:

{% extends "base_generic.html" %}

{% block content %}
  <h1>Renew: {{ book_instance.book.title }}</h1>
  <p>Borrower: {{ book_instance.borrower }}</p>
  <p{% if book_instance.is_overdue %} class="text-danger"{% endif %}>Due date: {{ book_instance.due_back }}</p>
    
  <form action="" method="post">
    {% csrf_token %}
    <table>
    {{ form.as_table }}
    </table>
    <input type="submit" value="Submit">
  </form>
{% endblock %}

La maggior parte di questo sarà completamente familiare dai tutorial precedenti. Estendiamo il modello di base e quindi ridefiniamo il blocco del contenuto. Siamo in grado di fare riferimento a {{book_instance}} (e alle sue variabili) perché è stato passato all'oggetto contesto nella funzione render (), e le possiamo usare per elencare il titolo del libro, il mutuatario e la data di scadenza originale.

Il codice del form è relativamente semplice. Per prima cosa dichiariamo i tag del form, specificando dove deve essere inviato il form (action) e il metodo per inviare i dati (in questo caso un "POST HTTP") - se si richiama la panoramica dei moduli HTML nella parte superiore della pagina, un'azione vuota come mostrato, significa che i dati del modulo saranno postati all'URL attuale della pagina (che è ciò che vogliamo!). All'interno dei tag, definiamo l'input di invio, che un utente può premere per inviare i dati. Il {% csrf_token%} aggiunto appena dentro i tag del modulo fa parte della protezione dalle contraffazioni di Django.

Note: Aggiungi il {% csrf_token%} a ogni modello Django che crei che utilizza POST per inviare i dati. Ciò ridurrà la possibilità che le form vengano corrotte da utenti malintenzionati

Tutto ciò che rimane è la variabile di template {{form}}, che abbiamo passato al template nel dizionario di contesto. Forse non sorprendentemente, se usato come mostrato, fornisce il rendering predefinito di tutti i campi del modulo, incluse le loro etichette, i widget e il testo della guida: il rendering è come mostrato di seguito:

<tr>
  <th><label for="id_renewal_date">Renewal date:</label></th>
  <td>
    <input id="id_renewal_date" name="renewal_date" type="text" value="2016-11-08" required>
    <br>
    <span class="helptext">Enter date between now and 4 weeks (default 3 weeks).</span>
  </td>
</tr>

Note: Forse non è ovvio perché abbiamo solo un campo, ma, per impostazione predefinita, ogni campo è definito nella propria riga della tabella. Questo stesso rendering viene fornito se si fa riferimento alla variabile del template {{form.as_table}}.

Se dovessi inserire una data non valida, avresti anche un elenco degli errori visualizzati nella pagina (mostrati in grassetto sotto).

<tr>
  <th><label for="id_renewal_date">Renewal date:</label></th>
    <td>
      <ul class="errorlist">
        <li>Invalid date - renewal in past</li>
      </ul>
      <input id="id_renewal_date" name="renewal_date" type="text" value="2015-11-08" required>
      <br>
      <span class="helptext">Enter date between now and 4 weeks (default 3 weeks).</span>
    </td>
</tr>

Altri modi di usare le variabili template

Utilizzando{{ form.as_table }} come mostrato sopra, ogni campo viene renderizzato come riga di una tabella. Puoi anche renderizzare ogni campo come elemento di una lista (usando {{ form.as_ul }} ) o come un paragrafo ({{ form.as_p }}).

È anche possibile avere il controllo completo sul rendering di ogni parte del form, indicizzando le sue proprietà mediante la notazione dei punti. Ad esempio, possiamo accedere a un numero di elementi separati per il nostro campo renewal_date:

  • {{ form.renewal_date }}: L'intero campo.
  • {{ form.renewal_date.errors }}: La lista degli errori.
  • {{ form.renewal_date.id_for_label }}: L'id della label.
  • {{ form.renewal_date.help_text }}: il testo di aiuto del campo.

Per ulteriori esempi su come eseguire il rendering manuale dei form nei modelli e ciclare dinamicamente sui campi del modello, vedere Working with forms > Rendering fields manually (Django docs).

Testare la pagina

Se hai accettato la sfida proposta in Django Tutorial Part 8: User authentication and permissions dovresti avere una lista di libri in prestito nella libreria, che saranno visibili solamente allo staff. Possiamo aggiungere un link alla nostra pagina di rinnovo per ogni elemento utilizzando il codice template sotto.

{% if perms.catalog.can_mark_returned %}- <a href="{% url 'renew-book-librarian' bookinst.id %}">Renew</a>  {% endif %}

Note: Remember that your test login will need to have the permission "catalog.can_mark_returned" in order to access the renew book page (perhaps use your superuser account).

In alternativa, puoi costruire manualmente un URL di prova come questo: http://127.0.0.1:8000/catalog/book/<bookinstance_id>/renew/ (è possibile ottenere un id valido per le librerie navigando a una pagina dei dettagli del libro nella libreria, e copiare il campo id).

Come appare?

Se hai successo, il form di default sarà così:

The form with an invalid value entered will look like this:

The list of all books with renew links will look like this:

ModelForms

La creazione di una classe Form utilizzando l'approccio descritto sopra è molto flessibile, consente infatti di creare qualsiasi tipo di pagina di form che ti piace e associarla a qualsiasi modello o modello. Tuttavia, se hai solo bisogno di un modulo per mappare i campi di un singolo modello, il tuo modello definirà già la maggior parte delle informazioni necessarie nel tuo modulo: campi, etichette, testo della guida, ecc. Invece di ricreare le definizioni del modello nel tuo forma, è più facile usare la classe helper ModelForm per creare il modulo dal tuo modello. Questo ModelForm può quindi essere utilizzato all'interno delle visualizzazioni esattamente nello stesso modo di un modulo ordinario.

Un modello di base ModelForm contenente gli stessi campi del nostro RenewBookForm è mostrato sotto. Tutto ciò che devi fare è aggiungere una classe class Meta con il modello associato (BookInstance) e un elenco dei campi del modello da includere nel modulo (è possibile includere tutti i campi utilizzando fields = '__all__', oppure è possibile utilizzare exclude (anziché i campi) per specificare i campi da non includere nel modello).

from django.forms import ModelForm

from catalog.models import BookInstance

class RenewBookModelForm(ModelForm):
    class Meta:
        model = BookInstance
        fields = ['due_back']

Note: Questo potrebbe non sembrare tutto molto più semplice del semplice utilizzo di un modulo (e non è in questo caso, perché abbiamo solo un campo). Tuttavia, se hai molti campi, puoi ridurre la quantità di codice in modo abbastanza significativo!

Il resto delle informazioni proviene dalle definizioni del campo del modello (ad esempio etichette, widget, testo della guida, messaggi di errore). Se questi non sono corretti, possiamo sostituirli nella nostra classe Meta, specificando un dizionario contenente il campo da modificare e il suo nuovo valore. Ad esempio, in questo modulo potremmo volere un'etichetta per il nostro campo di "Data di rinnovo" (piuttosto che l'impostazione predefinita in base al nome del campo: "Due Back"), e vogliamo anche che il nostro testo di aiuto sia specifico per questo caso d'uso. La Meta sotto mostra come sovrascrivere questi campi, e puoi impostare allo stesso modo widget e messaggi_errore se i valori predefiniti non sono sufficienti.

class Meta:
    model = BookInstance
    fields = ['due_back']
    labels = {'due_back': _('New renewal date')}
    help_texts = {'due_back': _('Enter a date between now and 4 weeks (default 3).')} 

Per aggiungere la convalida è possibile utilizzare lo stesso approccio di un modulo normale: si definisce una funzione denominata clean_field_name () e si generano eccezioni ValidationError per valori non validi. L'unica differenza rispetto alla nostra forma originale è che il campo del modello è denominato due_back e non "renewal_date". Questa modifica è necessaria poiché il campo corrispondente in BookInstance è chiamato due_back.

from django.forms import ModelForm

from catalog.models import BookInstance

class RenewBookModelForm(ModelForm):
    def clean_due_back(self):
       data = self.cleaned_data['due_back']
       
       # Controlla se la data non è passata.
       if data < datetime.date.today():
           raise ValidationError(_('Invalid date - renewal in past'))

       # Controlla se una data è nell'intervallo consentito (+4 settimane da oggi).
       if data > datetime.date.today() + datetime.timedelta(weeks=4):
           raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead'))

       # Ricorda di restituire sempre la data pulita.
       return data

    class Meta:
        model = BookInstance
        fields = ['due_back']
        labels = {'due_back': _('Renewal date')}
        help_texts = {'due_back': _('Enter a date between now and 4 weeks (default 3).')} 

La classe RenewBookModelForm adesso funziona come la nostra originale RenewBookForm. Puoi importarla e usarla ovunque attualmente usi  RenewBookForm a condizione che si aggiorni anche il nome della variabile corrispondente da  renewal_date a due_back come nella dichiarazione della seconda Form: RenewBookModelForm(initial={'due_back': proposed_renewal_date}.

Generic editing views

The form handling algorithm we used in our function view example above represents an extremely common pattern in form editing views. Django abstracts much of this "boilerplate" for you, by creating generic editing views for creating, editing, and deleting views based on models. Not only do these handle the "view" behavior, but they automatically create the form class (a ModelForm) for you from the model.

Note: In addition to the editing views described here, there is also a FormView class, which lies somewhere between our function view and the other generic views in terms of "flexibility" vs "coding effort". Using FormView, you still need to create your Form, but you don't have to implement all of the standard form-handling patterns. Instead, you just have to provide an implementation of the function that will be called once the submitted is known to be valid.

In this section we're going to use generic editing views to create pages to add functionality to create, edit, and delete Author records from our library — effectively providing a basic reimplementation of parts of the Admin site (this could be useful if you need to offer admin functionality in a more flexible way that can be provided by the admin site).

Views

Open the views file (locallibrary/catalog/views.py) and append the following code block to the bottom of it:

from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy

from catalog.models import Author

class AuthorCreate(CreateView):
    model = Author
    fields = '__all__'
    initial = {'date_of_death': '05/01/2018'}

class AuthorUpdate(UpdateView):
    model = Author
    fields = ['first_name', 'last_name', 'date_of_birth', 'date_of_death']

class AuthorDelete(DeleteView):
    model = Author
    success_url = reverse_lazy('authors')

As you can see, to create, update, or delete the views you need to derive from CreateView, UpdateView, and DeleteView (respectively) and then define the associated model.

For the "create" and "update" cases you also need to specify the fields to display in the form (using the same syntax as for ModelForm). In this case, we show both the syntax to display "all" fields and how you can list them individually. You can also specify initial values for each of the fields using a dictionary of field_name/value pairs (here we arbitrarily set the date of death for demonstration purposes — you might want to remove that!). By default, these views will redirect on success to a page displaying the newly created/edited model item, which in our case will be the author detail view we created in a previous tutorial. You can specify an alternative redirect location by explicitly declaring parameter success_url (as done for the AuthorDelete class).

The AuthorDelete class doesn't need to display any of the fields, so these don't need to be specified. You do however need to specify the success_url, because there is no obvious default value for Django to use. In this case, we use the reverse_lazy() function to redirect to our author list after an author has been deleted — reverse_lazy() is a lazily executed version of reverse(), used here because we're providing a URL to a class-based view attribute.

Templates

The "create" and "update" views use the same template by default, which will be named after your model: model_name_form.html (you can change the suffix to something other than _form using the template_name_suffix field in your view, e.g. template_name_suffix = '_other_suffix')

Create the template file locallibrary/catalog/templates/catalog/author_form.html and copy in the text below.

{% extends "base_generic.html" %}

{% block content %}
  <form action="" method="post">
    {% csrf_token %}
    <table>
    {{ form.as_table }}
    </table>
    <input type="submit" value="Submit">
  </form>
{% endblock %}

This is similar to our previous forms and renders the fields using a table. Note also how again we declare the {% csrf_token %} to ensure that our forms are resistant to CSRF attacks.

The "delete" view expects to find a template named with the format model_name_confirm_delete.html (again, you can change the suffix using template_name_suffix in your view). Create the template file locallibrary/catalog/templates/catalog/author_confirm_delete.html and copy in the text below.

{% extends "base_generic.html" %}

{% block content %}

<h1>Delete Author</h1>

<p>Are you sure you want to delete the author: {{ author }}?</p>

<form action="" method="POST">
  {% csrf_token %}
  <input type="submit" value="Yes, delete.">
</form>

{% endblock %}

URL configurations

Open your URL configuration file (locallibrary/catalog/urls.py) and add the following configuration to the bottom of the file:

urlpatterns += [  
    path('author/create/', views.AuthorCreate.as_view(), name='author_create'),
    path('author/<int:pk>/update/', views.AuthorUpdate.as_view(), name='author_update'),
    path('author/<int:pk>/delete/', views.AuthorDelete.as_view(), name='author_delete'),
]

There is nothing particularly new here! You can see that the views are classes, and must hence be called via .as_view(), and you should be able to recognize the URL patterns in each case. We must use pk as the name for our captured primary key value, as this is the parameter name expected by the view classes.

The author create, update, and delete pages are now ready to test (we won't bother hooking them into the site sidebar in this case, although you can do so if you wish).

Note: Observant users will have noticed that we didn't do anything to prevent unauthorized users from accessing the pages! We leave that as an exercise for you (hint: you could use the PermissionRequiredMixin and either create a new permission or reuse our can_mark_returned permission).

Testing the page

First, log in to the site with an account that has whatever permissions you decided are needed to access the author editing pages.

Then navigate to the author create page: http://127.0.0.1:8000/catalog/author/create/, which should look like the screenshot below.

Form Example: Create Author

Enter values for the fields and then press Submit to save the author record. You should now be taken to a detail view for your new author, with a URL of something like http://127.0.0.1:8000/catalog/author/10.

You can test editing records by appending /update/ to the end of the detail view URL (e.g. http://127.0.0.1:8000/catalog/author/10/update/) — we don't show a screenshot, because it looks just like the "create" page!

Finally, we can delete the page by appending delete to the end of the author detail-view URL (e.g. http://127.0.0.1:8000/catalog/author/10/delete/). Django should display the delete page shown below. Press Yes, delete. to remove the record and be taken to the list of all authors.

Challenge yourself

Create some forms to create, edit, and delete Book records. You can use exactly the same structure as for Authors. If your book_form.html template is just a copy-renamed version of the author_form.html template, then the new "create book" page will look like the screenshot below:

Summary

Creating and handling forms can be a complicated process! Django makes it much easier by providing programmatic mechanisms to declare, render, and validate forms. Furthermore, Django provides generic form editing views that can do almost all the work to define pages that can create, edit, and delete records associated with a single model instance.

There is a lot more that can be done with forms (check out our See also list below), but you should now understand how to add basic forms and form-handling code to your own websites.

See also

In this module