Crud form e SqlAlchemy

Utilizzo del modulo plone.z3cform.crud con elementi registrati su un database relazionale

Una chiacchierata con l'amico Sauzher, riguardo la sua esperienza con le crud form, mi ha spinto a fare un piccolo test per capire i problemi riguardanti l'utilizzo dell'apposito modulo del pacchetto plone.z3cform con oggetti memorizzati su database relazionali.

Premesse

In un altro post ho parlato di come utilizzare il traverse di Zope per la manipolazione dei dati presenti su database relazionali; in questo nuovo esempio utilizzeremo un database simile.

Il database agenda.sqlite contiene solamente una tabella persone i cui campi sono i seguenti:

  • id
  • nome
  • descrizione

Per la prova sono stati utilizzati i seguenti pacchetti:

  • Plone 3.3.x
  • SQLAlchemy 0.4.8
  • pysqlite 2.5.6
  • collective.lead 1.0
  • z3c.form 1.9.0
  • plone.z3cform 0.5.5
  • plone.app.z3cform 0.4.6

Cosa voglio ottenere

Dalla root del portale voglio accedere ad un elemento agenda che contiene la crud form per l'aggiunta e la cancellazione di elementi.

Siccome voglio che il campo descrizione dell'oggetto persona contenga dell'HTML e sia editabile con l'editor di default di Plone, nella crud form permetterò la modifica diretta solamente del nome dell'elemento mentre la descrizione sarà modificabile in una form separata.

La form per la modifica degli elementi del database sarà raggiungibile, a partire dall'oggetto agenda, con un url corrispondente all'id dell'oggetto.

Ognuno di questi oggetti dev'essere integrato in Plone ovvero nelle bread crumbs.

In un poche parole:

  • http://ilmiositoplone/agenda - porta alla crud form
  • http://ilmiositoplone/agenda/1/edit - porta alla form di modifica dell'elemento con id 1

Realizzazione

Per la realizzazione dell'esempio è stato creato un pacchetto che nominato text.crudform

Il file interfaces.py contiene la descrizione degli oggetti che mi occorreranno per realizzare quanto preventivato:

from zope.interface import Interface
from zope import schema

class IAgenda(Interface):
"""IAgenda è un marcatore per l'oggetto Agenda"""
...

class IPersona(Interface):
"""IPersona corrisponde ad un oggetto mappato su database"""
id = schema.TextLine(
title = u"Id",
required = True,
description =u"Identificativo univoco della persona")

nome = schema.TextLine(
title = u"Nome",
required = True,
description =u"")

descrizione = schema.Text(
title = u"Descrizione",
required = True,
description =u"descrizione breve della persona")

Nel file configure.zcml inserisco tutte le configurazioni necessarie:

...
<!-- includo i pacchetti esterni necessari al progetto -->
<include package="plone.app.z3cform" />
<include package="collective.lead" />

<!-- configuro l'utility per la connessione al database -->
<utility
provides="collective.lead.interfaces.IDatabase"
factory=".database.DBAgenda"
name="agenda"/>

<!-- configuro il traverse per l'oggetto agenda -->
<adapter factory = ".traverse.agendaTraverser"/>

<!-- configuro una vista per IAgenda contenente la crud form -->
<browser:page
for="test.crudform.interfaces.IAgenda"
name="view"
permission="cmf.ManagePortal"/>

<!-- rendo la precedente vista il default per IAgenda -->
<browser:defaultView
for="test.crudform.interfaces.IAgenda"
name="view"/>

<!-- configuro una vista per la modifica di IPersona -->
<browser:page
for="test.crudform.interfaces.IPersona"
name="edit"
permission="cmf.ManagePortal"/>

<!-- Siccome non realizzerà altre viste imposto la precedente vista di
default per IPersona -->
<browser:defaultView
for="test.crudform.interfaces.IPersona"
name="edit"/>
...

Il file database.py contiene la configurazione del database.

import sqlalchemy as sa
from collective.lead import Database
from test.crudform.persona import Persona

