Personal tools
You are here: Home Project Documentation Development Pages, Views and Forms in Plone

Pages, Views and Forms in Plone

How we deploy pages, views and forms in Zope and Plone.

There are a number of different methods we use for dynamic web pages, depending on the required functionality, and these are documented here.  As we use Plone 3.1 for the portal, we will use Zope 3 style development, but within the Zope 2.x installed runtime (ie selected features of Zope 3.x brought forward into the 2.x distribution to get developers using them an have smoother transition to the radically different Zope 3 architecture).

We've used paster to generate a Plone-installable Python egg product.  You can get the egg from the WASTAC svn egg repository.

Without providing an over-view of Zope 3, it is a bit different to other MVC products (eg JEE).  The idea is that Zope 3 is much more about native Python, with any Zopish bits well defined and completely out of the middleware model.  Any web code is contained within the browser directory as views.  Views and their URIs are configured within the relevant configure.zcml and will use a Zope Page Template for HTML or XML presentation and a View based python class to off-load any complex logic from the page template.

The following main cases are documented in order of increasing complexity:

  • Basic Zope 3 Page - No Plone Integration

  • Basic Zope 3 Page - Zope 3 View Class (still no Plone)

  • Basic Zope 3 Page - Modified Zope 3 View Class (still no Plone)
  • Plone Views

  • Plone Forms using z3c.form

 Read on to find out the details!

 

Basic Zope 3 Page - No Plone Integration

Let's start with the most basic type of dynamic page - one that does not require any Plone integration and simply renders a basic page template.  We use the example here of generating a simple bit of mostly static KML from a Zope 3 View.  This is not a web page with portlets, headings and images, but a plain template containing XML.

Start with configure.zcml to map the url http://wastac.ivec.org/folder.kml to the page template .../browser/kml_folder.pt(.)

    <browser:page
        name="folder.kml"
        for="*"
        permission="zope.Public"
        template="kml_folder.pt"/>

The page template is here;

<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://earth.google.com/kml/2.1"
     xmlns:tal="http://xml.zope.org/namespaces/tal"
     tal:define="dummy python:view.request.response.setHeader('Content-Type','application/vnd.google-earth.kml+xml')">

  <Folder>

    <name>Dynamic Tiles</name>
    <visibility>0</visibility>
    <open>0</open>
    <description>10km tiles</description>
    <NetworkLink>
      <name>Random Placemark</name>
      <visibility>0</visibility>
      <open>0</open>
      <description>Regular Earth Grid web link</description>
      <refreshVisibility>0</refreshVisibility>
      <flyToView>0</flyToView>
      <Link>
    <viewRefreshMode>onStop</viewRefreshMode>
    <href tal:content="string:${context/portal_url}/tiles.kml"/>
      </Link>
    </NetworkLink>

  </Folder>

</kml>

Pretty simple - just a URI pointing to a Zope Page Template.  We don't need an associated Python view class because there is no logic really that we need to offload from the page template.  This is the next case.

 

Basic Zope 3 Page - Zope 3 View Class (still no Plone)

Let's look at the same as above, but this time we have some logic to execute from the page template, which should be done externally in a python view class to keep the page template simple.

We take the example of KML tiles rendered to XML by querying the database for their geometry.  So, given we want some tiles to be served up from http://wastac.ivec.org/tiles.kml, we start by configuring configure.zcml

    <browser:page
        name="tiles.kml"
        class=".kml_tiles.KmlTiles"
        for="*"
        permission="zope.Public"
        template="kml_tiles.pt"/>

Note the appearance of the class parameter now saying we have an associated python class accessible from the familiar page template.

The above configuration will call this template:

