Web2py: creazione automatica di un form con SQLFORM

Dopo parecchio tempo, mea culpa, torniamo a web2py. Vorrei dimostrare quanto sia mostruosamente semplice creare una applicazione web con questo framework. In questo articolo vorrei essere poco teorico e molto pratico quindi passo direttamente al codice.

Dopo aver lanciato il server web2py.py e inserita una password per accedere all’interfaccia amministrativa, il browser viene aperto automaticamente all’indirizzo http://127.0.0.1:8000/welcome.
Da lì accediamo all’interfaccia amministrativa e creiamo una nuova applicazione “test”. Vedi le immagini seguenti:
welcome
crea test
applicazione test creata

A questo punto dobbiamo scrivere il codice: poichè stiamo utilizzando un pattern MVC, normalmente questo significherà scrivere 1 modello, 1 controller ed 1 vista.
E’ possibile scrivere tutto utilizzando il comodo web editor direttamente dalla pagina di modifica della applicazione; ma nulla vieta ovviamente di scrivere il codice attraverso il proprio editor preferito.

Vediamo per primo il modello, che come si vede viene chiamato di default db.py che contiene già del codice per la gestione della connessione al database e per la gestione delle autorizzazioni.

Per la connessione abbiamo:

if request.env.web2py_runtime_gae:            # if running on Google App Engine
    db = DAL('gae://mynamespace')             # connect to Google BigTable
    session.connect(request, response, db = db) # and store sessions and tickets there
    ### or use the following lines to store sessions in Memcache
    # from gluon.contrib.memdb import MEMDB
    # from google.appengine.api.memcache import Client
    # session.connect(request, response, db = MEMDB(Client()))
else:                                         # else use a normal relational database
    db = DAL('sqlite://storage.sqlite')       # if not, use SQLite or other DB

La prima parte dell’istruzione if riguarda il caso che la nostra applicazione venga caricata su GAE. La seconda parte, else, apre la connessione ad un database sqlite, assegnando all’oggetto il nome db.

Se invece volessimo connetterci ad un database MYSQL, dovremmo scrivere:

db=DAL('mysql://myuser:mypassword@www.epysoft.com/mydb')

Ci posizionamo adesso al di sotto di tutto il codice già presente in db.py e cominciamo a definire le nostre tabelle.

#una tabella fornitori con 3 campi di tipo string, lunghezza 200 caratteri; definiamo anche
#una label per ciascun campo: comparirà nel form html invece del nome del campo
db.define_table('fornitori',Field('descrizione','string',length=200,label='Fornitore'),
Field('contatto1','string',length=200,label='Contatto 1'),
Field('contatto2','string',length=200,label='Contatto 2'))

Ancora tabelle:

db.define_table('conf',Field('descrizione','string',length=100, label='Descrizione'))
db.define_table('um',Field('descrizione','string',length=10,label='UM'))
db.define_table('cat',Field('nome','string',length=50,label='Categoria'),
Field('note','string',length=250,label='Note'))
db.define_table('subcat',Field('id_cat',db.cat,label='Categoria'),Field('nome','string',length=50,label='Subcategoria'),Field('note','string',length=250,label='Note'))
db.subcat.id_cat.requires = IS_IN_DB(db, db.cat.id, '%(nome)s')

Unica cosa nuova qui è la riga 6, in essa definiamo un Validatore: richiediamo che il campo id_cat della tabella subcat faccia riferimento ad un id esistente nella tabella cat. Inoltre con l’ultima stringa chiediamo che nel form automatico che andremo a creare questo id_subcat compaia con il nome della subcategoria e non con il numero (per motivi estetici). Questo permette che nell’eventuale menu a tendina le sottocategorie appaiano con il loro nome e non con il loro id.
Continuiamo con le tabelle:

db.define_table('articoli',Field('codice','string',length=60,required=True,unique=True,label='Codice'),
Field('cod_ean','string',length=60,label='EAN'),Field('descrizione','string',length=250,label='Descrizione'),
Field('id_conf',db.conf,label='Confezione'),Field('id_um',db.um,label='UM'),Field('gramm','string',length=10,label='Grammatura'),
Field('id_fornitore',db.fornitori,label='Fornitore'),Field('data_ins','datetime',label='Data Inserimento'),
Field('note','text'),Field('id_cat',db.cat,label='Categoria'),Field('id_sub',db.subcat,label='Subcategoria'),Field('foto1','upload',autodelete=True,label='Foto 1 (alta)',uploadfolder=os.path.join(request.folder,'static/alte')),
Field('foto2','upload',autodelete=True,label='Foto 2 (bassa)',uploadfolder=os.path.join(request.folder,'static/basse')),Field('pdf1','upload',autodelete=True,label='Pdf 1',uploadfolder=os.path.join(request.folder,'static/pdf')),
Field('pdf2','upload',autodelete=True,label='Pdf 2',uploadfolder=os.path.join(request.folder,'static/pdf')),Field('file','upload',autodelete=True,label='Allegato'))