class DBAgenda(Database):

@property
def _url(self):
"""_url - restituisce un url per la connessione al database """
return sa.engine.url.URL(
drivername = 'sqlite',
database = "/path/assoluto/al/database/agenda.sqlite")

def _setup_tables(self, metadata, tables):
"""_setup_tables - per ogni tabella definisco le sue proprietà """
tables['persone'] = sa.Table('persone', metadata,
sa.Column('id', sa.Integer,
primary_key=True),
sa.Column('nome', sa.Text),
sa.Column('descrizione', sa.Text))

def _setup_mappers(self, tables, mappers):
"""_setup_mappers - definisco la mappatura tra le tabelle definite
 in precedenza e degli oggetti python"""
mappers['persone'] = sa.orm.mapper(Persona, tables['persone'])

Nel file persona.py definisco la classe Persona che implementerà IPersona:

from zope.interface import implements
from OFS.SimpleItem import SimpleItem
from test.crudform.interfaces import IPersona

class Persona(SimpleItem):
implements(IPersona)

__name__ = None
__parent__ = None
nome = u""
descrizione = u""

def getId(self):
return str(self.id)

def setId(self, id):
self.id = id

id = property(getId, setId)

def Title(self):
return self.nome

Nel file agenda.py definisco la classe Agenda che implementerà IAgenda; tale oggetto sarà attraversabile per raggiungere gli oggetti IPersona:

from OFS.SimpleItem import SimpleItem
from zope.interface import implements
from zope.component import getUtility
from zope.publisher.interfaces import IPublishTraverse
from zope.publisher.interfaces import NotFound
from collective.lead.interfaces import IDatabase
from test.crudform.interfaces import IAgenda
from test.crudform.persona import Persona

class Agenda(SimpleItem):
implements(IAgenda, IPublishTraverse)

__name__ = None
__parent__ = None
title = u'Agenda'
id = 'agenda'

def Title(self):
return self.title

def publishTraverse(self, request, id_persona):
if id_persona:
db = getUtility(IDatabase, name='agenda')
results = db.session.query(Persona).filter_by(id=id_persona)
if results.count() == 0:
raise NotFound(self, id_persona, request)

persona = results.one()
persona.__name__ = id_persona
persona.__parent__ = self
return persona.__of__(self)

In ultimo nel file traverse.py definisco il traverse per raggiungere l'oggetto IAgenda:

from zope.component import adapts
from zope.publisher.interfaces import IRequest
from ZPublisher.BaseRequest import DefaultPublishTraverse
from Products.CMFPlone.interfaces.siteroot import IPloneSiteRoot
from test.crudform.agenda import Agenda

class agendaTraverser(DefaultPublishTraverse):
adapts(IPloneSiteRoot, IRequest)

def fallback(self, request, name):
return super(agendaTraverser, self).publishTraverse(request, name)

def publishTraverse(self, request, name):
if name == 'agenda':
agenda = Agenda()
agenda.__name__ = name
agenda.__parent__ = self.context
return agenda.__of__(self.context)

return self.fallback(request, name)

Una volta definita correttamente la struttura passo a definire le viste per la realizzazione delle form.

Nel file browser.py inizio a creare il sistema di form per l'agenda:

from zope.component import getUtility
from z3c.form import field, form
from z3c.form.interfaces import INPUT_MODE
from plone.z3cform.crud import crud
from plone.z3cform.layout import wrap_form
from plone.app.z3cform.wysiwyg.widget import WysiwygFieldWidget
from collective.lead.interfaces import IDatabase
from test.crudform.interfaces import IPersona
from test.crudform.persona import Persona

class AgendaEditSubForm(crud.EditSubForm):
""" Form utilizzata per la modifica dei singoli elementi della crud form
Corrisponde a ogni singola riga della crud form.

In questo momento non mi serve a niente però era tanto per vedere
che si può fare :) """

