Benefit NOW from using GenericSetup and Zope 3 technologies
Note: Return to tutorial view.
Introduction
Goals
The goal of this tutorial is to get you familiar with GenericSetup and Zope 3 techniques and help you understand how these tools can help you become a more efficient Plone developer.
Step by step you will learn how to
- create a Plone Product with minimal effort,
- register a new view for an existing content type,
- do something useful with that view and
- register a new content type that uses that view as its default view.
What do you need?
You'll need a Zope instance that has Plone 2.5 installed.
Registering a view
First, we'll create a Product called MYPRODUCT. This is how you do it: In your Zope instance's Products folder, create a MYPRODUCT folder and therein create a file called __init__.py. Leave the __init__.py empty for now.
Then, in the same folder, create a file called configure.zcml with the following contents:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser">
<browser:page
for="Products.ATContentTypes.interface.IATFolder"
name="my-view.html"
class=".browser.MyView"
template="my_view.pt"
permission="zope2.View"
/>
</configure>
What we did here is register a new view for the IATFolder interface with the name my-view.html. The implementation of that view goes into the class .browser.MyView, which uses the template my_view.pt. This view is guarded with the zope2.View permission.
Now we'll create that MyView class and the my_view.pt page template. Inside the MYPRODUCT folder on the filesystem, create a file called browser.py and insert the following:
from Products.Five import BrowserView
class MyView(BrowserView):
pass
This is where we will define methods later on that our template can use like view/my_method. Right now, we'll leave the view class empty.
For the template, we'll copy and modify one of the templates that comes with Plone. Let's use folder_summary_view.pt from CMFPlone/skins/plone_content and strip out everything that's in the metal:listingmacro tag so that what we have left is this:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
lang="en"
metal:use-macro="here/main_template/macros/master"
i18n:domain="plone">
<body>
<div metal:fill-slot="main">
<metal:main-macro define-macro="main">
<div metal:use-macro="here/document_actions/macros/document_actions">
Document actions (print, sendto etc)
</div>
<h1 tal:content="object_title" class="documentFirstHeading">
Title or id
</h1>
<a href=""
class="link-parent"
tal:define="parent_url python:here.navigationParent(here, template_id)"
tal:condition="parent_url"
tal:attributes="href parent_url"
i18n:translate="go_to_parent_url">
Up one level
</a>
<p class="documentDescription"
tal:content="here/Description"
tal:condition="here/Description">
Description
</p>
<p> Hello world! </p>
</metal:main-macro>
</div>
</body>
</html>
Save this as MYPRODUCT/my_view.pt.
Now start up your Zope, go into the Plone interface, create a folder called whatever and then visit .../portal/whatever/my-view.html.
Do something useful
You can skip this part if you don't feel like doing something useful right now. :-)
Now that we have our view in place, we can start actually doing something with it. Like listing all keywords of the items that the folder contains, and letting the user do a search based on those.
For that, we'll add a couple of methods to our existing view in browser.py for our template to use.
listAllTags returns a list of keywords in the order of their popularity. This is the implementation:
class MyView(BrowserView):
def listAllTags(self):
weighted_tags = dict()
for item in self.query():
for keyword in item.Subject:
if keyword in weighted_tags:
weighted_tags[keyword] = weighted_tags[keyword] + 1
else:
weighted_tags[keyword] = 1
items = sorted(weighted_tags.items(), lambda a,b:cmp(b[1], a[1]))
return [i[0] for i in items]
def query(self, **kw):
path = '/'.join(self.context.getPhysicalPath())
return self.context.portal_catalog(path=path, **kw)
Then we'll create two other methods for our template: tagSelected will tell us whether a tag has been selected already, and addTagToURL will add a tag to the query parameters. The implementation of these methods goes like this:
from urllib import quote_plus
# ...
def tagSelected(self, tag):
return tag in self.getTags()
def addTagToURL(self, tag):
base = self.request.URL + '?'
query = ''
for existing_tag in self.getTags():
query += 'tags:list=%s&' % quote_plus(existing_tag)
query += 'tags:list=' + quote_plus(tag)
return base + query
def getTags(self):
return self.request.form.get('tags', [])
This is how we present the list of tags in the template:
<h2>Tags</h2>
<ul>
<li tal:repeat="tag view/listAllTags">
<a href="#"
tal:omit-tag="python:view.tagSelected(tag)"
tal:attributes="href python:view.addTagToURL(tag)"
tal:content="tag"
/>
</li>
</ul>
However, this is hardly useful without any results. Again, the real work is done in the view:
def searchResults(self):
return self.query(Subject=dict(query=self.getTags(), operator='and'))
and the template just calls searchResults:
<h2>Results</h2>
<ul>
<li tal:repeat="item view/searchResults">
<a href="#"
tal:attributes="href item/getURL;
title item/Description"
tal:content="item/Title"
/>
</li>
</ul>
Registering a setup profile
Next, we're going to register our own content type that uses the view that we just made (my-view.html) as its default view. The type will otherwise behave just like a normal folder.
We'll do this by registering a GenericSetup profile, which is a bunch of configuration settings that you can install in your Plone site. In order to register the profile, we need to add this to our MYPRODUCT/configure.zcml:
<gs:registerProfile
name="default"
title="MYPRODUCT profile"
directory="profiles/default"
description=""
provides="Products.GenericSetup.interfaces.EXTENSION"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
/>
If you're familiar with XML, you may have noticed that we're using another XML namespace. We'll have to define the gs namespace in the root configure statement in the same file, so that it becomes:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:gs="http://namespaces.zope.org/genericsetup">
Now that we registered the configuration, we can write the actual configuration, which goes into the MYPRODUCT/profiles/default folder. This is what we need to put into MYPRODUCT/profiles/default/types.xml to register our own type:
<?xml version="1.0"?>
<object name="portal_types" meta_type="Plone Types Tool">
<object name="My Folder"
meta_type="Factory-based Type Information with dynamic views"/>
</object>
Now copy Plone's CMFPlone/profiles/default/types/Folder.xml into the MYPRODUCT/profiles/default/types/ directory (which you create first) and rename the file to My_Folder.xml. This file corresponds to what you see in the portal_types tool in the ZMI when you go to .../portal/portal_types/Folder/manage_propertiesForm.
In your new My_Folder.xml, change this:
<property name="default_view">folder_listing</property>
into:
<property name="default_view">my-view.html</property>
and change this:
<object name="Folder"
meta_type="Factory-based Type Information with dynamic views"
into:
<object name="My Folder"
meta_type="Factory-based Type Information with dynamic views"
Also, add my-view.html to the list of selectable views in the view_methods property in the same XML file. And then you might want to change the title of the new type in the title property.
Done! Now you can visit the portal_setup tool in the ZMI and apply your MYPRODUCT profile to your site. But that user interface isn't really nice. So let's write a QuickInstaller method instead, so that your Product can be installed as usual through the Plone interface.
For your Product to be quick-installable, add a MYPRODUCT/Extensions/install.py and do:
from Products.CMFCore.utils import getToolByName
def install(portal):
setup_tool = getToolByName(portal, 'portal_setup')
old_context = setup_tool.getImportContextID()
setup_tool.setImportContext('profile-Products.MYPRODUCT:default')
setup_tool.runAllImportSteps()
setup_tool.setImportContext(old_context)
return "Ran all import steps."
Note that this step will not be necessary anymore in the future, because the portal_quickinstaller tool is going to be aware of GenericSetup profiles starting with Plone 3.0.
Of course all of these steps work also of you use sensible names instead of MYPRODUCT and my-view.html etc., but be sure you change those names throughout your whole Product!
Where to go from here
- Check out the PhotoContest Product, which is really similar to what we developed in this tutorial, except that uses formlib in the view. And experiment!
- Read the Testing in Plone tutorial. Testing helps you become a much more productive developer.
- Read this gentle introduction to the Python debugger. Together with testing, this will make your development super-fast.
- Watch the Seattle videos!
- Look at Understanding and Using GenericSetup in Plone if you want to learn more about GenericSetup.
- The RichDocument tutorial together with the b-org tutorial cover more techniques for Plone development.
- Check out my blog.
Happy view-ing!