<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://earth.google.com/kml/2.1"
     xmlns:tal="http://xml.zope.org/namespaces/tal"
     tal:define="dummy python:view.request.response.setHeader('Content-Type',
                                        'application/vnd.google-earth.kml+xml')">
     
  <Document>
  
    <name>RegularEarthTiles</name>
    <description>This kml file will draw a 10km grid over the surface of the earth across your view. A maximum of 5000 tiles are drawn</description>
    <Style id="tilePolyStyle">
      <LineStyle>
        <color>aa000000</color>
        <width>1</width>
      </LineStyle>
      <PolyStyle>
        <color>00555555</color>
        <fill>1</fill>
      </PolyStyle>
    </Style>

    <Placemark tal:repeat="tile view/getTiles">
      <name tal:content="string:${tile/tile_id}"></name>
      <Polygon>
      <styleUrl>#tilePolyStyle</styleUrl>
      <extrude>1</extrude>
      <tessellate>1</tessellate>
      <altitudeMode>relativeToGround</altitudeMode>
      <outerBoundaryIs>
      <LinearRing tal:define="b tile/border/bounds">
        <coordinates>
          <tal:block tal:replace="python:str(b[0]) + ',' + str(b[1])"/>
          <tal:block tal:replace="python:str(b[0]) + ',' + str(b[3])"/>
          <tal:block tal:replace="python:str(b[2]) + ',' + str(b[3])"/>
          <tal:block tal:replace="python:str(b[2]) + ',' + str(b[1])"/>
          <tal:block tal:replace="python:str(b[0]) + ',' + str(b[1])"/>
        </coordinates>
      </LinearRing>
      </outerBoundaryIs>
      </Polygon> 
    </Placemark>

  </Document>
</kml>


 The bit of logic here that we have placed externally is the retrieval of the tiles from the database - the loop over the view's getTiles() method as can be seen in the view/getTiles TALES expression above.

Here is the view class implementation.

from zope.publisher.browser import BrowserView

from wastac.wastacskin import db
from wastac.wastacskin.orm import TileGeometry


class KmlTiles(BrowserView):
    """
      Generate KML of our standard tiled grid.  If a Google Earth style BBOX
      is supplied this will be used in the tile query to restrict results.
    """
    
    def unpackBBox(self, bbox):
        """Utility method to get dictionary of x0, y0, x1, y1 from
           BBOX query string in request.  Used for substituting into SQL filter"""
        bbox = bbox.split(',')
        x0 = float(bbox[0])
        y0 = float(bbox[1])
        x1 = float(bbox[2])
        y1 = float(bbox[3])
        return {'x0':x0, 'y0':y0, 'x1':x1, 'y1':y1}


    def getTiles(self):
        """Called by browser as usual"""
        bbox = self.request.get('BBOX','115,-32.5,116.5,-31.5')

        unpackedCoords = self.unpackBBox(bbox)

        session = db.getSAWrapper().session
        q = session.query(TileGeometry).filter(
            "border && GeomFromText('POLYGON((:x0 :y0, :x1 :y0, :x1 :y1, :x0 :y1, :x0 :y0))', -1)"
            ).limit(5000)

        # Accessible through the view
        tiles = q.params(unpackedCoords).all()

        return tiles

Try to get past the ORM query at this stage and focus on the structure of the class because we're just trying to go over the page templating.

Note the use of zope.publisher.browser.BrowserView class - this is pure Zope 3 style.  (Note: you can also use zope.publisher.browser.BrowserPage and I don't really know what the difference is yet).

If you're curious, the list of tiles returned by getTiles will be a list of TileGeometry instances which, when loaded up using the ORM layer from database tables will look something like the following, which you can see accessed in the page template as tile/border:

class TileGeometry(object):
    """Represents wastac.t_tile_geometry"""
    tile_id # int
    grid_id # int
    border # Shapely geometry object
    centre # Shapely geometry object

This is looking alot more like the familar MVC pattern now.  To summarize we have a view (split into page template and associated python view class), and the model object which in this case is expressed as the TileGeometry ORM bean.

 

Basic Zope 3 Page - Modified Zope 3 View Class (still no Plone)

The above is fine for most scenarios, but say you want to actually do something when the view is called.  Examples may be to check for the existence of the right stored session information or pre-load some data into a class attribute.  In this scenario we use the same method as above, but implement a __call__ method on the class.  Remember views are simply Zope 3 adapters for the browser (adapters are fundamental to Zope 3, so read about them elsewhere).  So if we were to change the previous view, then we need only remove the template shortcut from the configure.zcml,

<browser:page
        name="tiles.kml"
        class=".kml_tiles.KmlTiles"
        for="*"
        permission="zope.Public" />

and make a change to the view class,

from zope.publisher.browser import BrowserView
from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile

from wastac.wastacskin import db
from wastac.wastacskin.orm import TileGeometry


class KmlTiles(BrowserView):
    """
      Generate KML of our standard tiled grid.  If a Google Earth style BBOX
      is supplied this will be used in the tile query to restrict results.
    """

    template = ViewPageTemplateFile('kml_tiles.pt')

    ...    

    def __call__(self):

        # do something

        return self.template()

Note the new import and the return of the executed template.

 

At Last - Plone Views

The above examples are pure Zope 3 and necessary to know.  Now say we want a view rendered within a Plone site, then we need to adopt the Plone page templates, which are Zope 2 style templates using stuff like METAL for defining "slots" in Plone's main_template.pt and "fills" in our local templates.

To do this we still use Zope 3 style view configuration/definition, but the critical change is that we replace the Zope 3 BrowserView and ViewPageTemplateFile (if used) classes with Five implementations to provide Zope 2 page template backwards compatibility.  In other words, we are constructing a Zope 3 view, but because we want to integrate with Plone, we will use Five to load a Plone-style page template (which is Zope 2 style).

Let's take the example of our search summary view, which displays the search summary at http://wastac.ivec.org/search_summary.html.  First the usual config (note we don't specify the template because we will want to provide a __call__ method to ensure the user has a valid submitted search already in the session):