db.articoli.id_conf.requires = IS_IN_DB(db, db.conf.id, '%(descrizione)s')
db.articoli.id_um.requires = IS_IN_DB(db, db.um.id, '%(descrizione)s')
db.articoli.id_fornitore.requires = IS_IN_DB(db, db.fornitori.id, '%(descrizione)s')
db.articoli.id_cat.requires = IS_IN_DB(db, db.cat.id, '%(nome)s')
db.articoli.id_sub.requires = IS_IN_DB(db, db.subcat.id, '%(nome)s')
db.articoli.codice.requires = IS_NOT_EMPTY()

Qualcosa di nuovo si ha alle righe 5-6-7:
qui definiamo alcuni campi di tipo ‘upload’ che verranno tradotti automaticamente nel nostro form con dei tag INPUT type=”file”. Definiamo poi un uploadfolder, ovvero in quale cartella andranno memorizzati i file caricati.
Se non dichiarassimo questo parametro i file verrebbero tutti salvati nella cartella predefinita di web2py ‘uploads’: in questo modo possiamo avere dei path personalizzati per i vari tipi di file caricati. Altra cosa compare un parametro autodelete=True: questo impone che se dalla nostra applicazione dovessimo avere una funzione che permette la cancellazione di un record, anche il relativo file verrebbe cancellato dal file system.
A seguire abbiamo una serie di validatori, sono tutti del tipo IS_IN_DB già discusso tranne l’ultimo, dove si richiede che il campo codice della tabella articoli sia non vuoto. In fase di invio del form, il campo codice verrà validato e il form respinto se l’utente avesse dimenticato di inserire il codice.

Vediamo ora il controller. Il controller di default è il file default.py che contiene anch’esso alcune funzioni prefedinite. Nel nostro esempio scriveremo 2 funzioni. La prima è index, per cui cancelliamo tutto il codice predefinito in questa funzione e scriviamo:

def index():
    #utilizziamo SQLFORM per generare in automatico un form html a partire dalla nostra tabella db.articoli
    form=SQLFORM(db.articoli)
    #selezioniamo tutti i record della tabella
    rows=db().select(db.articoli.ALL)
    #da qui inizia il processamento del form dopo che l'utente ha premuto Invia
    if form.accepts(request.vars,session):
        #se il form ha passato tutti i validatori dà il messaggio
        response.flash='Record inserito'
    elif form.errors:
        #se ci sono errori di validazione
        response.flash='Ci sono errori nel form'
    return dict(form=form) #passa l'oggetto form alla Vista che si chiamerà index.html

Tutto qui? Sì, si occupa di tutto la classe SQLFORM che renderizza ciascun field della tabella in un oggetto html appropriato.

Vediamo la banalità della Vista index.html:

#estendi la vista principale che si chiama layout.html
{{extend 'layout.html'}}
<h2>Inserimento Nuovi Articoli</h2>
#inserisci l'oggetto form che è stato passato dal controller
{{=form}}

Incredibile ma vero!

Avevo parlato di 2 funzioni nel controller default.py: vogliamo scrivere una funzione di callback Ajax che, quando l’utente sceglie dal menu a tendina la categoria del prodotto, automaticamente vada a popolare il menu a tendina delle sottocategorie proprie di quella particolare categoria. Esistono molti modi per raggiungere lo scopo e web2py offre “chiavi in mano” delle funzioni ajax già pronte. Ma noi scegliamo la strada più difficile: alla scelta della categoria dal menu a tendina useremo l’oggetto jquery $.getJSON per chiamare la funzione loadsubcat che andremo a scrivere nel nostro controller default.py. La nostra funzione inoltre, restituirà i valori richiesti in formato JSON.

Ma questo lo vedremo nella prossima parte, intanto guardiamo cosa sa fare SQLFORM con le nostre tabelle:
form

About the Author

Carlo Bazzo
Carlo Bazzo è fondatore di Epysoft, una start up tecnologica con sede a Treviso e CTO di Hdemo Network Business Solutions. Puoi contattare Carlo Bazzo su Linkedin.