Chapter 3: Panormaica di web2py
Panormaica di web2py
Avvio
web2py è distribuito in pacchetti binari per Windows e Mac OS X. E' scaricabile anche una versione in codice sorgente che può essere eseguita su Windows, Mac, Linux ed altri sistemi Unix. Le versioni binarie per Windows e per Mac OS X già includono il necessario interprete Python. La versione con codice sorgente presuppone che Python sia già installato nel computer.
web2py non richiede installazione. Per iniziare è sufficiente scompattare il file zip per il proprio sistema operativo ed eseguire il corrispondente file web2py
.
Su Windows:
web2py.exe
Su Mac OS X:
open web2py.app
Su Unix e Linux, dal pacchetto del codice sorgente:
python2.5 web2py.py
Il programma web2py accetta svariate opzioni sulla linea di comando che sono discusse in seguito.
Di default web2py visualizza una finestra di avvio:
e successivamente visualizza una finestra che chiede di inserire una password di amministrazione (valida solamente per la sessione corrente), l'indirizzo IP dell'interfaccia di rete utilizzata dal server web e un numero di porta sulla quale servire le richieste. Di default web2py è configurato per utilizzare l'indirizzo 127.0.0.1:8000 (la porta 8000 su localhost), ma può utilizzare un indirizzo IP e una porta qualsiasi tra quelli disponibili sul computer. Per conoscere l'indirizzo IP del proprio computer aprire una finestra della linea di comando e digitare il comando ipconfig
su Windows o ifconfig
su OS X and Linux. In questo libro si presuppone che web2py sia in esecuzione sulla porta 8000 di localhost (127.0.0.1:8000). Si può utlizzare 0.0.0.0:80 per rendere disponibile web2py su tutte le interfacce di rete del computer.
Se non si inserisce la password di amministrazione l'interfaccia amministrativa rimane disabilitata. Questa è una misura di sicurezza per evitare di esporre l'interfaccia di rete su una connessione pubblica non sicura.
L'interfaccia amministrativa admin è accessibile esclusivamente da localhost a meno di non eseguire web2py tramite il modulo mod_proxy di Apache. Se admin riconosce di essere dietro un proxy il cookie di sessione è impostato in modalità sicura e il login ad admin non è attivato a meno che la comunicazione tra client e proxy sia su una connessione sicura (HTTPS). Questa è un'altra misura di sicurezza. Tutte le comunicazione tra il client e l'applicazione admin devono essere locali o cifrate altrimenti un attaccante potrebbe tentare un attacco di tipo man-in-the-middle (dove l'attaccante si interpone tra il client ed il server ed ascolta le comunicazioni) o un attacco di tipo replay (dove l'attaccante si impossessa delle credenziali di autenticazione e simula l'identità del client) per eseguire codice malevolo sul server.
Dopo che la password di amministrazione è stata impostata web2py avvia il browser web alla pagina:
http://127.0.0.1:8000/
Nel caso che il computer non abbia impostato il browser di default questa operazione deve essere eseguita manualmente inserendo l'URL nel browser.
Cliccando su "administrative interface" si raggiunge la pagina di login per l'applicazione di amministrazione.
La password di amministratore è quella inserita all'avvio di web2py.
Esiste un solo amministratore, e pertanto una sola password di amministrazione. Per motivi di sicurezza lo sviluppatore deve utilizzare una nuova password ogni volta che web2py viene avviato a meno che non sia impostata l'opzione <recycle>. Questo comportamento non è previsto per le applicazioni sviluppate in web2py.
Dopo che l'amministratore si collega a web2py il suo browser è rediretto alla pagina site.
Questa pagina elenca tutte le applicazioni web2py installate e consente all'amministratore di gestirle. web2py include tre applicazioni:
- Un applicazione chiamata admin che è quella che si sta usando ora.
- Una applicazione examples con la documentazione interattiva online ed una replica del sito di web2py.
- Una applicazione welcome. Questa è il template di base per ogni altra nuova applicazione creata in web2py ed è definita come l'applicazione per "l'intelaiatura di base" (scaffolding). Questa è anche l'applicazione proposta di default all'utente.
Le applicazioni di web2py già pronte per l'utilizzo sono definite appliance. Molte appliance gratuite possono essere scaricate da [appliances] . Gli utenti di web2py sono incoraggiati a proporre nuove appliance sia in codice sorgente (open source) che già compilate (closed source).
Dalla pagina site dell'applicazione admin possono essere eseguite queste operazioni:
- installazione di una applicazione compilando il modulo sulla parte destra della pagina definendo un nome per l'applicazione e selezionando il file contenente l'applicazione compressa (o indicando la URL dove l'applicazione è disponibile).
- disinstallazione di un'applicazione premendo l'apposito pulsante. Verrà richiesta una conferma prima dell'operazione.
- creazione di un'applicazione scegliendo un nome e premendo il pulsante create.
- creare il pacchetto di distribuzione di un'applicazione premendo il relativo pulsante. Una applicazione scaricata è un file di tipo tar che contiene tutti i componenti dell'applicazione, database incluso. Non è mai necessario scompattare questo file perchè web2py lo scompatta automaticamente quando viene installato tramite admin.
- ripulitura di un'applicazione dai file temporanei (sessioni, errori e file di cache).
- modifica di un'applicazione.
Quando si crea una nuova applicazione utilizzando admin viene generato un clone dell'applicazione welcome con all'interno il modello "models/db.py" che definisce un database SQLite, si collega ad esso, instanzia Auth, Crud e Service e li configura. E' anche presente un "controller/default.py" che espone le azioni "index", "download", "user" (per la gestione dell'utente) e "call" (per i servizi). Negli esempi seguenti si presuppone che questi file siano stati cancellati in quanto verranno create da zero delle nuove applicazioni.
Hello!
Come esempio ecco una semplice applicazione che visualizza all'utente il messaggio "Hello from MyApp". L'applicazione sarà chiamata "myapp", Verrà aggiunto anche un contatore che indica quante volte lo stesso utente visita questa pagina.
Per creare una nuova applicazione è sufficiente inserire il suo nome nel campo a destra nella pagina site dell'applicazione admin:
Dopo aver premuto il pulsante submit l'applicazione è creata come una copia dell'applicazione welcome.
Per eseguire la nuova applicazione utilizzare la seguente URL:
http://127.0.0.1:8000/myapp
Questa, per ora, è una copia dell'applicazione welcome. Per modificare l'applicazione premere il pulsante edit nell'applicazione appena creata.
La pagina edit mostra il contenuto dell'applicazione.
Ogni applicazione creata in web2py consiste di un certo numero di file, la maggior parte dei quali cade in una di sei categorie:
- modelli: descrivono la rappresentazione dei dati.
- controller: descrivono la rappresentazione logica e il flusso dell'applicazione.
- viste: descrivono la presentazione dei dati.
- linguaggi: descrivono come tradurre le stringhe dell'applicazione in altre lingue.
- moduli: moduli Python che fanno parte dell'applicazione.
- file statici: immagini statiche, file CSS[css-w,css-o,css-school] , file JavaScript[js-w,js-b] , ecc.
Tutti questi file sono organizzati seguendo il modello di progettazione MVC (Model-View-Controller). Ciascuna sezione nella pagina di edit corrisponde ad una sotto-cartella nella cartella dell'applicazione. Cliccando sull'intestazione di ogni sezione si visualizza o si nasconde il suo contenuto. I nomi delle cartelle all'interno della sezione dei file statici sono cliccabili allo stesso modo.
Ogni file elencato nelle sezioni corrisponde ad un file fisicamente memorizzato nella sotto-cartella. Ogni operazione eseguita su un file tramite l'interfaccia dell'applicazione admin (creazione, cancellazione, modifica) può essere eseguita anche direttamente dalla shell utilizzando il proprio editor favorito.
L'applicazione contiene al suo interno altri tipi di file (database, file di sessione, file di errori, ecc.) che non sono elencati nella pagina di edit perchè non sono creati o modificati dall'amministratore, ma sono utilizzati direttamente dall'applicazione stessa.
I controller contengono la logica ed il flusso dell'applicazione. Ogni URL è collegata ad una delle funzioni del controller (azioni). Ci sono due controller creati di default: "appadmin.py" e "default.py". appadmin contiene l'interfaccia amministrativa per il database (e non sarà utilizzato in questo esempio). "default.py" è il controller che dovrà essere modificato e che è chiamato quando nella URL non è indicato uno specifico controller.
Modificare la funzione "index", nel controller "default.py" nel seguente modo:
def index():
return "Hello from MyApp"
Ecco come apparirà il contenuto del controller nell'editor online:
Memorizzare le modifiche e tornare indietro alla pagina edit. Cliccare sul link index per visitare la pagina appena creata.
Ora, quando si visita la URL
http://127.0.0.1:8000/myapp/default/index
verrà eseguita l'azine index del controller default dell'applicazione myapp'. Questa azione ritorna una stringa che viene visualizzata dal browser in questo modo:
Modificare ora la funzione index in questo modo:
def index():
return dict(message="Hello from MyApp")
Modificare inoltre nella pagina di edit la vista default/index.html (il nuovo file associato con l'azione creata) nel seguente modo:
<html>
<head></head>
<body>
<h1>{{=message}}</h1>
</body>
</html>
Ora l'azione ritorna un dizionario contenente un elemento message
. Quando una azione ritorna un dizionario web2py cerca una vista con il nome
[controller]/[function].[extension]
{{=message}}
indica a web2py di sostistuire questo codice con il valore contenuto nell'elemento message
del dizionario restituito dall'azione index. E' da notare che message
non è una parola chiave di web2py ma è definito dall'azione. Per ora non è stata utilizzata nessuna parova chiave di web2py.Se web2py non trova la vista richeista utilizza la vista "generic.html" che è presente in ogni applicazione.
Se viene utilizzata un'altra estensione al posto di "html" (per esempio "json") e la relativa vista "[controller]/[function].json" non esiste, web2py cerca una vista dal nome "generic.json". web2py contiene le viste "generic.html", "generic.json", "generic.xml" e "generic.rss". Queste viste generiche possono essere modificate singolarmente in ciascuna applicazione e viste aggiuntive possono essere facilmente aggiunte.
Per altre informazioni su questo argomento consultare il capitolo 9.
Tornando alla pagina di
edit e cliccando sull'azione index verrà visualizzata la seguente pagina HTML:Contiamo!
Verrà ora aggiunto un contatore alla pagina creata nell'esempio precedente che conterà quante volte lo stesso visitatore visualizza la pagina.
web2py traccia le visite in modo automatico e trasparente tramite le sessioni e i cookie. Per ciascun nuovo visitatore viene creata una sessione a cui è assegnato un "
session_id" univoco. La sessione è un contenitore per le variabili memorizzato sul server. Il session_id univoco è inviato al browser tramite un cookie. Quando il visitatore richieste un'altra pagina dalla stessa applicazione il browser rimanda indietro il cookie che è recuperato da web2py che ripristina la sessione corrispondente.Modificare il controller
default.py nel seguente modo per utilizzare le sessioni:def index():
if not session.counter:
session.counter = 1
else:
session.counter += 1
return dict(message="Hello from MyApp", counter=session.counter)
E' da notare che counter
non è una parola chiave di web2py ma session
lo è. In questo caso web2py controlla se la variabile counter
è presente in session
e nel caso che non sia presente la crea ed imposta il suo valore ad 1. Se counter
già esiste web2py incrementa il suo valore di 1. Infine il valore del contatore viene passato alla vista. Un modo più compatto di scrivere la stessa funzione è:
def index():
session.counter = (session.counter or 0) + 1
return dict(message="Hello from MyApp", counter=session.counter)
Modificare la vista per aggiungere una linea che visualizza il valore del contatore:
<html>
<head></head>
<body>
<h1>{{=message}}</h1>
<h2>Number of visits: {{=counter}}</h2>
</body>
</html>
Quando si visualizza nuovamente la pagina
index si ottiene la seguente pagina HTML:Il contatore è associato ad ogni visitatore ed è incrementato ogni volta che il visitatore ricarica la pagina. Ogni visitatore vede il proprio contatore di pagina.
Come ti chiami?
Saranno create ora due pagine (
first e second): la prima chiederà il nome del visitatore tramite una form ed eseguirà una redirezione alla seconda che saluterà il visitatore chiamandolo per nome.Scrivere le relative azioni nel controller di default:
def first():
return dict()
def second():
return dict()
Creare poi la vista "default/first.html" per la prima azione:
e scrivere:
{{extend 'layout.html'}}
What is your name?
<form action="second">
<input name="visitor_name" />
<input type="submit" />
</form>
Crere infine la vista "default/second.html" per la seconda azione:
{{extend 'layout.html'}}
<h1>Hello {{=request.vars.visitor_name}}</h1>
Nelle due viste è stato esteso lo schema di base "layout.html" che è fornito con web2py. Il layout mantiene coerente il
look & feel delle due pagine. Il file di layout può essere editato e sostituito facilmente poichè contiene principalmente codice HTML.Se ora si visita la priam pagina, inserendo il proprio nome:
ed inviando la form, si riceverà un saluto:
Auto-invio delle form
Il meccanismo di invio delle form appena visto è molto utilizzato, ma non è una buona pratica di programmazione. Ogni inserimento in una form dovrebbe sempre essere validato e, nell'esempio precedente, il controllo dell'input ricadrebbe sulla seconda azione. In questo modo l'azione che esegue il controllo sarebbe diversa dall'azione che ha generato la form. Questo può causare ridondanza nel codice. Uno schema migliore per l'invio delle form è di rimandarle alla stessa azione che le ha generate, in questo caso all'azione
first. L'azione dovrebbe ricevere le variabili, validarle, memorizzarle sul server e ridirigere il visitatore alla pagina second che recupera le variabili. Questo meccanismo è chiamato postback.Il controller di default deve essere modificato nel seguente modo per implementare l'auto invio del form:
def first():
if request.vars.visitor_name:
session.visitor_name = request.vars.visitor_name
redirect(URL('second'))
return dict()
def second():
return dict()
Di conseguenza si deve modificare la vista "default/first.html":
{{extend 'layout.html'}}
What is your name?
<form>
<input name="visitor_name" />
<input type="submit" />
</form>
e la vista "default/second.html" deve recuperare il nome dalla sessione (session
) invece che dalle variabili della richiesta (request.vars
:
{{extend 'layout.html'}}
<h1>Hello {{=session.visitor_name or "anonymous"}}</h1>
Dal punto di vista del visitatore il comportamento dell'auto-invio del form è esattamente lo stesso dell'implementazione precedente. Per ora non è stata aggiunta nessuna validazione ma è chiaro che in questo esempio la validazione debba essere eseguita dalla prima azione.
Questo approccio è migliore perchè il nome del visitatore è memorizzato nella sessione e può essere acceduto da tutte le azioni e le viste dell'applicazione senza doverlo passare esplicitamente.
Inoltre se l'azione 'second
è chiamata prima che il nome del visitatore è impostato nella sessione sarà visualizzato "Hello anonymous" perchèsession.visitor_name
ha il valore None
. In alternativa potremmo aver aggiunto il seguente codice nel controller (all'interno o all'esterno della funzione second):if not request.function=='first' and not session.visitor_name:
redirect(URL('first'))
Questo è un meccanismo generico per obbligare l'autorizzazione nei controller, sebbene nel capitolo 8 sarà descritto un metodo più completo per gestire l'autenticazione e l'autorizzazione.
Con web2py si può fare un passo in più e chiedere a web2py di generare il form con la validazione. web2py fornisce delle funzioni ausiliarie (helper per FORM, INPUT, TEXTAREA e SELECT/OPTION) con lo stesso nome dei corrispondenti tag HTML. Gli helper possono essere utilizzati per generare i form sia nei controller che nelle viste.
Per esempio questo è uno dei modi possibili di riscrivere l'azione first:
def first():
form = FORM(INPUT(_name='visitor_name', requires=IS_NOT_EMPTY()),
INPUT(_type='submit'))
if form.accepts(request.vars, session):
session.visitor_name = form.vars.visitor_name
redirect(URL('second'))
return dict(form=form)
dove si definisce un form che contiene due tag di tipo INPUT. Gli attributi dei tag di input sono specificati dagli argomenti con nome che iniziano con un underscore. L'argomento con nome requires
non è un tag HTML (perchè non inizia con un underscore) ma imposta un validatore per il valore del campo visitor_name. L'oggetto form
può essere facilmente serializzato in HTML incorporandolo nella vista "default/first.html":
{{extend 'layout.html'}}
What is your name?
{{=form}}
Il metodo form.accepts
esegue i controlli indicati nei validatori. Se il form auto-inviato passa la validazione le variabili vengono memorizzate nella sessione e l'utente viene reindirizzato come nell'esempio precedente. Se il form non passa la validazione i messaggi d'errore sono inseriti nel form e mostrati all'utente:
Nella prossima sezine si vedrà come i form possono essere generati automaticamente da un modello di dati.
Un Blog con immagini
Ecco un esempio più complesso. Si vuole creare un'applicazione web che consenta all'amministratore di inviare immagini e di dar loro un nome e agli utenti di vedere le immagini ed inviare commenti su di esse.
Come nell'esempio precedente creare la nuova applicazione dalla pagina site nell'applicazione admin ed entrare nella pagina edit dell'applicazione appena creata:
Procedere quindi alla creazione del modello, la rappresentazione dei dati persistenti dell'applicazione (le immagini da caricare, i loro nomi ed i commenti ad esse relativi). Andrà prima creato e modificato il file di modello "db.py". I modelli ed i controlli devono avere l'estensione .py
perchè sono codice Python. Se l'estensione non è indicata questa è aggiunta automaticamente da web2py. Le viste invece sono create di default con l'estensione .html
poichè contengono principalmente codice HTML.
Modificare il file "db.py" cliccando sul relativo pulsante edit:
ed inserire il seguente codice:
db = DAL("sqlite://storage.sqlite")
db.define_table('image',
Field('title'),
Field('file', 'upload'))
db.define_table('comment',
Field('image_id', db.image),
Field('author'),
Field('email'),
Field('body', 'text'))
db.image.title.requires = IS_NOT_IN_DB(db, db.image.title)
db.comment.image_id.requires = IS_IN_DB(db, db.image.id, '%(title)s')
db.comment.author.requires = IS_NOT_EMPTY()
db.comment.email.requires = IS_EMAIL()
db.comment.body.requires = IS_NOT_EMPTY()
db.comment.image_id.writable = db.comment.image_id.readable = False
Ecco l'analisi del codice linea per linea:
- Linea 1: definisce la variabile globale
db
che rappresenta la connessione al database. In questo esempio è una connessione ad un database SQLite memorizzato nel file "applications/images/databases/storage.sqlite". Nel caso di database di tipo SQLite se questi non esistono vengono creati automaticamente. Si può cambiare il nome del file ed anche la variable globaledb
, ma quest'ultima conviene mantenerla con questo nome, perchè è semplice da ricordare. - Linee 3-5: definiscono la tabella "image".
define_table
è un metodo dell'oggettodb
. Il primo argomento, "image" è il nome della tabella che si sta definendo. Gli altri argomenti sono i campi della tabella. Questa tabella ha un campo chiamato "title", un campo chiamato "file" ed un campo chiamato "id" per memorizzare la chiave primaria della tabella. ("id" non è dichiarato esplicitamente perchè tutte le tabelle in web2py hanno di default un campo "id"). Il campo "title" è una stringa ed il campo "file" è di tipo "upload". "upload" è un tipo speciale di campo utilizzato dal DAL (Data Abstraction Layer) di web2py per memorizzare i nomi dei file caricati dall'utente. web2py è in grado di caricare i file (tramite streaming se sono di grande dimensione), rinominarli in un modo sicuro e memorizzarli. Dopo la definizione di una tabella web2py intraprende una delle seguenti azioni: a) se la tabella non esiste, questa viene creata; b) se la tabella esiste e non corrisponde alla definizione, la tabella esistente è modificata di conseguenza e se un campo è di un tipo differente, web2py tenta di convertirne i contenuti; c) se la tabella esiste e corrisponde alla definizione, in questo caso web2py non fa nulla. Questo comportamento è chiamato "migrazione". In web2py le migrazioni sono automatiche, ma possono essere disattivate per ogni tabella passando l'argomento "migrate=False" come ultimo argomento indefine_table
. - Linee 7-11: definiscono un altra tabella chiamata "comment". Un commento ha un autore (author), un indirizzo email (email) per memorizzare l'indirizzo email dell'autore del commento, un corpo (body) di tipo testo (text) per memorizzare il commento inviato dall'autore ed un id dell'immagine (image_id) di tipo riferimento (reference) che punta a
db.image
tramite il campo "id". - Linea 13:
db.image.title
rappresenta il campo "title" della tabella "image". L'attributorequires
permette di impostare dei vincoli che saranno applicati dai form di web2py. In questo caso si richiede che il "title" sia univoco:. Gli oggetti che rappresentano questi vincoli sono chiamati "validatori". Validatori multipli possono essere raggruppati in una lista. I validatori sono eseguiti nell'ordine in cui appaiono.IS_NOT_IN_DB(db, db.image.title)
IS_NOT_IN_DB(a, b)
è un validatore speciale che controlla che il valore del campo b per un nuovo record non sia già in a. - Linea 14: definisce il vincolo per cui il campo "image_id" della tabella "comment" deve essere in
db.image.id
. Per come è definito il database questo è già stato dichiarato nella definizione della tabella "comment". In questo modo l'indicazione del vincolo è esplicita nel modello e viene controllata da web2py durante la validazione dei form, quando è inserito un nuovo commento. In questo modo i valori non validi non vengono propagati dal form al database. E' anche richiesto che il campo "image_id" sia rappresentato dal "title" ('%(title)s'
) del record corrispondente. - Linea 18: indica che il campo "image_id" della tabella "comment" non deve essere mostrato nei form (
writable=False
) nemmeno in quelli a sola lettura (readable=False
).
Il significato dei validatori nelle linee 15-17 dovrebbe essere evidente.
E' da notare che il validatore
db.comment.image_id.requires = IS_IN_DB(db, db.image.id, '%(title)s')
potrebbe essere omesso (e sarebbe implicito) se si specificasse un formato per la rappresentazione dell'immagine
db.define_table('image',....,format='%(title)s')
dove il formato può essere una stringa o una funzione che richiede un record e ritorna una stringa.
Quando il modello è definito e se non ci sono errori web2py crea un'interfaccia amministrativa dell'applicazione per gestire il database alla quale accedere tramite il pulsante "database administration" nella pagina edit oppure direttamente con la URL:
http://127.0.0.1:8000/images/appadmin
Ecco come si presenta l'interfaccia amministrativa appadmin:
Questa interfaccia è codificata nel controller chiamato "appadmin.py" e nella vista corrispondente "appadmin.html". Da qui in poi questa interfaccia sarà identificata semplicemente come appadmin. appadmin consente all'amministratore di inserire nuovi record nel database, modificare e cancellare i record esistenti, scorrere le tabelle ed eseguire delle unioni di database (join).
La prima volta che appadmin viene acceduta il modello è eseguito e le tabelle vengono create. Il DAL di web2py traduce il codice Python in comandi SQL specifici per il database selezionanto (SQLite in questo esempio). Il codice SQL generato può essere ispezionato dalla pagina edit cliccando sul pulsante "sql.log" nella sezione "models". Questo link non è presente finchè le tabelle non vengono create.
Se si modifica il modello e si accede nuovamente ad appadmin web2py genererebbe codice SQL per alterare le tabelle esistenti. Il codice cgenerato è memorizzato in "sql.log".
Ora in appadmin si provi ad inserire un nuovo record immagine:
web2py ha trasformato il campo di "upload" di db.image.file
in un form di caricamento per i file. Quando il form è inviato dall'utente ed un'immagine è caricata il file è rinominato in un modo sicuro (che preserva l'estensione), è memorizzato con il nuovo nome nella cartella "uploads" dell'applicazione ed il nuovo nome è memorizzato nel campo db.image.file
. Questo processo previene gli attacchi di tipo "directory traversal".
Ogni campo è visualizzato tramite un widget. I widget di default possono essere sostituiti. Quando si clicca sul nome di una tabella in appadmin web2py seleziona tutti i record della tabella corrente identificata dalla seguente query del DAL:
db.image.id > 0
e visualizza il risultato:
Si può selezionare un gruppo differente di record modicando la query SQL e premendo "apply".
Per modificare o cancellare un singolo record cliccare sul numero id del record.
A causa del validatore IS_IN_DB
il campo di riferimento "image_id" è visualizzato con un menu a tendina. Gli elementi nel menu a tendina sono memorizzati come chiavi (db.image.id
), ma sono rappresentati dal db.image.title
, come specificato nel validatore.
I validatori sono oggetti potenti che sono in grado di rappresentare i campi, i valori dei campi nei filtri, generare gli errori e formattare i valori estratti dal campo. Il seguente schema mostra cosa succede quando si invia un form che non supera la validazione:
Gli stessi form che sono generati automaticamente da appadmin possono anche essere generati da codice con l'helper SQLFORM
ed inseriti nelle applicazioni. Questi moduli sono configurabili e gestibili tramite CSS.
Ciascuna applicazione ha la sua appadmin che può pertanto essere modificata senza generare problemi per le altre applicazioni presenti.
Finora l'applicazione è in grado di memorizzare i dati, ai quali si può accedere tramite appadmin. L'accesso ad appadmin è riservato al'amministratore e appmin non deve essere considerata come un'interfaccia web da usare in produzione. Per questo ora verranno creati:
- Una pagina "index" che visualizza tutte le immagini disponibili ordinate per titolo con il link alle pagina di dettaglio per ogni immagine.
- Una pagina "show/[id]" che visualizza l'immagine richiesta e permette all'utente di visualizzare e inviare commenti.
- Un'azione "download/[name]" che consente di scaricare le immagini caricate.
Ecco la schematizzazione di queste azioni:
Tornando alla pagina edit modificare il controller "default.py" sostituendo il suo contenuto con:
def index():
images = db().select(db.image.ALL, orderby=db.image.title)
return dict(images=images)
Questa azione ritorna un dizionario. Le chiavi degli elementi del dizionario sono interpretate come variabili passate alla vista associata con l'azione. Se la vista non è presente l'azione è visualizzata dalla vista "generic.html" che è presente in ogni applicazione web2py.
L'azione index seleziona tutti i campi (db.image.ALL
) dalla tabella image e li ordina per db.image.title
. Il risultato della selezione è un oggetto Rows
che contiene i record e che viene assegnato alla variabile locale images
che verrà poi restituita alla vista tramite il dizionario. images
è un oggetto iterabile ed i suoi elementi sono le righe selezionate nella tabella. Per ciascuna riga le colonne sono accedute come dizionari: images[0]['title']
o images[0].title
.
Nel caso non sia presente una vista il dizionario è visualizzto dalla vista "views/generic.html" è l'utilizzo dell'azione index avrebbe il seguente risultato:
Poichè non è stata creata nessuna vista per questa azione web2py visualizza i record in una semplice tabella.
Per creare una vista per l'azione index ritornare alla pagina di edit e modificare la vista "default/index.html" sostituendo il suo contenuto con:
{{extend 'layout.html'}}
<h1>Current Images</h1>
<ul>
{{for image in images:}}
{{=LI(A(image.title, _href=URL("show", args=image.id)))}}
{{pass}}
</ul>
La prima cosa da notare è che una vista è del semplice codice HTML con l'aggiunta degli speciali tag "{{" e "}}". Il codice incluso nei tag "{{" e "}}" è codice Python con una particolarità: l'indentazione è irrilevante. I blocchi di codice iniziano con linee che terminano con i due punti (:) e terminano con una linea che inizia con la parola chiave pass. In alcuni casi la fine del blocco è evidente dal contesto e l'utilizzo di pass non è necessario.
Le linee 5-7 ciclano su tutte le immagini e per ogni immagine eseguono il codice:
LI(A(image.title, _href=URL('show', args=image.id))
che genera codice HTML con i tag <li>...</li>
che contiengono i tag <a href="...">...</a>
che contiene image.title
. Il valore del link (attributo href) è:
URL('show', args=image.id)
cioè la URL dell'applicazione e del controller che ha generato la richiesta corrente che richiede la funzione chiamata "show" passandole un singolo argomento args=image.id
. LI
, A
, e le altre funzioni sono helper di web2py che creano i corrispondenti tag HTML. Gli argomenti di queste funzioni sono interpretati come oggetti serializzati ed inseriti all'interno del tag. Gli argomenti con nome che iniziano con underscore (per esempio _href
) sono interpretati come attributi del tag ma senza l'underscore. Per esempio _href
è l'attributo href
, _class
è l'attributo class
, ecc.
Per esempio il seguente codice:
{{=LI(A('something', _href=URL('show', args=123))}}
è trasformato in:
<li><a href="/images/default/show/123">something</a></li>
Alcuni helper (INPUT
, TEXTAREA
, OPTION
e SELECT
) dispongono anche di attributi con nome speciali che non iniziano con underscore (value
e requires
). Questi attributi sono importanti per costruire form personalizzati e saranno discussi in seguito. Tornado indietro alla pagina di edit ora indica che default.py espone index. Cliccando su index si esegue l'azione e si raggiunge la pagina appena creata:
http://127.0.0.1:8000/images/default/index
che visualizza:
Se si clicca sul link di un'immagine si viene reindirizzati a:
http://127.0.0.1:8000/images/default/show/1
e questo genera un errore poichè non esiste ancora un'azione chiamata "show" nel controller "default.py".
Modicare il controller "default.py" e sostituire il suo contenuto con:
def index():
images = db().select(db.image.ALL, orderby=db.image.title)
return dict(images=images)
def show():
image = db(db.image.id==request.args(0)).select().first()
form = SQLFORM(db.comment)
form.vars.image_id = image.id
if form.accepts(request.vars, session):
response.flash = 'your comment is posted'
comments = db(db.comment.image_id==image.id).select()
return dict(image=image, comments=comments, form=form)
def download():
return response.download(request, db)
Il controller contiene due azioni: "show" e "download". L'azione "show" seleziona l'immagine con l'id
indicato negli argomenti della richiesta con tutti i relativi commenti e passa tutti i dati recuperati alla vista "default/show.html".
L'id dell'immagine identificato da:
URL('show', args=image.id)
in "default/index.html", può essere acceduto con:
request.args(0)
dall'azione "show".
L'azione "download" si aspetta un nome di file in request.args(0)
, costruisce un path alla cartella dove il file dovrebbe essere e lo manda al cliente. Se il file è di grandi dimensioni viene inviato in streaming all'utente per evitare problemi di sovraccarico della memoria.
Notare nelle seguenti righe:
- Linea 7: crea un SQLFORM di inserimento per la tabella
db.comment
utilizzando solamente i campi specificati. - Linea 8: imposta il valore per il campo di riferimento che non fa parte del form in quanto non è elencato tra i campi specificati prima.
- Linea 9: elabora il form inviato (le variabili del form sono in
request.vars
) all'interno della sessione corrente (la sessione è utilizzata per evitare un doppio invio del form e per tener traccia della navigazione). Se le variabili sono validate il nuovo commento è inserito nella tabelladb.comment
; in caso contrario il form è modificato per includere i messaggi d'errore (per esempio, nel caso che l'indirizzo di email dell'autore non sia valido). Tutto questo è fatto nella sola linea 9! - Linea 10: è eseguita solamente se il form è accettato, dopo che il record è stato inserito nella tabella del database.
response.flash
è una variabile di web2py che viene visualizzata nelle viste ed è usata per notificare agli utenti che una certa operazione è avvenuta. - Linea 11: seleziona tutti i commenti dell'immagine corrente.
L'azione "download" è già definita nel controller "default.py" dell'applicazione "welcome" che viene utilizzata come schema di base delle nuove applicazioni.
L'azione "download" non restituisce un dizionario e così non necessita di una vista. L'azione "show" dovrebbe invece avere una vista. Tornare alla pagina di edit e creare una nuova vista chiamata "default/show.html" digitando "default/show" nel campo di creazione di una nuova vista:
Modificare questo nuovo file e sostituire il suo contenuto con:
{{extend 'layout.html'}}
<h1>Image: {{=image.title}}</h1>
<center>
<img width="200px"
src="{{=URL('download', args=image.file)}}" />
</center>
{{if len(comments):}}
<h2>Comments</h2><br /><p>
{{for comment in comments:}}
<p>{{=comment.author}} says <i>{{=comment.body}}</i></p>
{{pass}}</p>
{{else:}}
<h2>No comments posted yet</h2>
{{pass}}
<h2>Post a comment</h2>
{{=form}}
Questa vista visualizza image.file richiamando l'azione "download" all'interno dei tag <img ... />
. Se sono presenti commenti vengono tutti visualizzati tramite un ciclo.
Ecco come la pagina appare all'utente:
Quando un utente invia un commento tramite questa pagina il commento è memorizzato nel database ed aggiunto alla fine della pagina stessa.
Aggiungere le funzionalità CRUD (Create, Read, Update, Delete)
seb2py include anche un'API CRUD (Create, Read, Update, Delete) che semplifica ancor di più la gestione delle form. Per utilizzare CRUD è necessiario definirlo da qualche parte come per esempio nel modulo "db.py":
from gluon.tools import Crud
crud = Crud(globals(), db)
Queste due linee sono già presenti nel modello creato di default con la nuova applicazione
L'oggetto crud
dispone di metodi di alto livello come, per esempio:
form = crud.create(table)
che può essere utilizzato per sostituire il seguente schema di programmazione:
form = SQLFORM(table)
if form.accepts(request.post_vars,session):
session.flash = '...'
redirect('...')
Ecco come riscrivere l'azione "show" utilizzando CRUD ottenendo alcuni miglioramenti:
def show():
image = db.image(request.args(0)) or redirect(URL('index'))
db.comment.image_id.default = image.id
form = crud.create(db.comment,
message='your comment is posted',
next=URL(args=image.id))
comments = db(db.comment.image_id==image.id).select()
return dict(image=image, comments=comments, form=form)
Prima di tutto notare che è stata usata la seguente sintassi:
db.image(request.args(0)) or redirect(...)
per recuperare il record richiesto. Poichè table(id)
restituisce None se il record non è presente è possibile utilizzare or redirect(...)
in una sola linea.
L'argomento next
di crud.create
è la URL a cui reindirizzare il form se viene accettato. L'argomento message
è il messaggio che verrà visualizzato in caso di accettazione del form. Ulteriori informazioni sulla API CRUD sono disponibili nel capitolo 7.
Aggiungere l'autenticazione
L'API di web2py per il controllo d'accesso basato sui ruoli (RBAC, Role-based Access Control) è molto sofisticata, ma per ora si vedrà come restringere l'accesso alle azioni agli utenti autenticati, posticipando al capitolo 8 una discussione più dettagliata.
Per limitare l'accesso agli utenti autenticati, è necessario completare tre passaggi. In un modello, per esempio "db.py" è necessario aggiungere:
from gluon.tools import Auth
auth = Auth(globals(), db)
auth.define_tables()
Nel controller è necessario aggiungere un'azione:
def user():
return dict(form=auth())
Infine le funzioni che devono essere riservate devono essere decorate:
@auth.requires_login()
def show():
image = db.image(request.args(0)) or redirect(URL('index'))
db.comment.image_id.default = image.id
form = crud.create(db.comment, next=URL(args=image.id),
message='your comment is posted')
comments = db(db.comment.image_id==image.id).select()
return dict(image=image, comments=comments, form=form)
Ora, qualsiasi accesso a
http://127.0.0.1:8000/images/default/show/[image_id]
richiederà l'accesso. Se l'utente non è collegato sarà rendirizzato a :
http://127.0.0.1:8000/images/default/user/login
user
espone, tra le altre, le seguenti azioni:http://127.0.0.1:8000/images/default/user/logout
http://127.0.0.1:8000/images/default/user/register
http://127.0.0.1:8000/images/default/user/profile
http://127.0.0.1:8000/images/default/user/change_password
http://127.0.0.1:8000/images/default/user/request_reset_password
http://127.0.0.1:8000/images/default/user/retrieve_username
http://127.0.0.1:8000/images/default/user/retrieve_password
http://127.0.0.1:8000/images/default/user/verify_email
http://127.0.0.1:8000/images/default/user/impersonate
http://127.0.0.1:8000/images/default/user/not_authorized
Un utente che si collega per la prima volta dovrà registrarsi per poter leggere ed inviare commenti.
Sia l'oggetto
auth
che la funzioneuser
sono già definite nell'applicazione di default. L'oggettoauth
è ampiamente personalizzabile e può gestire la verifica dell'email, i CAPTCHA, l'approvazione della registrazione e metodi alternativi d'accesso tramite plug-in aggiuntivi.
Wiki
Il seguente diagramma illustra le azioni da implementare e i link che le connettono tra di loro:
Per iniziare, creare una nuova applicazione chiamata "mywiki".
Il modello deve contenere tre tabelle: page, comment e document. Sia i commenti che i documenti fanno riferimento alle pagine perchè appartengono ad una pagina. Un documento contiene un campo file di tipo upload come nella precedente applicazione.
Ecco il modello completo:
db = DAL('sqlite://storage.sqlite')
from gluon.tools import *
auth = Auth(globals(),db)
auth.define_tables()
crud = Crud(globals(),db)
db.define_table('page',
Field('title'),
Field('body', 'text'),
Field('created_on', 'datetime', default=request.now),
Field('created_by', db.auth_user, default=auth.user_id),
format='%(title)s')
db.define_table('comment',
Field('page_id', db.page),
Field('body', 'text'),
Field('created_on', 'datetime', default=request.now),
Field('created_by', db.auth_user, default=auth.user_id))
db.define_table('document',
Field('page_id', db.page),
Field('name'),
Field('file', 'upload'),
Field('created_on', 'datetime', default=request.now),
Field('created_by', db.auth_user, default=auth.user_id),
format='%(name)s')
db.page.title.requires = IS_NOT_IN_DB(db, 'page.title')
db.page.body.requires = IS_NOT_EMPTY()
db.page.created_by.readable = db.page.created_by.writable = False
db.page.created_on.readable = db.page.created_on.writable = False
db.comment.body.requires = IS_NOT_EMPTY()
db.comment.page_id.readable = db.comment.page_id.writable = False
db.comment.created_by.readable = db.comment.created_by.writable = False
db.comment.created_on.readable = db.comment.created_on.writable = False
db.document.name.requires = IS_NOT_IN_DB(db, 'document.name')
db.document.page_id.readable = db.document.page_id.writable = False
db.document.created_by.readable = db.document.created_by.writable = False
db.document.created_on.readable = db.document.created_on.writable = False
Modificare il controller "default.py" e creare le seguenti azioni:
- index: elenca tutte le pagine del wiki
- create: crea una nuova pagina con i dati inseriti dall'utente
- show: mostra una pagina del wiki con i commenti e consente di aggiungerne altri
- edit: modifica una pagina esistente
- documents: gestisce i documenti allegati ad una pagina
- download: scarica un documento (come nell'esempio precedente)
- search: visualizza una finestra di recerca e, tramite una chiamata Ajax, restituisce tutti i titoli trovati mentre il visitatore sta ancora scrivendo
- bg_find: la funzione di ricerca Ajax. Restituisce il codice HTML da includere nella pagina di ricerca
Ecco il controller "default.py":
def index():
""" this controller returns a dictionary rendered by the view
it lists all wiki pages
>>> index().has_key('pages')
True
"""
pages = db().select(db.page.id,db.page.title,orderby=db.page.title)
return dict(pages=pages)
@auth.requires_login()
def create():
"creates a new empty wiki page"
form = crud.create(db.page, next = URL('index'))
return dict(form=form)
def show():
"shows a wiki page"
this_page = db.page(request.args(0)) or redirect(URL('index'))
db.comment.page_id.default = this_page.id
form = crud.create(db.comment) if auth.user else None
pagecomments = db(db.comment.page_id==this_page.id).select()
return dict(page=this_page, comments=pagecomments, form=form)
@auth.requires_login()
def edit():
"edit an existing wiki page"
this_page = db.page(request.args(0)) or redirect(URL('index'))
form = crud.update(db.page, this_page,
next = URL('show', args=request.args))
return dict(form=form)
@auth.requires_login()
def documents():
"lists all documents attached to a certain page"
this_page = db.page(request.args(0)) or redirect(URL('index'))
db.document.page_id.default = this_page.id
form = crud.create(db.document)
pagedocuments = db(db.document.page_id==this_page.id).select()
return dict(page=this_page, documents=pagedocuments, form=form)
def user():
return dict(form=auth())
def download():
"allows downloading of documents"
return response.download(request, db)
def search():
"an ajax wiki search page"
return dict(form=FORM(INPUT(_id='keyword',
_onkeyup="ajax('bg_find', ['keyword'], 'target');")),
target_div=DIV(_id='target'))
def bg_find():
"an ajax callback that returns a <ul> of links to wiki pages"
pattern = '%' + request.vars.keyword.lower() + '%'
pages = db(db.page.title.lower().like(pattern)) .select(orderby=db.page.title)
items = [A(row.title, _href=URL('show', args=row.id)) for row in pages]
return UL(*items).xml()
Le linee 2-6 forniscono un commento per l'azione index. Le linee 4-5 all'interno del commento sono interpretate da Python come codice di test (doctest). I test possono essere eseguiti dall'interfaccia di amministrazione. In questo caso il test verifica che l'azione index sia eseguita senza errori.
Le linee 18, 27 e 35 tentano di recuperare un record page
con l'id request.args(0)
.
Le linee 13, 20 e 37 definiscono e processano i form di creazione rispettivamente per una nuova pagina, un nuovo commento ed un nuovo documento.
La linea 28 definisce e processa un form di aggiornamento per una pagina wiki.
Alla linea 51 c'è un po' di magia. Viene impostato l'attributo onkeyup
del tag INPUT "keyword". Ogni volta che il visitatore preme o rilascia un tasto il codice Javascript all'interno dell'attributo onkeyup
viene eseguito nel browser dell'utente. Ecco il codice JavaScript:
ajax('bg_find', ['keyword'], 'target');
ajax
è una funzione definita nel file "web2py_ajax.html" che è incluso nel layout di default "layout.html". Questa funzione richiede tre parametri: la URL dell'azione che esegue la chiamata sincrona ("bg_find"), una lista degli ID delle variabili che devono essere inviate alla funzione (["keyword"]) e l'ID dove deve essere inserita la risposta ("target").
Non appena si digita qualcosa nel campo di ricerca e si rilascia il tasto, il client contatta il server e invia il contenuto del campo 'keyword' e, quando il server risponde, la risposta è inserita nella pagina stessa come codice HTML interno del tag 'target'.
Il tag 'target' è un DIV definito nella linea 75. Potrebbe essere stato definito anche nella vista.
Ecco il codice per la vista "default/create.html":
{{extend 'layout.html'}}
<h1>Create new wiki page</h1>
{{=form}}
VIsitando la pagina create si ottiene:
Ecco il codice per la vista "default/index.html":
{{extend 'layout.html'}}
<h1>Available wiki pages</h1>
[ {{=A('search', _href=URL('search'))}} ]<br />
<ul>{{for page in pages:}}
{{=LI(A(page.title, _href=URL('show', args=page.id)))}}
{{pass}}</ul>
[ {{=A('create page', _href=URL('create'))}} ]
che genera la seguente pagina:
Ecco il codice per la vista "default/show.html":
{{extend 'layout.html'}}
<h1>{{=page.title}}</h1>
[ {{=A('edit', _href=URL('edit', args=request.args))}}
| {{=A('documents', _href=URL('documents', args=request.args))}} ]<br />
{{=MARKMIN(page.body)}}
<h2>Comments</h2>
{{for comment in comments:}}
<p>{{=db.auth_user[comment.created_by].first_name}} on {{=comment.created_on}}
says <I>{{=comment.body}}</i></p>
{{pass}}
<h2>Post a comment</h2>
{{=form}}
Se si desidera utilizzare la sintatti Markdown invece di quella Markmin, basta importare WIKI
da gluon.contrib.markdown
ed utilizzarlo invece dell'helper MARKMIN
. Alternativamente si può decidere di accettare semplice HTML invece della sintassi Markmin, in questo caso si deve sostituire:
{{=MARKMIN(page.body)}}
con:
{{=XML(page.body)}}
(in modo da evitare che l'XML non venga controllato, che è il comportamento di default di web2py). Un modo migliore per fare questo è:
{{=XML(page.body, sanitize=True)}}
Impostando sanitize=True
web2py controllerà i tag non sicuri di XML (come, per esempio, "<script>") evitando le vulnerabilità di tipo XSS.
Se ora si seleziona il titolo di una pagina dalla pagina index, sarà visualizzata la pagina che è stata creata:
Ecco il codice della vista "default/edit.html":
{{extend 'layout.html'}}
<h1>Edit wiki page</h1>
[ {{=A('show', _href=URL('show', args=request.args))}} ]<br />
{{=form}}
che genera una pagina che sembra quasi identica alla pagina di creazione:
Ecco il codice per la vista "default/documents.html":
{{extend 'layout.html'}}
<h1>Documents for page: {{=page.title}}</h1>
[ {{=A('show', _href=URL('show', args=request.args))}} ]<br />
<h2>Documents</h2>
{{for document in documents:}}
{{=A(document.name, _href=URL('download', args=document.file))}}
<br />
{{pass}}
<h2>Post a document</h2>
{{=form}}
Se si clicca su un documento nella pagina "show", si può gestire il documento allegato alla pagina.
Ecco infine il codice per la vista "default/search.html":
{{extend 'layout.html'}}
<h1>Search wiki pages</h1>
[ {{=A('listall', _href=URL('index'))}}]<br />
{{=form}}<br />{{=target_div}}
che genera il seguente form di ricerca Ajax:
E' possibile anche tentare di richiamare l'azione direttamente visitando, per esempio, la seguente URL:
http://127.0.0.1:8000/mywiki/default/search?keyword=wiki
Nell'HTML della pagina di risposta si potrà vedere il codice restituito dalla funzione:
<ul><li><a href="/mywiki/default/show/4">I made a Wiki</a></li></ul>
La generazione del feed RSS delle pagine memorizzate è semplice in web2py perchè web2py include gluon.contrib.rss2
. E' sufficiente aggiungere lea seguente azione al controller di default:
def news():
"generates rss feed form the wiki pages"
pages = db().select(db.page.ALL, orderby=db.page.title)
return dict(
title = 'mywiki rss feed',
link = 'http://127.0.0.1:8000/mywiki/default/index',
description = 'mywiki news',
created_on = request.now,
items = [
dict(title = row.title,
link = URL('show', args=row.id),
description = MARKMIN(row.body).xml(),
created_on = row.created_on
) for row in pages])
e quando si visita la pagina
http://127.0.0.1:8000/mywiki/default/news.rss
sarà possibile visualizzare il feed (l'output esatto dipende dal visualizzatore di RSS). Il dict è automaticamente convertito in RSS, grazie all'estensione .rss nella URL.
web2py include anche feedparser per leggere feed esterni.
Ecco infine l'interfaccia XML-RPC per accedere alla ricerca nel wiki da un altro programma:
service=Service(globals())
@service.xmlrpc
def find_by(keyword):
"finds pages that contain keyword for XML-RPC"
return db(db.page.title.lower().like('%' + keyword + '%')) .select().as_list()
def call():
"exposes all registered services, including XML-RPC"
return service()
Qui l'azione semplicemente pubblica (via XML-RPC) le funzioni specificate nella lista. In questo caso find_by
. find_by
non è un'azione (perchè richiede un argomento). Interroga il database con .select()
ed estrae i record con .response
come una lista e la restituisce.
Ecco un esempio di come accede all'handler XML-RPC da un programma esterno in Python:
>>> import xmlrpclib
>>> server = xmlrpclib.ServerProxy(
'http://127.0.0.1:8000/mywiki/default/call/xmlrpc')
>>> for item in server.find_by('wiki'):
print item.created_on, item.title
L'handler può essere acceduto dai linguaggi di programmazione in grado di utilizzare XML-RPC (per esempio C, C++, C# e Java).
Altre funzionalità di admin
L'interfaccia amministrativa mette a disposizione le seguenti altre funzionalità, qui illustrate brevemente.
site
Questa pagina elenca tutte le applicazioni installate. Ci sono due form in basso, il primo consente di creare una nuova applicazione specificandone il nome
il secondo consente di caricare un'applicazione esistente sia da un file locale che da una URL remota. Quando si carica un'applicazione deve essere specificato il nome con il quale la si vuole creare, che può anche non essere il suo nome originale. In questo modo è possibile installare più copie della stessa applicazione. Si può, per esempio, caricare il Content Management System KPAX da:
http://web2py.com/appliances/default/download/app.source.221663266939.tar
Le applicazioni caricate possono essere file di tipo .tar
(utilizzati nelle precedenti versioni) e file di tipo .w2p
. I file di tipo .w2p
sono file tar compressi con gzip. Possono essere decompressi manualmente con il comando tar zxvf [nomefile]
sebbene questo non sia necessario.
Dopo aver caricato un'applicazione web2py visualizza il checksum MD5 del file caricato che può essere utilizzato per verificare che il file non si sia corrotto nell'upload.
In admin cliccare su KPAX per esegure l'applicazione.
I file dell'applicazione sono memorizzati come file w2p (tar gzip), ma non è necessario comprimerli o decomprimerli manualmente perchè questo è fatto direttamente da web2py.
Per ogni applicazione la pagina site consente di:
- Disinstallare l'applicazione.
- Andare alla pagina about (illustrata più sotto)
- Andare alla pagina edit (illustrata più sotto)
- Andare alla pagina errors (illustrata più sotto)
- Azzerare i file temporanei (sessioni, errori e file di cache.disk)
- Comprimere l'applicazione. Questo restituisce un file tar contenente una copia completa dell'applicazione. E' bene cancellare i file temporanei prima di eseguire tale operazione.
- Compilare l'applicazione. Se non ci sono errori questa operazione compilerà in bytecode tutti i modelli, i controller e le viste. Poichè le viste possono includere altre viste, tutte le viste di un controller sono incluse in un unico file. Il risultato di questa operazione è che le applicazioni compilate in bytecode sono più veloci perchè non viene eseguita a runtime l'analisi dei template o la sostituzione delle stringhe.
- Comprimere l'applicazione compilata. Questa opzione è presente solamente nelle applicazioni compilate in bytecode. Consente di comprimere una applicazione senza il codice sorgente per distribuirla come closed source. Python (come qualsiasi altro linguaggio di programmazione) può tecnicamente essere decompilato; perciò la compilazione non fornisce una protezione completa del codice sorgente. La decompilazione comunque può essere complessa da eseguire ed essere illegale.
- Rimuovere un'applicazione compilata. Rimuove solamente i modelli, le viste ed i controller compilati in bytecode. Se l'applicazione è stata compressa con il codice sorgente, oppure è stata progettata internamente non c'è pericolo nel rimuovere i file compilati, l'applicazione continuerà a funzionare correttamente. Se l'applicazione è stata recuperata da un file compresso contenente un'applicazione compilata, allora non è buona regola rimuoverla, perchè non essendo disponibile il codice sorgente l'applicazione non funzionerà più.
Tutte le funzionalità disponibili dall'applicazione admin di web2py sono anche accessibili da programma tramite le API definite nel modulo
gluon/admin.py
. Basta aprire una shell Python ed importare questo modulo.
about
E' possibile usare la sintassi MARKMIN
o quella gluon.contrib.markdown.WIKI
per questi file così come descritto in [markdown2] .
edit
La pagina edit è stata ampiamente usata in questo capitolo. Queste sono le altre funzialità accessibili da questa pagina:
- Cliccando sul nome di un file se ne visualizza il contenuto con la sintassi Python evidenziata.
- Cliccando su edit si può modificare il file tramite un'interfaccia web.
- Cliccando su delete si può cancellare il file in modo permanente.
- Cliccando su test web2py eseguirà i test definiti dallo sviluppatore utilizzando il modulo doctest di Python. Ogni funzione dovrebbe avere i propri test.
- E' possibile aggiungere nuovi file di linguaggio, scansionare l'applicazione per trovare nuove stringhe ed editare le traduzioni delle stringhe tramite l'interfaccia web.
- Se i file statici sono organizzati in una gerarchia di cartelle, queste possono essere aperte e chiuse cliccando sul loro nome.
L'immagine seguente mostra l'output della pagina di test per l'applicazione welcome.
L'immagine segunte mostra la sezione dei linguaggi per l'applicazione welcome.
L'immagine seguente mostra come modificare un file di linguaggio, in questo caso quello relativo alla lingua "it" (Italiano) per l'applicazione welcome.
shell
Cliccando su "shell" nella sezione dei controller web2py aprira una pagina web con una shell Python dove eseguirà i modelli dell'applicazione. Questo consente di interagire con l'applicazione.
crontab
Sempre sotto la sezione dei controller è presente il pulsante "crontab". Cliccando su di esso è possibile editare il file di crontab di web2py. Questo file segue la stessa sintassi del file di crontab di Unix, sebbene non utilizzi il cron di Unix. In effetti richiede solamente web2py e funziona anche in ambiente Windows. Questo consente di registrare delle azioni che saranno eseguite in background in momenti prestabiliti.
errors
Durante la programmazione di applicazioni in web2py è inevitabile fare errori ed introdurre bug. web2py aiuta lo sviluppatore in due modi: 1) consente di creare dei test per ogni funzione che può essere eseguita nel browser dalla pagina di edit e 2) quando si presenta un errore viene emesso un 'biglietto' (ticket) al visitatore e le informazioni relative all'errore sono memorizzate.
Ecco un errore introdotto di proposito nell'applicazione del blog di immagini:
def index():
images = db().select(db.image.ALL,orderby=db.image.title)
1/0
return dict(images=images)
Quando si accede all'azione index si otterrà il seguente ticket:
Solo l'amministratore di web2py può accedere al ticket:
Il ticket mostra il traceback ed il contenuto del file che ha causato il problema. Se l'errore è all'interno di una vista web2py mostra la vista convertita dall'HTML in codice Python. Questo consente di identificare facilmente la struttura logica del file.
In admin il codice Python è sempre mostrato con la sintassi evidenziata dai colori (per esempio nei report d'errore le keyword di web2py sono mostrate in arancione). Cliccando su una keyword di web2py si viene reindirizzati alla relativa pagina di documentazione.
Ze si corregge il bug dell'azione index e si introduce un nuovo bug nella vista index:
{{extend 'layout.html'}}
<h1>Current Images</h1>
<ul>
{{for image in images:}}
{{1/0}}
{{=LI(A(image.title, _href=URL("show", args=image.id)))}}
{{pass}}
</ul>
si otterrà il seguente ticket:
E' da notare che web2py ha convertito la vista dall'HTML in un file Python e per questo l'errore descritto nel ticket si riferisce al codice generato da Python e non alla vista originale.
Le prime volte questo può disorientare, ma in pratica rende il debug più facile perche l'indentazione di Python evidenzia la struttura logica del codice che è incorporato nelle viste.
Il codice è mostrato alla fine della stessa pagina. Tutti i ticket sono elencati in admin nella pagina errors di ogni applicazione:
Mercurial
Con le applicazioni in codice sorgente ed avendo installato le librerie di controllo delle versioni Mercurial:
easy_install mercurial
l'interfaccia amministrativa mostrerà un pulsante in più chiamato "mercurial". Questa opzione crea automaticamente un repository locale per l'applicazione utilizzando Mercurial. Premendo il pulsante "commit" l'applicazione attuale verrà salvata nel repository.
Questa funzione è sperimentale e sarà migliorata in futuro.
Altre funzionalità di appadmin
appadmin non è pensata per essere resa accessibile al pubblico. E' progettata per aiutare lo sviluppatore consentendo un facile accesso al database. Consiste di due soli file: un controller "appadmin.py" e una vista "appadmin.html" utilizzato da tutte le azioni del controller. Il controller appadmin è relativamente piccolo e leggibile e fornisce un esempio della progettazione di un'interfaccia per il database.
appadmin mostra quali database sono disponibili e quali tabelle esistono in ogni database. E' possibile aggiungere record ed elencare tutti i record per ogni tabella. appadmin presenta 100 record per pagina.
Quando un set di record è selezionato, l'intestazione delle pagine cambia, consentendo l'aggiornamento o la cancellazione dei record selezionati.
Per aggiornare i record inserire un comando SQL nel campo della stringa di Query:
title = 'test'
i valori delle stringhe devono essere racchiusi con apici singole ('). I campi possono essere separati dalla virgola. Per cancellare un record cliccare sulla sua casella di spunta e confermare l'operazione. Se il filtro SQL contiene una condizione che interessa più tabelle appadmin può anche eseguire delle unioni (join). Per esempio:
db.image.id == db.comment.image_id
web2py passa questa condizione al DAL il quale comprende che la query collega due tabelle, e di conseguenza ambedue le tabelle sono selezionate con una INNER JOIN:
Cliccando il numero del campo ID si ottiene una pagina di modifica per il record con l'ID corrispondente. Se si clicca sul numero di un campo di riferimento si ottiente una pagina di modifica per il record referenziato. Non è possibile aggiornare o cancellare le righe selezionate in una JOIN perchè queste rappresentano record provenienti da tabelle multiple e l'operazione sarebbe ambigua.