<browser:page
        name="search_summary.html"
        for="Products.CMFPlone.Portal.PloneSite"
        class=".search_summary.SearchSummaryView"
        permission="zope.Public"
        />

The class:

from Products.Five import BrowserView
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile

from wastac.wastacskin import db
from wastac.wastacskin.sql import getSwathQuery

class SearchSummaryView(BrowserView):
    
    template = ViewPageTemplateFile('search_summary.pt')
    
    def getSearch(self):
        """Returns a SwathSearch"""
        return self.request['SESSION'].get('search', None)

    def getCount(self):
        session = db.getSAWrapper().session
        query = getSwathQuery(self.getSearch(), session)
        return query.getCount()
    
    def __call__(self):
        
        # The only reason we override __call__ here is to
        # intercept the display of the template in order to
        # check we have a session and search established.

        if self.getSearch() is None:
            return self.request.response.redirect('search_raw.html')

        return self.template()

And to save space, the page template can be seen here.  Note that in the above, to get the view methods we use view.getCount() etc from the view page template as normal.  Note that we're using Five products in the imports - otherwise the class looks similar in structure to the previous section.

 

Plone Forms Using z3c.form

Ok this case is the trickiest, but doable thanks to the plone.app.z3cform product.  Say we want to use a Zope 3 style schema definition (read about it here) to drive a search form inside Plone, and have z3c.form provide the template rendering and form validation automatically.  This shouldn't be hard, but was because there is no native z3c.form integration for Plone out of the box (Plone uses another form engine called formlib).  Remember z3c.form is community contributed and formlib is bundled with Plone.

