Combobox dipendenti (o in sequenza) con z3c.form

Come realizzare una serie di widget dipendenti uno dall'altro con z3c.form e jquery

Devo realizzare una form che permetta ad un utente di selezionare uno o più valori attraverso una sequenza di scelte obbligate. Per esempio scegliere una provincia italiana in base alla regione selezionata precedentemente.

Per fare questo sarebbe molto utile, nel caso stessi lavorando con Archetypes, il prodotto Master Select Widget; purtroppo, o per fortuna, non devo utilizzare Archetypes bensì z3c.form che non dispone di un pacchetto preconfezionato per fare questo.

Devo quindi inventarmi un modo per farlo direttamente con quello che ho a disposizione:

  • z3c.form
  • jQuery

Il funzionamento dei widget sulla carta è abbastanza semplice; quando l'utente visualizza la form il primo combobox visualizzerà una serie di opzioni possibili (le regioni italiane) mentre il secondo widget rimarrà vuoto.

Una volta selezionata una regione, il javascript farà una richiesta asincrona al server e riceverà in risposta l'elenco delle province filtrato in base alla regione scelta. L'elenco delle province così ricevuto sarà utilizzato per creare le opzioni del secondo widget.

Ecco qui la lista della spesa:

  • un'interfaccia che descriva la form
  • una classe che rappresenti la form
  • una BrowserView che visualizzi la form e il javascript necessario
  • una vista che restituisca in json le province italiane selezionate in base a una regione data

Posso iniziare a registrare le due vista in zcml

...
<!-- vista per la form -->
<browser:page
for="Products.CMFPlone.interfaces.siteroot.IPloneSiteRoot"
name="regioni_e_province"
class=".regionieprovince.SearchProvince"
permission="zope2.View"
/>

<!-- vista che mi restituisce l'elenco delle province filtrato per regione -->
<browser:page
for="Products.CMFPlone.interfaces.siteroot.IPloneSiteRoot"
name="get_province.json"
class=".regionieprovince.JSONProvinceView"
permission="zope2.View"
/>
...

e scrivere un'interfaccia che descriva la mia form:

from zope.interface import Interface

class ISearch(Interface):
""" form di ricerca per regione e provincia """

regione = schema.Choice(
vocabulary="regioniVocab",
title = _(u'Regione'),
description=_(u""),
required = False,)

provincia = schema.Choice(
vocabulary="provinceVocab",
title = _(u'Provincia'),
description=_(u""),
required = False,)

in questo primo esempio regioniVocab e provinceVocab sono due utility che mi restituiscono l'elenco completo sia delle regioni che delle province; nel caso l'elenco di tutte le province fosse troppo lungo sarebbe meglio utilizzare un vocabolario vuoto per il widget (vedi più sotto).

Passo quindi a impostare la form come da manuale

from z3c.form import field, form, button

class searchForm(form.Form):
fields = field.Fields(ISearch)
ignoreContext = True
prefix = 'search_form'

@button.buttonAndHandler(_(u'Search'))
def handleApply(self, action):
data, errors = self.extractData()
# segue l'azione della form
...

e infine a realizzare la vista e il template per visualizzare la form. La parte importante riguarda la realizzazione del javascript che deve caricare l'elenco delle province in due distinti momenti:

  • al load della pagina
  • al cambio di valore del widget regioni (evento onChance)

Abbiamo quindi una semplice vista che eredita da FormWrapper

from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from plone.z3cform.layout import FormWrapper

class SearchProvince(FormWrapper):
form = JPNewForm
index = ViewPageTemplateFile('search_province.pt')

e il relativo template con il javascript

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
...
<head>
<metal:js fill-slot="javascript_head_slot">
<script type="text/javascript">

/* azione al caricamento della pagina */
jq(document).ready( function(){

/* richiamo la vista get_province.json che mi restituisce l'elenco delle province data una regione */
jq.getJSON("@@get_province.json",
{regione: jq("select#search_form-widgets-regione").val(), ajax: 'true'},
function(j){

/* alla callback creo un elenco di tag option secondo i dati ricevuti */
var options = '<option id="search_form-widgets-provincia-novalue" value="--NOVALUE--">Nessun valore</option>';
for (var i = 0; i < j.length; i++) {
options += '<option value="' + j[i].optionValue + '">' + j[i].optionDisplay + '</option>';
}

/* assegno i valori al widget delle province */
jq("select#search_form-widgets-provincia").html(options);
});
});


/* azione al change del widget delle regioni
fa esattamente gli stessi passaggi della precedente funzione */
jq(function(){

jq("select#search_form-widgets-regione").change(function(){

jq.getJSON("@@get_province.json",
{regione: jq(this).val(), ajax: 'true'},
function(j){

var options = '<option id="search_form-widgets-provincia-novalue" value="--NOVALUE--">Nessun valore</option>';

for (var i = 0; i < j.length; i++) {
options += '<option id="search_form-widgets-provincia-' + i + '" value="' + j[i].optionValue + '">' + j[i].optionDisplay + '</option>';
}

jq("select#search_form-widgets-provincia").html(options);
})
});

</script>
</metal:js>
...
</head>
...
</body>
</html>

come ultima cosa dobbiamo realizzare la vista che restituisce l'elenco delle province:

import simplejson

class JSONProvinceView(BrowserView):
def __call__(self, regione):
province = [dict(optionValue=prov.sigla, optionDisplay=prov.provincia) \
for prov in selezionaprovince(regione)]
return simplejson.dumps(province)

selezionaprovince è un modulo che creato ad hoc che mi permette di selezionare le province in base a una regione data. Non è importante il funzionamento dello stesso bensì è importante capire la semplicità con cui viene restituito l'elenco delle province in json.

Un ultimo appunto merita il caso in cui non avessi associato un vocabolario al widget direttamente dall'interfaccia. Modificando l'interfaccia della form nel modo seguente

...
class ISearch(Interface):
""" form di ricerca per regione e provincia """
...

provincia = schema.Choice(
values=('Nessun valore',),
title = _(u'Provincia'),
description=_(u""),
required = False,)
...

z3c.form non saprebbe trovare il valore della provincia in quanto esso non è presente all'interno del vocabolario associato al campo. Prima di estrarre i dati devo quindi associare un vocabolario al campo e al widget.

...
class searchForm(form.Form):
...
@button.buttonAndHandler(_(u'Search'))
def handleApply(self, action):
...
# assegno un vocabolario al field e al widget 'provincia'
self.fields['provincia'].field.vocabulary = self.get_province(self.widget['regione'].value[0])
self.widgets['provincia'].terms.terms = self.get_province(self.widget['regione'].value[0])


# una volta assegnato il vocabolario corretto il seguente metodo funzionerà normalmente
data, errors = self.extractData()
# segue l'azione della form
...

def get_province(regione):
"""questa funzione deve restituire un vocabolario
di province in base alla regione data """
...

Un grazie a Maurizio (miziodel) che mi ha dato un po' di suo codice da copiare a patto che ne scrivessi un post; mi è sembrato un ottimo scambio.

blog comments powered by Disqus