class AgendaEditForm(crud.EditForm):
""" Form principale per la modifica e la cancellazione degli elementi
della crud form.

Corrisponde al contenitore degli elementi rappresentati
da crud.EditSubForm

In questo modo posso personalizzare questa form modificandone
le proprietà. """

label = u"Gestione agenda"
editsubform_factory = AgendaEditSubForm

class AgendaCrudForm(crud.CrudForm):
"""AgendaCrudForm è la parte principale del sistema di form, contiene i metodi
e le proprità fondamentali per il funzionamento di tutto.

view_schema - permette di selezionare i campi che si vogliono solamente
visualizzare nella crud form, in questo caso mi permette di avere
un link sul nome della persona che porterà alla form di modifica

update_schema - permette di selezionare i campi degli elementi che
si potranno modificare nella crud form

add_schema - permette i selezionare i campi necessari per l'aggiunta
di un nuovo elemento """

view_schema = field.Fields(IPersona).select('nome', 'descrizione')
view_schema['descrizione'].widgetFactory = WysiwygFieldWidget

update_schema = field.Fields(IPersona).select('nome')

add_schema = field.Fields(IPersona).select('nome', 'descrizione')
add_schema['descrizione'].widgetFactory[INPUT_MODE] = WysiwygFieldWidget

editform_factory = AgendaEditForm

def __init__(self, context, request):
super(AgendaCrudForm, self).__init__(context, request)
# questo è il mio database
self.db = getUtility(IDatabase, name='agenda')
# disabilito l'aera di modifica di Plone
self.request.set('disable_border', True)

def get_items(self):
"""get_items mi restituisce una lista di elementi che corrispondono
agli elementi della crud form nella seguente forma:

[(id, oggetto), (id, oggetto)] """

persone = self.db.session.query(Persona).order_by(Persona.nome)
return [(persona.id, persona) for persona in persone.all()]

def add(self, data):
"""add contiene e azioni per l'aggiunta di un nuovo elemento"""
persona = Persona()
persona.nome = data['nome']
persona.descrizione = data['descrizione']

self.db.session.save(persona)
self.db.session.flush()

def remove(self, (id, persona)):
"""remove contiene l'azione per la cancellazione degli elementi"""
self.db.session.delete(persona)
self.db.session.flush()

def link(self, item, field):
"""link permette di associare un link ai campi visualizzati nella
crud form.
In questo caso il link porta alla modifica 'estesa' dell'elemento
"""
if field == 'nome':
return'%s/%s/edit' % \
(self.context.absolute_url(), item.id)

# con questa funzione la form viene inclusa in una pagina Plone
Agenda = wrap_form(AgendaCrudForm)

E la modifica degli elementi si chiederà qualcuno di voi? Beh è la magia della ZCA viene fatto tutto nell'azione della form crud.EditForm (cfr. plone.z3cform.crud.EditForm)

Manca un ultima cosa, la form di modifica del singolo elemento; la aggiungo in fondo al file browser.py

class PersonaEditForm(form.EditForm):
"""PersonaEditForm semplice form per la modifica di un oggetto IPersona"""

fields = field.Fields(IPersona).select('nome', 'descrizione')
fields['descrizione'].widgetFactory = WysiwygFieldWidget
...

PersonaEdit = wrap_form(PersonaEditForm)

Plone crud form

 

 

Sembra piuttosto complicato ma vi assicuro che è più difficile spiegarlo che farlo.

Con pochi altri adattamenti potremmo personalizzare ogni singola riga della nostra crud form piuttosto che il layout generale di tutto il sistema di form.

Nel modulo crud di plone.z3cform esiste anche un metodo per la paginazione degli elementi della crudform.

Non ne ho parlato apertamente in questo post ma consiglio a tutti di dare un'occhiata direttamente al modulo interessato.

z3c.form è un ottimo sistema per realizzare form complesse in Zope; è un sistema piuttosto complesso che, se usato correttamente, permette di avere uno strumento potentissimo configurabile in ogni sua piccola parte.

blog comments powered by Disqus