So why use z3c.form instead then?  Because we trying to do context-less MVC style forms, which is a fair bit different to how Zope and Plone forms are commonly used (ie executed in the context of an object such as an ATDocument or NewsItem).  Our portal searches are querying an external database, are not executed in context of a local content object in the ZODB, and we are not using the local CMS catalogue either.  Therefore we have no context except the top-level "CMFPloneSite", which in fact all our requests have.  (Remember Zope 2 context acquisition with http://...../mysite/folder/contentObject/myView).

Ok onto the solution.  First off, our model bean is to be expressed using zope 3 style schema ISwathSearch for the model, and associated SwathSearch object definition will be used to drive any forms that seek to build a search.  Vocabularies are used to dynamically populate the fields from database.

from zope import interface, schema

class ISwathSearch(interface.Interface):
    
    sensorId = schema.Choice(title = u'Sensor',
                             required = True,
                             vocabulary = "sensorVocabulary")

    satelliteId = schema.Choice(title = u'Satellite',
                             required = True,
                             vocabulary = "satelliteVocabulary")

    acquirerId = schema.Choice(title = u'Acquirer',
                             required = True,
                             vocabulary = "acquirerVocabulary")
    
    swathTimestampStart = schema.Date(title = u'Date search range start',
                                      required= False
                                      )
    
    swathTimestampEnd = schema.Date(title = u'Date search range end',
                                    required = False
                                    )

    quicklook = schema.Choice(title = u'Quicklook availability',
                            required = True,
                            vocabulary = "quicklookVocabulary")
    
    ....

If you're curious how the vocabs are defined, then it's just a Zope 3 utility adapter (utility is a singleton that is pre-loaded, adapter being something that can be called).

 <utility name="sensorVocabulary"
            component="wastac.wastacskin.vocabularies.sensorVocabulary"/>

and this (yes a function can be called so a function can be an adapter!):

def sensorVocabulary(context):
    sensors = db.getSAWrapper().session.query(Sensor)
    terms = [SimpleVocabulary.createTerm("", "", "Any")]

    for sensor in sensors:
        term = SimpleVocabulary.createTerm(sensor.sensor_id,
                                           sensor.sensor_id,
                                           sensor.sensor_name)
        terms.append(term)
    vocab = SimpleVocabulary(terms)
    return vocab

Anyway getting back to the model definition, the implementation of ISwathSearch that we use for building the search and storing in the session for retrieving when searching,

from zope import interface

from wastac.wastacskin.interfaces import ISwathSearch
from wastac.wastacskin.vocabularies import *

from shapely.geometry import Polygon

class SwathSearch(object):
    
    interface.implements(ISwathSearch)
    
    # Defaults
    offset = 0
    limit = 20
    
    def __init__(self, context, **kw):
        self.update(context, **kw)
        # Setup any defaults
        self.offset = 0
        self.limit = 20
        self.order_by = None
        self.order_dir = u'asc'    
    
    
    def update(self, context, **kw):
        """Update all fields."""
        for (k,v) in kw.items():
            setattr(self, k, v)

        # Manually update those fields not populated by form from the database
        # These are mainly the readable names of ids the form submits
        # TODO: Should be automatic
        if kw.has_key('sensorId'):
            self.sensorName = sensorVocabulary(context).getTerm(kw['sensorId']).title

        if kw.has_key('acquirerId'):
            self.acquirerName = acquirerVocabulary(context).getTerm(kw['acquirerId']).title
        
    ....

 Note the complete absence of web semantics in any of above.  It's essentially our middleware model (Zope style).

Now for the web part, starting with the familiar configure.zcml in the browser directory:

   <browser:page
        name="search_form.html"
        for="Products.CMFPlone.Portal.PloneSite"
        class=".search_form_raw.SearchFormRawView"
        permission="zope.Public"
        />

Now there is no template here.  We want the form engine to generate the template and associated validation automatically from the schema definition above.  However we do provide the form handler class so we can do something (populate the search object in session) when the form is successfully submitted and validated.  Here is the SearchBaseForm class, which is wrapped into the SearchFormRawView (declared above) using plone.app.z3cform.

from z3c.form import form, field, button
from z3c.form.interfaces import INPUT_MODE

from plone.app.z3cform.layout import wrap_form

from collective.z3cform.datepicker.widget import DatePickerFieldWidget
from collective.z3cform.datepicker.widget import DateTimePickerFieldWidget

from wastac.wastacskin.SwathSearch import SwathSearch
from wastac.wastacskin.interfaces import ISwathSearch


class SearchFormRaw(form.Form):
   
    fields = field.Fields(ISwathSearch).select('sensorId', 'satelliteId', 'acquirerId',
                                               'swathTimestampStart', 'swathTimestampEnd',
                                               'quicklook')

    fields['swathTimestampStart'].widgetFactory[INPUT_MODE] = DatePickerFieldWidget
    fields['swathTimestampEnd'].widgetFactory[INPUT_MODE] = DatePickerFieldWidget

    ignoreContext = True 

    label = 'Search All Data'
    
    @button.buttonAndHandler(u'Search')
    def handleSearch(self, action):
        """Execute the form's search submission"""
    
        data, errors = self.extractData()
        
        if not errors:
            swathSearch = SwathSearch(self, **data)
            self.request['SESSION']['search'] = swathSearch
            self.request.response.redirect('search_summary.html')

# Wrap z3c form for Plone - this is the class we expose       
SearchFormRawView = wrap_form(SearchFormRaw)

Ignore the rebinding of the widget - we're using a community contributed date picker widget.  The important things to note are,

  • the parent of the class
  • the definition of the fields attribute
  • the definition of the handler function on the class (by using a decoration)
  • the wrapping of the z3c.form into a Plone page template using plone.app.z3cform on the last line.

 

References:

 http://plone.org/documentation/how-to/easy-forms-with-plone3

 http://kayeva.wordpress.com/2008/07/16/using-z3cform-for-our-forms-in-plone/

 

Mistakes or Corrections?

nick@petangent.net

Document Actions
Log in


Forgot your password?