[The code below has now been update to reflect the advice from Martin in the comments below]
Plone 4
A few people have asked recently if Plone 4 is ready for production use yet, and so I thought I'd share the experiences of developing the Plone Conference 2010 website, which is running Plone 4.0b3.
The short answer seems to be 'yes'. The longer answer probably depends on if you are starting from scratch on a new project or upgrading an existing Plone 3 site. There are a number of 3rd party Plone products that still need further testing on Plone 4, but I'm please to see that the ones I tried all seemed to work.
Not only are we using Plone 4, but we are also using Dexterity (1.0b1), the new content types system for Plone; PloneGetPaid (0.9.1), the e-Commerce system for Plone; and plone.app.caching (1.0a1), the replacement for CacheSetup which enables Plone to work nicely with HTTP caches such as Varnish (which we are using here). All of these are the released versions from pypi, so no SVN versions needed, and work very well together. So we really are 'eating our own dogfood' here and whilst it might seem a bit scary to be on the cutting edge for a production site, the whole experience has been very good.
Plone 4 is lightning fast, and all the hard work put in by the community really makes it a joy to work with. Restarting Plone during development is much quicker than with previous versions of Plone.
If fact we are using a combination of both Dexterity and Archetypes on this site. All the regular content on the site (folders, news items, pages, images, etc) are Archetypes based, but the content types for the registrations system are Dexterity based. Why? Because the site was developed by two people, and one wanted to stick to Archetypes as that was what they were familiar with, and the other person wanted to give Dexterity a try. Both run side by side just fine.
The Registration System
My goal for the registration system was to make it as simple and user-friendly as possible. So many times previously I have registered for conferences in which the registration system is so convoluted. I especially hate it when I need to register four or five people from Netsight and I have to register each person separately and enter my credit card details over and over again for each registration. One particular European python conference can't handle the credit card owner and the attendee being separate people and so you have to abort the registration process half way through for each attendee, wait for an email to be sent (to the attendee, which they then have to forward to you) with the registration reference number in, you have to phone them read out each reference number and manually process the order!
So I was determined to make this better. I hope I've succeeded. And if you are a person that has to register multiple people for the conference, I hope you appreciate this attention to detail and you can buy me a beer ;)
The reason we tried Dexterity for the registration content types was so that we could easily create a custom 'add' form using z3c.form. By using plone.directives.form we were able to really easily create a custom form which would allow us to tailor the form layout better ie we could easily change the usual 'Save' button which we would have when adding Archetypes based content to Plone to a 'Register' button. We are also able to use Fieldsets from plone.supermodel.model to group our fields together into specific fieldsets to improve the readability of the form.
Using a z3c.form also makes it easier to allow anonymous users to create 'Attendee' objects on the site -- which is how our registration system works:
When someone clicks the 'register' link on the site, it takes them to a custom z3c.form generated form for the Attendee content type. The user fills in their details and hits 'register' this causes an Attendee item to be created in a specific folder in the site, and the item to be then added to the GetPaid shopping cart and takes the user to the cart view page. The user can then register additional people, if they want, before being taken to Paypal to pay for their registration (using getpaid.paypal). Once the payment is successful, Paypal makes a callback to the site which causes GetPaid to transition the order from 'Charging' to 'Charged' state and in turn transition all Attendee objects in that order to the 'Paid' state. The code is really quite simple. First we define our interface for out Attendee object, this is where we define the schema of the object:
# attendee.py
from five import grok
from zope.schema import TextLine, Choice, Bool
from plone.directives import form, dexterity
from netsight.ploneconf2010_registration import _
class IAttendee(form.Schema):
"""A conference attendee"""
### Contact details
firstname = TextLine(
title=_(u"First Name"),
)
lastname = TextLine(
title=_(u"Last Name"),
)
city = TextLine(
title=u'City',
required=True,
)
shirt = Choice(
title=u'Plone Conference 2010 T-Shirt size',
required=True,
vocabulary="netsight.ploneconf2010_registration.shirts"
)
#..... further fields omitted for brevity
Then in browser/registration.py we create our custom registration form:
from five import grok
from plone.directives import form
from z3c.form import field, group
from netsight.ploneconf2010_registration.interfaces import IRegistrationFolder
from netsight.ploneconf2010_registration.attendee import IAttendee
from netsight.ploneconf2010_registration import _
class ContactGroup(group.Group):
label=u"Contact Details"
description=u"Enter your contact details below"
fields=field.Fields(IAttendee).select(
'firstname', 'lastname', 'city', 'postcode', 'country', 'email',
)
class PreferencesGroup(group.Group):
label=u"Preferences"
description=u"Let us know of any dietry requirements you have, t-shirt size and if you will be attending the sprints"
fields=field.Fields(IAttendee).select(
'food', 'food_other', 'shirt', 'sprints',
)
class RegistrationForm(group.GroupForm, form.Form):
grok.name('registration')
grok.require('zope2.View')
grok.context(IRegistrationFolder)
schema = IAttendee
ignoreContext = True
label = _(u"Register for Plone Conference 2010")
# we want to further customise the form...
template = ViewPageTemplateFile('templates/registration_form.pt')
# Group the fields into fieldsets, only included a few for brevity
groups = (ContactGroup, PreferencesGroup)
# We want the fieldsets presented one below the other, not in tabs
enable_form_tabbing = False
def update(self):
# disable Plone's editable border
self.request.set('disable_border', True)
# call the base class version - this is very important!
super(RegistrationForm, self).update()
@button.buttonAndHandler(_(u'Register'))
def handleApply(self, action):
data, errors = self.extractData()
if errors:
self.status = self.formErrorsMessage
return
# Create the attendee object in the attendees folder
# setting checkContraints to false means we skip the security checking and allow anon to add
attendees = self.context.attendees
attendee = createContentInContainer(attendees, 'netsight.ploneconf2010_registration.attendee',
checkConstraints=False, **data)
# Add attendee to shopping cart
self.addToCart(attendee)
portal_url = getToolByName( self.context, 'portal_url').getPortalObject().absolute_url()
return self.request.response.redirect('%s/register/@@getpaid-cart' % portal_url)
I wanted to add a chunk of HTML at the start of the form, with a table of prices and instructions on payment options, so I created a custom template referred to in the form with the HTML in templates/registration_form.pt
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="plone.z3cform"
metal:use-macro="context/main_template/macros/master">
<metal:block fill-slot="main">
<h1 class="documentFirstHeading" tal:content="view/label | nothing" />
<img src="http://farm4.static.flickr.com/3505/4074083883_797e6c371f_m.jpg"
align="right" width="240" height="169" alt="Plone Conference 2009 delegates" />
<p>To register for Plone Conference 2010, fill in the registration form below.
If you are registering a number of people then you will have a chance to add
multiple registrations to your cart before checking out to pay.</p>
<!-- more HTML -->
<div id="content-core">
<metal:block use-macro="context/@@ploneform-macros/titlelessform" />
</div>
</metal:block>
</html>
This give me the flexibility to add extra stuff to the form, but to retain all of z3c.forms machinery for generating the form from the IAttendee interface and to do all the validation and checking of mandatory fields. You may notice that I set the view to be registered to IRegistrationsFolder, read my previous blog post for an easy way to set the default view on an arbitrary folder.
A further refinement to the Attendee object was to get it to be able to return a title from the first and last names entered. Due to Plone using the catalog for folder view listings, and for legacy reasons it looks for an attribute or method on an object called 'Title' we needed to add this method to our class. So the extended version of the attendee class is:
# attendee.py
from five import grok
from zope.schema import TextLine, Choice, Bool
from plone.directives import form, dexterity
from netsight.ploneconf2010_registration import _
from plone.app.content.interfaces import INameFromTitle
from plone.dexterity.content import Item
class Attendee(Item):
""" An attendee """
def Title(self):
""" return the title """
return INameFromTitle(self).title
class IAttendee(form.Schema):
"""A conference attendee"""
### Contact details
firstname = TextLine(
title=_(u"First Name"),
)
lastname = TextLine(
title=_(u"Last Name"),
)
city = TextLine(
title=u'City',
required=True,
)
shirt = Choice(
title=u'Plone Conference 2010 T-Shirt size',
required=True,
vocabulary="netsight.ploneconf2010_registration.shirts"
)
#..... further fields omitted for brevity
class TitleAdapter(grok.Adapter):
grok.provides(INameFromTitle)
grok.context(IAttendee)
def __init__(self, context):
self.context = context
@property
def title(self):
return '%s, %s' % (self.context.lastname, self.context.firstname)
By registering an adapter for INameFromTitle is means that the object creation mechanism can look up what the title of the object is, and then convert that to a more meaningful id for the object created.
GetPaid
This was the first time I'd looked at GetPaid, and credit to all the people who have worked on it, it really is a fantastic product. Previously we have always ended up coding our own eCommerce products for Plone from scratch as every customer's requirements have been different enough to make using any of the existing shopping cart systems for Plone difficult. They always seemed to have made assumptions about a particular country's tax or shipping system that didn't work for us. And making small changes always ended up in us subclassing the entire system, which was a pain. GetPaid uses the Zope Component Architecture to the full and the ability to simply mark any regular piece of content as buyable makes building stores with Plone a breeze.
Imagine you wanted to create a shop online that sold jewellery. With just stock Plone and PloneGetPaid installed you could simply upload photos of each jewellery piece using the standard Plone image content type, and set the folder to thumbnail view. Then mark each image as 'buyable' and give them a price and stock code and you are done. Add getpaid.paypal and configure a Paypal account in a few minutes and you can now accept payment. No custom coding needed whatsoever. No merchant account needed. Combine that with collective.xdv and a free theme from one of the free template sites, and you are set.
Conclusion
This was the first time I've actually used z3c.form from within Plone, and combined with Dexterity it provides a very powerful system which makes it very easy to quickly develop content types and really nice flexible forms for them. I'm looking forward to using this combination in future Plone projects. The only area where Archetypes still leads is the number (and polish) of some of the widgets, but I think that z3c.form is catching up quickly. Porting over some of the more complex Widgets from Archetypes to z3c.form would be a fantastic sprint topic, and if no-one gets there beforehand, I'm going to raise it as a topic for the sprint after the Plone Conference.
Plone 4 really is amazing, and the best version of Plone yet.
See you at the Plone Conference 2010 in Bristol! Registration is Open!
-Matt






